diff --git a/.editorconfig b/.editorconfig index 812c991..50bfd79 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,3 +6,6 @@ indent_size = 4 [Makefile] indent_style = tab + +[*.yml] +indent_size = 4 diff --git a/.gitignore b/.gitignore index d400d6e..262d491 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.phar *.bak src/vendor/* +src/conf/* diff --git a/CHANGELOG.md b/CHANGELOG.md index d0cd95b..9f9ab1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,29 @@ CliTools Changelog ================== +2.0.0 - 2015-06-16 +------------------ +- Added GitHub based `self-update` +- Added `make` (auto search for Makefile in tree) +- Added `php:composer` (auto search for composer.json in tree) +- Added `mysql:convert` for automatic changing charset and collation of one database +- Added `sync:server` for syncing any configured server to your local development system (reads clisync.yml or .clisync.yml) +- Added `sync:backup` for backup to a shared server (reads clisync.yml or .clisync.yml) +- Added `sync:restore` for restore from a shared server (reads clisync.yml or .clisync.yml) +- Added `sync:deploy` for lightweight deployment to a foreign server (reads clisync.yml or .clisync.yml) +- Added `typo3:domain --list` for only list the domains of one or all databases +- Added `typo3:domain --remove=domain/pattern` for domain cleanup (eg. vagrant share) +- Added `typo3:domain --duplication=suffix` for domain duplication +- Added `typo3:domain --baseurl` for setting config.baseURL in SetupTS +- Added `vagrant:share` with automatic domain setting for TYPO3 projects (ALPHA! not finished!) +- TTY banner now will be reloaded (SIGHUB is send to getty tty1) +- Added docker detection for sync features +- Updated to Symfony 2.7.1 +- Refactored some classes +- Fixed some issues +- Added gzip compression for PHAR +- SLOC: 5,999 + 1.9.0 - 2015-05-06 ------------------ - Added `mysql:backup` (with --filter=typo3, support for plain sql, gzip, bzip2, lzma compression) @@ -12,6 +35,7 @@ CliTools Changelog - Refactored shell command execution (again) - Fixed code styling - Improved code and fixed some smaller bugs +- SLOC: 4,038 1.8.0 - 2015-04-26 ------------------ @@ -27,36 +51,44 @@ CliTools Changelog - Implemented command check - Improved disk usage warning (wall and growl, will trigger when usage is >=90 in local and remote mounts) - Refactored shell command execution +- SLOC: 3,562 1.7.4 - 2015-04-21 ------------------ - Improved `docker:tshark` +- SLOC: 2,787 1.7.3 - 2015-04-21 ------------------ - Fixed `docker:tshark` +- SLOC: 2,780 1.7.2 - 2015-04-21 ------------------ - Added required php modules checks - Added interactive error return code check +- SLOC: 2,777 1.7.0 - 2015-04-19 ------------------ - Added `docker:tshark`, easy network sniffing - Added `php:trace --all`, for immediate tracing all php processes - Fixed bugs +- SLOC: 2,755 1.6.3 - 2015-04-16 ------------------ - Added `docker:tshark`, easy network sniffing - Added `php:trace --all`, for immediate tracing all php processes - Fixed bugs +- SLOC: 2,832 1.6.2 - 2015-04-15 ------------------ - Fixed bugs +- SLOC: 2,811 1.5.1 - 2015-03-29 ------------------ - Added growl support +- SLOC: 2,773 diff --git a/Documentation/ALIASES.md b/Documentation/ALIASES.md new file mode 100644 index 0000000..c7b6c9b --- /dev/null +++ b/Documentation/ALIASES.md @@ -0,0 +1,33 @@ +[<-- Back to main section](../README.md) + +## Shell aliases + +```bash +# Shortcut for docker-compose (autosearch docker-compose.yml in up-dir, you don't have to be in directory with docker-compose.yml) +alias dcc='ct docker:compose' + +# Startup docker-container (and shutdown previous one, v1.9.0 and up) +alias dccup='ct docker:up' +alias dccstop='ct docker:compose stop' + +# Enter main docker container (as CLI_USER if available - if not specified then root is used) +alias dcshell='ct docker:shell' +alias dcsh='ct docker:shell' + +# Enter main docker container (as root) +alias dcroot='ct docker:root' + +# Execute predefined cli in docker container +alias dccrun='ct docker:cli' + +# Run command +alias dcexec='ct docker:exec' + +# Execute mysql client in docker container +alias dcsql='ct docker:mysql' +alias dcmysql='ct docker:mysql' + +# General shortcuts (with up-dir tree searching) +alias composer='ct php:composer' +alias make='ct make' +``` diff --git a/Documentation/COMMANDS.md b/Documentation/COMMANDS.md new file mode 100644 index 0000000..42f3922 --- /dev/null +++ b/Documentation/COMMANDS.md @@ -0,0 +1,161 @@ +[<-- Back to main section](../README.md) + +## Commands + +### Special commands + +| Command | Description | +|----------------------------|---------------------------------------------------------------------------| +| ct self-update | Update ct command (download new version) | +| ct update | Updates all system components, ssh configuration, ct command update etc. | +| ct make | Search for "Makefile" in tree and start "make" in this directory | + +### System commands + +| Command | Description | +|----------------------------|---------------------------------------------------------------------------| +| ct shutdown (alias) | Shutdown system | + +### Log commands + +All log commands are using a grep-filter (specified as optional argument) + +| Command | Description | +|----------------------------|---------------------------------------------------------------------------| +| ct log:mail | Shows mail logs | + +### Docker commands + +| Command | Description | +|----------------------------|---------------------------------------------------------------------------| +| ct docker:create | Create new docker boilerplate in directory (first argument) | +| | __ct docker:create projectname__ -> Create new docker boilerplate instance in directory "projectname" | +| | __ct docker:create projectname --code=git@github.com/foo/bar__ -> Create new docker boilerplate instance in directory "projectname" and git code repository | +| | __ct docker:create projectname --docker=git@github.com/foo/bar__ -> Create new docker boilerplate instance in directory "projectname" and custom docker boilerplate repository | +| | __ct docker:create projectname --code=git@github.com/foo/bar --make=build__ -> Create new docker boilerplate instance in directory "projectname" and git code repository, will run automatic make (Makefile) task "build" after checkout | +| ct docker:shell | Jump into a shell inside a docker container (using predefined user defined with CLI_USER in docker env) | +| | __ct docker:shell__ -> enter main container | +| | __ct docker:shell mysql__ -> enter mysql container | +| | __ct docker:shell --user=www-data -> enter main container as user www-data | +| ct docker:root | Jump into a shell inside a docker container as root user | +| ct docker:mysql | Jump into a mysql client inside a docker container | +| | __ct docker:mysql__ -> execute mysql client inside main container | +| ct docker:sniff | Start network sniffer for various protocols | +| | __ct docker:sniff http__ -> start HTTP sniffing | +| ct docker:exec | Execute command in docker container | +| | __ct docker:exec ps__ -> run 'ps' inside main container | +| ct docker:cli | Execute special cli command in docker container | +| | __ct docker:cli scheduler__ -> run 'scheduler' in TYPO3 CMS | +| ct docker:compose | Execute docker-compose (recursive up-searching for docker-compose.yml) | +| | __ct docker:compose ps__ -> list all running docker-compose containers | + +### MySQL commands + +| Command | Description | +|----------------------------|---------------------------------------------------------------------------| +| ct mysql:clear | Clear database (remove all tables in database) | +| | __ct mysql:clear typo3__ | +| ct mysql:connections | Lists all current connections | +| ct mysql:create | Create (and drops if already exists) a database | +| | __ct mysql:create typo3__ | +| ct mysql:debug | Shows mysql debug log (lists all queries) with basic filter support | +| | __ct mysql:debug__ (full log) | +| | __ct mysql:debug tt_content__ (full log) | +| ct mysql:slowlog | Shows mysql slow log | +| | __ct mysql:slowlog__ (show slow queries with 1 sec and more) | +| | __ct mysql:slowlog --time=10__ (show slow queries with 10 sec and more) | +| | __ct mysql:slowlog --no-index__ (show not using index and slow (1sec) queries) | +| ct mysql:drop | Drops a database | +| | __ct mysql:drop typo3__ | +| ct mysql:list | Lists all databases with some statitics | +| ct mysql:restart | Restart MySQL server | +| ct mysql:backup | Backup a database to file | +| | Compression type will be detected from file extension (default plain sql) | +| | __ct mysql:restore typo3 dump.sql__ -> plain sql dump | +| | __ct mysql:restore typo3 dump.sql.gz__ -> gzip'ed sql dump | +| | __ct mysql:restore typo3 dump.sql.bzip2__ -> bzip2'ed sql dump | +| | __ct mysql:restore typo3 dump.sql.xz__ -> xz'ed (lzma'ed) sql dump | +| | __ct mysql:restore typo3 dump.sql --filter=typo3__ -> No TYPO3 cache tables in dump | +| ct mysql:restore | Create (and drops if already exists) a database and restore from a dump | +| | Dump file can be plaintext, gziped, bzip2 or lzma compressed | +| | and will automatically detected | +| | __ct mysql:restore typo3 dump.sql.bz2__ | +| ct mysql:convert | Convert character set and collation of a database | +| | __ct mysql:convert typo3__ -> Convert typo3 into UTF-8 with utf8_general_ci | +| | __ct mysql:convert typo3 --charset=latin1__ -> Convert typo3 into LATIN-1 | +| | __ct mysql:convert typo3 --collation=utf8_unicode_ci__ -> Convert typo3 into UTF-8 with utf8_unicode_ci | +| | __ct mysql:convert typo3 --stdout__ -> Print sql statements to stdout | + +### Sync commands + +| Command | Description | +|----------------------------|---------------------------------------------------------------------------| +| ct sync:init | Create example clisync.yml in current working directory | +| ct sync:backup | Search for clisync.yml in tree and start backup to shared server | +| | __ct sync:backup__ -> Backup files and database from share | +| | __ct sync:backup --rsync__ -> Backup only files from share | +| | __ct sync:backup --mysql__ -> Backup only database from share | +| ct sync:restore | Search for clisync.yml in tree and start restore from shared server | +| | __ct sync:restore__ -> Restore files and database from share | +| | __ct sync:restore --rsync__ -> Restore only files from share | +| | __ct sync:restore --mysql__ -> Restore only database from share | +| ct sync:server | Search for clisync.yml in tree and start server synchronization (eg. from live or preview to local development instance | +| | __ct sync:server production__ -> Use "production" configuration and start sync | +| | __ct sync:server preview --rsync__ -> Use "preview" configuration and start only rsync | +| | __ct sync:server staging --mysql__ -> Use "staging" configuration and start only mysql sync | + +### PHP commands + +| Command | Description | +|----------------------------|---------------------------------------------------------------------------| +| ct php:trace | Trace syscalls from one or all PHP processes (strace) | +| | __ct php:trace --all__ -> Trace all php processes immediately | +| ct php:composer | Search for "composer.yml" in tree and start "composer" in this directory | + +### Samba commands + +| Command | Description | +|----------------------------|---------------------------------------------------------------------------| +| ct samba:restart | Restart Samba server | + + +### System commands + +| Command | Description | +|----------------------------|---------------------------------------------------------------------------| +| ct system:env | Lists common environment variables | +| ct system:openfiles | Lists current open files count grouped by process | +| ct system:shutdown | Shutdown system | +| ct system:swap | Show swap usage for running processes | +| ct system:update | Updates all system components, ssh configuration, ct command update etc. | +| ct system:version | Shows version for common packages | + +### TYPO3 commands + +| Command | Description | +|----------------------------|---------------------------------------------------------------------------| +| ct typo3:beuser | Injects a dev user (pass dev) to all or one specified TYPO3 database | +| | __ct typo3:beuser__ | +| | __ct typo3:beuser typo3__ | +| ct typo3:cleanup | Cleanup command tables to same some table space | +| | __ct typo3:cleanup__ | +| | __ct typo3:cleanup typo3__ | +| ct typo3:domain | Add default suffix to all domains (default: .vm) | +| | __ct typo3:domain --baseurl__ Also update config.baseURL in SetupTS | +| | __ct typo3:domain --list__ Print list of domains and exit | +| | __ct typo3:domain --remove='*.vagrantshare.com'__ Remove all *.vagrantshare.com domains (used by vagrant:share command) | +| | __ct typo3:domain --duplicate='foobar.vagrantshare.com'__ Duplicates all domains and add suffix 'foobar.vagrantshare.com' (used by vagrant:share command) | + +### Vagrant commands + +| Command | Description | +|----------------------------|---------------------------------------------------------------------------| +| ct vagrant:share | Start sharing (with some workflow stuff) (ALPHA! not finished!) | + +### User commands + +| Command | Description | +|----------------------------|---------------------------------------------------------------------------| +| ct user:rebuildsshconfig | Rebuild SSH config from ct repository (/vagrant/provision/sshconfig) | + + diff --git a/Documentation/Examples/clisync.yml b/Documentation/Examples/clisync.yml new file mode 100644 index 0000000..59af1c1 --- /dev/null +++ b/Documentation/Examples/clisync.yml @@ -0,0 +1,228 @@ +########################################################### +# Global (applied to all server sections) +########################################################### +# +# Configuration merge order: +# 1. context configuration will override all settings +# 2. sync/deploy/share GLOBAL will override default settings +# 3. GLOBAL are the defaults +# +# EXAMPLE: +# if you ned eg. another mysql hostname just set it in the +# context: +# +# sync: +# production: +# mysql: +# hostname: 192.168.56.2 +# +# All other configurations can be overwritten as well +# +# HINT: +# You can check the configuration with the "--config" option +# + +GLOBAL: + ## MYSQL + mysql: + # mysql connection + hostname: localhost + + # MySQL predefined filter for typo3 (eg. no caching tables) + filter: typo3 + + # MySQL custom filter (preg_match) + #filter: + # - "/^cachingframework_.*/i" + # - "/^cf_.*/i" + # - "/^cache_.*/i" + # - "/^index_.*/i" + # - "/^sys_log$/i" + # - "/^sys_history$/i" + # - "/^tx_extbase_cache.*/i" + + # Transfer compression (none if empty, bzip2 or gzip) + compression: bzip2 + + # specific mysqldump settings + mysqldump: + option: "--opt --skip-lock-tables --single-transaction" + + ## RSYNC + rsync: + # set target as sub directroy (will be appended to working directory) + workdir: "" + + # exclude list/patterns for files and directories + exclude: + # Temp files + - "*~" + - "._*" + + # VCS + - ".git*" + - ".gitignore" + - ".gitmodules" + - ".svn" + + # Build files + - "composer.json" + - "bower.json" + - "gulpfile.js" + - "Gruntfile.js" + - "Makefile" + + # Caches and other files + - "node_modules" + - ".sass-cache" + - ".settings" + - ".bowerrc" + - ".buildpath" + - ".project" + + ## commands + command: + # Start-Tasks: shell command which should be run before run + startup: + # add some here + + # Final-Tasks: shell command which should be after run + finalize: + # add some here + + # EXAMPLE: local task + # - date + + # EXAMPLE: remote task (will be send over ssh) + #- { type: 'remote', command: 'date' } + + # EXAMPLE: create user "dev" with password "dev" + - "ct typo3:beuser" + # EXAMPLE: append toplevel-domain .vm to all domains + - "ct typo3:domain" + + + + +########################################################### +# Sync from server (eg. live server) +########################################################### +sync: + + ################## + # Global config (for sync) + ################## + GLOBAL: + mysql: + ## put your mysql settings here (see global conf) + + rsync: + # directory list/patterns for synchronization + directory: + - "/fileadmin/" + - "/uploads/" + - "/typo3conf/l10n/" + + # directory exclude list/patterns + exclude: + - "/fileadmin/_processed_/**" + - "/fileadmin/_temp_/**" + + ################## + # Context "production" + ################## + production: + # ssh server host or name (see .ssh/config, eg for mysql/mysqldump) + ssh: + hostname: live-server + + # rsync for some directories + rsync: + # server and source directory (server host or name - see .ssh/config) + path: "live-server:/var/www/website/htdocs" + + #conf: + # maxSize: 20M + # minSize: 10kb + + mysql: + username: typo3 + password: loremipsum + + # List of databases for synchronization + # examples: + # local:foreign + # samename + database: + - typo3:website_live + + + + +########################################################### +# Deployment to server +########################################################### +deploy: + + ################## + # Global config (for deploy) + ################## + GLOBAL: + mysql: + # global mysql configuration + + rsync: + # directory list/patterns for synchronization + directory: + - "/typo3conf/ext/" + + # directory exclude list/patterns + exclude: + - "/fileadmin/" + - "/uploads/" + - "/typo3conf/l10n/" + + ################## + # Context "production" + ################## + production: + # ssh server host or name (see .ssh/config, eg for mysql/mysqldump) + ssh: + hostname: live-server + + # rsync for some directories + rsync: + # server and source directory (server host or name - see .ssh/config) + path: "live-server:/var/www/website/htdocs" + + + + +########################################################### +# Shared server (sharing between developers) +########################################################### +share: + + ################## + # Global config (for share) + ################## + GLOBAL: + mysql: + # List of databases for backup + database: + - typo3 + + rsync: + # List of directories for backup + directory: + - "/fileadmin/" + - "/uploads/" + - "/typo3conf/l10n/" + + ################## + # Context "development" + ################## + development: + rsync: + # source/target directory or server via ssh (eg. backup-server:/backup/projectname) + path: "/tmp/foo/" diff --git a/Documentation/INSTALL.md b/Documentation/INSTALL.md new file mode 100644 index 0000000..d0a75d6 --- /dev/null +++ b/Documentation/INSTALL.md @@ -0,0 +1,121 @@ +[<-- Back to main section](../README.md) + +# Installation + +## Requirements + +- PHP 5.5 (CLI) with pcntl module +- Tools + - git + - wget + - multitail + - tshark + - tcpdump + - ngrep + - strace + - lsof + - sudo + - moreutils (ifdata) + - coreutils (grep, sort, uniq, awk, cat, df, ip, cut, lsb_release, wall) + - docker and docker-compose (if you want to use docker) + - mysql (if you want to use mysql) + + +## Install clitools + +```bash +# Download latest tools (or in ~/bin if you have it in $PATH) +wget -O/usr/local/bin/ct https://www.achenar.net/clicommand/clitools.phar + +# Set executable bit +chmod 777 /usr/local/bin/ct + +# Download example config +wget -O"$HOME/.clitools.ini" https://raw.githubusercontent.com/mblaschke/vagrant-development/develop/provision/ansible/roles/clitools/files/clitools.ini +``` + +## Aliases + +Now you can use following aliases (some aliases requires clitools 1.8.0!): + +```bash +# Shortcut for auto-tree-searching make +alias make='ct make' + +# Shortcut for auto-tree-searching make +alias composer='ct php:composer' + +# Shortcut for docker-compose (autosearch docker-compose.yml in up-dir, you don't have to be in directory with docker-compose.yml) +alias dcc='ct docker:compose' + +# Startup docker-container (and shutdown previous one, v1.9.0 and up) +alias dccup='ct docker:up' +alias dccstop='ct docker:compose stop' + +# Enter main docker container (as CLI_USER if available - if not specified then root is used) +alias dcshell='ct docker:shell' +alias dcsh='ct docker:shell' + +# Enter main docker container (as root) +alias dcroot='ct docker:root' + +# Execute predefined cli in docker container +alias dccrun='ct docker:cli' +alias dcrun='ct docker:cli' + +# Execute mysql client in docker container +alias dcsql='ct docker:mysql' +alias dcmysql='ct docker:mysql' +``` + +## Configuration + +CliTools will read /etc/clitools.ini (system wide) and ~/.clitools.ini (personal) for configuration + +The [default configuration](https://github.com/mblaschke/vagrant-clitools/blob/develop/src/config.ini) is inside the phar. + +### Docker specific configuration +```ini +[config] +; ssh_conf_path = "/vagrant/provision/sshconfig/" + +[db] +dsn = "mysql:host=127.0.0.1;port=13306" +username = "root" +password = "dev" +debug_log_dir = "/tmp/debug/" + +[syscheck] +enabled = 1 +wall = 1 +growl = 1 +diskusage = 85 + +[growl] +server = 192.168.56.1 +password = + +[commands] +; not used commands here +ignore[] = "CliTools\Console\Command\Log\ApacheCommand" +ignore[] = "CliTools\Console\Command\Log\PhpCommand" +ignore[] = "CliTools\Console\Command\Log\DebugCommand" +ignore[] = "CliTools\Console\Command\Apache\RestartCommand" +ignore[] = "CliTools\Console\Command\Mysql\RestartCommand" +ignore[] = "CliTools\Console\Command\Php\RestartCommand" +ignore[] = "CliTools\Console\Command\System\UpdateCommand" +ignore[] = "CliTools\Console\Command\System\RebootCommand" +``` + +## Update clitools + +```bash +# Stable channel +ct self-udpate + +## Beta channel +ct self-update --beta + +## Fallback update (if GitHub fails) +ct self-update --fallback +``` diff --git a/Documentation/USAGE-DOCKER.md b/Documentation/USAGE-DOCKER.md new file mode 100644 index 0000000..d8eb717 --- /dev/null +++ b/Documentation/USAGE-DOCKER.md @@ -0,0 +1,101 @@ +[<-- Back to main section](../README.md) + +# Usage of `ct docker:...` + +## Docker creation + +You can easly create new docker instances (from my or a custom docker boilerplate) also with code intalization +and Makefile running. + +```bash +# Startup new docker boilerplate into foobar directory +ct docker:create foobar + +# Startup new custom docker boilerplate +ct docker:create foobar --docker=git... + +# Startup new docker boilerplate with code repository +ct docker:create foobar --code=git... + +# Startup new docker boilerplate with code repository and makefile run +ct docker:create foobar --code=git... --make=build +``` + +## Docker startup + +The `docker:up` command will search the `docker-compose.yml` in the current parent directroy tree and +execute `docker-compose` from this directroy - you don't have to change the current directroy. + +Also the previous docker instance will be shut down to avoid port conflicts. + +```bash +# Startup docker-compose +ct docker:up +``` + +## Custom docker commands + +As `docker:up` the `docker:compose` will search the `docker-compose.yml` and will execute your command +from this directroy. + +```bash +# Stop docker instance +ct docker:compose stop + +# Show docker container status +ct docker:compose ps +``` + +Hint: You can use `alias dcc='ct docker:compose'` for this. + +## Docker shell access + +There are many ways to jump into docker containers: + +```bash +# Jump into a root shell +ct docker:root + +# Jump into a root shell in mysql container +ct docker:root mysql + +# Jump into a user shell (defined by CLI_USER as docker env) +ct docker:shell + +# Jump into a root user in mysql container (defined by CLI_USER as docker env) +ct docker:root mysql +``` + +## Docker command execution + +```bash +# Execute command "ps" in "main" container +ct docker:exec ps +``` + +## Docker cli execution + +You can define a common CLI script entrypoint with the environment variable CLI_SCRIPT in your docker containers. +The environment variable will be read by `ct docker:cli` and will be executed - you don't have to jump +into your containers, you can start your CLI_SCRIPTs from the outide. + +```bash +# Execute predefined cli command with argument "help" in "main" container +ct docker:cli help +``` + +## Docker debugging + +If you want to debug a docker application (eg. your webpage inside docker) the `ct docker:sniff` provides you +a network sniffer set for various protocols (eg. http or mysql). + +```bash +# Show basic http traffic +ct docker:sniff http + +# Show full http traffic +ct docker:sniff http --full + +# Show mysql querys by using network sniffer +ct docker:sniff mysql +``` diff --git a/Documentation/USAGE-MYSQL.md b/Documentation/USAGE-MYSQL.md new file mode 100644 index 0000000..269e482 --- /dev/null +++ b/Documentation/USAGE-MYSQL.md @@ -0,0 +1,89 @@ +[<-- Back to main section](../README.md) + +# Usage of `ct mysql:...` + +# Common commands + +```bash +# Create database typo3 (recreate and clears database if exists) +ct mysql:create typo3 + +# Drop database typo3 +ct mysql:drop typo3 + +# List databases with statistics +ct mysql:list +``` + +# Debugging + +The `ct mysql:querylog` and `ct mysql:slowlog` provides a convinent way to access the general query log +and the slow log. + +In the query log you can see all queries send and executed by the MySQL database. +The slow query log can be used to see long running queries. + +```bash +# Enable and show query log +ct mysql:querylog + +# Enable and show query log +ct mysql:slowlog + +# Enable and show query log for all queries running longer than 1 sec +ct mysql:slowlog --time=1 + +# Enable and show query log for all queries which don't uses indizes +ct mysql:slowlog --no-index + +``` + +# Backup database +You can easily backup a MySQL database (including compression compressions) and a filter set for tables. + +```bash +# Backup typo3 database (without compression) +ct mysql:backup typo3 dump.sql + +# Backup typo3 database (with gzip) +ct mysql:backup typo3 dump.sql.gz + +# Backup typo3 database (with bzip2) +ct mysql:backup typo3 dump.sql.bz2 + +# Backup typo3 database (with LZMA/xz) +ct mysql:backup typo3 dump.sql.xz +``` + +# Restore database + +Restoring is as easy as backuping a database, the `ct mysql:restore` will drop the database (if exists), +recreate it and restores the dump into the database. With this workflow you also removes all tables which +are not part of the dump file - it's a clean restore of the dump. +Also the compression is automatically detected by file mime type. + +```bash +# Restore typo3 database (auto compression detection) +ct mysql:restore typo3 dump.sql +``` + +# Datbase charset conversion + +```bash +# Convert database to UTF8 +ct mysql:convert typo3 + +# Convert database typo3 to UTF8 and collation utf8_unicode_ci +ct mysql:convert typo3 --collation=utf8_unicode_ci + +# Convert database typo3 to Latin1 +ct mysql:convert typo3 --charset=latin1 + +# Convert database typo3 to Latin1, only show queries +ct mysql:convert typo3 --stdout +``` + + + + + diff --git a/Documentation/USAGE-PHP.md b/Documentation/USAGE-PHP.md new file mode 100644 index 0000000..911ec67 --- /dev/null +++ b/Documentation/USAGE-PHP.md @@ -0,0 +1,33 @@ +[<-- Back to main section](../README.md) + +# Usage `ct php:...` + +## Composer (auto searching in path tree) + +Because you always need to jump into `composer.json` directroy `ct php:composer` will do this for you + +```bash +# Run composer install task +ct php:composer install + +# Run composer update task +ct php:composer update +``` + +Hint: You can use `alias composer='ct php:composer'` for this. + + +## Sys-Tracing PHP Processes + +Because strace'ing already running processes requires some shell knowledge `ct php:trace` will make this handy for you. + +```bash +# Trace one or all php processes (interactive mode) +ct php:trace + +# Trace all processes immediately +ct php:trace --all +``` + + + diff --git a/Documentation/USAGE-SYNC.md b/Documentation/USAGE-SYNC.md new file mode 100644 index 0000000..11bed8d --- /dev/null +++ b/Documentation/USAGE-SYNC.md @@ -0,0 +1,105 @@ +[<-- Back to main section](../README.md) + +# Usage `ct sync:...` + +## Init and configuration of `sync` + +With the `sync` commands you can update your local development installation to the current state of your +server installations. Currently filesync (rsync) and database fetching is supported. + +First you need to create a `clisync.yml` in your project directory, the CliTools will provide your an example +of this file by using following command: + +```bash +ct sync:init +``` + +If you need special SSH settings (ports, compression, identify...) please use your `~/.ssh/config` file +to such settings. + +You can commit this clisync.yml into your project so other developers can use the sync feature, too. + +## Synchronisation with servers (ct sync:server) + +The synchronisation with your servers is one way only, it just syncs your server installation to your +local installation (CliTools are no deployment tools!). + +In the `clisync.yml` you can specify multiple servers. + +Now you can sync your `production` server to your local installation: + +```bash +# Full sync (files and database, with interactive context selection) +ct sync:server + +# Only MySQL (from production context, without interactive context selection) +ct sync:server production --mysql + +# Only Files (from production context, without interactive context selection) +ct sync:server production --rsync +``` + +## Project sharing (ct sync:backup and ct sync:restore) + +The sharing can be used to share files (assets) and databases between developers. +Please use a common development/storage server with ssh access for each developer for this feature. + +```bash +# Make backup of current state and transfer to share server +ct sync:backup + +# ... only MySQL +ct sync:backup --mysql + +# ... only files +ct sync:backup --rsync + +# Restore to state from the share server +ct sync:restore + +# ... only MySQL +ct sync:restore --mysql + +# ... only files +ct sync:restore --rsync + +``` + +## Lightweight deployment (ct sync:deploy) + +With `sync:deploy` you can push your files to your production servers. +Please keep in mind that this feature is just an wrapped rsync and should only be +the simplest solution for deployment. For more advanced or centralized deployemnt try +solutions build on Jenkis, Ansible and others. + +```bash +# Push your project to your servers (with interactive context selection) +ct sync:deploy + +# Push your project to your staging server (without interactive context selection) +ct sync:deploy staging +``` + +## Advanced ssh options + +If you need some advaned ssh options (eg. other ports) use your `~/.ssh/config` configuration file: + + Host project-server + Hostname project-server.example.com + Port 12345 + User root + +If you have a proxy server you can configure it like this: + + Host ssh-proxy + Hostname ssh-proxy.example.com + User foo + + Host project-server + Hostname project-server.example.com + Port 12345 + User root + ProxyCommand ssh ssh-proxy -W %h:%p + + +Now you can use `project-server` as ssh-hostname and your settings will automatically used from your `~/.ssh/config`. diff --git a/Documentation/USAGE-TYPO3.md b/Documentation/USAGE-TYPO3.md new file mode 100644 index 0000000..75d0acc --- /dev/null +++ b/Documentation/USAGE-TYPO3.md @@ -0,0 +1,46 @@ +[<-- Back to main section](../README.md) + +# Usage `ct typo3:...` + +## Backend user injection + +The `ct typo3:beuser` can be used for creating a backend user in all or a specific TYPO3 database +(with salted password support). + +Default username: dev +Default password: dev + +```bash +# Create the user in all databases +ct typo3:beuser + +# Create the user in typo3 databases +ct typo3:beuser typo3 + +# Create the user in typo3 databases with plain password (no salted password) +ct typo3:beuser typo3 --plain +``` + +## Automatic domain manipulation + +The `ct typo3:domain` can be used for manipulation of the domain records eg. for matching your +development environment. + +```bash +# Add a .vm at the and of all domains in all databases +ct typo3:domain + +# Add a .vm at the and of all domains in typo3 database +ct typo3:domain typo3 + +# ... and also add config.baseURL to SetupTS +ct typo3:domain typo3 --baseurl + +# Add a .vm at the and of all domains and remove all *.vagrantshare.com domains (used by vagrant:share) +ct typo3:domain --remove='*.vagrantshare.com' + +# Add a .vm at the and of all domains and duplicate all domains with the suffix .vagrantshare.com (used by vagrant:share) +ct typo3:domain --duplicate='.vagrantshare.com' +``` + + diff --git a/Makefile b/Makefile index 789d5f6..58f910f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,10 @@ +all: autoload build install + build: bash compile.sh install: cp clitools.phar /usr/local/bin/ct -all: build install +autoload: + sh -c "cd src ; composer dump-autoload --optimize --no-dev" diff --git a/README.md b/README.md index c827801..2258f8b 100644 --- a/README.md +++ b/README.md @@ -1,232 +1,37 @@ -# CliTools for Vagrant VM, Debian and Ubuntu (and others) +# CliTools for Docker, PHP und MySQL development -![latest v1.9.0](https://img.shields.io/badge/latest-v1.9.0-green.svg?style=flat) -![License GPL3](https://img.shields.io/badge/license-GPL3-blue.svg?style=flat) -[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/mblaschke/vagrant-clitools.svg)](http://isitmaintained.com/project/mblaschke/vagrant-clitools "Average time to resolve an issue") -[![Percentage of issues still open](http://isitmaintained.com/badge/open/mblaschke/vagrant-clitools.svg)](http://isitmaintained.com/project/mblaschke/vagrant-clitools "Percentage of issues still open") +[![latest v2.0.0](https://img.shields.io/badge/latest-v2.0.0-green.svg?style=flat)](https://github.com/mblaschke/clitools/releases/tag/2.0.0) +[![License GPL3](https://img.shields.io/badge/license-GPL3-blue.svg?style=flat)](/LICENSE) +[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/mblaschke/clitools.svg)](http://isitmaintained.com/project/mblaschke/clitools "Average time to resolve an issue") +[![Percentage of issues still open](http://isitmaintained.com/badge/open/mblaschke/clitools.svg)](http://isitmaintained.com/project/mblaschke/clitools "Percentage of issues still open") [![SensioLabsInsight](https://insight.sensiolabs.com/projects/9f12f125-3623-4b9d-b01b-07090f91e416/big.png)](https://insight.sensiolabs.com/projects/9f12f125-3623-4b9d-b01b-07090f91e416) -CliTools is a terminal utility for some handy convierence tasks based on Symfony Components (Console). +## Introduction -Documentation is still WIP :) +CliTools is a terminal utility for faster development. It should make some daily task very easy. -## Requirements +CliTools is based on Symfony Components (Console). -- PHP 5.5 (CLI) -- Tools - - git - - wget - - multitail - - tshark - - tcpdump - - ngrep - - strace - - lsof - - sudo - - moreutils (ifdata) - - coreutils (grep, sort, uniq, awk, cat, df, ip, cut, lsb_release, wall) - - docker and docker-compose (if you want to use docker) - - mysql (if you want to use mysql) +## Table of contents -## Installation +- [Installation and requirements](/Documentation/INSTALL.md) +- [Usage `ct docker:...` commands](/Documentation/USAGE-DOCKER.md) +- [Usage `ct sync:...` commands](/Documentation/USAGE-SYNC.md) +- [Usage `ct mysql:...` commands](/Documentation/USAGE-MYSQL.md) +- [Usage `ct typo3:...` commands](/Documentation/USAGE-TYPO3.md) +- [Usage `ct php:...` commands](/Documentation/USAGE-PHP.md) +- [Command overview](/Documentation/COMMANDS.md) +- [Shell aliases](/Documentation/ALIASES.md) +## Credits -```bash -# Download latest tools (or in ~/bin if you have it in $PATH) -wget -O/usr/local/bin/ct https://www.achenar.net/clicommand/clitools.phar - -# Set executable bit -chmod 777 /usr/local/bin/ct - -# Download example config -wget -O"$HOME/.clitools.ini" https://raw.githubusercontent.com/mblaschke/vagrant-development/develop/provision/ansible/roles/clitools/files/clitools.ini -``` - -Now you can use following aliases (some aliases requires clitools 1.8.0!): - -```bash -# Shortcut for docker-compose (autosearch docker-compose.yml in up-dir, you don't have to be in directory with docker-compose.yml) -alias dcc='ct docker:compose' - -# Startup docker-container (and shutdown previous one, v1.9.0 and up) -alias dccup='ct docker:up' -alias dccstop='ct docker:compose stop' - -# Enter main docker container (as CLI_USER if available - if not specified then root is used) -alias dcshell='ct docker:shell' -alias dcsh='ct docker:shell' - -# Enter main docker container (as root) -alias dcroot='ct docker:root' - -# Execute predefined cli in docker container -alias dccrun='ct docker:cli' -alias dcrun='ct docker:cli' - -# Execute mysql client in docker container -alias dcsql='ct docker:mysql' -alias dcmysql='ct docker:mysql' -``` - -## Configuration - -CliTools will read /etc/clitools.ini (system wide) and ~/.clitools.ini (personal) for configuration - -The [default configuration](https://github.com/mblaschke/vagrant-clitools/blob/develop/src/config.ini) is inside the phar. - -### Docker specific configuration -```ini -[config] -; ssh_conf_path = "/vagrant/provision/sshconfig/" - -[db] -dsn = "mysql:host=127.0.0.1;port=13306" -username = "root" -password = "dev" -debug_log_dir = "/tmp/debug/" - -[syscheck] -enabled = 1 -wall = 1 -growl = 1 -diskusage = 85 - -[growl] -server = 192.168.56.1 -password = - -[commands] -; not used commands here -ignore[] = "CliTools\Console\Command\Log\ApacheCommand" -ignore[] = "CliTools\Console\Command\Log\PhpCommand" -ignore[] = "CliTools\Console\Command\Log\DebugCommand" -ignore[] = "CliTools\Console\Command\Apache\RestartCommand" -ignore[] = "CliTools\Console\Command\Mysql\RestartCommand" -ignore[] = "CliTools\Console\Command\Php\RestartCommand" -ignore[] = "CliTools\Console\Command\System\UpdateCommand" -ignore[] = "CliTools\Console\Command\System\RebootCommand" -``` - -## Commands - -### Special commands - -| Command | Description | -|----------------------------|---------------------------------------------------------------------------| -| ct self-update | Update ct command (download new version) | -| ct update | Updates all system components, ssh configuration, ct command update etc. | - -### System commands - -| Command | Description | -|----------------------------|---------------------------------------------------------------------------| -| ct shutdown (alias) | Shutdown system | - -### Log commands - -All log commands are using a grep-filter (specified as optional argument) - -| Command | Description | -|----------------------------|---------------------------------------------------------------------------| -| ct log:mail | Shows mail logs | - -### Docker commands - -| Command | Description | -|----------------------------|---------------------------------------------------------------------------| -| ct docker:create | Create new docker boilerplate in directory (first argument) | -| | __ct docker:create projectname__ -> Create new docker boilerplate instance in directory "projectname" | -| | __ct docker:create projectname --code=git@github.com/foo/bar__ -> Create new docker boilerplate instance in directory "projectname" and custom code repository | -| | __ct docker:create projectname --docker=git@github.com/foo/bar__ -> Create new docker boilerplate instance in directory "projectname" and custom docker boilerplate repository | -| ct docker:shell | Jump into a shell inside a docker container (using predefined user defined with CLI_USER in docker env) | -| | __ct docker:shell__ -> enter main container | -| | __ct docker:shell mysql__ -> enter mysql container | -| | __ct docker:shell --user=www-data -> enter main container as user www-data | -| ct docker:root | Jump into a shell inside a docker container as root user | -| ct docker:mysql | Jump into a mysql client inside a docker container | -| | __ct docker:mysql__ -> execute mysql client inside main container | -| ct docker:sniff | Start network sniffer for various protocols | -| | __ct docker:sniff http__ -> start HTTP sniffing | -| ct docker:exec | Execute command in docker container | -| | __ct docker:exec ps__ -> run 'ps' inside main container | -| ct docker:cli | Execute special cli command in docker container | -| | __ct docker:cli scheduler__ -> run 'scheduler' in TYPO3 CMS | -| ct docker:compose | Execute docker-compose (recursive up-searching for docker-compose.yml) | -| | __ct docker:compose ps__ -> list all running docker-compose containers | - -### MySQL commands - -| Command | Description | -|----------------------------|---------------------------------------------------------------------------| -| ct mysql:clear | Clear database (remove all tables in database) | -| | __ct mysql:clear typo3__ | -| ct mysql:connections | Lists all current connections | -| ct mysql:create | Create (and drops if already exists) a database | -| | __ct mysql:create typo3__ | -| ct mysql:debug | Shows mysql debug log (lists all queries) with basic filter support | -| | __ct mysql:debug__ (full log) | -| | __ct mysql:debug tt_content__ (full log) | -| ct mysql:slowlog | Shows mysql slow log | -| | __ct mysql:slowlog__ (show slow queries with 1 sec and more) | -| | __ct mysql:slowlog --time=10__ (show slow queries with 10 sec and more) | -| | __ct mysql:slowlog --no-index__ (show not using index and slow (1sec) queries) | -| ct mysql:drop | Drops a database | -| | __ct mysql:drop typo3__ | -| ct mysql:list | Lists all databases with some statitics | -| ct mysql:restart | Restart MySQL server | -| ct mysql:backup | Backup a database to file | -| | Compression type will be detected from file extension (default plain sql) | -| | __ct mysql:restore typo3 dump.sql__ -> plain sql dump | -| | __ct mysql:restore typo3 dump.sql.gz__ -> gzip'ed sql dump | -| | __ct mysql:restore typo3 dump.sql.bzip2__ -> bzip2'ed sql dump | -| | __ct mysql:restore typo3 dump.sql.xz__ -> xz'ed (lzma'ed) sql dump | -| | __ct mysql:restore typo3 dump.sql --filter=typo3__ -> No TYPO3 cache tables in dump | -| ct mysql:restore | Create (and drops if already exists) a database and restore from a dump | -| | Dump file can be plaintext, gziped, bzip2 or lzma compressed | -| | and will automatically detected | -| | __ct mysql:restore typo3 dump.sql.bz2__ | - -### PHP commands - -| Command | Description | -|----------------------------|---------------------------------------------------------------------------| -| ct php:trace | Trace syscalls from one or all PHP processes (strace) | -| | __ct php:trace --all__ -> Trace all php processes immediately | - -### Samba commands - -| Command | Description | -|----------------------------|---------------------------------------------------------------------------| -| ct samba:restart | Restart Samba server | - - -### System commands - -| Command | Description | -|----------------------------|---------------------------------------------------------------------------| -| ct system:env | Lists common environment variables | -| ct system:openfiles | Lists current open files count grouped by process | -| ct system:shutdown | Shutdown system | -| ct system:swap | Show swap usage for running processes | -| ct system:update | Updates all system components, ssh configuration, ct command update etc. | -| ct system:version | Shows version for common packages | - -### TYPO3 commands - -| Command | Description | -|----------------------------|---------------------------------------------------------------------------| -| ct typo3:beuser | Injects a dev user (pass dev) to all or one specified TYPO3 database | -| | __ct typo3:beuser__ | -| | __ct typo3:beuser typo3__ | -| ct typo3:cleanup | Cleanup command tables to same some table space | -| | __ct typo3:cleanup__ | -| | __ct typo3:cleanup typo3__ | - -### User commands - -| Command | Description | -|----------------------------|---------------------------------------------------------------------------| -| ct user:rebuildsshconfig | Rebuild SSH config from ct repository (/vagrant/provision/sshconfig) | +Thanks for support, ideas and issues ... +- [Ingo Pfennigstorf](https://twitter.com/krautsock) +- [Florian Tatzel](https://twitter.com/PanadeEdu) +- [Philipp Kitzberger](https://github.com/Kitzberger) +- my (old) colleagues at [Lightwerk GmbH](http://www.lightwerk.de/) +- my colleagues at [cron IT GmbH](http://www.cron.eu/) +Did I forget anyone? Send me a tweet or create pull request! diff --git a/build.json b/box.json similarity index 87% rename from build.json rename to box.json index 9cdc14b..5a9a2d0 100644 --- a/build.json +++ b/box.json @@ -15,5 +15,6 @@ ], "main": "src/command.php", "output": "clitools.phar", - "stub": true + "stub": true, + "compression": "GZ" } diff --git a/compile.sh b/compile.sh index a7cef5f..d1a67b2 100755 --- a/compile.sh +++ b/compile.sh @@ -9,10 +9,16 @@ SCRIPT_DIR=$(dirname $(readlink -f "$0")) OLD_PWD=`pwd` +## copy configs +cp "$SCRIPT_DIR/Documentation/Examples/clisync.yml" "$SCRIPT_DIR/src/conf/" + +## run composer cd "$SCRIPT_DIR/src" -composer install +composer install --no-dev +composer dump-autoload --optimize --no-dev +## create phar cd "$SCRIPT_DIR/" -box.phar build -c build.json +box.phar build -c box.json cd "$OLD_PWD" diff --git a/src/app/CliTools/Console/Application.php b/src/app/CliTools/Console/Application.php index 4d3151a..e1e8920 100644 --- a/src/app/CliTools/Console/Application.php +++ b/src/app/CliTools/Console/Application.php @@ -22,6 +22,7 @@ use CliTools\Database\DatabaseConnection; use CliTools\Service\SettingsService; +use CliTools\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\ArgvInput; @@ -98,6 +99,7 @@ public function getConfigValue($area, $confKey, $defaultValue = null) { * Initialize */ public function initialize() { + $this->initializeErrorHandler(); $this->initializeChecks(); $this->initializeConfiguration(); $this->initializePosixTrap(); @@ -160,6 +162,9 @@ public function doRun(InputInterface $input, OutputInterface $output) { } else { $ret = parent::doRun($input, $output); } + } catch(\CliTools\Exception\StopException $e) { + $this->callTearDown(); + $ret = (int)$e->getMessage(); } catch (\Exception $e) { $this->callTearDown(); throw $e; @@ -170,14 +175,44 @@ public function doRun(InputInterface $input, OutputInterface $output) { return $ret; } + + /** + * Configures the input and output instances based on the user arguments and options. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + */ + protected function configureIO(InputInterface $input, OutputInterface $output) { + parent::configureIO($input, $output); + + $style = new OutputFormatterStyle(); + $style->setApplication($this); + $style->setWrap('-', '-'); + $output->getFormatter()->setStyle('h1', $style); + + $style = new OutputFormatterStyle(); + $style->setPaddingOutside(' ===> '); + $output->getFormatter()->setStyle('h2', $style); + + $style = new OutputFormatterStyle(); + $style->setPaddingOutside(' - '); + $output->getFormatter()->setStyle('p', $style); + + $style = new OutputFormatterStyle('white', 'red'); + $style->setPadding(' [EE] '); + $output->getFormatter()->setStyle('p-error', $style); + } + /** * Initialize POSIX trap */ protected function initializePosixTrap() { declare(ticks = 1); - $signalHandler = function ($signal) { - $this->callTearDown(); + $me = $this; + + $signalHandler = function ($signal) use($me) { + $me->callTearDown(); // Prevent terminal messup echo "\n"; @@ -187,6 +222,25 @@ protected function initializePosixTrap() { pcntl_signal(SIGINT, $signalHandler); } + /** + * Init error handler + */ + protected function initializeErrorHandler() { + $errorHandler = function ($errno, $errstr, $errfile, $errline) { + $msg = array( + 'Message: ' . $errstr, + 'File: ' . $errfile, + 'Line: ' . $errline, + ); + + $msg = implode("\n", $msg); + + throw new \RuntimeException($msg, $errno); + }; + + set_error_handler($errorHandler); + } + /** * PHP Checks */ @@ -321,4 +375,14 @@ public function getSettingsService() { } return $this->settingsService; } + + /** + * Set terminal title + * + * @param string $title Title + */ + public function setTerminalTitle($title) { + // DECSLPP. + echo "\033]0;" . 'ct: ' . $title . "\033\\"; + } } diff --git a/src/app/CliTools/Console/Command/AbstractCommand.php b/src/app/CliTools/Console/Command/AbstractCommand.php index 9806e45..99873e2 100644 --- a/src/app/CliTools/Console/Command/AbstractCommand.php +++ b/src/app/CliTools/Console/Command/AbstractCommand.php @@ -24,11 +24,18 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\FullSelfCommandBuilder; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\FullSelfCommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; abstract class AbstractCommand extends Command { + /** + * Message list (will be shown at the end) + * + * @var array + */ + protected $finishMessageList = array(); + /** * Input * @@ -57,8 +64,44 @@ protected function initialize(InputInterface $input, OutputInterface $output) { $this->output = $output; ConsoleUtility::initialize($input, $output); + + // Set default terminal title + $this->setTerminalTitle(explode(':', $this->getName())); } + /** + * Runs the command. + * + * The code to execute is either defined directly with the + * setCode() method or by overriding the execute() method + * in a sub-class. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + * + * @return int The command exit code + * + * @throws \Exception + * + * @see setCode() + * @see execute() + * + * @api + */ + public function run(InputInterface $input, OutputInterface $output) { + + try { + $ret = parent::run($input, $output); + $this->showFinishMessages(); + } catch (\Exception $e) { + $this->showFinishMessages(); + throw $e; + } + + return $ret; + } + + /** * Get full parameter list * @@ -99,7 +142,7 @@ protected function elevateProcess(InputInterface $input, OutputInterface $output } catch (\Exception $e) { // do not display exception here because it's a child process } - exit(0); + throw new \CliTools\Exception\StopException(0); } else { // running as root } @@ -123,12 +166,14 @@ protected function showLog($logList, $input, $output, $grep = null, $optionList // check if logfiles are accessable foreach ($logList as $log) { if (!is_readable($log)) { - $output->writeln('Can\'t read ' . $log . ''); + $output->writeln('Can\'t read ' . $log . ''); return 1; } } + $output->writeln('

Reading logfile with multitail

'); + $command = new CommandBuilder('multitail', '--follow-all'); // Add grep @@ -142,4 +187,73 @@ protected function showLog($logList, $input, $output, $grep = null, $optionList return 0; } + + /** + * Add message to finish list + * + * @param string $message Message + */ + protected function addFinishMessage($message) { + $this->output->writeln($message); + $this->finishMessageList[] = $message; + } + + /** + * Show all finish messages + */ + protected function showFinishMessages() { + + if (!empty($this->finishMessageList)) { + $this->output->writeln(''); + $this->output->writeln('Replay finish message log:'); + + foreach ($this->finishMessageList as $message) { + $this->output->writeln(' - ' . $message); + } + } + + $this->finishMessageList = array(); + } + + /** + * Gets the application instance for this command. + * + * @return \CliTools\Console\Application An Application instance + * + * @api + */ + public function getApplication() { + return parent::getApplication(); + } + + /** + * Sets the terminal title of the command. + * + * This feature should be used only when creating a long process command, + * like a daemon. + * + * PHP 5.5+ or the proctitle PECL library is required + * + * @param string $title The terminal title + * + * @return Command The current instance + */ + public function setTerminalTitle($title) { + $args = func_get_args(); + + $titleList = array(); + foreach($args as $value) { + if (is_array($value)) { + $value = implode(' ', $value); + } + + $titleList[] = trim($value); + } + + $title = implode(' ', $titleList); + $title = trim($title); + + $this->getApplication()->setTerminalTitle($title); + return $this; + } } diff --git a/src/app/CliTools/Console/Command/AbstractTraceCommand.php b/src/app/CliTools/Console/Command/AbstractTraceCommand.php index fc545b4..13162f4 100644 --- a/src/app/CliTools/Console/Command/AbstractTraceCommand.php +++ b/src/app/CliTools/Console/Command/AbstractTraceCommand.php @@ -20,7 +20,7 @@ * along with this program. If not, see . */ -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -90,21 +90,28 @@ public function execute(InputInterface $input, OutputInterface $output) { $command->setOutputRedirect(CommandBuilder::OUTPUT_REDIRECT_ALL_STDOUT); + $output->writeln('

Starting process stracing

'); + if (empty($pid)) { list($pidList, $processList) = $this->buildProcessList(); if ($input->getOption('all')) { $pid = 'all'; } else { - $question = new ChoiceQuestion('Please choose process for tracing', $processList); + try { + $question = new ChoiceQuestion('Please choose process for tracing', $processList); + $question->setMaxAttempts(1); - $questionDialog = new QuestionHelper(); + $questionDialog = new QuestionHelper(); - $pid = $questionDialog->ask($input, $output, $question); + $pid = $questionDialog->ask($input, $output, $question); + } catch(\InvalidArgumentException $e) { + // Invalid value, just stop here + throw new \CliTools\Exception\StopException(1); + } } } - if (!empty($pid)) { switch ($pid) { case 'all': @@ -157,7 +164,7 @@ protected function buildProcessList() { $currentPid = posix_getpid(); $processList = array( - 'all processes' => 'all', + 'all' => 'all processes', ); $command = new CommandBuilder('ps'); @@ -183,8 +190,8 @@ protected function buildProcessList() { continue; } - $pidList[] = (int)$pid; - $processList[$cmd] = (int)$pid; + $pidList[] = (int)$pid; + $processList[(int)$pid] = $cmd; } return array($pidList, $processList); diff --git a/src/app/CliTools/Console/Command/Apache/RestartCommand.php b/src/app/CliTools/Console/Command/Apache/RestartCommand.php index ec1ee82..4dd389c 100644 --- a/src/app/CliTools/Console/Command/Apache/RestartCommand.php +++ b/src/app/CliTools/Console/Command/Apache/RestartCommand.php @@ -22,7 +22,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; class RestartCommand extends \CliTools\Console\Command\AbstractCommand { @@ -30,7 +30,8 @@ class RestartCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('apache:restart') + $this + ->setName('apache:restart') ->setDescription('Restart Apache'); } diff --git a/src/app/CliTools/Console/Command/Apache/TraceCommand.php b/src/app/CliTools/Console/Command/Apache/TraceCommand.php index 4faf2e8..2330425 100644 --- a/src/app/CliTools/Console/Command/Apache/TraceCommand.php +++ b/src/app/CliTools/Console/Command/Apache/TraceCommand.php @@ -33,8 +33,10 @@ class TraceCommand extends \CliTools\Console\Command\AbstractTraceCommand { * Configure command */ protected function configure() { - $this->setName('apache:trace') + $this + ->setName('apache:trace') ->setDescription('Debug Apache processes with strace'); + parent::configure(); } diff --git a/src/app/CliTools/Console/Command/Docker/UpgradeCommand.php b/src/app/CliTools/Console/Command/Common/MakeCommand.php similarity index 52% rename from src/app/CliTools/Console/Command/Docker/UpgradeCommand.php rename to src/app/CliTools/Console/Command/Common/MakeCommand.php index 612f3e5..c21c5fd 100644 --- a/src/app/CliTools/Console/Command/Docker/UpgradeCommand.php +++ b/src/app/CliTools/Console/Command/Common/MakeCommand.php @@ -1,6 +1,6 @@ . */ +use CliTools\Utility\UnixUtility; +use CliTools\Utility\PhpUtility; +use CliTools\Shell\CommandBuilder\CommandBuilder; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\CommandBuilder; -class UpgradeCommand extends AbstractCommand { +class MakeCommand extends \CliTools\Console\Command\AbstractCommand implements \CliTools\Console\Filter\AnyParameterFilterInterface { /** * Configure command */ protected function configure() { - $this->setName('docker:upgrade') - ->setDescription('Upgrade docker version'); + $this + ->setName('make') + ->setDescription('Search Makefile updir and start makefile'); } /** @@ -43,11 +46,28 @@ protected function configure() { * @return int|null|void */ public function execute(InputInterface $input, OutputInterface $output) { - $this->elevateProcess($input, $output); + $paramList = $this->getFullParameterList(); + $path = UnixUtility::findFileInDirectortyTree('Makefile'); - $command = new CommandBuilder('wget', '-qO- %s', array('https://get.docker.com/')); - $command->addPipeCommand(new CommandBuilder('sh')); - $command->executeInteractive(); + if (!empty($path)) { + $path = dirname($path); + $this->output->writeln('Found Makefile directory: ' . $path . ''); + + // Switch to directory of docker-compose.yml + PhpUtility::chdir($path); + + $command = new CommandBuilder('make'); + + if (!empty($paramList)) { + $command->setArgumentList($paramList); + } + + $command->executeInteractive(); + } else { + $this->output->writeln('No Makefile found in tree'); + + return 1; + } return 0; } diff --git a/src/app/CliTools/Console/Command/Common/SelfUpdateCommand.php b/src/app/CliTools/Console/Command/Common/SelfUpdateCommand.php index 2368acb..8daf646 100644 --- a/src/app/CliTools/Console/Command/Common/SelfUpdateCommand.php +++ b/src/app/CliTools/Console/Command/Common/SelfUpdateCommand.php @@ -21,6 +21,7 @@ */ use CliTools\Service\SelfUpdateService; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -30,9 +31,28 @@ class SelfUpdateCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('self-update') + $this + ->setName('self-update') ->setAliases(array('selfupdate')) - ->setDescription('Self update of CliTools Command'); + ->setDescription('Self update of CliTools Command') + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Force update' + ) + ->addOption( + 'beta', + null, + InputOption::VALUE_NONE, + 'Allow update to beta releases' + ) + ->addOption( + 'fallback', + null, + InputOption::VALUE_NONE, + 'Fallback to old update url' + ); } /** @@ -44,8 +64,18 @@ protected function configure() { * @return int|null|void */ public function execute(InputInterface $input, OutputInterface $output) { + $force = (bool)$input->getOption('force'); + $updateService = new SelfUpdateService($this->getApplication(), $output); + if ($input->getOption('beta')) { + $updateService->enablePreVersions(); + } + + if ($input->getOption('fallback')) { + $updateService->enableUpdateFallback(); + } + // Check if we need root rights if (!$this->getApplication()->isRunningAsRoot() && $updateService->isElevationNeeded()) @@ -53,10 +83,6 @@ public function execute(InputInterface $input, OutputInterface $output) { $this->elevateProcess($input, $output); } - $updateService->update(); - - - - + $updateService->update($force); } } diff --git a/src/app/CliTools/Console/Command/Docker/AbstractCommand.php b/src/app/CliTools/Console/Command/Docker/AbstractCommand.php index 6e70126..1cf5d23 100644 --- a/src/app/CliTools/Console/Command/Docker/AbstractCommand.php +++ b/src/app/CliTools/Console/Command/Docker/AbstractCommand.php @@ -20,8 +20,8 @@ * along with this program. If not, see . */ -use CliTools\Console\Builder\CommandBuilder; -use CliTools\Console\Builder\CommandBuilderInterface; +use CliTools\Shell\CommandBuilder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilderInterface; use CliTools\Utility\PhpUtility; abstract class AbstractCommand extends \CliTools\Console\Command\AbstractCommand { @@ -40,8 +40,12 @@ abstract class AbstractCommand extends \CliTools\Console\Command\AbstractCommand */ protected function getDockerPath() { if ($this->dockerPath === null) { - $this->dockerPath = \CliTools\Utility\DockerUtility::searchDockerDirectoryRecursive(); - $this->output->writeln('Found docker directory: ' . $this->dockerPath . ''); + $composePath = \CliTools\Utility\DockerUtility::searchDockerDirectoryRecursive(); + + if (!empty($composePath)) { + $this->dockerPath = dirname($composePath); + $this->output->writeln('Found docker directory: ' . $this->dockerPath . ''); + } } return $this->dockerPath; @@ -59,22 +63,26 @@ protected function getDockerEnv($containerName, $envName) { $ret = null; if (empty($containerName)) { - $this->output->writeln('No container specified'); + $this->output->writeln('No container specified'); return false; } if (empty($envName)) { - $this->output->writeln('No environment name specified'); + $this->output->writeln('No environment name specified'); return false; } + // Search updir for docker-compose.yml $path = $this->getDockerPath(); if (!empty($path)) { + // Genrate full docker container name $dockerContainerName = \CliTools\Utility\DockerUtility::getDockerInstanceName($containerName, 1, $path); + // Switch to directory of docker-compose.yml PhpUtility::chdir($path); + // Get docker confguration (fetched directly from docker) $conf = \CliTools\Utility\DockerUtility::getDockerConfiguration($dockerContainerName); if (empty($conf)) { @@ -99,20 +107,23 @@ protected function getDockerEnv($containerName, $envName) { */ protected function executeDockerExec($containerName, CommandBuilderInterface $command) { if (empty($containerName)) { - $this->output->writeln('No container specified'); + $this->output->writeln('No container specified'); return 1; } if (!$command->isExecuteable()) { - $this->output->writeln('No command specified or not executeable'); + $this->output->writeln('No command specified or not executeable'); return 1; } + // Search updir for docker-compose.yml $path = $this->getDockerPath(); if (!empty($path)) { + // Genrate full docker container name $dockerContainerName = \CliTools\Utility\DockerUtility::getDockerInstanceName($containerName, 1, $path); + // Switch to directory of docker-compose.yml PhpUtility::chdir($path); $this->output->writeln('Executing "' . $command->getCommand() . '" in docker container "' . $dockerContainerName . '" ...'); @@ -121,7 +132,7 @@ protected function executeDockerExec($containerName, CommandBuilderInterface $co $dockerCommand->append($command, false); $dockerCommand->executeInteractive(); } else { - $this->output->writeln('No docker-compose.yml found in tree'); + $this->output->writeln('No docker-compose.yml found in tree'); return 1; } @@ -137,16 +148,20 @@ protected function executeDockerExec($containerName, CommandBuilderInterface $co * @return int|null|void */ protected function executeDockerCompose(CommandBuilderInterface $command = null) { + // Search updir for docker-compose.yml $path = \CliTools\Utility\DockerUtility::searchDockerDirectoryRecursive(); if (!empty($path)) { + $path = dirname($path); $this->output->writeln('Found docker directory: ' . $path . ''); + + // Switch to directory of docker-compose.yml PhpUtility::chdir($path); $command->setCommand('docker-compose'); $command->executeInteractive(); } else { - $this->output->writeln('No docker-compose.yml found in tree'); + $this->output->writeln('No docker-compose.yml found in tree'); return 1; } @@ -163,9 +178,11 @@ protected function executeDockerCompose(CommandBuilderInterface $command = null) * @return int|null|void */ protected function executeDockerComposeRun($containerName, CommandBuilderInterface $command) { + // Search updir for docker-compose.yml $path = $this->getDockerPath(); if (!empty($path)) { + // Switch to directory of docker-compose.yml PhpUtility::chdir($path); $this->output->writeln('Executing "' . $command->getCommand() . '" in docker container "' . $containerName . '" ...'); @@ -174,7 +191,7 @@ protected function executeDockerComposeRun($containerName, CommandBuilderInterfa $dockerCommand->append($command, false); $dockerCommand->executeInteractive(); } else { - $this->output->writeln('No docker-compose.yml found in tree'); + $this->output->writeln('No docker-compose.yml found in tree'); return 1; } diff --git a/src/app/CliTools/Console/Command/Docker/CliCommand.php b/src/app/CliTools/Console/Command/Docker/CliCommand.php index 79f455b..38f1844 100644 --- a/src/app/CliTools/Console/Command/Docker/CliCommand.php +++ b/src/app/CliTools/Console/Command/Docker/CliCommand.php @@ -20,9 +20,9 @@ * along with this program. If not, see . */ +use CliTools\Shell\CommandBuilder\RemoteCommandBuilder; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\RemoteCommandBuilder; class CliCommand extends AbstractCommand implements \CliTools\Console\Filter\AnyParameterFilterInterface { @@ -30,8 +30,9 @@ class CliCommand extends AbstractCommand implements \CliTools\Console\Filter\Any * Configure command */ protected function configure() { - $this->setName('docker:cli') - ->setDescription('Run cli command in docker container (defined by CLI_SCRIPT and CLI_USER as docker environment variable)'); + $this + ->setName('docker:cli') + ->setDescription('Run cli command in docker container (defined by CLI_SCRIPT and CLI_USER as docker environment variable)'); } /** @@ -58,7 +59,7 @@ public function execute(InputInterface $input, OutputInterface $output) { $cliUser = $this->getDockerEnv($container, 'CLI_USER'); if (empty($cliScript)) { - $output->writeln('Docker container "' . $container . '" doesn\'t have environment variable "CLI_SCRIPT"'); + $output->writeln('Docker container "' . $container . '" doesn\'t have environment variable "CLI_SCRIPT"'); return 1; } @@ -88,7 +89,7 @@ public function execute(InputInterface $input, OutputInterface $output) { break; default: - $output->writeln('CliMethod "' . $cliMethod .'" not defined'); + $output->writeln('CliMethod "' . $cliMethod .'" not defined'); $ret = 1; break; } diff --git a/src/app/CliTools/Console/Command/Docker/ComposeCommand.php b/src/app/CliTools/Console/Command/Docker/ComposeCommand.php index 6f9c88b..9d1d716 100644 --- a/src/app/CliTools/Console/Command/Docker/ComposeCommand.php +++ b/src/app/CliTools/Console/Command/Docker/ComposeCommand.php @@ -22,7 +22,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; class ComposeCommand extends AbstractCommand implements \CliTools\Console\Filter\AnyParameterFilterInterface { @@ -30,8 +30,9 @@ class ComposeCommand extends AbstractCommand implements \CliTools\Console\Filter * Configure command */ protected function configure() { - $this->setName('docker:compose') - ->setDescription('Run general docker-compose command in docker container'); + $this + ->setName('docker:compose') + ->setDescription('Run general docker-compose command in docker container'); } /** @@ -51,6 +52,8 @@ public function execute(InputInterface $input, OutputInterface $output) { $command->setArgumentList($paramList); } + $this->setTerminalTitle('docker-compose', $paramList); + $ret = $this->executeDockerCompose($command); return $ret; diff --git a/src/app/CliTools/Console/Command/Docker/CreateCommand.php b/src/app/CliTools/Console/Command/Docker/CreateCommand.php index aee7af4..0dc5757 100644 --- a/src/app/CliTools/Console/Command/Docker/CreateCommand.php +++ b/src/app/CliTools/Console/Command/Docker/CreateCommand.php @@ -20,9 +20,10 @@ * along with this program. If not, see . */ -use CliTools\Console\Builder\CommandBuilder; -use CliTools\Console\Builder\SelfCommandBuilder; use CliTools\Utility\PhpUtility; +use CliTools\Shell\CommandBuilder\CommandBuilder; +use CliTools\Shell\CommandBuilder\SelfCommandBuilder; +use CliTools\Shell\CommandBuilder\EditorCommandBuilder; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; @@ -34,7 +35,8 @@ class CreateCommand extends AbstractCommand { * Configure command */ protected function configure() { - $this->setName('docker:create') + $this + ->setName('docker:create') ->setDescription('Create new docker boilerplate') ->addArgument( 'path', @@ -52,6 +54,12 @@ protected function configure() { 'c', InputOption::VALUE_REQUIRED, 'Code repository' + ) + ->addOption( + 'make', + 'm', + InputOption::VALUE_REQUIRED, + 'Makefile command' ); } @@ -64,6 +72,8 @@ protected function configure() { * @return int|null|void */ public function execute(InputInterface $input, OutputInterface $output) { + $currDir = getcwd(); + $path = $input->getArgument('path'); if ($this->input->getOption('docker')) { @@ -74,20 +84,73 @@ public function execute(InputInterface $input, OutputInterface $output) { $boilerplateRepo = $this->getApplication()->getConfigValue('docker', 'boilerplate'); } + $output->writeln('

Creating new docker boilerplate instance in "' . $path . '"

'); + // Init docker boilerplate $this->createDockerInstance($path, $boilerplateRepo); + PhpUtility::chdir($currDir); // Init code if ($this->input->getOption('code')) { + + $output->writeln('

Init code repository

'); $this->initCode($path, $input->getOption('code')); + PhpUtility::chdir($currDir); + + $output->writeln('

Init document root

'); + // detect document root + $this->initDocumentRoot($path); + PhpUtility::chdir($currDir); + + // Run makefile + if ($this->input->getOption('make')) { + try { + $output->writeln('

Run Makefile

'); + $this->runMakefile($path, $input->getOption('make')); + PhpUtility::chdir($currDir); + } catch (\Exception $e) { + $this->addFinishMessage('Make command failed: ' . $e->getMessage() . ''); + } + } } + // Start interactive editor + $this->startInteractiveEditor($path . '/docker-compose.yml'); + $this->startInteractiveEditor($path . '/docker-env.yml'); + // Start docker + $output->writeln('

Build and start docker containers

'); + PhpUtility::chdir($currDir); $this->startDockerInstance($path); return 0; } + /** + * Start interactive editor + * + * @param string $path Path to file + */ + protected function startInteractiveEditor($path) { + if (file_exists($path)) { + // Start editor with file (if $EDITOR is set) + try { + $editor = new EditorCommandBuilder(); + + $this->setTerminalTitle('Edit', basename($path)); + + $this->output->writeln('

Starting interactive EDITOR for file ' .$path . '

'); + sleep(1); + + $editor + ->addArgument($path) + ->executeInteractive(); + } catch (\Exception $e) { + $this->addFinishMessage('' . $e->getMessage() . ''); + } + } + } + /** * Create docker instance from git repository * @@ -95,7 +158,7 @@ public function execute(InputInterface $input, OutputInterface $output) { * @param string $repo Repository */ protected function createDockerInstance($path, $repo) { - $this->output->writeln('Create new docker boilerplate in "' . $path . '"'); + $this->setTerminalTitle('Cloning docker'); $command = new CommandBuilder('git','clone --branch=master --recursive %s %s', array($repo, $path)); $command->executeInteractive(); @@ -108,6 +171,8 @@ protected function createDockerInstance($path, $repo) { * @param string $repo Repository */ protected function initCode($path, $repo) { + $this->setTerminalTitle('Cloning code'); + $path .= '/code'; $this->output->writeln('Initialize new code instance in "' . $path . '"'); @@ -120,7 +185,8 @@ protected function initCode($path, $repo) { // Remove code directory $command = new CommandBuilder('rmdir'); - $command->addArgumentSeparator() + $command + ->addArgumentSeparator() ->addArgument($path) ->executeInteractive(); } @@ -129,12 +195,77 @@ protected function initCode($path, $repo) { $command->executeInteractive(); } + + /** + * Create docker instance from git repository + * + * @param string $path Path + */ + protected function initDocumentRoot($path) { + $codePath = $path . '/code'; + $dockerEnvFile = $path . '/docker-env.yml'; + + $documentRoot = null; + + // try to detect document root + if (is_dir($codePath . '/html')) { + $documentRoot = 'code/html'; + } elseif (is_dir($codePath . '/htdocs')) { + $documentRoot = 'code/htdocs'; + } elseif (is_dir($codePath . '/Web')) { + $documentRoot = 'code/Web'; + } elseif (is_dir($codePath . '/web')) { + $documentRoot = 'code/web'; + } + + if ($documentRoot && is_file($dockerEnvFile) ) { + $dockerEnv = PhpUtility::fileGetContentsArray($dockerEnvFile); + + unset($line); + foreach ($dockerEnv as &$line) { + $line = preg_replace('/^[\s]*DOCUMENT_ROOT[\s]*=code\/?[\s]*$/ms', 'DOCUMENT_ROOT=' . $documentRoot, $line); + } + unset($line); + + $dockerEnv = implode("\n", $dockerEnv); + + PhpUtility::filePutContents($dockerEnvFile, $dockerEnv); + } + } + + /** + * Run make task + * + * @param string $path Path of code + * @param string $makeCommand Makefile command + */ + protected function runMakefile($path, $makeCommand) { + $this->setTerminalTitle('Run make'); + + $path .= '/code'; + + $this->output->writeln('Running make with command "' . $makeCommand . '"'); + try { + PhpUtility::chdir($path); + + // Remove code directory + $command = new CommandBuilder('make'); + $command + ->addArgument($makeCommand) + ->executeInteractive(); + } catch (\Exception $e) { + $this->addFinishMessage('Make command failed: ' . $e->getMessage() . ''); + } + } + /** * Build and startup docker instance * * @param string $path Path */ protected function startDockerInstance($path) { + $this->setTerminalTitle('Start docker'); + $this->output->writeln('Building docker containers "' . $path . '"'); PhpUtility::chdir($path); @@ -143,4 +274,5 @@ protected function startDockerInstance($path) { $command->addArgument('docker:up'); $command->executeInteractive(); } + } diff --git a/src/app/CliTools/Console/Command/Docker/ExecCommand.php b/src/app/CliTools/Console/Command/Docker/ExecCommand.php index f6547f6..19a7a1f 100644 --- a/src/app/CliTools/Console/Command/Docker/ExecCommand.php +++ b/src/app/CliTools/Console/Command/Docker/ExecCommand.php @@ -22,7 +22,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\RemoteCommandBuilder; +use CliTools\Shell\CommandBuilder\RemoteCommandBuilder; class ExecCommand extends AbstractCommand implements \CliTools\Console\Filter\AnyParameterFilterInterface { @@ -30,7 +30,8 @@ class ExecCommand extends AbstractCommand implements \CliTools\Console\Filter\An * Configure command */ protected function configure() { - $this->setName('docker:exec') + $this + ->setName('docker:exec') ->setDescription('Run defined command in docker container'); } @@ -46,6 +47,7 @@ public function execute(InputInterface $input, OutputInterface $output) { $paramList = $this->getFullParameterList(); $container = $this->getApplication()->getConfigValue('docker', 'container'); + if (!empty($paramList)) { $firstParam = array_shift($paramList); @@ -53,7 +55,7 @@ public function execute(InputInterface $input, OutputInterface $output) { $ret = $this->executeDockerExec($container, $command); } else { - $output->writeln('No command/parameter specified'); + $output->writeln('No command/parameter specified'); $ret = 1; } diff --git a/src/app/CliTools/Console/Command/Docker/IftopCommand.php b/src/app/CliTools/Console/Command/Docker/IftopCommand.php index 0c260db..6c9da42 100644 --- a/src/app/CliTools/Console/Command/Docker/IftopCommand.php +++ b/src/app/CliTools/Console/Command/Docker/IftopCommand.php @@ -22,7 +22,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; class IftopCommand extends \CliTools\Console\Command\AbstractCommand { @@ -30,8 +30,9 @@ class IftopCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('docker:iftop') - ->setDescription('Exec iftop for Docker'); + $this + ->setName('docker:iftop') + ->setDescription('Exec iftop for Docker'); } /** diff --git a/src/app/CliTools/Console/Command/Docker/MysqlCommand.php b/src/app/CliTools/Console/Command/Docker/MysqlCommand.php index 4ba4fcf..85fe6e0 100644 --- a/src/app/CliTools/Console/Command/Docker/MysqlCommand.php +++ b/src/app/CliTools/Console/Command/Docker/MysqlCommand.php @@ -22,7 +22,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\RemoteCommandBuilder; +use CliTools\Shell\CommandBuilder\RemoteCommandBuilder; class MysqlCommand extends AbstractCommand { @@ -30,7 +30,8 @@ class MysqlCommand extends AbstractCommand { * Configure command */ protected function configure() { - $this->setName('docker:mysql') + $this + ->setName('docker:mysql') ->setDescription('Enter mysql in docker container'); } diff --git a/src/app/CliTools/Console/Command/Docker/RootCommand.php b/src/app/CliTools/Console/Command/Docker/RootCommand.php index a6ab553..de8fdf8 100644 --- a/src/app/CliTools/Console/Command/Docker/RootCommand.php +++ b/src/app/CliTools/Console/Command/Docker/RootCommand.php @@ -23,7 +23,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\RemoteCommandBuilder; +use CliTools\Shell\CommandBuilder\RemoteCommandBuilder; class RootCommand extends AbstractCommand { @@ -31,7 +31,8 @@ class RootCommand extends AbstractCommand { * Configure command */ protected function configure() { - $this->setName('docker:root') + $this + ->setName('docker:root') ->setDescription('Enter shell as root in docker container') ->addArgument( 'container', @@ -55,6 +56,8 @@ public function execute(InputInterface $input, OutputInterface $output) { $container = $input->getArgument('container'); } + $this->setTerminalTitle('docker', 'root', $container); + $command = new RemoteCommandBuilder('bash'); $ret = $this->executeDockerExec($container, $command); diff --git a/src/app/CliTools/Console/Command/Docker/ShellCommand.php b/src/app/CliTools/Console/Command/Docker/ShellCommand.php index 73c6963..575285c 100644 --- a/src/app/CliTools/Console/Command/Docker/ShellCommand.php +++ b/src/app/CliTools/Console/Command/Docker/ShellCommand.php @@ -24,7 +24,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\RemoteCommandBuilder; +use CliTools\Shell\CommandBuilder\RemoteCommandBuilder; class ShellCommand extends AbstractCommand { @@ -32,7 +32,8 @@ class ShellCommand extends AbstractCommand { * Configure command */ protected function configure() { - $this->setName('docker:shell') + $this + ->setName('docker:shell') ->setDescription('Enter shell in docker container') ->addArgument( 'container', @@ -70,6 +71,8 @@ public function execute(InputInterface $input, OutputInterface $output) { $cliUser = $this->getDockerEnv($container, 'CLI_USER'); } + $this->setTerminalTitle('docker', 'shell', $container); + $command = new RemoteCommandBuilder('bash'); if (!empty($cliUser)) { diff --git a/src/app/CliTools/Console/Command/Docker/SniffCommand.php b/src/app/CliTools/Console/Command/Docker/SniffCommand.php index d297f42..81129c4 100644 --- a/src/app/CliTools/Console/Command/Docker/SniffCommand.php +++ b/src/app/CliTools/Console/Command/Docker/SniffCommand.php @@ -21,10 +21,11 @@ */ use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; class SniffCommand extends AbstractCommand { @@ -32,18 +33,13 @@ class SniffCommand extends AbstractCommand { * Configure command */ protected function configure() { - $this->setName('docker:sniff') + $this + ->setName('docker:sniff') ->setDescription('Start network sniffing with docker') ->addArgument( 'protocol', - InputArgument::REQUIRED, + InputArgument::OPTIONAL, 'Protocol' - ) - ->addOption( - 'full', - null, - InputOption::VALUE_NONE, - 'Show full output (if supported by protocol)' ); } @@ -60,8 +56,9 @@ public function execute(InputInterface $input, OutputInterface $output) { $dockerInterface = $this->getApplication()->getConfigValue('docker', 'interface'); - $protocol = $input->getArgument('protocol'); - $fullOutput = $input->getOption('full'); + $output->writeln('

Starting network sniffing

'); + + $protocol = $this->getProtocol(); $command = new CommandBuilder(); @@ -74,6 +71,7 @@ public function execute(InputInterface $input, OutputInterface $output) { // ARP // ############## case 'arp': + $output->writeln('

Using protocol "arp"

'); $command->setCommand('tshark'); $command->addArgument('arp'); break; @@ -86,6 +84,7 @@ public function execute(InputInterface $input, OutputInterface $output) { // ICMP // ############## case 'icmp': + $output->writeln('

Using protocol "icmp"

'); $command->setCommand('tshark'); $command->addArgument('icmp'); break; @@ -99,6 +98,7 @@ public function execute(InputInterface $input, OutputInterface $output) { // ############## case 'con': case 'tcp': + $output->writeln('

Using protocol "tcp"

'); $command->setCommand('tshark'); $command->addArgumentRaw('-R "tcp.flags.syn==1 && tcp.flags.ack==0"'); break; @@ -111,19 +111,25 @@ public function execute(InputInterface $input, OutputInterface $output) { // HTTP // ############## case 'http': + $output->writeln('

Using protocol "http"

'); $command->setCommand('tshark'); + $command->addArgumentRaw('tcp port 80 or tcp port 443 -2 -V -R "http.request" -Tfields -e ip.dst -e http.request.method -e http.request.full_uri'); + break; - if ($fullOutput) { - $command->addArgumentRaw('tcp port 80 or tcp port 443 -2 -V -R "http.request || http.response"'); - } else { - $command->addArgumentRaw('tcp port 80 or tcp port 443 -2 -V -R "http.request" -Tfields -e ip.dst -e http.request.method -e http.request.full_uri'); - } + // ############## + // HTTP (full) + // ############## + case 'http-full': + $output->writeln('

Using protocol "http" (full mode)

'); + $command->setCommand('tshark'); + $command->addArgumentRaw('tcp port 80 or tcp port 443 -2 -V -R "http.request || http.response"'); break; // ############## // SOLR // ############## case 'solr': + $output->writeln('

Using protocol "solr"

'); $command->setCommand('tcpdump'); $command->addArgumentRaw('-nl -s0 -w- port 8983'); @@ -136,6 +142,7 @@ public function execute(InputInterface $input, OutputInterface $output) { // ELASTICSEARCH // ############## case 'elasticsearch': + $output->writeln('

Using protocol "elasticsearch"

'); $command->setCommand('tcpdump'); $command->addArgumentRaw('-A -nn -s 0 \'tcp dst port 9200 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)\''); break; @@ -145,6 +152,7 @@ public function execute(InputInterface $input, OutputInterface $output) { // ############## case 'memcache': case 'memcached': + $output->writeln('

Using protocol "memcache"

'); $command->setCommand('tcpdump'); $command->addArgumentRaw('-s 65535 -A -ttt port 11211| cut -c 9- | grep -i \'^get\|set\''); break; @@ -153,6 +161,7 @@ public function execute(InputInterface $input, OutputInterface $output) { // REDIS // ############## case 'redis': + $output->writeln('

Using protocol "redis"

'); $command->setCommand('tcpdump'); $command->addArgumentRaw('-s 65535 tcp port 6379'); break; @@ -162,6 +171,7 @@ public function execute(InputInterface $input, OutputInterface $output) { // ############## case 'smtp': case 'mail': + $output->writeln('

Using protocol "smtp"

'); $command->setCommand('tshark'); $command->addArgumentRaw('tcp -f "port 25" -R "smtp"'); break; @@ -170,6 +180,7 @@ public function execute(InputInterface $input, OutputInterface $output) { // MYSQL // ############## case 'mysql': + $output->writeln('

Using protocol "mysql"

'); $command->setCommand('tshark'); $command->addArgumentRaw('tcp -d tcp.port==3306,mysql -T fields -e mysql.query "port 3306"'); break; @@ -178,6 +189,7 @@ public function execute(InputInterface $input, OutputInterface $output) { // DNS // ############## case 'dns': + $output->writeln('

Using protocol "dns"

'); $command->setCommand('tshark'); $command->addArgumentRaw('-nn -e ip.src -e dns.qry.name -E separator=" " -T fields port 53'); break; @@ -186,31 +198,80 @@ public function execute(InputInterface $input, OutputInterface $output) { // HELP // ############## default: - $output->writeln('Protocol not supported:'); - $output->writeln(' OSI layer 7: http, solr, elasticsearch, memcache, redis, smtp, mysql, dns'); - $output->writeln(' OSI layer 4: tcp'); - $output->writeln(' OSI layer 3: icmp'); - $output->writeln(' OSI layer 2: arp'); + $output->writeln('Protocol not supported:'); + $output->writeln(' OSI layer 7: http, solr, elasticsearch, memcache, redis, smtp, mysql, dns'); + $output->writeln(' OSI layer 4: tcp'); + $output->writeln(' OSI layer 3: icmp'); + $output->writeln(' OSI layer 2: arp'); return 1; break; } switch ($command->getCommand()) { case 'tshark': + $output->writeln('

Using sniffer "tshark"

'); $command->addArgumentTemplate('-i %s', $dockerInterface); break; case 'tcpdump': + $output->writeln('

Using sniffer "tcpdump"

'); $command->addArgumentTemplate('-i %s', $dockerInterface); break; case 'ngrep': + $output->writeln('

Using sniffer "ngrep"

'); $command->addArgumentTemplate('-d %s', $dockerInterface); break; } + $this->setTerminalTitle('sniffer', $protocol, '(' . $command->getCommand() .')'); + $command->executeInteractive(); return 0; } + + + /** + * Get protocol + * + * @return string + */ + protected function getProtocol() { + $ret = null; + + if(!$this->input->getArgument('protocol')) { + $protocolList = array( + 'http' => 'HTTP (requests only)', + 'http-full' => 'HTTP (full)', + 'solr' => 'Solr', + 'elasticsearch' => 'Elasticsearch', + 'memcache' => 'Memcache', + 'redis' => 'Redis', + 'smtp' => 'SMTP', + 'mysql' => 'MySQL queries', + 'dns' => 'DNS', + 'tcp' => 'TCP', + 'icmp' => 'ICMP', + 'arp' => 'ARP', + ); + + try { + $question = new ChoiceQuestion('Please choose network protocol for sniffing', $protocolList); + $question->setMaxAttempts(1); + + $questionDialog = new QuestionHelper(); + + $ret = $questionDialog->ask($this->input, $this->output, $question); + } catch(\InvalidArgumentException $e) { + // Invalid server context, just stop here + throw new \CliTools\Exception\StopException(1); + } + } else { + $ret = $this->input->getArgument('protocol'); + } + + + return $ret; + } } diff --git a/src/app/CliTools/Console/Command/Docker/UpCommand.php b/src/app/CliTools/Console/Command/Docker/UpCommand.php index 893ab59..b558adb 100644 --- a/src/app/CliTools/Console/Command/Docker/UpCommand.php +++ b/src/app/CliTools/Console/Command/Docker/UpCommand.php @@ -22,7 +22,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; class UpCommand extends AbstractCommand { @@ -30,7 +30,8 @@ class UpCommand extends AbstractCommand { * Configure command */ protected function configure() { - $this->setName('docker:up') + $this + ->setName('docker:up') ->setDescription('Start docker container (with fast switching)'); } @@ -44,9 +45,15 @@ protected function configure() { */ public function execute(InputInterface $input, OutputInterface $output) { - $dockerPath = \CliTools\Utility\DockerUtility::searchDockerDirectoryRecursive(); + $dockerPath = \CliTools\Utility\DockerUtility::searchDockerDirectoryRecursive(); $lastDockerPath = $this->getApplication()->getSettingsService()->get('docker.up.last'); + if (!empty($dockerPath)) { + $dockerPath = dirname($dockerPath); + } + + $output->writeln('

Starting docker containers

'); + // Stop last docker instance if ($dockerPath && $lastDockerPath) { // Only stop if instance is another one @@ -56,6 +63,7 @@ public function execute(InputInterface $input, OutputInterface $output) { } // Start current docker containers + $this->output->writeln('

Start docker containers in "' . $dockerPath . '"

'); $command = new CommandBuilder(null, 'up -d'); $ret = $this->executeDockerCompose($command); @@ -76,7 +84,7 @@ protected function stopContainersFromPrevRun($path) { $currentPath = getcwd(); try { - $this->output->writeln('Trying to stop last running docker container in "' . $path . '"'); + $this->output->writeln('

Trying to stop last running docker container in "' . $path . '"

'); // Jump into last docker dir \CliTools\Utility\PhpUtility::chdir($path); diff --git a/src/app/CliTools/Console/Command/Log/ApacheCommand.php b/src/app/CliTools/Console/Command/Log/ApacheCommand.php index cf4afa8..55aba20 100644 --- a/src/app/CliTools/Console/Command/Log/ApacheCommand.php +++ b/src/app/CliTools/Console/Command/Log/ApacheCommand.php @@ -30,7 +30,8 @@ class ApacheCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('log:apache') + $this + ->setName('log:apache') ->setAliases(array('apache:log')) ->setDescription('Show up apache log') ->addArgument( @@ -55,6 +56,7 @@ public function execute(InputInterface $input, OutputInterface $output) { $grep = $input->getArgument('grep'); } + $output->writeln('

Starting apache log tail

'); // Show log $logList = array( diff --git a/src/app/CliTools/Console/Command/Log/DebugCommand.php b/src/app/CliTools/Console/Command/Log/DebugCommand.php index 20f4aeb..67ed98a 100644 --- a/src/app/CliTools/Console/Command/Log/DebugCommand.php +++ b/src/app/CliTools/Console/Command/Log/DebugCommand.php @@ -30,7 +30,8 @@ class DebugCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('log:debug') + $this + ->setName('log:debug') ->setAliases(array('debug')) ->setDescription('Show up debugging log') ->addArgument( @@ -55,6 +56,7 @@ public function execute(InputInterface $input, OutputInterface $output) { $grep = $input->getArgument('grep'); } + $output->writeln('

Starting debug log tail

'); // Show log $logList = array( diff --git a/src/app/CliTools/Console/Command/Log/MailCommand.php b/src/app/CliTools/Console/Command/Log/MailCommand.php index 31b699f..778c8d8 100644 --- a/src/app/CliTools/Console/Command/Log/MailCommand.php +++ b/src/app/CliTools/Console/Command/Log/MailCommand.php @@ -30,7 +30,8 @@ class MailCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('log:mail') + $this + ->setName('log:mail') ->setDescription('Show up mail log') ->addArgument( 'grep', @@ -54,6 +55,7 @@ public function execute(InputInterface $input, OutputInterface $output) { $grep = $input->getArgument('grep'); } + $output->writeln('

Starting mail log tail

'); // Show log $logList = array( diff --git a/src/app/CliTools/Console/Command/Log/PhpCommand.php b/src/app/CliTools/Console/Command/Log/PhpCommand.php index 48536c3..425d233 100644 --- a/src/app/CliTools/Console/Command/Log/PhpCommand.php +++ b/src/app/CliTools/Console/Command/Log/PhpCommand.php @@ -30,7 +30,8 @@ class PhpCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('log:php') + $this + ->setName('log:php') ->setAliases(array('php:log')) ->setDescription('Show up php log') ->addArgument( @@ -55,6 +56,8 @@ public function execute(InputInterface $input, OutputInterface $output) { $grep = $input->getArgument('grep'); } + $output->writeln('

Starting php log tail

'); + // Show log $logList = array( '/var/log/php-fpm/dev.error.log', diff --git a/src/app/CliTools/Console/Command/Mysql/AbstractCommand.php b/src/app/CliTools/Console/Command/Mysql/AbstractCommand.php new file mode 100644 index 0000000..3389d8b --- /dev/null +++ b/src/app/CliTools/Console/Command/Mysql/AbstractCommand.php @@ -0,0 +1,100 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use CliTools\Database\DatabaseConnection; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +abstract class AbstractCommand extends \CliTools\Console\Command\AbstractCommand { + + /** + * Configure command + */ + protected function configure() { + $this + ->addOption( + 'host', + null, + InputOption::VALUE_REQUIRED, + 'MySQL host' + ) + ->addOption( + 'port', + null, + InputOption::VALUE_REQUIRED, + 'MySQL port' + ) + ->addOption( + 'user', + 'u', + InputOption::VALUE_REQUIRED, + 'MySQL user' + ) + ->addOption( + 'password', + 'p', + InputOption::VALUE_REQUIRED, + 'MySQL host' + ); + } + + /** + * Initializes the command just after the input has been validated. + * + * This is mainly useful when a lot of commands extends one main command + * where some things need to be initialized based on the input arguments and options. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + */ + protected function initialize(InputInterface $input, OutputInterface $output) { + parent::initialize($input, $output); + + $dsn = null; + $user = null; + $password = null; + + if ($this->input->hasOption('host') && $this->input->getOption('host')) { + $host = $this->input->getOption('host'); + $port = 3306; + + if ($this->input->getOption('port')) { + $port = $this->input->getOption('port'); + } + + $dsn = 'mysql:host=' . urlencode($host) . ';port=' . (int)$port; + } + + if ($this->input->hasOption('user') && $this->input->getOption('user')) { + $user = $this->input->getOption('user'); + } + + if ($this->input->hasOption('password') && $this->input->getOption('password')) { + $password = $this->input->getOption('password'); + } + + if ($user !== null || $password !== null) { + DatabaseConnection::setDsn($dsn, $user, $password); + } + } +} diff --git a/src/app/CliTools/Console/Command/Mysql/BackupCommand.php b/src/app/CliTools/Console/Command/Mysql/BackupCommand.php index 033581e..91bae94 100644 --- a/src/app/CliTools/Console/Command/Mysql/BackupCommand.php +++ b/src/app/CliTools/Console/Command/Mysql/BackupCommand.php @@ -21,31 +21,36 @@ */ use CliTools\Database\DatabaseConnection; -use CliTools\Console\Builder\CommandBuilder; -use CliTools\Console\Builder\CommandBuilderInterface; +use CliTools\Shell\CommandBuilder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilderInterface; +use CliTools\Utility\FilterUtility; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class BackupCommand extends \CliTools\Console\Command\AbstractCommand { +class BackupCommand extends AbstractCommand { /** * Configure command */ protected function configure() { - $this->setName('mysql:backup') - ->setDescription('Backup database') - ->addArgument( + parent::configure(); + + $this + ->setName('mysql:backup') + ->setDescription('Backup database') + ->addArgument( 'db', InputArgument::REQUIRED, 'Database name' - ) - ->addArgument( + ) + ->addArgument( 'file', InputArgument::REQUIRED, 'File (mysql dump)' - )->addOption( + ) + ->addOption( 'filter', 'f', InputOption::VALUE_REQUIRED, @@ -67,11 +72,13 @@ public function execute(InputInterface $input, OutputInterface $output) { $filter = $input->getOption('filter'); if (!DatabaseConnection::databaseExists($database)) { - $output->writeln('Database "' . $database . '" does not exists'); + $output->writeln('Database "' . $database . '" does not exists'); return 1; } + $output->writeln('

Dumping database "' . $database . '" into file "' . $dumpFile . '"

'); + $fileExt = pathinfo($dumpFile, PATHINFO_EXTENSION); // Inserting @@ -82,20 +89,22 @@ public function execute(InputInterface $input, OutputInterface $output) { switch ($fileExt) { case 'bz': + case 'bz2': case 'bzip2': - $output->writeln('Using BZIP2 compression'); + $output->writeln('

Using BZIP2 compression

'); $commandCompressor = new CommandBuilder('bzip2'); break; case 'gz': case 'gzip': - $output->writeln('Using GZIP compression'); + $output->writeln('

Using GZIP compression

'); $commandCompressor = new CommandBuilder('gzip'); break; case 'lzma': + case 'lz': case 'xz': - $output->writeln('Using LZMA compression'); + $output->writeln('

Using LZMA compression

'); $commandCompressor = new CommandBuilder('xz'); $commandCompressor->addArgument('--compress') ->addArgument('--stdout'); @@ -104,6 +113,15 @@ public function execute(InputInterface $input, OutputInterface $output) { $command = new CommandBuilder('mysqldump','--user=%s %s --single-transaction', array(DatabaseConnection::getDbUsername(), $database)); + // Set server connection details + if ($input->getOption('host')) { + $command->addArgumentTemplate('-h %s', $input->getOption('host')); + } + + if ($input->getOption('port')) { + $command->addArgumentTemplate('-P %s', $input->getOption('port')); + } + if (!empty($filter)) { $command = $this->addFilterArguments($command, $database, $filter); } @@ -112,13 +130,13 @@ public function execute(InputInterface $input, OutputInterface $output) { $command->addPipeCommand($commandCompressor); $commandCompressor->setOutputRedirectToFile($dumpFile); } else { - $output->writeln('Using no compression'); + $output->writeln('

Using no compression

'); $command->setOutputRedirectToFile($dumpFile); } $command->executeInteractive(); - $output->writeln('Database "' . $database . '" stored to "' . $dumpFile . '"'); + $output->writeln('

Database "' . $database . '" stored to "' . $dumpFile . '"

'); } /** @@ -140,18 +158,11 @@ protected function addFilterArguments(CommandBuilderInterface $commandDump, $dat throw new \RuntimeException('MySQL dump filters "' . $filter . '" not available"'); } + $this->output->writeln('Using filter "' . $filter . '"'); + // Get filtered tables - $tableList = DatabaseConnection::tableList($database); - - $tableListFiltered = array(); - foreach ($tableList as $table) { - foreach ($filterList as $filter) { - if (preg_match($filter, $table)) { - continue 2; - } - } - $tableListFiltered[] = $table; - } + $tableList = DatabaseConnection::tableList($database); + $ignoredTableList = FilterUtility::mysqlIgnoredTableFilter($tableList, $filterList, $database); // Dump only structure $commandStructure = clone $command; @@ -159,12 +170,14 @@ protected function addFilterArguments(CommandBuilderInterface $commandDump, $dat // Dump only data (only filtered tables) $commandData = clone $command; - $commandData - ->addArgument('--no-create-info') - ->addArgumentList($tableListFiltered); + $commandData->addArgument('--no-create-info'); + + if (!empty($ignoredTableList)) { + $commandData->addArgumentTemplateMultiple('--ignore-table=%s', $ignoredTableList); + } // Combine both commands to one - $command = new \CliTools\Console\Builder\OutputCombineCommandBuilder(); + $command = new \CliTools\Shell\CommandBuilder\OutputCombineCommandBuilder(); $command ->addCommandForCombinedOutput($commandStructure) ->addCommandForCombinedOutput($commandData); diff --git a/src/app/CliTools/Console/Command/Mysql/ClearCommand.php b/src/app/CliTools/Console/Command/Mysql/ClearCommand.php index bda8cff..c4db1c2 100644 --- a/src/app/CliTools/Console/Command/Mysql/ClearCommand.php +++ b/src/app/CliTools/Console/Command/Mysql/ClearCommand.php @@ -25,13 +25,16 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class ClearCommand extends \CliTools\Console\Command\AbstractCommand { +class ClearCommand extends AbstractCommand { /** * Configure command */ protected function configure() { - $this->setName('mysql:clear') + parent::configure(); + + $this + ->setName('mysql:clear') ->setAliases(array('mysql:create')) ->setDescription('Clear (recreate) database') ->addArgument( @@ -52,15 +55,19 @@ protected function configure() { public function execute(InputInterface $input, OutputInterface $output) { $database = $input->getArgument('db'); - $output->writeln('Dropping Database "' . $database . '"...'); - $query = 'DROP DATABASE IF EXISTS ' . DatabaseConnection::sanitizeSqlDatabase($database); - DatabaseConnection::exec($query); + $output->writeln('

Clearing database "' . $database . '"

'); + + if (DatabaseConnection::databaseExists($database)) { + $output->writeln('

Dropping database

'); + $query = 'DROP DATABASE ' . DatabaseConnection::sanitizeSqlDatabase($database); + DatabaseConnection::exec($query); + } - $output->writeln('Creating Database "' . $database . '"...'); + $output->writeln('

Creating database

'); $query = 'CREATE DATABASE ' . DatabaseConnection::sanitizeSqlDatabase($database); DatabaseConnection::exec($query); - $output->writeln('Database "' . $database . '" dropped and recreated'); + $output->writeln('

Database "' . $database . '" recreated

'); return 0; } diff --git a/src/app/CliTools/Console/Command/Mysql/ConnectionsCommand.php b/src/app/CliTools/Console/Command/Mysql/ConnectionsCommand.php index af13c23..318f241 100644 --- a/src/app/CliTools/Console/Command/Mysql/ConnectionsCommand.php +++ b/src/app/CliTools/Console/Command/Mysql/ConnectionsCommand.php @@ -25,13 +25,16 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class ConnectionsCommand extends \CliTools\Console\Command\AbstractCommand { +class ConnectionsCommand extends AbstractCommand { /** * Configure command */ protected function configure() { - $this->setName('mysql:connections') + parent::configure(); + + $this + ->setName('mysql:connections') ->setDescription('List current connections'); } diff --git a/src/app/CliTools/Console/Command/Mysql/ConvertCommand.php b/src/app/CliTools/Console/Command/Mysql/ConvertCommand.php new file mode 100644 index 0000000..9594872 --- /dev/null +++ b/src/app/CliTools/Console/Command/Mysql/ConvertCommand.php @@ -0,0 +1,139 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use CliTools\Database\DatabaseConnection; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ConvertCommand extends AbstractCommand { + + /** + * Configure command + */ + protected function configure() { + parent::configure(); + + $this + ->setName('mysql:convert') + ->setDescription('Convert charset/collation of a database') + ->addArgument( + 'database', + InputArgument::REQUIRED, + 'Database name' + ) + ->addOption( + 'charset', + null, + InputOption::VALUE_REQUIRED, + 'Charset (default: utf8)' + ) + ->addOption( + 'collation', + null, + InputOption::VALUE_REQUIRED, + 'Collation (default: utf8_general_ci)' + ) + ->addOption( + 'stdout', + null, + InputOption::VALUE_NONE, + 'Only print sql statements, do not execute it' + ); + } + + /** + * Execute command + * + * @param InputInterface $input Input instance + * @param OutputInterface $output Output instance + * + * @return int|null|void + */ + public function execute(InputInterface $input, OutputInterface $output) { + $charset = 'utf8'; + $collation = 'utf8_general_ci'; + $stdout = false; + + $database = $input->getArgument('database'); + + if ($input->getOption('charset')) { + $charset = (string)$input->getOption('charset'); + } + + if ($input->getOption('collation')) { + $collation = (string)$input->getOption('collation'); + } + + if ($input->getOption('stdout')) { + $stdout = true; + } + + // ################## + // Alter database + // ################## + + $query = 'ALTER DATABASE %s CHARACTER SET %s COLLATE %s'; + $query = sprintf($query, + DatabaseConnection::sanitizeSqlDatabase($database), + DatabaseConnection::quote($charset), + DatabaseConnection::quote($collation) + ); + + if (!$stdout) { + // Execute + $output->writeln('

Converting database ' . $database . '

'); + DatabaseConnection::exec($query); + } else { + // Show only + $output->writeln($query . ';'); + } + + // ################## + // Alter tables + // ################## + $tableList = DatabaseConnection::tableList($database); + + foreach ($tableList as $table) { + // Build statement + $query = 'ALTER TABLE %s.%s CONVERT TO CHARACTER SET %s COLLATE %s'; + $query = sprintf($query, + DatabaseConnection::sanitizeSqlDatabase($database), + DatabaseConnection::sanitizeSqlTable($table), + DatabaseConnection::quote($charset), + DatabaseConnection::quote($collation) + ); + + if (!$stdout) { + // Execute + $output->writeln('

Converting table ' . $table . '

'); + DatabaseConnection::exec($query); + } else { + // Show only + $output->writeln($query . ';'); + } + } + + return 0; + } +} diff --git a/src/app/CliTools/Console/Command/Mysql/DebugCommand.php b/src/app/CliTools/Console/Command/Mysql/DebugCommand.php index edb162a..e72f217 100644 --- a/src/app/CliTools/Console/Command/Mysql/DebugCommand.php +++ b/src/app/CliTools/Console/Command/Mysql/DebugCommand.php @@ -25,13 +25,14 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class DebugCommand extends \CliTools\Console\Command\AbstractCommand { +class DebugCommand extends AbstractCommand { /** * Configure command */ protected function configure() { - $this->setName('mysql:debug') + $this + ->setName('mysql:debug') ->setAliases(array('mysql:querylog')) ->setDescription('Debug mysql connections') ->addArgument( @@ -55,11 +56,13 @@ public function execute(InputInterface $input, OutputInterface $output) { $debugLogLocation = $this->getApplication()->getConfigValue('db', 'debug_log_dir'); $debugLogDir = dirname($debugLogLocation); + $output->writeln('

Starting MySQL general query log

'); + // Create directory if not exists if (!is_dir($debugLogDir)) { if (!mkdir($debugLogDir, 0777, true)) { - $output->writeln('Could not create "' . $debugLogDir . '" directory'); - exit(1); + $output->writeln('Could not create "' . $debugLogDir . '" directory'); + throw new \CliTools\Exception\StopException(1); } } @@ -77,14 +80,14 @@ public function execute(InputInterface $input, OutputInterface $output) { if (!empty($logFileRow['Value'])) { // Enable general log - $output->writeln('Enabling general log'); + $output->writeln('

Enabling general log

'); $query = 'SET GLOBAL general_log = \'ON\''; DatabaseConnection::exec($query); // Setup teardown cleanup $tearDownFunc = function () use ($output) { // Disable general log - $output->writeln('Disabling general log'); + $output->writeln('

Disabling general log

'); $query = 'SET GLOBAL general_log = \'OFF\''; DatabaseConnection::exec($query); }; @@ -109,7 +112,7 @@ public function execute(InputInterface $input, OutputInterface $output) { return 0; } else { - $output->writeln('MySQL general_log_file not set'); + $output->writeln('MySQL general_log_file not set'); return 1; } diff --git a/src/app/CliTools/Console/Command/Mysql/DropCommand.php b/src/app/CliTools/Console/Command/Mysql/DropCommand.php index d7f6af8..e2a7015 100644 --- a/src/app/CliTools/Console/Command/Mysql/DropCommand.php +++ b/src/app/CliTools/Console/Command/Mysql/DropCommand.php @@ -25,13 +25,16 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class DropCommand extends \CliTools\Console\Command\AbstractCommand { +class DropCommand extends AbstractCommand { /** * Configure command */ protected function configure() { - $this->setName('mysql:drop') + parent::configure(); + + $this + ->setName('mysql:drop') ->setDescription('Drop database') ->addArgument( 'db', @@ -51,11 +54,11 @@ protected function configure() { public function execute(InputInterface $input, OutputInterface $output) { $database = $input->getArgument('db'); - $output->writeln('Dropping Database "' . $database . '"...'); + $output->writeln('

Dropping Database "' . $database . '"...

'); $query = 'DROP DATABASE IF EXISTS ' . DatabaseConnection::sanitizeSqlDatabase($database); DatabaseConnection::exec($query); - $output->writeln('Database "' . $database . '" dropped'); + $output->writeln('

Database dropped

'); return 0; } diff --git a/src/app/CliTools/Console/Command/Mysql/ListCommand.php b/src/app/CliTools/Console/Command/Mysql/ListCommand.php index f04351f..3971b0d 100644 --- a/src/app/CliTools/Console/Command/Mysql/ListCommand.php +++ b/src/app/CliTools/Console/Command/Mysql/ListCommand.php @@ -28,13 +28,16 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class ListCommand extends \CliTools\Console\Command\AbstractCommand { +class ListCommand extends AbstractCommand { /** * Configure command */ protected function configure() { - $this->setName('mysql:list') + parent::configure(); + + $this + ->setName('mysql:list') ->setDescription('List all databases') ->addOption( 'sort-name', null, @@ -72,10 +75,7 @@ protected function configure() { public function execute(InputInterface $input, OutputInterface $output) { // Get list of databases - $query = 'SELECT SCHEMA_NAME - FROM information_schema.SCHEMATA'; - $databaseList = DatabaseConnection::getCol($query); - + $databaseList = DatabaseConnection::databaseList(); if (!empty($databaseList)) { // ######################## @@ -84,11 +84,6 @@ public function execute(InputInterface $input, OutputInterface $output) { $databaseRowList = array(); foreach ($databaseList as $database) { - // Skip internal mysql databases - if (in_array(strtolower($database), array('mysql', 'information_schema', 'performance_schema'))) { - continue; - } - // Get all tables $query = 'SELECT COUNT(*) AS count FROM information_schema.tables @@ -227,7 +222,7 @@ public function execute(InputInterface $input, OutputInterface $output) { $table->render(); } else { - $output->writeln('No databases found'); + $output->writeln('No databases found'); } return 0; diff --git a/src/app/CliTools/Console/Command/Mysql/RestartCommand.php b/src/app/CliTools/Console/Command/Mysql/RestartCommand.php index bc2afd8..fa012bc 100644 --- a/src/app/CliTools/Console/Command/Mysql/RestartCommand.php +++ b/src/app/CliTools/Console/Command/Mysql/RestartCommand.php @@ -22,15 +22,16 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; -class RestartCommand extends \CliTools\Console\Command\AbstractCommand { +class RestartCommand extends AbstractCommand { /** * Configure command */ protected function configure() { - $this->setName('mysql:restart') + $this + ->setName('mysql:restart') ->setDescription('Restart MySQL'); } diff --git a/src/app/CliTools/Console/Command/Mysql/RestoreCommand.php b/src/app/CliTools/Console/Command/Mysql/RestoreCommand.php index ce79bfe..6758226 100644 --- a/src/app/CliTools/Console/Command/Mysql/RestoreCommand.php +++ b/src/app/CliTools/Console/Command/Mysql/RestoreCommand.php @@ -21,18 +21,22 @@ */ use CliTools\Database\DatabaseConnection; +use CliTools\Utility\PhpUtility; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; -class RestoreCommand extends \CliTools\Console\Command\AbstractCommand { +class RestoreCommand extends AbstractCommand { /** * Configure command */ protected function configure() { - $this->setName('mysql:restore') + parent::configure(); + + $this + ->setName('mysql:restore') ->setDescription('Restore database') ->addArgument( 'db', @@ -59,73 +63,78 @@ public function execute(InputInterface $input, OutputInterface $output) { $dumpFile = $input->getArgument('file'); if (!is_file($dumpFile) || !is_readable($dumpFile)) { - $output->writeln('File is not readable'); + $output->writeln('File is not readable'); return 1; } - // Get mime type from file - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $dumpFileType = finfo_file($finfo, $dumpFile); - finfo_close($finfo); - - if ($dumpFileType === 'application/octet-stream') { - $finfo = finfo_open(); - $dumpFileInfo = finfo_file($finfo, $dumpFile); - finfo_close($finfo); + $dumpFileType = PhpUtility::getMimeType($dumpFile); - if (strpos($dumpFileInfo, 'LZMA compressed data') !== false) { - $dumpFileType = 'application/x-lzma'; - } - } + $output->writeln('

Restoring dump "' . $dumpFile . '" into database "' . $database . '"

'); if (DatabaseConnection::databaseExists($database)) { // Dropping - $output->writeln('Dropping Database "' . $database . '"...'); + $output->writeln('

Dropping database

'); $query = 'DROP DATABASE IF EXISTS ' . DatabaseConnection::sanitizeSqlDatabase($database); DatabaseConnection::exec($query); } // Creating - $output->writeln('Creating Database "' . $database . '"...'); + $output->writeln('

Creating database

'); $query = 'CREATE DATABASE ' . DatabaseConnection::sanitizeSqlDatabase($database); DatabaseConnection::exec($query); // Inserting - $output->writeln('Restoring dump into Database "' . $database . '"...'); putenv('USER=' . DatabaseConnection::getDbUsername()); putenv('MYSQL_PWD=' . DatabaseConnection::getDbPassword()); $commandMysql = new CommandBuilder('mysql','--user=%s %s --one-database', array(DatabaseConnection::getDbUsername(), $database)); + // Set server connection details + if ($input->getOption('host')) { + $commandMysql->addArgumentTemplate('-h %s', $input->getOption('host')); + } + + if ($input->getOption('port')) { + $commandMysql->addArgumentTemplate('-P %s', $input->getOption('port')); + } + $commandFile = new CommandBuilder(); $commandFile->addArgument($dumpFile); $commandFile->addPipeCommand($commandMysql); switch ($dumpFileType) { case 'application/x-bzip2': + $output->writeln('

Using BZIP2 decompression

'); $commandFile->setCommand('bzcat'); break; case 'application/gzip': + case 'application/x-gzip': + $output->writeln('

Using GZIP decompression

'); $commandFile->setCommand('gzcat'); break; case 'application/x-lzma': case 'application/x-xz': + $output->writeln('

Using LZMA decompression

'); $commandFile->setCommand('xzcat'); break; default: + $output->writeln('

Using plaintext (no decompression)

'); $commandFile->setCommand('cat'); break; } + $output->writeln('

Reading dump

'); $commandFile->executeInteractive(); - $output->writeln('Database "' . $database . '" restored'); + $output->writeln('

Database "' . $database . '" restored

'); return 0; } + + } diff --git a/src/app/CliTools/Console/Command/Mysql/SlowLogCommand.php b/src/app/CliTools/Console/Command/Mysql/SlowLogCommand.php index 400b26b..7b09cd1 100644 --- a/src/app/CliTools/Console/Command/Mysql/SlowLogCommand.php +++ b/src/app/CliTools/Console/Command/Mysql/SlowLogCommand.php @@ -26,31 +26,32 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class SlowLogCommand extends \CliTools\Console\Command\AbstractCommand { +class SlowLogCommand extends AbstractCommand { /** * Configure command */ protected function configure() { - $this->setName('mysql:slowlog') - ->setDescription('Enable and show slow query log') - ->addArgument( - 'grep', - InputArgument::OPTIONAL, - 'Grep' - ) + $this + ->setName('mysql:slowlog') + ->setDescription('Enable and show slow query log') + ->addArgument( + 'grep', + InputArgument::OPTIONAL, + 'Grep' + ) ->addOption( 'time', 't', InputOption::VALUE_REQUIRED, 'Slow query time (default 1 second)' ) - ->addOption( - 'no-index', - 'i', - InputOption::VALUE_NONE, - 'Enable log queries without indexes log' - ); + ->addOption( + 'no-index', + 'i', + InputOption::VALUE_NONE, + 'Enable log queries without indexes log' + ); } /** @@ -80,11 +81,13 @@ public function execute(InputInterface $input, OutputInterface $output) { $debugLogLocation = $this->getApplication()->getConfigValue('db', 'debug_log_dir'); $debugLogDir = dirname($debugLogLocation); + $output->writeln('

Starting MySQL slow query log

'); + // Create directory if not exists if (!is_dir($debugLogDir)) { if (!mkdir($debugLogDir, 0777, true)) { - $output->writeln('Could not create "' . $debugLogDir . '" directory'); - exit(1); + $output->writeln('Could not create "' . $debugLogDir . '" directory'); + throw new \CliTools\Exception\StopException(1); } } @@ -100,22 +103,22 @@ public function execute(InputInterface $input, OutputInterface $output) { if (!empty($logFileRow['Value'])) { // Enable slow log - $output->writeln('Enabling slow log'); + $output->writeln('

Enabling slow log

'); $query = 'SET GLOBAL slow_query_log = \'ON\''; DatabaseConnection::exec($query); // Enable slow log - $output->writeln('Set long_query_time to ' . (int)abs($slowLogQueryTime) . ' seconds'); + $output->writeln('

Set long_query_time to ' . (int)abs($slowLogQueryTime) . ' seconds

'); $query = 'SET GLOBAL long_query_time = ' . (int)abs($slowLogQueryTime); DatabaseConnection::exec($query); // Enable log queries without indexes log if ($logNonIndexedQueries) { - $output->writeln('Enabling logging of queries without using indexes'); + $output->writeln('

Enabling logging of queries without using indexes

'); $query = 'SET GLOBAL log_queries_not_using_indexes = \'ON\''; DatabaseConnection::exec($query); } else { - $output->writeln('Disabling logging of queries without using indexes'); + $output->writeln('

Disabling logging of queries without using indexes

'); $query = 'SET GLOBAL log_queries_not_using_indexes = \'OFF\''; DatabaseConnection::exec($query); } @@ -123,7 +126,7 @@ public function execute(InputInterface $input, OutputInterface $output) { // Setup teardown cleanup $tearDownFunc = function () use ($output, $logNonIndexedQueries) { // Disable general log - $output->writeln('Disable slow log'); + $output->writeln('

Disable slow log

'); $query = 'SET GLOBAL slow_query_log = \'OFF\''; DatabaseConnection::exec($query); @@ -154,7 +157,7 @@ public function execute(InputInterface $input, OutputInterface $output) { return 0; } else { - $output->writeln('MySQL general_log_file not set'); + $output->writeln('MySQL general_log_file not set'); return 1; } diff --git a/src/app/CliTools/Console/Command/Php/ComposerCommand.php b/src/app/CliTools/Console/Command/Php/ComposerCommand.php new file mode 100644 index 0000000..652370f --- /dev/null +++ b/src/app/CliTools/Console/Command/Php/ComposerCommand.php @@ -0,0 +1,77 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use CliTools\Utility\UnixUtility; +use CliTools\Utility\PhpUtility; +use CliTools\Shell\CommandBuilder\CommandBuilder; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ComposerCommand extends \CliTools\Console\Command\AbstractCommand implements \CliTools\Console\Filter\AnyParameterFilterInterface { + + /** + * Configure command + */ + protected function configure() { + $this + ->setName('php:composer') + ->setDescription('Search composer.json updir and start composer'); + } + + /** + * Execute command + * + * @param InputInterface $input Input instance + * @param OutputInterface $output Output instance + * + * @return int|null|void + */ + public function execute(InputInterface $input, OutputInterface $output) { + $composerCmd = $this->getApplication()->getConfigValue('bin', 'composer'); + + $paramList = $this->getFullParameterList(); + $composerJsonPath = UnixUtility::findFileInDirectortyTree('composer.json'); + + if (!empty($composerJsonPath)) { + $path = dirname($composerJsonPath); + $this->output->writeln('Found composer.json directory: ' . $path . ''); + + // Switch to directory of docker-compose.yml + PhpUtility::chdir($path); + + $command = new CommandBuilder(); + $command->parse($composerCmd); + + if (!empty($paramList)) { + $command->setArgumentList($paramList); + } + + $command->executeInteractive(); + } else { + $this->output->writeln('No composer.json found in tree'); + + return 1; + } + + return 0; + } +} diff --git a/src/app/CliTools/Console/Command/Php/RestartCommand.php b/src/app/CliTools/Console/Command/Php/RestartCommand.php index 7b6b18b..86fca58 100644 --- a/src/app/CliTools/Console/Command/Php/RestartCommand.php +++ b/src/app/CliTools/Console/Command/Php/RestartCommand.php @@ -22,7 +22,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; class RestartCommand extends \CliTools\Console\Command\AbstractCommand { @@ -30,7 +30,8 @@ class RestartCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('php:restart') + $this + ->setName('php:restart') ->setDescription('Restart PHP FPM'); } diff --git a/src/app/CliTools/Console/Command/Php/TraceCommand.php b/src/app/CliTools/Console/Command/Php/TraceCommand.php index d4477e1..e5b0b92 100644 --- a/src/app/CliTools/Console/Command/Php/TraceCommand.php +++ b/src/app/CliTools/Console/Command/Php/TraceCommand.php @@ -33,7 +33,8 @@ class TraceCommand extends \CliTools\Console\Command\AbstractTraceCommand { * Configure command */ protected function configure() { - $this->setName('php:trace') + $this + ->setName('php:trace') ->setDescription('Debug PHP processes with strace'); parent::configure(); } diff --git a/src/app/CliTools/Console/Command/Samba/RestartCommand.php b/src/app/CliTools/Console/Command/Samba/RestartCommand.php index a5c62c3..349be43 100644 --- a/src/app/CliTools/Console/Command/Samba/RestartCommand.php +++ b/src/app/CliTools/Console/Command/Samba/RestartCommand.php @@ -22,7 +22,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; class RestartCommand extends \CliTools\Console\Command\AbstractCommand { @@ -30,7 +30,8 @@ class RestartCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('samba:restart') + $this + ->setName('samba:restart') ->setDescription('Restart Samba SMB daemon'); } diff --git a/src/app/CliTools/Console/Command/Sync/AbstractCommand.php b/src/app/CliTools/Console/Command/Sync/AbstractCommand.php new file mode 100644 index 0000000..8d07784 --- /dev/null +++ b/src/app/CliTools/Console/Command/Sync/AbstractCommand.php @@ -0,0 +1,1106 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use CliTools\Utility\PhpUtility; +use CliTools\Utility\UnixUtility; +use CliTools\Utility\ConsoleUtility; +use CliTools\Utility\FilterUtility; +use CliTools\Shell\CommandBuilder\CommandBuilder; +use CliTools\Shell\CommandBuilder\RemoteCommandBuilder; +use CliTools\Shell\CommandBuilder\SelfCommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilderInterface; +use CliTools\Shell\CommandBuilder\OutputCombineCommandBuilder; +use CliTools\Reader\ConfigReader; +use CliTools\Database\DatabaseConnection; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Yaml\Yaml; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Question\ChoiceQuestion; + +abstract class AbstractCommand extends \CliTools\Console\Command\AbstractCommand { + + const CONFIG_FILE = 'clisync.yml'; + const GLOBAL_KEY = 'GLOBAL'; + + /** + * Config area + * + * @var string + */ + protected $confArea; + + /** + * Project working path + * + * @var string|boolean|null + */ + protected $workingPath; + + /** + * Project configuration file path + * + * @var string|boolean|null + */ + protected $confFilePath; + + /** + * Temporary storage dir + * + * @var string|null + */ + protected $tempDir; + + /** + * Configuration + * + * @var ConfigReader + */ + protected $config = array(); + + /** + * Context configuration + * + * @var ConfigReader + */ + protected $contextConfig = array(); + + /** + * Configure command + */ + protected function configure() { + $this + ->setDescription('Sync files and database from server') + ->addArgument( + 'context', + InputArgument::OPTIONAL, + 'Configuration name for server' + ) + ->addOption( + 'mysql', + null, + InputOption::VALUE_NONE, + 'Run only mysql' + ) + ->addOption( + 'rsync', + null, + InputOption::VALUE_NONE, + 'Run only rsync' + ) + ->addOption( + 'config', + null, + InputOption::VALUE_NONE, + 'Show generated config' + ); + } + + /** + * Initializes the command just after the input has been validated. + * + * This is mainly useful when a lot of commands extends one main command + * where some things need to be initialized based on the input arguments and options. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + * @throws \RuntimeException + */ + protected function initialize(InputInterface $input, OutputInterface $output) { + parent::initialize($input, $output); + + $this->initializeConfiguration(); + } + + /** + * Init configuration + */ + protected function initializeConfiguration() { + // Search for configuration in path + $this->findConfigurationInPath(); + + // Read configuration + $this->readConfiguration(); + } + + /** + * Validate configuration + * + * @return boolean + */ + protected function validateConfiguration() { + $ret = true; + + // Rsync (optional) + if ($this->contextConfig->exists('rsync')) { + if (!$this->validateConfigurationRsync()) { + $ret = false; + } + } + + // MySQL (optional) + if ($this->contextConfig->exists('mysql.database')) { + if (!$this->validateConfigurationMysql()) { + $ret = false; + } + } else { + // Clear mysql if any options set + $this->contextConfig->clear('mysql'); + } + + return $ret; + } + + /** + * Find configuration file in current path + */ + protected function findConfigurationInPath() { + $confFileList = array( + self::CONFIG_FILE, + '.' . self::CONFIG_FILE, + ); + + // Find configuration file + $this->confFilePath = UnixUtility::findFileInDirectortyTree($confFileList); + if (empty($this->confFilePath)) { + $this->output->writeln('No ' . self::CONFIG_FILE . ' found in tree'); + throw new \CliTools\Exception\StopException(1); + } + + $this->workingPath = dirname($this->confFilePath); + + $this->output->writeln('Found ' . self::CONFIG_FILE . ' directory: ' . $this->workingPath . ''); + } + + /** + * Read and validate configuration + */ + protected function readConfiguration() { + $this->config = new ConfigReader(); + + if (empty($this->confArea)) { + throw new \RuntimeException('Config area not set, cannot continue'); + } + + if (!file_exists($this->confFilePath)) { + throw new \RuntimeException('Config file "' . $this->confFilePath . '" not found'); + } + + $conf = Yaml::parse(PhpUtility::fileGetContents($this->confFilePath)); + + // Switch to area configuration + if (!empty($conf)) { + $this->config->setData($conf); + } else { + throw new \RuntimeException('Could not parse "' . $this->confFilePath . '"'); + } + } + + /** + * Get context list from current configuration + * + * @return array|null + */ + protected function getContextListFromConfiguration() { + return $this->config->getArray($this->confArea); + } + + /** + * Get command list from current configuration + * + * @param string $section Section name for commands (startup, final) + * @return array + */ + protected function getCommandList($section) { + $ret = array(); + + if ($this->contextConfig->exists('command.' . $section)) { + $ret = $this->contextConfig->get('command.' . $section); + } + + return $ret; + } + + /** + * Build context configuration + * + * @param $context + */ + protected function buildContextConfiguration($context) { + $this->contextConfig = new ConfigReader(); + + // Fetch global conf + $globalConf = array(); + if ($this->config->exists(self::GLOBAL_KEY)) { + $globalConf = $this->config->get(self::GLOBAL_KEY); + } + + // Fetch area conf + $areaConf = $this->config->get($this->confArea); + + // Fetch area global conf + $areaGlobalConf = array(); + if ($this->config->exists($this->confArea . '.' . self::GLOBAL_KEY)) { + $areaGlobalConf = $this->config->get($this->confArea . '.' . self::GLOBAL_KEY); + } + + // Fetch context conf + if (empty($areaConf[$context])) { + $this->output->writeln('No context "' . $context . '" found'); + throw new \CliTools\Exception\StopException(1); + } + $contextConf = $areaConf[$context]; + + + $arrayFilterRecursive = function($input, $callback) use (&$arrayFilterRecursive) { + $ret = array(); + foreach ($input as $key => $value) { + if (is_array($value)) { + $value = $arrayFilterRecursive($value, $callback); + } else { + if (strlen($value)==0) { + $value = null; + } + } + + if ($value !== null && $value !== false && $value !== true) { + $ret[$key] = $value; + } + } + + return $ret; + }; + + // Merge + $globalConf = $arrayFilterRecursive( $globalConf, 'strlen' ); + $areaGlobalConf = $arrayFilterRecursive( $areaGlobalConf, 'strlen' ); + $contextConf = $arrayFilterRecursive( $contextConf, 'strlen' ); + + $conf = array_replace_recursive($globalConf, $areaGlobalConf, $contextConf); + + // Set configuration + $this->contextConfig->setData($conf); + } + + /** + * Execute command + * + * @param InputInterface $input Input instance + * @param OutputInterface $output Output instance + * + * @return int|null|void + * @throws \Exception + */ + public function execute(InputInterface $input, OutputInterface $output) { + try { + // Get context selection + $this->initContext(); + + if ($this->input->getOption('config')) { + // only show configuration + $this->showContextConfig(); + } else { + // Create temp directory and check environment + $this->startup(); + + // Run playbook + $this->runCommands('startup'); + $this->runMain(); + $this->runCommands('finalize'); + } + + } catch (\Exception $e) { + $this->cleanup(); + throw $e; + } + + $this->cleanup(); + } + + /** + * Init context + */ + protected function initContext() { + $context = $this->getContextFromUser(); + $this->buildContextConfiguration($context); + + // Validate configuration + if (!$this->validateConfiguration()) { + $this->output->writeln('Configuration could not be validated'); + throw new \CliTools\Exception\StopException(1); + } + } + + /** + * Get context from user + */ + protected function getContextFromUser() { + $ret = null; + + if (!$this->input->getArgument('context')) { + // ######################## + // Ask user for server context + // ######################## + + $serverList = $this->config->getList($this->confArea); + $serverList = array_diff($serverList, array(self::GLOBAL_KEY)); + + if (empty($serverList)) { + throw new \RuntimeException('No valid servers found in configuration'); + } + + $serverOptionList = array(); + + foreach ($serverList as $context) { + $line = array(); + + // hostname + $optPath = $this->confArea . '.' . $context . '.ssh.hostname'; + if ($this->config->exists($optPath)) { + $line[] = 'host:' . $this->config->get($optPath); + } + + // rsync path + $optPath = $this->confArea . '.' . $context . '.rsync.path'; + if ($this->config->exists($optPath)) { + $line[] = 'rsync:' . $this->config->get($optPath); + } + + // mysql database list + $optPath = $this->confArea . '.' . $context . '.mysql.database'; + if ($this->config->exists($optPath)) { + $dbList = $this->config->getArray($optPath); + $foreignDbList = array(); + + foreach ($dbList as $databaseConf) { + if (strpos($databaseConf, ':') !== false) { + // local and foreign database in one string + $databaseConf = explode(':', $databaseConf, 2); + $foreignDbList[] = $databaseConf[1]; + } else { + // database equal + $foreignDbList[] = $databaseConf; + } + } + + if (!empty($foreignDbList)) { + $line[] .= 'mysql:' . implode(', ', $foreignDbList); + } + } + + if (!empty($line)) { + $line = implode(' ', $line); + } else { + // fallback + $line = $context; + } + + $serverOptionList[$context] = $line; + } + + try { + $question = new ChoiceQuestion('Please choose server context for synchronization', $serverOptionList); + $question->setMaxAttempts(1); + + $questionDialog = new QuestionHelper(); + + $ret = $questionDialog->ask($this->input, $this->output, $question); + } catch(\InvalidArgumentException $e) { + // Invalid server context, just stop here + throw new \CliTools\Exception\StopException(1); + } + } else { + $ret = $this->input->getArgument('context'); + } + + return $ret; + } + + /** + * Show context configuration + */ + protected function showContextConfig() { + print_r($this->contextConfig->get()); + } + + /** + * Validate configuration (rsync) + * + * @return boolean + */ + protected function validateConfigurationRsync() { + $ret = true; + + // Check if rsync target exists + if (!$this->getRsyncPathFromConfig()) { + $this->output->writeln('No rsync path configuration found'); + $ret = false; + } else { + $this->output->writeln('Using rsync path "' . $this->getRsyncPathFromConfig() . '"'); + } + + // Check if there are any rsync directories + if (!$this->contextConfig->exists('rsync.directory')) { + $this->output->writeln('No rsync directory configuration found, filesync disabled'); + } + + return $ret; + } + + /** + * Validate configuration (mysql) + * + * @return boolean + */ + protected function validateConfigurationMysql() { + $ret = true; + + // Check if one database is configured + if (!$this->contextConfig->exists('mysql.database')) { + $this->output->writeln('No mysql database configuration found'); + $ret = false; + } + + return $ret; + } + + /** + * Startup task + */ + protected function startup() { + $this->tempDir = '/tmp/.clisync-'.getmypid(); + $this->clearTempDir(); + PhpUtility::mkdir($this->tempDir, 0777, true); + PhpUtility::mkdir($this->tempDir . '/mysql/', 0777, true); + + $this->checkIfDockerExists(); + } + + /** + * Cleanup task + */ + protected function cleanup() { + $this->clearTempDir(); + } + + /** + * Clear temp. storage directory if exists + */ + protected function clearTempDir() { + // Remove storage dir + if (!empty($this->tempDir) && is_dir($this->tempDir)) { + $command = new CommandBuilder('rm', '-rf'); + $command->addArgumentSeparator() + ->addArgument($this->tempDir); + $command->executeInteractive(); + } + } + + /** + * Check if docker exists + * + * @throws \CliTools\Exception\StopException + */ + protected function checkIfDockerExists() { + $dockerPath = \CliTools\Utility\DockerUtility::searchDockerDirectoryRecursive(); + + if (!empty($dockerPath)) { + $this->output->writeln('Running docker containers:'); + + // Docker instance found + $docker = new CommandBuilder('docker', 'ps'); + $docker->executeInteractive(); + + $answer = ConsoleUtility::questionYesNo('Are these running containers the right ones?', 'no'); + + if (!$answer) { + throw new \CliTools\Exception\StopException(1); + } + } + } + + /** + * Run defined commands + */ + protected function runCommands($area) { + $commandList = $this->getCommandList($area); + + if (!empty($commandList)) { + $this->output->writeln(' ---- Starting ' . strtoupper($area) . ' commands ---- '); + + foreach ($commandList as $commandRow) { + + if (is_string($commandRow)) { + // Simple, local task + $command = new CommandBuilder(); + $command->parse($commandRow); + } elseif(is_array($commandRow)) { + // Complex task + $command = $this->buildComplexTask($commandRow); + } + + if ($command) { + $command->executeInteractive(); + } + } + } + } + + /** + * Build complex task + * + * @param array $task Task configuration + * + * @return CommandBuilder|CommandBuilderInterface + */ + protected function buildComplexTask(array $task) { + if (empty($task['type'])) { + $task['type'] = 'local'; + } + + if (empty($task['command'])) { + throw new \RuntimeException('Task command is empty'); + } + + // Process task type + switch ($task['type']) { + case 'remote': + // Remote command + $command = new RemoteCommandBuilder(); + $command->parse($task['command']); + $command = $this->wrapRemoteCommand($command); + break; + + case 'local': + // Local command + $command = new CommandBuilder(); + $command->parse($task['command']); + break; + + default: + throw new \RuntimeException('Unknown task type'); + break; + } + + return $command; + } + + /** + * Create rsync command for sync + * + * @param string $source Source directory + * @param string $target Target directory + * @param string $confKey List of files (patterns) + * + * @return CommandBuilder + */ + protected function createRsyncCommandWithConfiguration($source, $target, $confKey) { + $options = array(); + + // ############# + // Filelist + // ############# + $fileList = array(); + if ($this->contextConfig->exists($confKey . '.directory')) { + $fileList = $this->contextConfig->get($confKey . '.directory'); + } + + // ############# + // Excludes + // ############# + $excludeList = array(); + if ($this->contextConfig->exists($confKey . '.exclude')) { + $excludeList = $this->contextConfig->get($confKey . '.exclude'); + } + + // ############# + // Max size + // ############# + if ($this->contextConfig->exists($confKey . '.conf.maxSize')) { + $options['max-size'] = array( + 'template' => '--max-size=%s', + 'params' => array( + $this->contextConfig->get($confKey . '.conf.maxSize') + ), + ); + } + + // ############# + // Min size + // ############# + if ($this->contextConfig->exists($confKey . '.conf.minSize')) { + $options['min-size'] = array( + 'template' => '--min-size=%s', + 'params' => array( + $this->contextConfig->get($confKey . '.conf.minSize') + ), + ); + } + + return $this->createRsyncCommand($source, $target, $fileList, $excludeList, $options); + } + + /** + * Create rsync command for sync + * + * @param string $source Source directory + * @param string $target Target directory + * @param array|null $filelist List of files (patterns) + * @param array|null $exclude List of excludes (patterns) + * @param array|null $options Custom rsync options + * + * @return CommandBuilder + */ + protected function createRsyncCommand($source, $target, array $filelist = null, array $exclude = null, array $options = null) { + $this->output->writeln('Rsync from ' . $source . ' to ' . $target . ''); + + $command = new CommandBuilder('rsync', '-rlptD --delete-after --progress --human-readable'); + + // Add file list (external file with --files-from option) + if (!empty($filelist)) { + $this->rsyncAddFileList($command, $filelist); + } + + // Add exclude (external file with --exclude-from option) + if (!empty($exclude)) { + $this->rsyncAddExcludeList($command, $exclude); + } + + if (!empty($options)) { + foreach ($options as $optionValue) { + if (is_array($optionValue)) { + $command->addArgumentTemplateList($optionValue['template'], $optionValue['params']); + } else { + $command->addArgument($optionValue); + } + + } + } + + // Paths should have leading / to prevent sync issues + $source = rtrim($source, '/') . '/'; + $target = rtrim($target, '/') . '/'; + + // Set source and target + $command->addArgument($source) + ->addArgument($target); + + return $command; + } + + /** + * Get rsync path from configuration + * + * @return boolean|string + */ + protected function getRsyncPathFromConfig() { + $ret = false; + if ($this->contextConfig->exists('rsync.path')) { + // Use path from rsync + $ret = $this->contextConfig->get('rsync.path'); + } elseif($this->contextConfig->exists('ssh.hostname') && $this->contextConfig->exists('ssh.path')) { + // Build path from ssh configuration + $ret = $this->contextConfig->get('ssh.hostname') . ':' . $this->contextConfig->get('ssh.path'); + } + + return $ret; + } + + + /** + * Get rsync working path (with target if set in config) + * + * @return boolean|string + */ + protected function getRsyncWorkingPath() { + $ret = $this->workingPath; + + // remove right / + $ret = rtrim($ret, '/'); + + if ($this->contextConfig->exists('rsync.workdir')) { + $ret .= '/' . $this->contextConfig->get('rsync.workdir'); + } + + return $ret; + } + + /** + * Add file (pattern) list to rsync command + * + * @param CommandBuilder $command Rsync Command + * @param array $list List of files + */ + protected function rsyncAddFileList(CommandBuilder $command, array $list) { + $rsyncFilter = $this->tempDir . '/.rsync-filelist'; + + PhpUtility::filePutContents($rsyncFilter, implode("\n", $list)); + + $command->addArgumentTemplate('--files-from=%s', $rsyncFilter); + + // cleanup rsync file + $command->getExecutor()->addFinisherCallback(function () use ($rsyncFilter) { + unlink($rsyncFilter); + }); + + } + + /** + * Add exclude (pattern) list to rsync command + * + * @param CommandBuilder $command Rsync Command + * @param array $list List of excludes + */ + protected function rsyncAddExcludeList(CommandBuilder $command, $list) { + $rsyncFilter = $this->tempDir . '/.rsync-exclude'; + + PhpUtility::filePutContents($rsyncFilter, implode("\n", $list)); + + $command->addArgumentTemplate('--exclude-from=%s', $rsyncFilter); + + // cleanup rsync file + $command->getExecutor()->addFinisherCallback(function () use ($rsyncFilter) { + unlink($rsyncFilter); + }); + } + + /** + * Create mysql backup command + * + * @param string $database Database name + * @param string $dumpFile MySQL dump file + * + * @return SelfCommandBuilder + */ + protected function createMysqlRestoreCommand($database, $dumpFile) { + $command = new SelfCommandBuilder(); + $command->addArgumentTemplate('mysql:restore %s %s', $database, $dumpFile); + return $command; + } + + /** + * Create mysql backup command + * + * @param string $database Database name + * @param string $dumpFile MySQL dump file + * @param null|string $filter Filter name + * + * @return SelfCommandBuilder + */ + protected function createMysqlBackupCommand($database, $dumpFile, $filter = null) { + $command = new SelfCommandBuilder(); + $command->addArgumentTemplate('mysql:backup %s %s', $database, $dumpFile); + + if ($filter !== null) { + $command->addArgumentTemplate('--filter=%s', $filter); + } + + return $command; + } + + /** + * Wrap command with ssh if needed + * + * @param CommandBuilderInterface $command + * @return CommandBuilderInterface + */ + protected function wrapRemoteCommand(CommandBuilderInterface $command) { + // Wrap in ssh if needed + if ($this->contextConfig->exists('ssh.hostname')) { + $sshCommand = new CommandBuilder('ssh', '-o BatchMode=yes'); + $sshCommand->addArgument($this->contextConfig->get('ssh.hostname')) + ->append($command, true); + + $command = $sshCommand; + } + + return $command; + } + + /** + * Create new mysql command + * + * @param null|string $database Database name + * + * @return RemoteCommandBuilder + */ + protected function createRemoteMySqlCommand($database = null) { + $command = new RemoteCommandBuilder('mysql'); + $command + // batch mode + ->addArgument('-B') + // skip column names + ->addArgument('-N'); + + // Add username + if ($this->contextConfig->exists('mysql.username')) { + $command->addArgumentTemplate('-u%s', $this->contextConfig->get('mysql.username')); + } + + // Add password + if ($this->contextConfig->exists('mysql.password')) { + $command->addArgumentTemplate('-p%s', $this->contextConfig->get('mysql.password')); + } + + // Add hostname + if ($this->contextConfig->exists('mysql.hostname')) { + $command->addArgumentTemplate('-h%s', $this->contextConfig->get('mysql.hostname')); + } + + if ($database !== null) { + $command->addArgument($database); + } + + return $command; + } + + + /** + * Create new mysql command + * + * @param null|string $database Database name + * + * @return RemoteCommandBuilder + */ + protected function createLocalMySqlCommand($database = null) { + $command = new RemoteCommandBuilder('mysql'); + $command + // batch mode + ->addArgument('-B') + // skip column names + ->addArgument('-N'); + + // Add username + if (DatabaseConnection::getDbUsername()) { + $command->addArgumentTemplate('-u%s', DatabaseConnection::getDbUsername()); + } + + // Add password + if (DatabaseConnection::getDbPassword()) { + $command->addArgumentTemplate('-p%s', DatabaseConnection::getDbPassword()); + } + + // Add hostname + if (DatabaseConnection::getDbHostname()) { + $command->addArgumentTemplate('-h%s', DatabaseConnection::getDbHostname()); + } + + // Add hostname + if (DatabaseConnection::getDbPort()) { + $command->addArgumentTemplate('-P%s', DatabaseConnection::getDbPort()); + } + + if ($database !== null) { + $command->addArgument($database); + } + + return $command; + } + + /** + * Create new mysqldump command + * + * @param null|string $database Database name + * + * @return RemoteCommandBuilder + */ + protected function createRemoteMySqlDumpCommand($database = null) { + $command = new RemoteCommandBuilder('mysqldump'); + + // Add username + if ($this->contextConfig->exists('mysql.username')) { + $command->addArgumentTemplate('-u%s', $this->contextConfig->get('mysql.username')); + } + + // Add password + if ($this->contextConfig->exists('mysql.password')) { + $command->addArgumentTemplate('-p%s', $this->contextConfig->get('mysql.password')); + } + + // Add hostname + if ($this->contextConfig->exists('mysql.hostname')) { + $command->addArgumentTemplate('-h%s', $this->contextConfig->get('mysql.hostname')); + } + + // Add custom options + if ($this->contextConfig->exists('mysql.mysqldump.option')) { + $command->addArgumentRaw($this->contextConfig->get('mysql.mysqldump.option')); + } + + // Transfer compression + switch($this->contextConfig->get('mysql.compression')) { + case 'bzip2': + // Add pipe compressor (bzip2 compressed transfer via ssh) + $command->addPipeCommand( new CommandBuilder('bzip2', '--compress --stdout') ); + break; + + case 'gzip': + // Add pipe compressor (gzip compressed transfer via ssh) + $command->addPipeCommand( new CommandBuilder('gzip', '--stdout') ); + break; + } + + if ($database !== null) { + $command->addArgument($database); + } + + return $command; + } + + /** + * Create new mysqldump command + * + * @param null|string $database Database name + * + * @return RemoteCommandBuilder + */ + protected function createLocalMySqlDumpCommand($database = null) { + $command = new RemoteCommandBuilder('mysqldump'); + + // Add username + if (DatabaseConnection::getDbUsername()) { + $command->addArgumentTemplate('-u%s', DatabaseConnection::getDbUsername()); + } + + // Add password + if (DatabaseConnection::getDbPassword()) { + $command->addArgumentTemplate('-p%s', DatabaseConnection::getDbPassword()); + } + + // Add hostname + if (DatabaseConnection::getDbHostname()) { + $command->addArgumentTemplate('-h%s', DatabaseConnection::getDbHostname()); + } + + // Add hostname + if (DatabaseConnection::getDbPort()) { + $command->addArgumentTemplate('-P%s', DatabaseConnection::getDbPort()); + } + + // Add custom options + if ($this->contextConfig->exists('mysql.mysqldump.option')) { + $command->addArgumentRaw($this->contextConfig->get('mysql.mysqldump.option')); + } + + if ($database !== null) { + $command->addArgument($database); + } + + // Transfer compression + switch($this->contextConfig->get('mysql.compression')) { + case 'bzip2': + // Add pipe compressor (bzip2 compressed transfer via ssh) + $command->addPipeCommand( new CommandBuilder('bzip2', '--compress --stdout') ); + break; + + case 'gzip': + // Add pipe compressor (gzip compressed transfer via ssh) + $command->addPipeCommand( new CommandBuilder('gzip', '--stdout') ); + break; + } + + return $command; + } + + + /** + * Add mysqldump filter to command + * + * @param CommandBuilderInterface $commandDump Command + * @param string $database Database + * @param boolean $isRemote Remote filter + * + * @return CommandBuilderInterface + */ + protected function addMysqlDumpFilterArguments(CommandBuilderInterface $commandDump, $database, $isRemote = true) { + $command = $commandDump; + + $filter = $this->contextConfig->get('mysql.filter'); + + // get filter + if (is_array($filter)) { + $filterList = (array)$filter; + $filter = 'custom table filter'; + } else { + $filterList = $this->getApplication()->getConfigValue('mysql-backup-filter', $filter); + } + + if (empty($filterList)) { + throw new \RuntimeException('MySQL dump filters "' . $filter . '" not available"'); + } + + $this->output->writeln('

Using filter "' . $filter . '"

'); + + // Get table list (from cloned mysqldump command) + if ($isRemote) { + $tableListDumper = $this->createRemoteMySqlCommand($database); + } else { + $tableListDumper = $this->createLocalMySqlCommand($database); + } + + $tableListDumper->addArgumentTemplate('-e %s', 'show tables;'); + + // wrap with ssh (for remote execution) + if ($isRemote) { + $tableListDumper = $this->wrapRemoteCommand($tableListDumper); + } + + $tableList = $tableListDumper->execute()->getOutput(); + + // Filter table list + $ignoredTableList = FilterUtility::mysqlIgnoredTableFilter($tableList, $filterList, $database); + + // Dump only structure + $commandStructure = clone $command; + $commandStructure + ->addArgument('--no-data') + ->clearPipes(); + + // Dump only data (only filtered tables) + $commandData = clone $command; + $commandData + ->addArgument('--no-create-info') + ->clearPipes(); + + if (!empty($ignoredTableList)) { + $commandData->addArgumentTemplateMultiple('--ignore-table=%s', $ignoredTableList); + } + + $commandPipeList = $command->getPipeList(); + + // Combine both commands to one + $command = new OutputCombineCommandBuilder(); + $command + ->addCommandForCombinedOutput($commandStructure) + ->addCommandForCombinedOutput($commandData); + + // Read compression pipe + if (!empty($commandPipeList)) { + $command->setPipeList($commandPipeList); + } + + return $command; + } + +} diff --git a/src/app/CliTools/Console/Command/Sync/AbstractRemoteSyncCommand.php b/src/app/CliTools/Console/Command/Sync/AbstractRemoteSyncCommand.php new file mode 100644 index 0000000..f26efa2 --- /dev/null +++ b/src/app/CliTools/Console/Command/Sync/AbstractRemoteSyncCommand.php @@ -0,0 +1,50 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +abstract class AbstractRemoteSyncCommand extends AbstractCommand { + + /** + * Validate configuration + * + * @return boolean + */ + protected function validateConfiguration() { + $ret = parent::validateConfiguration(); + + $output = $this->output; + + // ################## + // SSH (optional) + // ################## + + if ($this->config->exists('ssh')) { + // Check if one database is configured + if (!$this->config->exists('ssh.hostname')) { + $output->writeln('No ssh hostname configuration found'); + $ret = false; + } + } + + return $ret; + } + +} diff --git a/src/app/CliTools/Console/Command/Sync/AbstractShareCommand.php b/src/app/CliTools/Console/Command/Sync/AbstractShareCommand.php new file mode 100644 index 0000000..ddc04cb --- /dev/null +++ b/src/app/CliTools/Console/Command/Sync/AbstractShareCommand.php @@ -0,0 +1,51 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +abstract class AbstractShareCommand extends AbstractCommand { + + const PATH_DUMP = '/dump/'; + const PATH_DATA = '/data/'; + + /** + * Configure command + */ + protected function configure() { + parent::configure(); + + $this->confArea = 'share'; + } + + /** + * Validate configuration + * + * @return boolean + */ + protected function validateConfiguration() { + $ret = parent::validateConfiguration(); + + // Rsync required for share + $ret = $ret && $this->validateConfigurationRsync(); + + return $ret; + } + +} diff --git a/src/app/CliTools/Console/Command/Sync/BackupCommand.php b/src/app/CliTools/Console/Command/Sync/BackupCommand.php new file mode 100644 index 0000000..5c261a2 --- /dev/null +++ b/src/app/CliTools/Console/Command/Sync/BackupCommand.php @@ -0,0 +1,131 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use CliTools\Shell\CommandBuilder\OutputCombineCommandBuilder; + +class BackupCommand extends AbstractShareCommand { + + /** + * Configure command + */ + protected function configure() { + parent::configure(); + + $this + ->setName('sync:backup') + ->setDescription('Backup files and database from share'); + } + + /** + * Startup task + */ + protected function startup() { + $this->output->writeln('

Starting share backup

'); + parent::startup(); + } + + /** + * Backup task + */ + protected function runMain() { + // ################## + // Option specific runners + // ################## + $runRsync = true; + $runMysql = true; + + if ($this->input->getOption('mysql') || $this->input->getOption('rsync')) { + // don't run rsync if not specifiecd + $runRsync = $this->input->getOption('rsync'); + + // don't run mysql if not specifiecd + $runMysql = $this->input->getOption('mysql'); + } + + // ################## + // Backup dirs + // ################## + if ($runRsync && $this->contextConfig->exists('rsync.directory')) { + $this->runTaskRsync(); + } + + // ################## + // Backup databases + // ################## + if ($runMysql && $this->contextConfig->exists('mysql.database')) { + $this->runTaskMysql(); + } + } + + /** + * Sync files with rsync + */ + protected function runTaskRsync() { + $source = $this->getRsyncWorkingPath(); + $target = $this->getRsyncPathFromConfig() . self::PATH_DATA; + + $command = $this->createRsyncCommandWithConfiguration($source, $target, 'rsync'); + $command->executeInteractive(); + } + + /** + * Sync database + */ + protected function runTaskMysql() { + // ################## + // Sync databases + // ################## + foreach ($this->contextConfig->getArray('mysql.database') as $database) { + // make sure we don't have any leading whitespaces + $database = trim($database); + + // dump database + $dumpFile = $this->tempDir . '/mysql/' . $database . '.dump'; + + // ########## + // Dump from server + // ########## + $this->output->writeln('

Dumping database "' . $database . '"

'); + + $mysqldump = $this->createLocalMySqlDumpCommand($database); + + if ($this->contextConfig->exists('mysql.filter')) { + $mysqldump = $this->addMysqlDumpFilterArguments($mysqldump, $database, false); + } + + $command = new OutputCombineCommandBuilder(); + $command->addCommandForCombinedOutput($mysqldump); + + $command + ->setOutputRedirectToFile($dumpFile) + ->executeInteractive(); + } + + // ################## + // Backup mysql dump + // ################## + $source = $this->tempDir; + $target = $this->getRsyncPathFromConfig() . self::PATH_DUMP; + $command = $this->createRsyncCommand($source, $target); + $command->executeInteractive(); + } +} diff --git a/src/app/CliTools/Console/Command/Sync/DeployCommand.php b/src/app/CliTools/Console/Command/Sync/DeployCommand.php new file mode 100644 index 0000000..12fff68 --- /dev/null +++ b/src/app/CliTools/Console/Command/Sync/DeployCommand.php @@ -0,0 +1,103 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use CliTools\Database\DatabaseConnection; + +class DeployCommand extends AbstractRemoteSyncCommand { + + /** + * Configure command + */ + protected function configure() { + parent::configure(); + + $this->confArea = 'deploy'; + + $this + ->setName('sync:deploy') + ->setDescription('Deploy files and database to server'); + } + + /** + * Startup task + */ + protected function startup() { + $this->output->writeln('

Starting server deployment

'); + parent::startup(); + } + + /** + * Backup task + */ + protected function runMain() { + // ################## + // Option specific runners + // ################## + $runRsync = true; + $runMysql = true; + + if ($this->input->getOption('mysql') || $this->input->getOption('rsync')) { + // don't run rsync if not specifiecd + $runRsync = $this->input->getOption('rsync'); + + // don't run mysql if not specifiecd + $runMysql = $this->input->getOption('mysql'); + } + + // ################## + // Run tasks + // ################## + + // Check database connection + if ($runMysql && $this->contextConfig->exists('mysql')) { + DatabaseConnection::ping(); + } + + // Sync files with rsync to local storage + if ($runRsync && $this->contextConfig->exists('rsync')) { + $this->output->writeln('

Starting FILE deployment

'); + $this->runTaskRsync(); + } + + // Sync database to local server + if ($runMysql && $this->contextConfig->exists('mysql')) { + $this->output->writeln('

Starting MYSQL deployment

'); + $this->output->writeln('

TODO - not implemented'); + } + } + + /** + * Sync files with rsync + */ + protected function runTaskRsync() { + // ################## + // Deploy dirs + // ################## + $source = $this->getRsyncWorkingPath(); + $target = $this->getRsyncPathFromConfig(); + + $command = $this->createRsyncCommandWithConfiguration($source, $target, 'rsync'); + + $command->executeInteractive(); + } + +} diff --git a/src/app/CliTools/Console/Command/Sync/InitCommand.php b/src/app/CliTools/Console/Command/Sync/InitCommand.php new file mode 100644 index 0000000..31cbf99 --- /dev/null +++ b/src/app/CliTools/Console/Command/Sync/InitCommand.php @@ -0,0 +1,75 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use CliTools\Utility\PhpUtility; +use CliTools\Shell\CommandBuilder\EditorCommandBuilder; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class InitCommand extends \CliTools\Console\Command\AbstractCommand { + + /** + * Configure command + */ + protected function configure() { + $this + ->setName('sync:init') + ->setDescription('Create example clisync.yml'); + } + + /** + * Execute command + * + * @param InputInterface $input Input instance + * @param OutputInterface $output Output instance + * + * @return int|null|void + * @throws \Exception + */ + public function execute(InputInterface $input, OutputInterface $output) { + $cliSyncFilePath = getcwd() . '/' . AbstractCommand::CONFIG_FILE; + + if (file_exists($cliSyncFilePath)) { + $this->output->writeln('Configuration file ' . AbstractCommand::CONFIG_FILE . ' already exists'); + return 1; + } + + // fetch example + $content = PhpUtility::fileGetContents(CLITOOLS_ROOT_FS . '/conf/clisync.yml'); + + // store in current working dir + PhpUtility::filePutContents($cliSyncFilePath, $content); + + // Start editor with file (if $EDITOR is set) + try { + $editor = new EditorCommandBuilder(); + $editor + ->addArgument($cliSyncFilePath) + ->executeInteractive(); + } catch (\Exception $e) { + $this->output->writeln('' . $e->getMessage() . ''); + } + + $this->output->writeln('Successfully created ' . AbstractCommand::CONFIG_FILE . ' '); + } + +} diff --git a/src/app/CliTools/Console/Command/Sync/RestoreCommand.php b/src/app/CliTools/Console/Command/Sync/RestoreCommand.php new file mode 100644 index 0000000..c2699c6 --- /dev/null +++ b/src/app/CliTools/Console/Command/Sync/RestoreCommand.php @@ -0,0 +1,113 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +class RestoreCommand extends AbstractShareCommand { + + /** + * Configure command + */ + protected function configure() { + parent::configure(); + + $this + ->setName('sync:restore') + ->setDescription('Restore files and database from share'); + } + + /** + * Startup task + */ + protected function startup() { + $this->output->writeln('

Starting share restore

'); + parent::startup(); + } + + /** + * Restore task + */ + protected function runMain() { + // ################## + // Option specific runners + // ################## + $runRsync = true; + $runMysql = true; + + if ($this->input->getOption('mysql') || $this->input->getOption('rsync')) { + // don't run rsync if not specifiecd + $runRsync = $this->input->getOption('rsync'); + + // don't run mysql if not specifiecd + $runMysql = $this->input->getOption('mysql'); + } + + // ################## + // Restore dirs + // ################## + if ($runRsync && $this->contextConfig->exists('rsync.directory')) { + $this->runTaskRsync(); + } + + // ################## + // Restore mysql dump + // ################## + if ($runMysql) { + $this->runTaskMysql(); + } + } + + /** + * Sync files with rsync + */ + protected function runTaskRsync() { + $source = $this->getRsyncPathFromConfig() . self::PATH_DATA; + $target = $this->getRsyncWorkingPath(); + + $command = $this->createRsyncCommandWithConfiguration($source, $target, 'rsync'); + $command->executeInteractive(); + } + + /** + * Sync files with mysql + */ + protected function runTaskMysql() { + $source = $this->getRsyncPathFromConfig() . self::PATH_DUMP; + $target = $this->tempDir; + $command = $this->createRsyncCommand($source, $target); + $command->executeInteractive(); + + $iterator = new \DirectoryIterator($this->tempDir . '/mysql'); + foreach ($iterator as $item) { + // skip dot + if ($item->isDot()) { + continue; + } + + list($database) = explode('.', $item->getFilename(), 2); + + if (!empty($database)) { + $this->output->writeln('

Restoring database ' . $database . '

'); + + $this->createMysqlRestoreCommand($database, $item->getPathname())->executeInteractive(); + } + } + } +} diff --git a/src/app/CliTools/Console/Command/Sync/ServerCommand.php b/src/app/CliTools/Console/Command/Sync/ServerCommand.php new file mode 100644 index 0000000..27a0988 --- /dev/null +++ b/src/app/CliTools/Console/Command/Sync/ServerCommand.php @@ -0,0 +1,176 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use CliTools\Database\DatabaseConnection; + +class ServerCommand extends AbstractRemoteSyncCommand { + + /** + * Configure command + */ + protected function configure() { + parent::configure(); + + $this->confArea = 'sync'; + + $this + ->setName('sync:server') + ->setDescription('Sync files and database from server'); + } + + /** + * Startup task + */ + protected function startup() { + $this->output->writeln('

Starting server synchronization

'); + parent::startup(); + } + + /** + * Validate configuration + * + * @return boolean + */ + protected function validateConfiguration() { + $ret = parent::validateConfiguration(); + + $output = $this->output; + + // ################## + // SSH (optional) + // ################## + + if ($this->contextConfig->exists('ssh')) { + // Check if one database is configured + if (!$this->contextConfig->exists('ssh.hostname')) { + $output->writeln('No ssh hostname configuration found'); + $ret = false; + } + } + + return $ret; + } + + /** + * Backup task + */ + protected function runMain() { + // ################## + // Option specific runners + // ################## + $runRsync = true; + $runMysql = true; + + if ($this->input->getOption('mysql') || $this->input->getOption('rsync')) { + // don't run rsync if not specifiecd + $runRsync = $this->input->getOption('rsync'); + + // don't run mysql if not specifiecd + $runMysql = $this->input->getOption('mysql'); + } + + // ################## + // Run tasks + // ################## + + // Check database connection + if ($runMysql && $this->contextConfig->exists('mysql')) { + DatabaseConnection::ping(); + } + + // Sync files with rsync to local storage + if ($runRsync && $this->contextConfig->exists('rsync')) { + $this->output->writeln('

Starting FILE sync

'); + $this->runTaskRsync(); + } + + // Sync database to local server + if ($runMysql && $this->contextConfig->exists('mysql')) { + $this->output->writeln('

Starting MYSQL sync

'); + $this->runTaskDatabase(); + } + } + + /** + * Sync files with rsync + */ + protected function runTaskRsync() { + // ################## + // Restore dirs + // ################## + $source = $this->getRsyncPathFromConfig(); + $target = $this->getRsyncWorkingPath(); + + $command = $this->createRsyncCommandWithConfiguration($source, $target, 'rsync'); + $command->executeInteractive(); + } + + /** + * Sync database + */ + protected function runTaskDatabase() { + // ################## + // Sync databases + // ################## + foreach ($this->contextConfig->getArray('mysql.database') as $databaseConf) { + if (strpos($databaseConf, ':') !== false) { + // local and foreign database in one string + list($localDatabase, $foreignDatabase) = explode(':', $databaseConf, 2); + } else { + // database equal + $localDatabase = $databaseConf; + $foreignDatabase = $databaseConf; + } + + // make sure we don't have any leading whitespaces + $localDatabase = trim($localDatabase); + $foreignDatabase = trim($foreignDatabase); + + $dumpFile = $this->tempDir . '/' . $localDatabase . '.sql.dump'; + + // ########## + // Dump from server + // ########## + $this->output->writeln('

Fetching foreign database "' . $foreignDatabase . '"

'); + + $mysqldump = $this->createRemoteMySqlDumpCommand($foreignDatabase); + + if ($this->contextConfig->exists('mysql.filter')) { + $mysqldump = $this->addMysqlDumpFilterArguments($mysqldump, $foreignDatabase, $this->contextConfig->get('mysql.filter')); + } + + $command = $this->wrapRemoteCommand($mysqldump); + $command->setOutputRedirectToFile($dumpFile); + + $command->executeInteractive(); + + // ########## + // Restore local + // ########## + $this->output->writeln('

Restoring database "' . $localDatabase . '"

'); + + $this->createMysqlRestoreCommand($localDatabase, $dumpFile)->executeInteractive(); + } + } + + +} diff --git a/src/app/CliTools/Console/Command/System/BannerCommand.php b/src/app/CliTools/Console/Command/System/BannerCommand.php index bdf498c..93f7027 100644 --- a/src/app/CliTools/Console/Command/System/BannerCommand.php +++ b/src/app/CliTools/Console/Command/System/BannerCommand.php @@ -31,7 +31,8 @@ class BannerCommand extends \CliTools\Console\Command\AbstractCommand implements * Configure command */ protected function configure() { - $this->setName('system:banner') + $this + ->setName('system:banner') ->setDescription('Banner generator for /etc/issue'); } diff --git a/src/app/CliTools/Console/Command/System/CrontaskCommand.php b/src/app/CliTools/Console/Command/System/CrontaskCommand.php index f6e0865..df3671c 100644 --- a/src/app/CliTools/Console/Command/System/CrontaskCommand.php +++ b/src/app/CliTools/Console/Command/System/CrontaskCommand.php @@ -21,7 +21,7 @@ */ use CliTools\Utility\UnixUtility; -use CliTools\Console\Builder\SelfCommandBuilder; +use CliTools\Shell\CommandBuilder\SelfCommandBuilder; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -38,7 +38,8 @@ class CrontaskCommand extends \CliTools\Console\Command\AbstractCommand implemen * Configure command */ protected function configure() { - $this->setName('system:crontask') + $this + ->setName('system:crontask') ->setDescription('System cron task'); } @@ -73,6 +74,8 @@ protected function setupBanner() { file_put_contents('/etc/issue', $outputIssue); file_put_contents('/etc/motd', $output); + + UnixUtility::reloadTtyBanner('tty1'); } /** diff --git a/src/app/CliTools/Console/Command/System/EnvCommand.php b/src/app/CliTools/Console/Command/System/EnvCommand.php index cc988dd..af76e5b 100644 --- a/src/app/CliTools/Console/Command/System/EnvCommand.php +++ b/src/app/CliTools/Console/Command/System/EnvCommand.php @@ -30,7 +30,8 @@ class EnvCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('system:env') + $this + ->setName('system:env') ->setDescription('List environment variables'); } diff --git a/src/app/CliTools/Console/Command/System/OpenFilesCommand.php b/src/app/CliTools/Console/Command/System/OpenFilesCommand.php index 999f82e..5979602 100644 --- a/src/app/CliTools/Console/Command/System/OpenFilesCommand.php +++ b/src/app/CliTools/Console/Command/System/OpenFilesCommand.php @@ -21,7 +21,7 @@ */ use CliTools\Utility\FormatUtility; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; @@ -33,7 +33,8 @@ class OpenFilesCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('system:openfiles') + $this + ->setName('system:openfiles') ->setDescription('List swap usage'); } diff --git a/src/app/CliTools/Console/Command/System/RebootCommand.php b/src/app/CliTools/Console/Command/System/RebootCommand.php index 06c1c1b..36bbc0c 100644 --- a/src/app/CliTools/Console/Command/System/RebootCommand.php +++ b/src/app/CliTools/Console/Command/System/RebootCommand.php @@ -22,7 +22,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; class RebootCommand extends \CliTools\Console\Command\AbstractCommand { @@ -30,7 +30,8 @@ class RebootCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('system:reboot') + $this + ->setName('system:reboot') ->setAliases(array('reboot')) ->setDescription('Reboot system'); } diff --git a/src/app/CliTools/Console/Command/System/ShutdownCommand.php b/src/app/CliTools/Console/Command/System/ShutdownCommand.php index 88938ae..317ca11 100644 --- a/src/app/CliTools/Console/Command/System/ShutdownCommand.php +++ b/src/app/CliTools/Console/Command/System/ShutdownCommand.php @@ -22,7 +22,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; class ShutdownCommand extends \CliTools\Console\Command\AbstractCommand { @@ -30,7 +30,8 @@ class ShutdownCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('system:shutdown') + $this + ->setName('system:shutdown') ->setAliases(array('shutdown')) ->setDescription('Shutdown system'); } diff --git a/src/app/CliTools/Console/Command/System/StartupCommand.php b/src/app/CliTools/Console/Command/System/StartupCommand.php index cd96e97..f6dcaaa 100644 --- a/src/app/CliTools/Console/Command/System/StartupCommand.php +++ b/src/app/CliTools/Console/Command/System/StartupCommand.php @@ -20,9 +20,10 @@ * along with this program. If not, see . */ +use CliTools\Utility\UnixUtility; use CliTools\Database\DatabaseConnection; -use CliTools\Console\Builder\CommandBuilder; -use CliTools\Console\Builder\SelfCommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; +use CliTools\Shell\CommandBuilder\SelfCommandBuilder; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -32,7 +33,8 @@ class StartupCommand extends \CliTools\Console\Command\AbstractCommand implement * Configure command */ protected function configure() { - $this->setName('system:startup') + $this + ->setName('system:startup') ->setDescription('System startup task'); } @@ -64,6 +66,8 @@ protected function setupBanner() { file_put_contents('/etc/issue', $outputIssue); file_put_contents('/etc/motd', $output); + + UnixUtility::reloadTtyBanner('tty1'); } /** diff --git a/src/app/CliTools/Console/Command/System/SwapCommand.php b/src/app/CliTools/Console/Command/System/SwapCommand.php index baf24ad..d0db100 100644 --- a/src/app/CliTools/Console/Command/System/SwapCommand.php +++ b/src/app/CliTools/Console/Command/System/SwapCommand.php @@ -33,7 +33,8 @@ class SwapCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('system:swap') + $this + ->setName('system:swap') ->setDescription('List swap usage'); } diff --git a/src/app/CliTools/Console/Command/System/UpdateCommand.php b/src/app/CliTools/Console/Command/System/UpdateCommand.php index 71b0637..c39bcb3 100644 --- a/src/app/CliTools/Console/Command/System/UpdateCommand.php +++ b/src/app/CliTools/Console/Command/System/UpdateCommand.php @@ -21,8 +21,8 @@ */ use CliTools\Service\SelfUpdateService; -use CliTools\Console\Builder\CommandBuilder; -use CliTools\Console\Builder\SelfCommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; +use CliTools\Shell\CommandBuilder\SelfCommandBuilder; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -32,7 +32,8 @@ class UpdateCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('system:update') + $this + ->setName('system:update') ->setAliases(array('update')) ->setDescription('Update system'); } @@ -86,7 +87,7 @@ protected function userUpdate(InputInterface $input, OutputInterface $output) { $command = new CommandBuilder('git', 'pull'); $command->executeInteractive(); - $command = new \CliTools\Console\Builder\SelfCommandBuilder(); + $command = new \CliTools\Shell\CommandBuilder\SelfCommandBuilder(); $command->addArgument('user:rebuildsshconfig'); $command->executeInteractive(); } catch (\RuntimeException $e) { diff --git a/src/app/CliTools/Console/Command/System/VersionCommand.php b/src/app/CliTools/Console/Command/System/VersionCommand.php index a41a181..c0205b4 100644 --- a/src/app/CliTools/Console/Command/System/VersionCommand.php +++ b/src/app/CliTools/Console/Command/System/VersionCommand.php @@ -21,7 +21,7 @@ */ use CliTools\Database\DatabaseConnection; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; use CliTools\Utility\UnixUtility; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; @@ -33,7 +33,8 @@ class VersionCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('system:version') + $this + ->setName('system:version') ->setDescription('List common version'); } diff --git a/src/app/CliTools/Console/Command/TYPO3/BeUserCommand.php b/src/app/CliTools/Console/Command/TYPO3/BeUserCommand.php index f1aa756..631c29b 100644 --- a/src/app/CliTools/Console/Command/TYPO3/BeUserCommand.php +++ b/src/app/CliTools/Console/Command/TYPO3/BeUserCommand.php @@ -33,7 +33,8 @@ class BeUserCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('typo3:beuser') + $this + ->setName('typo3:beuser') ->setDescription('Add backend admin user to database') ->addArgument( 'database', @@ -74,6 +75,8 @@ public function execute(InputInterface $input, OutputInterface $output) { $username = $input->getArgument('user'); $password = $input->getArgument('password'); + $output->writeln('

Injecting TYPO3 backend user

'); + // Set default user if not specified if (empty($username)) { $username = 'dev'; @@ -81,18 +84,18 @@ public function execute(InputInterface $input, OutputInterface $output) { // check username if (!preg_match('/^[-_a-zA-Z0-9\.]+$/', $username)) { - $output->writeln('Invalid username'); + $output->writeln('Invalid username'); return 1; } - $output->writeln('Using user: "' . htmlspecialchars($username) . '"'); + $output->writeln('

Using user: "' . htmlspecialchars($username) . '"

'); // Set default password if not specified if (empty($password)) { $password = 'dev'; } - $output->writeln('Using pass: "' . htmlspecialchars($password) . '"'); + $output->writeln('

Using pass: "' . htmlspecialchars($password) . '"

'); // ################## // Salting @@ -101,11 +104,11 @@ public function execute(InputInterface $input, OutputInterface $output) { if ($input->getOption('plain')) { // Standard md5 $password = Typo3Utility::generatePassword($password, Typo3Utility::PASSWORD_TYPE_MD5); - $this->output->writeln('Generating plain (non salted) md5 password'); + $this->output->writeln('

Generating plain (non salted) md5 password

'); } else { // Salted md5 $password = Typo3Utility::generatePassword($password, Typo3Utility::PASSWORD_TYPE_MD5_SALTED); - $this->output->writeln('Generating salted md5 password'); + $this->output->writeln('

Generating salted md5 password

'); } // ############## @@ -117,20 +120,12 @@ public function execute(InputInterface $input, OutputInterface $output) { // All databases // ############## - // Get list of databases - $query = 'SELECT SCHEMA_NAME - FROM information_schema.SCHEMATA'; - $databaseList = DatabaseConnection::getCol($query); + $databaseList = DatabaseConnection::databaseList(); $dbFound = false; foreach ($databaseList as $dbName) { - // Skip internal mysql databases - if (in_array(strtolower($dbName), array('mysql', 'information_schema', 'performance_schema'))) { - continue; - } - // Check if database is TYPO3 instance - $query = 'SELECT COUNT(*) as count + $query = 'SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = ' . DatabaseConnection::quote($dbName) . ' AND table_name = \'be_users\''; @@ -143,7 +138,7 @@ public function execute(InputInterface $input, OutputInterface $output) { } if (!$dbFound) { - $output->writeln('No valid TYPO3 database found'); + $output->writeln('No valid TYPO3 database found'); } } else { // ############## @@ -206,13 +201,13 @@ protected function setTypo3UserForDatabase($database, $username, $password) { try { // Get uid from current dev user (if already existing) $query = 'SELECT uid - FROM `' . DatabaseConnection::sanitizeSqlDatabase($database) . '`.be_users + FROM ' . DatabaseConnection::sanitizeSqlDatabase($database) . '.be_users WHERE username = ' . DatabaseConnection::quote($username) . ' AND deleted = 0'; $beUserId = DatabaseConnection::getOne($query); // Insert or update user in TYPO3 database - $query = 'INSERT INTO `' . DatabaseConnection::sanitizeSqlDatabase($database) . '`.be_users + $query = 'INSERT INTO ' . DatabaseConnection::sanitizeSqlDatabase($database) . '.be_users (uid, tstamp, crdate, realName, username, password, TSconfig, admin, disable, starttime, endtime) VALUES( ' . DatabaseConnection::quote($beUserId) . ', @@ -236,12 +231,12 @@ protected function setTypo3UserForDatabase($database, $username, $password) { DatabaseConnection::exec($query); if ($beUserId) { - $this->output->writeln('User successfully updated to "' . $database . '"'); + $this->output->writeln('

User successfully updated to "' . $database . '"

'); } else { - $this->output->writeln('User successfully added to "' . $database . '"'); + $this->output->writeln('

User successfully added to "' . $database . '"

'); } } catch (\Exception $e) { - $this->output->writeln('User adding failed'); + $this->output->writeln('User adding failed'); } } } diff --git a/src/app/CliTools/Console/Command/TYPO3/CleanupCommand.php b/src/app/CliTools/Console/Command/TYPO3/CleanupCommand.php index 056ee58..a8bbdb6 100644 --- a/src/app/CliTools/Console/Command/TYPO3/CleanupCommand.php +++ b/src/app/CliTools/Console/Command/TYPO3/CleanupCommand.php @@ -31,7 +31,8 @@ class CleanupCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('typo3:cleanup') + $this + ->setName('typo3:cleanup') ->setDescription('Cleanup caches, logs and indexed search') ->addArgument( 'db', @@ -54,6 +55,8 @@ public function execute(InputInterface $input, OutputInterface $output) { // ################## $dbName = $input->getArgument('db'); + $output->writeln('

Cleanup TYPO3 database

'); + // ############## // Loop through databases // ############## @@ -104,10 +107,7 @@ protected function cleanupTypo3Database($database) { $cleanupTableList = array(); // Check if database is TYPO3 instance - $query = 'SELECT table_name - FROM information_schema.tables - WHERE table_schema = ' . DatabaseConnection::quote($database); - $tableList = DatabaseConnection::getCol($query); + $tableList = DatabaseConnection::tableList($database); foreach ($tableList as $table) { $clearTable = false; @@ -165,19 +165,19 @@ protected function cleanupTypo3Database($database) { } } - $this->output->writeln('Starting cleanup of database ' . $database . '...'); + $this->output->writeln('

Starting cleanup of database "' . $database . '"

'); - DatabaseConnection::exec('USE `' . $database . '`'); + DatabaseConnection::switchDatabase(DatabaseConnection::sanitizeSqlDatabase($database)); foreach ($cleanupTableList as $table) { - $query = 'TRUNCATE `' . $table . '`'; + $query = 'TRUNCATE ' . DatabaseConnection::sanitizeSqlTable($table); DatabaseConnection::exec($query); if ($this->output->isVerbose()) { - $this->output->writeln(' -> Truncating table ' . $table . ''); + $this->output->writeln('

Truncating table ' . $table . '

'); } } - $this->output->writeln(' -> finished'); + $this->output->writeln('

finished

'); } } diff --git a/src/app/CliTools/Console/Command/TYPO3/ClearCacheCommand.php b/src/app/CliTools/Console/Command/TYPO3/ClearCacheCommand.php index b7ec986..f5c161c 100644 --- a/src/app/CliTools/Console/Command/TYPO3/ClearCacheCommand.php +++ b/src/app/CliTools/Console/Command/TYPO3/ClearCacheCommand.php @@ -21,7 +21,7 @@ */ use CliTools\Utility\Typo3Utility; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -32,7 +32,8 @@ class ClearCacheCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('typo3:clearcache') + $this + ->setName('typo3:clearcache') ->setDescription('Clear cache on all (or one specific) TYPO3 instances') ->addArgument( 'path', @@ -56,6 +57,8 @@ public function execute(InputInterface $input, OutputInterface $output) { $basePath = $this->getApplication()->getConfigValue('config', 'www_base_path', '/var/www/'); $maxDepth = 3; + $output->writeln('

Clear TYPO3 cache

'); + if ($input->getArgument('path')) { $basePath = $input->getArgument('path'); } @@ -67,7 +70,7 @@ public function execute(InputInterface $input, OutputInterface $output) { // Check if coreapi is installed if (!is_dir($dirPath . '/typo3conf/ext/coreapi/')) { - $output->writeln('EXT:coreapi is missing on ' . $dirPath . ', skipping'); + $output->writeln('

EXT:coreapi is missing on ' . $dirPath . ', skipping

'); continue; } @@ -77,14 +80,14 @@ public function execute(InputInterface $input, OutputInterface $output) { 'cache:clearallcaches' ); - $output->writeln('Running clearcache command on ' . $dirPath . ''); + $output->writeln('

Running clearcache command on ' . $dirPath . '

'); try { $command = new CommandBuilder('php'); $command->setArgumentList($params) ->executeInteractive(); } catch (\Exception $e) { - $output->writeln(' Failed with exception: ' . $e->getMessage() . ''); + $output->writeln(' Failed with exception: ' . $e->getMessage() . ''); } } diff --git a/src/app/CliTools/Console/Command/TYPO3/DomainCommand.php b/src/app/CliTools/Console/Command/TYPO3/DomainCommand.php index 63b8a80..321423e 100644 --- a/src/app/CliTools/Console/Command/TYPO3/DomainCommand.php +++ b/src/app/CliTools/Console/Command/TYPO3/DomainCommand.php @@ -21,6 +21,7 @@ */ use CliTools\Database\DatabaseConnection; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -31,12 +32,37 @@ class DomainCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('typo3:domain') + $this + ->setName('typo3:domain') ->setDescription('Add common development domains to database') ->addArgument( 'db', InputArgument::OPTIONAL, 'Database name' + ) + ->addOption( + 'baseurl', + null, + InputOption::VALUE_NONE, + 'Also set config.baseURL setting' + ) + ->addOption( + 'list', + null, + InputOption::VALUE_NONE, + 'List only databases' + ) + ->addOption( + 'remove', + null, + InputOption::VALUE_REQUIRED, + 'Remove domain (with wildcard support)' + ) + ->addOption( + 'duplicate', + null, + InputOption::VALUE_REQUIRED, + 'Add duplication domains (will duplicate all domains in system, eg. for vagrant share)' ); } @@ -54,6 +80,8 @@ public function execute(InputInterface $input, OutputInterface $output) { // ################## $dbName = $input->getArgument('db'); + $output->writeln('

Updating TYPO3 domain entries

'); + // ############## // Loop through databases // ############## @@ -62,134 +90,168 @@ public function execute(InputInterface $input, OutputInterface $output) { // ############## // All databases // ############## - - // Get list of databases - $query = 'SELECT SCHEMA_NAME - FROM information_schema.SCHEMATA'; - $databaseList = DatabaseConnection::getCol($query); + $databaseList = DatabaseConnection::databaseList(); foreach ($databaseList as $dbName) { - // Skip internal mysql databases - if (in_array(strtolower($dbName), array('mysql', 'information_schema', 'performance_schema'))) { - continue; - } - // Check if database is TYPO3 instance - $query = 'SELECT COUNT(*) as count + $query = 'SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = ' . DatabaseConnection::quote($dbName) . ' AND table_name = \'sys_domain\''; $isTypo3Database = DatabaseConnection::getOne($query); if ($isTypo3Database) { - $this->setupDevelopmentDomainsForDatabase($dbName); + $this->runTaskForDomain($dbName); } } } else { // ############## // One databases // ############## - $this->setupDevelopmentDomainsForDatabase($dbName); + $this->runTaskForDomain($dbName); } - - return 0; } /** - * Set development domains for TYPO3 database - * - * @param string $database Database - * - * @return void + * Run tasks for one domain */ - protected function setupDevelopmentDomainsForDatabase($database) { - - // ################## - // Build domain - // ################## - $domain = null; + protected function runTaskForDomain($dbName) { + DatabaseConnection::switchDatabase($dbName); - if (preg_match('/^([^_]+)_([^_]+).*/i', $database, $matches)) { - $domain = $matches[2] . '.' . $matches[1]; + if ($this->input->getOption('list')) { + // Show domain list (and skip all other tasks) + $this->showDomainList($dbName); } else { - return false; - } + // Remove domains (eg. for cleanup) + if ($this->input->getOption('remove')) { + $this->removeDomains($this->input->getOption('remove')); + } - // ################## - // Check if multi site - // ################## - $isMultiSite = false; + // Set development domains + $this->manipulateDomains(); - $query = 'SELECT uid - FROM ' . DatabaseConnection::sanitizeSqlDatabase($database) . '.pages - WHERE is_siteroot = 1 - AND deleted = 0'; - $rootPageSiteList = DatabaseConnection::getCol($query); + // Add sharing domains + if ($this->input->getOption('baseurl')) { + $this->updateBaseUrlConfig(); + } + + // Add sharing domains + if ($this->input->getOption('duplicate')) { + $this->addDuplicateDomains($this->input->getOption('duplicate')); + } - if (count($rootPageSiteList) >= 2) { - $isMultiSite = true; + // Show domain list + $this->showDomainList($dbName); } + } - // ################## - // Disable all other domains - // ################## - $query = 'UPDATE ' . DatabaseConnection::sanitizeSqlDatabase($database) . '.sys_domain - SET hidden = 1'; + /** + * Remove domains + */ + protected function removeDomains($pattern) { + $pattern = str_replace('*', '%', $pattern); + + $query = 'DELETE FROM sys_domain WHERE domainName LIKE %s'; + $query = sprintf($query, DatabaseConnection::quote($pattern)); DatabaseConnection::exec($query); + } + /** + * Update baseURL config + */ + protected function updateBaseUrlConfig() { + $query = 'SELECT st.uid as template_id, + st.config as template_config, + (SELECT sd.domainName + FROM sys_domain sd + WHERE sd.pid = st.pid + ORDER BY sd.forced DESC, + sd.sorting ASC + LIMIT 1) as domain_name + FROM sys_template st + WHERE st.root = 1 + AND st.deleted = 0 + HAVING domain_name IS NOT NULL'; + $templateIdList = DatabaseConnection::getAll($query); - // Get development domains from config - $tldList = (array)$this->getApplication()->getConfigValue('config', 'domain_dev', array()); + foreach ($templateIdList as $row) { + $templateId = $row['template_id']; + $domainName = $row['domain_name']; + $templateConf = $row['template_config']; - foreach ($tldList as $tld) { - $fullDomain = $domain . '.' . $tld; + // Remove old baseURL entries (no duplciates) + $templateConf = preg_replace('/^config.baseURL = .*$/m', '', $templateConf); + $templateConf = trim($templateConf); - // ############## - // Loop through root pages - // ############## - foreach ($rootPageSiteList as $rootPageUid) { - $rootPageDomain = $fullDomain; + // Add new baseURL + $templateConf .= "\n" . 'config.baseURL = http://' . $domainName .'/'; - // Add rootpage id to domain if TYPO3 instance is multi page - // eg. 123.dev.foobar.dev - if ($isMultiSite) { - $rootPageDomain = $rootPageUid . '.' . $rootPageDomain; - } + $query = 'UPDATE sys_template SET config = %s WHERE uid = %s'; + $query = sprintf($query, DatabaseConnection::quote($templateConf), (int)$templateId); + DatabaseConnection::exec($query); + } + } - // Check if we have already an entry - $query = 'SELECT uid - FROM ' . DatabaseConnection::sanitizeSqlDatabase($database) . '.sys_domain - WHERE pid = ' . (int)$rootPageUid . ' - AND domainName = ' . DatabaseConnection::quote($rootPageDomain); - $sysDomainId = DatabaseConnection::getOne($query); - - // Add/Update domain - $query = 'INSERT INTO ' . DatabaseConnection::sanitizeSqlDatabase($database) . '.sys_domain - (uid, pid, tstamp, crdate, cruser_id, hidden, domainName, sorting, forced) - VALUES ( - ' . (int)$sysDomainId . ', - ' . (int)$rootPageUid . ', - ' . time() . ', - ' . time() . ', - 1, - 0, - ' . DatabaseConnection::quote($rootPageDomain) . ', - 1, - 1 - ) ON DUPLICATE KEY UPDATE - pid = VALUES(pid), - hidden = VALUES(hidden), - domainName = VALUES(domainName), - sorting = VALUES(sorting), - forced = VALUES(forced)'; - DatabaseConnection::exec($query); - - if ($sysDomainId) { - $this->output->writeln('Domain "' . $rootPageDomain . '" updated to "' . $database . '"'); - } else { - $this->output->writeln('Domain "' . $rootPageDomain . '" added to "' . $database . '"'); - } - } + /** + * Add share domains (eg. for vagrantshare) + * + * @param string $suffix Domain suffix + */ + protected function addDuplicateDomains($suffix) { + $devDomain = '.' . $this->getApplication()->getConfigValue('config', 'domain_dev'); + + $query = 'SELECT * FROM sys_domain'; + $domainList = DatabaseConnection::getAll($query); + + foreach ($domainList as $domain) { + unset($domain['uid']); + + $domainName = $domain['domainName']; + + // remove development suffix + $domainName = preg_replace('/' . preg_quote($devDomain). '$/', '', $domainName); + + // add share domain + $domainName .= '.' . ltrim($suffix, '.'); + + $domain['domainName'] = $domainName; + + DatabaseConnection::insert('sys_domain', $domain); + } + } + + /** + * Show list of domains + * + * @param string $dbName Domain name + */ + protected function showDomainList($dbName) { + $query = 'SELECT domainName FROM sys_domain ORDER BY domainName ASC'; + $domainList = DatabaseConnection::getCol($query); + + $this->output->writeln('

Domain list of "' . $dbName . '":

'); + + foreach ($domainList as $domain) { + $this->output->writeln('

' . $domain . '

'); } + $this->output->writeln(''); + } + + /** + * Set development domains for TYPO3 database + * + * @return void + */ + protected function manipulateDomains() { + $devDomain = '.' . $this->getApplication()->getConfigValue('config', 'domain_dev'); + $domainLength = strlen($devDomain); + + // ################## + // Fix domains + // ################## + $query = 'UPDATE sys_domain + SET domainName = CONCAT(domainName, ' . DatabaseConnection::quote($devDomain) . ') + WHERE RIGHT(domainName, ' . $domainLength . ') <> ' . DatabaseConnection::quote($devDomain); + DatabaseConnection::exec($query); } } diff --git a/src/app/CliTools/Console/Command/TYPO3/InstallerCommand.php b/src/app/CliTools/Console/Command/TYPO3/InstallerCommand.php index 3827dd7..c827bfd 100644 --- a/src/app/CliTools/Console/Command/TYPO3/InstallerCommand.php +++ b/src/app/CliTools/Console/Command/TYPO3/InstallerCommand.php @@ -32,7 +32,8 @@ class InstallerCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('typo3:installer') + $this + ->setName('typo3:installer') ->setDescription('Enable installer on all (or one specific) TYPO3 instances') ->addArgument( 'path', diff --git a/src/app/CliTools/Console/Command/TYPO3/ListCommand.php b/src/app/CliTools/Console/Command/TYPO3/ListCommand.php index d77a071..9b141aa 100644 --- a/src/app/CliTools/Console/Command/TYPO3/ListCommand.php +++ b/src/app/CliTools/Console/Command/TYPO3/ListCommand.php @@ -32,7 +32,8 @@ class ListCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('typo3:list') + $this + ->setName('typo3:list') ->setDescription('List all TYPO3 instances') ->addArgument( 'path', diff --git a/src/app/CliTools/Console/Command/TYPO3/SchedulerCommand.php b/src/app/CliTools/Console/Command/TYPO3/SchedulerCommand.php index ceca306..83f5d66 100644 --- a/src/app/CliTools/Console/Command/TYPO3/SchedulerCommand.php +++ b/src/app/CliTools/Console/Command/TYPO3/SchedulerCommand.php @@ -21,7 +21,7 @@ */ use CliTools\Utility\Typo3Utility; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -32,13 +32,14 @@ class SchedulerCommand extends \CliTools\Console\Command\AbstractCommand { * Configure command */ protected function configure() { - $this->setName('typo3:scheduler') + $this + ->setName('typo3:scheduler') ->setDescription('Run scheduler on all (or one specific) TYPO3 instances') ->addArgument( 'path', InputArgument::OPTIONAL, 'Path to TYPO3 instance' - ); +); } /** diff --git a/src/app/CliTools/Console/Command/User/RebuildSshConfigCommand.php b/src/app/CliTools/Console/Command/User/RebuildSshConfigCommand.php index 8e9fb84..8adcf21 100644 --- a/src/app/CliTools/Console/Command/User/RebuildSshConfigCommand.php +++ b/src/app/CliTools/Console/Command/User/RebuildSshConfigCommand.php @@ -29,7 +29,8 @@ class RebuildSshConfigCommand extends \CliTools\Console\Command\AbstractCommand * Configure command */ protected function configure() { - $this->setName('user:rebuildsshconfig') + $this + ->setName('user:rebuildsshconfig') ->setDescription('Rebuild SSH Config for current user'); } @@ -45,7 +46,7 @@ public function execute(InputInterface $input, OutputInterface $output) { $userHome = getenv('HOME'); $userName = getenv('USER'); - $output->writeln('Rebuilding ~/.ssh/config ...'); + $output->writeln('

Rebuilding ~/.ssh/config ...

'); $targetConfFile = $userHome . '/.ssh/config'; $userConfFile = $userHome . '/.ssh/config.user'; @@ -77,13 +78,13 @@ public function execute(InputInterface $input, OutputInterface $output) { $confContent[] = '# from: ' . $defaultUserFile; $confContent[] = file_get_contents($defaultUserFile); - $output->writeln('Using user defaults from ' . $defaultUserFile . ''); + $output->writeln('

Using user defaults from ' . $defaultUserFile . '

'); } elseif (file_exists($defaultFile)) { // System default $confContent[] = '# from: ' . $defaultFile; $confContent[] = file_get_contents($defaultFile); - $output->writeln('Using system defaults from ' . $defaultFile . ''); + $output->writeln('

Using system defaults from ' . $defaultFile . '

'); } else { // No default found, provide at least good defaults $confContent[] = '# from: no config'; @@ -95,7 +96,7 @@ public function execute(InputInterface $input, OutputInterface $output) { $confContent[] = ' ServerAliveInterval 60'; $confContent[] = ' ForwardAgent no'; - $output->writeln('No defaults found, setting internal defaults'); + $output->writeln('

No defaults found, setting internal defaults

'); } $confContent[] = ''; @@ -131,7 +132,7 @@ public function execute(InputInterface $input, OutputInterface $output) { $confContent[] = file_get_contents($filePath); $confContent[] = ''; - $output->writeln('Using ' . $filePath . ''); + $output->writeln('

Using ' . $filePath . '

'); } } @@ -146,7 +147,7 @@ public function execute(InputInterface $input, OutputInterface $output) { if (file_exists($userConfFile)) { $confContent[] = file_get_contents($userConfFile); - $output->writeln('Using ' . $userConfFile . ''); + $output->writeln('

Using ' . $userConfFile . '

'); } $confContent[] = ''; @@ -166,7 +167,7 @@ public function execute(InputInterface $input, OutputInterface $output) { // Write file file_put_contents($targetConfFile, $confContent); - $output->writeln('Finished rebuilding ssh configuration'); + $output->writeln('

Finished rebuilding ssh configuration

'); return 0; } diff --git a/src/app/CliTools/Console/Command/Vagrant/ShareCommand.php b/src/app/CliTools/Console/Command/Vagrant/ShareCommand.php new file mode 100644 index 0000000..2c97fa3 --- /dev/null +++ b/src/app/CliTools/Console/Command/Vagrant/ShareCommand.php @@ -0,0 +1,187 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use CliTools\Shell\CommandBuilder\CommandBuilder; +use CliTools\Shell\CommandBuilder\SelfCommandBuilder; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; + +class ShareCommand extends \CliTools\Console\Command\AbstractCommand { + + /** + * Configure command + */ + protected function configure() { + $this + ->setName('vagrant:share') + ->setDescription('Start share for vagrant') + ->addArgument( + 'name', + InputArgument::OPTIONAL, + 'Specific name for the share' + ) + ->addOption( + 'http', + null, + InputOption::VALUE_REQUIRED, + 'Local HTTP port to forward to' + ) + ->addOption( + 'https', + null, + InputOption::VALUE_REQUIRED, + 'Local HTTPS port to forward to' + ) + ->addOption( + 'name', + null, + InputOption::VALUE_REQUIRED, + 'Specific name for the share' + ) + ->addOption( + 'ssh', + null, + InputOption::VALUE_NONE, + 'Allow \'vagrant connect --ssh\' access' + ) + ->addOption( + 'ssh-no-password', + null, + InputOption::VALUE_NONE, + 'Key won\'t be encrypted with --ssh' + ) + ->addOption( + 'ssh-port', + null, + InputOption::VALUE_REQUIRED, + 'Specific port for SSH when using --ssh' + ) + ->addOption( + '--ssh-once', + null, + InputOption::VALUE_NONE, + 'Allow \'vagrant connect --ssh\' only one time' + ); + } + + /** + * Execute command + * + * @param InputInterface $input Input instance + * @param OutputInterface $output Output instance + * + * @return int|null|void + */ + public function execute(InputInterface $input, OutputInterface $output) { + + $runningCallback = function($process, $status) { + static $domainFound = false; + if ($domainFound) { + return; + } + + $pid = $status['pid']; + + exec('pgrep -P ' . (int)$pid . ' | xargs ps -o command=', $output); + + if (!empty($output)) { + foreach ($output as $line) { + if (preg_match('/register\.vagrantshare\.com/', $line)) { + + if (preg_match('/-name ([^\s]+)/', $line, $matches)) { + $domainName = $matches[1]; + + $typo3Domain = new SelfCommandBuilder(); + $typo3Domain + ->addArgument('typo3:domain') + ->addArgumentTemplate('--remove=%s', '*.vagrantshare.com') + ->addArgumentTemplate('--duplicate=%s', $domainName . '.vagrantshare.com') + ->execute(); + + $domainFound = true; + } + } + } + } + }; + + $cleanupCallback = function() { + $typo3Domain = new SelfCommandBuilder(); + $typo3Domain + ->addArgument('typo3:domain') + ->addArgumentTemplate('--remove=%s', '*.vagrantshare.com') + ->execute(); + }; + $this->getApplication()->registerTearDown($cleanupCallback); + + $opts = array( + 'runningCallback' => $runningCallback, + ); + + $vagrant = new CommandBuilder('vagrant', 'share'); + + // Share name + if ($input->getOption('name')) { + $vagrant->addArgumentTemplate('--name %s', $input->getOption('name')); + } elseif ($input->getArgument('name')) { + $vagrant->addArgumentTemplate('--name %s', $input->getArgument('name')); + } + + + // HTTP port + if ($input->getOption('http')) { + $vagrant->addArgumentTemplate('--http %s', $input->getOption('http')); + } else { + $vagrant->addArgumentTemplate('--http %s', 80); + } + + // HTTPS port + if ($input->getOption('https')) { + $vagrant->addArgumentTemplate('--http %s', $input->getOption('https')); + } else { + $vagrant->addArgumentTemplate('--https %s', 443); + } + + + // SSH stuff + if ($input->getOption('ssh')) { + $vagrant->addArgument('--ssh'); + } + + if ($input->getOption('ssh-no-password')) { + $vagrant->addArgument('--ssh-no-password'); + } + + if ($input->getOption('ssh-port')) { + $vagrant->addArgumentTemplate('--ssh-port %s', $input->getOption('ssh-port')); + } + + if ($input->getOption('ssh-once')) { + $vagrant->addArgument('--ssh-once'); + } + + + $vagrant->executeInteractive($opts); + } +} diff --git a/src/app/CliTools/Console/Formatter/OutputFormatterStyle.php b/src/app/CliTools/Console/Formatter/OutputFormatterStyle.php new file mode 100644 index 0000000..17024a2 --- /dev/null +++ b/src/app/CliTools/Console/Formatter/OutputFormatterStyle.php @@ -0,0 +1,164 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +class OutputFormatterStyle extends \Symfony\Component\Console\Formatter\OutputFormatterStyle { + + protected static $availableForegroundColors = array( + 'black' => array('set' => 30, 'unset' => 39), + 'red' => array('set' => 31, 'unset' => 39), + 'green' => array('set' => 32, 'unset' => 39), + 'yellow' => array('set' => 33, 'unset' => 39), + 'blue' => array('set' => 34, 'unset' => 39), + 'magenta' => array('set' => 35, 'unset' => 39), + 'cyan' => array('set' => 36, 'unset' => 39), + 'white' => array('set' => 37, 'unset' => 39), + ); + protected static $availableBackgroundColors = array( + 'black' => array('set' => 40, 'unset' => 49), + 'red' => array('set' => 41, 'unset' => 49), + 'green' => array('set' => 42, 'unset' => 49), + 'yellow' => array('set' => 43, 'unset' => 49), + 'blue' => array('set' => 44, 'unset' => 49), + 'magenta' => array('set' => 45, 'unset' => 49), + 'cyan' => array('set' => 46, 'unset' => 49), + 'white' => array('set' => 47, 'unset' => 49), + ); + protected static $availableOptions = array( + 'bold' => array('set' => 1, 'unset' => 22), + 'underscore' => array('set' => 4, 'unset' => 24), + 'blink' => array('set' => 5, 'unset' => 25), + 'reverse' => array('set' => 7, 'unset' => 27), + 'conceal' => array('set' => 8, 'unset' => 28), + ); + + /** + * Padding + * + * @var null|integer + */ + protected $padding; + + /** + * Padding + * + * @var null|integer + */ + protected $paddingOutside; + + /** + * Wrap + * + * @var null|string + */ + protected $wrap; + + /** + * Application + * + * @var \CliTools\Console\Application + */ + protected $application; + + /** + * Set padding + * + * @param integer|string $padding Padding + */ + public function setPadding($padding) { + $this->padding = $padding; + } + + /** + * Set padding + * + * @param integer|string $padding Padding + */ + public function setPaddingOutside($padding) { + $this->paddingOutside = $padding; + } + + /** + * Set application + * + * @param \CliTools\Console\Application $app Application + */ + public function setApplication(\CliTools\Console\Application $app) { + $this->application = $app; + } + + /** + * Set wrap + * + * @param string $wrap Wrap value + */ + public function setWrap($wrap) { + $this->wrap = $wrap; + } + + /** + * Applies the style to a given text. + * + * @param string $text The text to style + * + * @return string + */ + public function apply($text) { + + $ret = $text; + + // ################## + // Padding + // ################## + + if (!empty($this->padding)) { + $ret = $this->padding . $ret; + } + + + // ################## + // Wrap + // ################## + + if (!empty($this->wrap)) { + list($width) = $this->application->getTerminalDimensions(); + + $length = strlen($text); + $wrapLength = (int)($width - $length - 2)/2 * 0.5; + + if ($wrapLength >= 1) { + $ret = str_repeat($this->wrap, $wrapLength) . ' '. $ret . ' ' . str_repeat($this->wrap, $wrapLength); + } + } + + $ret = parent::apply($ret); + + // ################## + // Padding + // ################## + + if (!empty($this->paddingOutside)) { + $ret = $this->paddingOutside . $ret; + } + + return $ret; + } +} diff --git a/src/app/CliTools/Database/DatabaseConnection.php b/src/app/CliTools/Database/DatabaseConnection.php index 883a21e..fb2a178 100644 --- a/src/app/CliTools/Database/DatabaseConnection.php +++ b/src/app/CliTools/Database/DatabaseConnection.php @@ -76,6 +76,15 @@ public static function setDsn($dsn, $username = null, $password = null) { self::$connection = null; } + /** + * Get Db DSN + * + * @return string + */ + public static function getDsn() { + return self::$dbDsn; + } + /** * Get Db Username * @@ -94,6 +103,25 @@ public static function getDbPassword() { return self::$dbPassword; } + /** + * Get Db Hostname + * + * @return string + */ + public static function getDbHostname() { + return self::parseDsnValue('host'); + } + + /** + * Get Db Port + * + * @return string + */ + public static function getDbPort() { + return self::parseDsnValue('port'); + } + + /** * Get connection * @@ -121,6 +149,24 @@ public static function getConnection() { return self::$connection; } + + /** + * Ping server + * + * @return bool + */ + public static function ping() { + ConsoleUtility::verboseWriteln('DB::PING', null); + try { + self::getConnection()->query('SELECT 1'); + } catch (\PDOException $e) { + ConsoleUtility::verboseWriteln('DB::QUERY::EXCEPTION', $e); + throw $e; + } + + return true; + } + /** * Execute SELECT query * @@ -142,6 +188,17 @@ public static function query($query) { return $ret; } + /** + * Switch database + * + * @param string $database Database + * + * @throws \PDOException + */ + public static function switchDatabase($database) { + self::exec('USE ' . self::sanitizeSqlDatabase($database)); + } + /** * Execute INSERT/DELETE/UPDATE query * @@ -163,6 +220,29 @@ public static function exec($query) { return $ret; } + + /** + * Generate and execute INSERT query + * + * @param string $table Table name + * @param array $values Values + * + * @return int + * @throws \PDOException + */ + public static function insert($table, $values) { + $fieldList = array_keys($values); + + $valueList = array(); + foreach ($values as $value) { + $valueList[] = self::quote($value); + } + + $query = 'INSERT INTO %s (%s) VALUES (%s)'; + $query = sprintf($query, $table, implode(',',$fieldList), implode(',',$valueList)); + self::exec($query); + } + /** * Quote * @@ -358,6 +438,22 @@ public static function databaseExists($database) { return ($ret === 1 ); } + /** + * Return list of databases + * + * @return array + */ + public static function databaseList() { + // Get list of databases + $query = 'SELECT SCHEMA_NAME FROM information_schema.SCHEMATA'; + $ret = DatabaseConnection::getCol($query); + + // Filter mysql specific databases + $ret = array_diff($ret, array('mysql', 'information_schema', 'performance_schema')); + + return $ret; + } + /** * Return list of tables of one database * @@ -372,6 +468,22 @@ public static function tableList($database) { return $ret; } + + /** + * Check if table exists in database + * + * @param string $database Database name + * @param string $table Table name + * @return boolean + */ + public static function tableExists($database, $table) { + $query = 'SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s'; + $query = sprintf($query, self::quote($database), self::quote($table) ); + $ret = (bool)self::getOne($query); + + return $ret; + } + /** * Begin transaction */ @@ -487,7 +599,7 @@ public static function sanitizeSqlField($field) { * @return string */ public static function sanitizeSqlTable($table) { - return preg_replace('/[^_a-zA-Z0-9]/', '', $table); + return '`' . preg_replace('/[^_a-zA-Z0-9]/', '', $table) . '`'; } /** @@ -498,6 +610,24 @@ public static function sanitizeSqlTable($table) { * @return string */ public static function sanitizeSqlDatabase($database) { - return preg_replace('/[^_a-zA-Z0-9]/', '', $database); + return '`' . preg_replace('/[^_a-zA-Z0-9]/', '', $database) . '`'; + } + + /** + * Parse DSN and return value + * + * @param string $key DSN Key + * @param string|null $default Default value + * @return string|null + */ + protected static function parseDsnValue($key, $default = NULL) { + $ret = $default; + + $pattern = sprintf('~%s=([^;]*)(?:;|$)~', preg_quote($key, '~')); + if (preg_match($pattern, self::$dbDsn, $matches)) { + $ret = $matches[1]; + } + + return $ret; } } diff --git a/src/app/CliTools/Exception/CommandExecutionException.php b/src/app/CliTools/Exception/CommandExecutionException.php index 0896fda..9fa52d5 100644 --- a/src/app/CliTools/Exception/CommandExecutionException.php +++ b/src/app/CliTools/Exception/CommandExecutionException.php @@ -20,8 +20,7 @@ * along with this program. If not, see . */ -use CliTools\Console\Builder\CommandBuilder; -use CliTools\Console\Builder\CommandBuilderInterface; +use CliTools\Shell\CommandBuilder\CommandBuilderInterface; class CommandExecutionException extends \RuntimeException { diff --git a/src/app/bootstrap.php b/src/app/CliTools/Exception/StopException.php similarity index 73% rename from src/app/bootstrap.php rename to src/app/CliTools/Exception/StopException.php index 8c73e30..0d565b8 100644 --- a/src/app/bootstrap.php +++ b/src/app/CliTools/Exception/StopException.php @@ -1,4 +1,7 @@ @@ -17,14 +20,6 @@ * along with this program. If not, see . */ -error_reporting(E_ALL); - -// #################################### -// Autoload -// #################################### +class StopException extends \RuntimeException { -$loader = new Symfony\Component\ClassLoader\UniversalClassLoader(); -$loader->registerNamespaces(array( - 'CliTools' => __DIR__ - )); -$loader->register(); +} diff --git a/src/app/CliTools/Reader/ConfigReader.php b/src/app/CliTools/Reader/ConfigReader.php new file mode 100644 index 0000000..fa4ce16 --- /dev/null +++ b/src/app/CliTools/Reader/ConfigReader.php @@ -0,0 +1,199 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +class ConfigReader implements \ArrayAccess { + + /** + * Data storage + * + * @var array + */ + protected $data = array(); + + /** + * Constructor + * + * @param array $data Data configuration + */ + public function __construct(array $data = null) { + if ($data !== null) { + $this->setData($data); + } + } + + /** + * Set configuration data + * + * @param array $data Data configuration + */ + public function setData(array $data) { + $this->data = $data; + } + + /** + * Get value from specific node (dotted array notation) + * + * @param string|null $path Path to node (eg. foo.bar.baz) + * @return mixed|null + */ + public function get($path = null) { + return $this->getNode($path); + } + + /** + * Get array value from specific node (dotted array notation) + * + * @param string|null $path Path to node (eg. foo.bar.baz) + * @return array|null + */ + public function getArray($path = null) { + $ret = $this->getNode($path); + + if (!is_array($ret)) { + $ret = array(); + } + + return $ret; + } + + /** + * Get list of keys from specific node (dotted array notation) + * + * @param string|null $path Path to node (eg. foo.bar.baz) + * @return array|null + */ + public function getList($path = null) { + $ret = $this->getNode($path); + + if (is_array($ret)) { + $ret = array_keys($ret); + } else { + $ret = array(); + } + + return $ret; + } + + /** + * Set value to specific node (dotted array notation) + * + * @param string $path Path to node (eg. foo.bar.baz) + * @param mixed $value Value to set + */ + public function set($path, $value) { + $node =& $this->getNode($path); + $node = $value; + } + + /** + * Clear value at specific node (dotted array notation) + * + * @param null|string $path Path to node (eg. foo.bar.baz) + */ + public function clear($path = null) { + $node =& $this->getNode($path); + $node = null; + } + + /** + * Check if specific node exists + * + * @param null|string $path Path to node (eg. foo.bar.baz) + * @return bool + */ + public function exists($path = null) { + return ($this->getNode($path) !== null); + } + + /** + * Get node by reference + * + * @param string|null $path Path to node (eg. foo.bar.baz) + * @return mixed|null + */ + protected function &getNode($path) { + $data = &$this->data; + + if ($path !== null) { + $pathList = explode('.', $path); + foreach ($pathList as $node) { + if (isset($data[$node])) { + $data = &$data[$node]; + } else { + unset($data); + $data = null; + break; + } + } + } + + if ($data !== null) { + $ret = &$data; + } + + return $ret; + } + + /** + * Array accessor: Set value to offset + * + * @param string $offset Array key + * @param mixed $value Value + */ + public function offsetSet($offset, $value) { + if ($offset === null) { + $this->data[] = $value; + } else { + $this->data[$offset] = $value; + } + } + + /** + * Array accessor: Check if offset exists + * + * @param string $offset Array key + * @return boolean + */ + public function offsetExists($offset) { + return isset($this->data[$offset]); + } + + /** + * Array accessor: Unset offset + * + * @param string $offset Array key + */ + public function offsetUnset($offset) { + unset($this->data[$offset]); + } + + /** + * Array accessor: Get value at offset + * + * @param string $offset Array key + * @return mixed + */ + public function offsetGet($offset) { + return isset($this->data[$offset]) ? $this->data[$offset] : null; + } + +} diff --git a/src/app/CliTools/Service/SelfUpdateService.php b/src/app/CliTools/Service/SelfUpdateService.php index c3d89e9..89a34c4 100644 --- a/src/app/CliTools/Service/SelfUpdateService.php +++ b/src/app/CliTools/Service/SelfUpdateService.php @@ -2,7 +2,7 @@ namespace CliTools\Service; -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; /* * CliTools Command @@ -23,6 +23,12 @@ */ class SelfUpdateService { + /** + * Github repo url + * + * @var null|string + */ + protected $githubRepo; /** * Update url @@ -31,6 +37,27 @@ class SelfUpdateService { */ protected $updateUrl; + /** + * Version + * + * @var null|string + */ + protected $updateVersion; + + /** + * Changelog + * + * @var null|string + */ + protected $updateChangelog; + + /** + * Update github url + * + * @var null|string + */ + protected $githubReleaseUrl; + /** * Path to current clitools command * @@ -62,6 +89,13 @@ class SelfUpdateService { */ protected $application; + /** + * If pre releases should be used + * + * @var bool + */ + protected $updateAllowPreRelease = false; + /** * Constructor * @@ -75,6 +109,27 @@ public function __construct($app, $output) { $this->collectInformations(); } + /** + * Enable prerelease versions (beta) + * + * @return $this + */ + public function enablePreVersions() { + $this->updateAllowPreRelease = true; + return $this; + } + + /** + * Enable update from old server + * + * @return $this + */ + public function enableUpdateFallback() { + $this->updateUrl = $this->application->getConfigValue('config', 'update_fallback_url', null); + $this->updateVersion = 'fallback'; + return $this; + } + /** * Check if super user rights are required * @@ -92,29 +147,100 @@ public function isElevationNeeded() { /** * Update clitools command + * + * @param boolean $force Force update */ - public function update() { - $this->updateUrl = $this->application->getConfigValue('config', 'self_update_url', null); + public function update($force = false) { + + // Only ask for github if update url is not set + if (!$this->updateUrl) { + if (!empty($this->githubReleaseUrl)) { + $this->fetchLatestReleaseFromGithub(); + } else { + throw new \RuntimeException('GitHub Release URL not set'); + } + } - if (empty($this->updateUrl)) { - throw new \RuntimeException('Self-Update url is not set'); + if ($this->checkIfUpdateNeeded($force)) { + // Update needed + $this->doUpdate(); + } + } + + /** + * Check if update is needed + * + * @param boolean $force Force update + * + * @return bool + */ + protected function checkIfUpdateNeeded($force) { + $ret = false; + + $this->output->write('Checking version... '); + + // Check if version is equal + if ($this->updateVersion !== CLITOOLS_COMMAND_VERSION) { + $this->output->write('new version "' . $this->updateVersion . '" found'); + $ret = true; + } else { + $this->output->write('already up to date'); } - $this->output->writeln('Update URL: ' . $this->updateUrl . ''); + // Check if update is forced + if ($force) { + $this->output->write(' [forced]'); + $ret = true; + } - $this->output->writeln('Download new clitools command version...'); - $this->downloadUpdate(); + $this->output->writeln(''); + + return $ret; + } + + /** + * Do update + */ + protected function doUpdate() { + if (empty($this->updateUrl)) { + throw new \RuntimeException('Self-Update url is not found'); + } try { - $versionString = $this->testUpdate(); + // ############## + // Download + // ############## + + $this->output->writeln('Update URL: ' . $this->updateUrl . ''); + + $this->output->write('Downloading.'); + $this->downloadUpdate(); + $this->output->writeln(' done'); + + // ############## + // Test + // ############## + $this->testUpdate(); - $this->output->writeln('Deploy update...'); + // ############## + // Deploy + // ############## + $this->output->writeln('Deploying update... '); $this->deployUpdate(); + // ############## + // Summary + // ############## + + // Version $this->output->writeln(''); - $this->output->writeln('Updated to:'); - $this->output->writeln(' ' . $versionString); + $this->output->writeln('Updated from Version ' . CLITOOLS_COMMAND_VERSION . ' to ' . $this->updateVersion . ''); $this->output->writeln(''); + + // Changelog + if (!empty($this->updateChangelog)) { + $this->showChangelog(); + } } catch (\Exception $e) { $this->output->writeln('Update failed'); } @@ -122,6 +248,81 @@ public function update() { $this->cleanup(); } + /** + * Fetch latest release from github api + */ + protected function fetchLatestReleaseFromGithub() { + $this->output->write('Getting informations from GitHub... '); + + $releaseList = \CliTools\Utility\PhpUtility::curlFetch($this->githubReleaseUrl); + $releaseList = json_decode($releaseList, true); + + if (!empty($releaseList)) { + foreach ($releaseList as $release) { + // Check release + if (!empty($release['draft'])) { + // no valid release + continue; + } + + // Check for pre release + if (!$this->updateAllowPreRelease && !empty($release['prerelease'])) { + // no pre release allowed + continue; + } + + // Check for required tag_name + if (empty($release['tag_name'])) { + // no valid release (requires version tag) + continue; + } + + // Get basic informations + $this->updateVersion = trim($release['tag_name']); + $this->updateChangelog = $release['body']; + + foreach ($release['assets'] as $asset) { + if ($asset['name'] === 'clitools.phar') { + $this->updateUrl = $asset['browser_download_url']; + } + } + + if (!empty($this->updateVersion) && !empty($this->updateUrl)) { + // valid version found + break; + } + } + } + + if (!empty($this->updateUrl)) { + $this->output->writeln('done'); + } else { + $this->output->writeln('failed'); + throw new \RuntimeException('Could not fetch new version - maybe GitHub API is down or other error occurred'); + } + } + + /** + * Show changelog + */ + protected function showChangelog() { + + $message = $this->updateChangelog; + + // Pad lines + $message = explode("\n", $message); + $message = array_map(function($line) { + return ' ' . $line; + }, $message); + $message = implode("\n", $message); + + $message = preg_replace('/`([^`]+)`/', '\1', $message); + + $this->output->writeln('Changelog:'); + $this->output->writeln($message); + $this->output->writeln(''); + } + /** * Get current file informations= */ @@ -146,29 +347,35 @@ protected function collectInformations() { $this->cliToolsCommandPerms['perms'] = fileperms($this->cliToolsCommandPath); $this->cliToolsCommandPerms['owner'] = (int)fileowner($this->cliToolsCommandPath); $this->cliToolsCommandPerms['group'] = (int)filegroup($this->cliToolsCommandPath); + + // ################## + // Set github defaults + // ################## + $this->githubRepo = $this->application->getConfigValue('config', 'github_repo', null); + $this->githubReleaseUrl = 'https://api.github.com/repos/' . $this->githubRepo . '/releases'; } /** * Download file */ protected function downloadUpdate() { - $curlHandle = curl_init(); - curl_setopt($curlHandle, CURLOPT_URL, $this->updateUrl); - curl_setopt($curlHandle, CURLOPT_VERBOSE, 0); - curl_setopt($curlHandle, CURLOPT_HEADER, 0); - curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, 1); - curl_setopt($curlHandle, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, 1); - - $curlData = curl_exec($curlHandle); - if (curl_errno($curlHandle) || empty($curlData)) { - throw new \RuntimeException('Could not download update: ' . curl_error($curlHandle)); - } - curl_close($curlHandle); + $output = $this->output; + + // Progress counter + $progress = function($downloadTotal, $downoadProgress) use ($output) { + static $counter = 0; + + if($counter % 30 === 0) { + $output->write('.'); + } + + $counter++; + }; + + $data = \CliTools\Utility\PhpUtility::curlFetch($this->updateUrl, $progress); $tmpFile = tempnam(sys_get_temp_dir(), 'ct'); - file_put_contents($tmpFile, $curlData); + file_put_contents($tmpFile, $data); $this->cliToolsUpdatePath = $tmpFile; } @@ -211,7 +418,7 @@ protected function deployUpdate() { } /** - * Test update and show version + * Test update and try to get version * * @return string */ @@ -228,5 +435,9 @@ protected function testUpdate() { * Cleanup */ protected function cleanup() { + // Remove old update file if set and exists + if ($this->cliToolsUpdatePath && file_exists($this->cliToolsUpdatePath)) { + unlink($this->cliToolsUpdatePath); + } } } diff --git a/src/app/CliTools/Console/Builder/AbstractCommandBuilder.php b/src/app/CliTools/Shell/CommandBuilder/AbstractCommandBuilder.php similarity index 77% rename from src/app/CliTools/Console/Builder/AbstractCommandBuilder.php rename to src/app/CliTools/Shell/CommandBuilder/AbstractCommandBuilder.php index 106fefe..45781b9 100644 --- a/src/app/CliTools/Console/Builder/AbstractCommandBuilder.php +++ b/src/app/CliTools/Shell/CommandBuilder/AbstractCommandBuilder.php @@ -1,6 +1,6 @@ /dev/null'; + + /** + * Redirect STDERR to STDOUT + */ const OUTPUT_REDIRECT_ALL_STDOUT = ' 2>&1'; - const OUTPUT_REDIRECT_NO_STDERR = ' 2> /dev/null'; + + /** + * Redirect STDERR to /dev/null (no error output) + */ + const OUTPUT_REDIRECT_NO_STDERR = ' 2> /dev/null'; // ########################################## // Attributs @@ -55,7 +66,7 @@ class AbstractCommandBuilder implements CommandBuilderInterface { * * @var null|string */ - protected $outputRedirect = null; + protected $outputRedirect; /** * Command pipe @@ -174,7 +185,8 @@ public function addArgumentSeparator() { * @return $this */ public function setArgumentList(array $args) { - $this->argumentList = $args; + $this->clearArguments(); + $this->appendArgumentsToList($args); return $this; } @@ -209,10 +221,10 @@ public function addArgument($arg) { } /** - * Set argument with template + * Add argument with template * - * @param string $arg Argument sprintf - * @param string $params Argument parameters + * @param string $arg Argument sprintf + * @param string $params... Argument parameters * * @return $this */ @@ -223,6 +235,22 @@ public function addArgumentTemplate($arg, $params) { return $this->addArgumentTemplateList($arg, $funcArgs); } + /** + * Add argument with template multiple times + * + * @param string $arg Argument sprintf + * @param array $paramList Argument parameters + * + * @return $this + */ + public function addArgumentTemplateMultiple($arg, $paramList) { + foreach ($paramList as $param) { + $this->addArgumentTemplate($arg, $param); + } + + return $this; + } + /** * Set argument with template * @@ -232,6 +260,8 @@ public function addArgumentTemplate($arg, $params) { * @return $this */ public function addArgumentTemplateList($arg, array $params) { + $this->validateArgumentValue($arg); + $params = array_map('escapeshellarg', $params); $this->argumentList[] = vsprintf($arg, $params); return $this; @@ -245,12 +275,45 @@ public function addArgumentTemplateList($arg, array $params) { * @return $this */ public function addArgumentList(array $arg, $escape = true) { + $this->appendArgumentsToList($arg, $escape); + return $this; + } + + /** + * Append one argument to list + * + * @param array $arg Arguments + * @param boolean $escape Enable argument escaping + * + * @return $this + */ + protected function appendArgumentToList($arg, $escape = true) { + $this->validateArgumentValue($arg); + if ($escape) { - $arg = array_map('escapeshellarg', $arg); + $arg = escapeshellarg($arg); } - $this->argumentList = array_merge($this->argumentList, $arg); - return $this; + $this->argumentList[] = $arg; + } + + /** + * Append multiple arguments to list + * + * @param array $args Arguments + * @param boolean $escape Enable argument escaping + * + * @return $this + */ + protected function appendArgumentsToList($args, $escape = true) { + // Validate each argument value + array_walk($args, array($this, 'validateArgumentValue')); + + if ($escape) { + $args = array_map('escapeshellarg', $args); + } + + $this->argumentList = array_merge($this->argumentList, $args); } /** @@ -294,6 +357,16 @@ public function setOutputRedirectToFile($filename) { return $this; } + /** + * Clear output redirect + * + * @return $this + */ + public function clearOutputRedirect() { + $this->outputRedirect = null; + return $this; + } + /** * Parse command and attributs from exec line * @@ -303,10 +376,20 @@ public function setOutputRedirectToFile($filename) { * @return $this */ public function parse($str) { - list($command, $attributs) = explode(' ', $str, 2); + $parsedCmd = explode(' ', $str, 2); - $this->setCommand($command); - $this->setArgumentList(array($attributs), false); + // Check required command + if (empty($parsedCmd[0])) { + throw new \RuntimeException('Command is empty'); + } + + // Set command (first value) + $this->setCommand($parsedCmd[0]); + + // Set arguments (second values) + if (!empty($parsedCmd[1])) { + $this->addArgumentRaw($parsedCmd[1]); + } return $this; } @@ -378,6 +461,15 @@ public function setPipeList(array $pipeList) { return $this; } + /** + * Clear pipe list + * + * @return $this + */ + public function clearPipes() { + $this->pipeList = array(); + return $this; + } /** * Add pipe command @@ -400,7 +492,7 @@ public function build() { $ret = array(); if (!$this->isExecuteable()) { - throw new \RuntimeException('Command "' . $this->getCommand() . '" is not executable or available'); + throw new \RuntimeException('Command "' . $this->getCommand() . '" is not executable or available, please install it'); } // Add command @@ -460,10 +552,23 @@ public function execute() { /** * Execute command * + * @param array $opts Option array * @return Executor */ - public function executeInteractive() { - return $this->getExecutor()->execInteractive(); + public function executeInteractive(array $opts = null) { + return $this->getExecutor()->execInteractive($opts); + } + + /** + * Validate argument value + * + * @param mixed $value Value + * @throws \RuntimeException + */ + protected function validateArgumentValue($value) { + if (strlen($value) === 0) { + throw new \RuntimeException('Argument value cannot be empty'); + } } // ########################################## diff --git a/src/app/CliTools/Console/Builder/CommandBuilder.php b/src/app/CliTools/Shell/CommandBuilder/CommandBuilder.php similarity index 95% rename from src/app/CliTools/Console/Builder/CommandBuilder.php rename to src/app/CliTools/Shell/CommandBuilder/CommandBuilder.php index bdac5c2..2757325 100644 --- a/src/app/CliTools/Console/Builder/CommandBuilder.php +++ b/src/app/CliTools/Shell/CommandBuilder/CommandBuilder.php @@ -1,6 +1,6 @@ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +class EditorCommandBuilder extends CommandBuilder { + + /** + * Initalized command + * + * @throws \RuntimeException + */ + protected function initialize() { + parent::initialize(); + + $editorCmd = getenv('EDITOR'); + + if (empty($editorCmd)) { + throw new \RuntimeException('No $EDITOR environment variable set'); + } + + $this->parse($editorCmd); + } +} diff --git a/src/app/CliTools/Console/Builder/FullSelfCommandBuilder.php b/src/app/CliTools/Shell/CommandBuilder/FullSelfCommandBuilder.php similarity index 97% rename from src/app/CliTools/Console/Builder/FullSelfCommandBuilder.php rename to src/app/CliTools/Shell/CommandBuilder/FullSelfCommandBuilder.php index 3471975..5b1e384 100644 --- a/src/app/CliTools/Console/Builder/FullSelfCommandBuilder.php +++ b/src/app/CliTools/Shell/CommandBuilder/FullSelfCommandBuilder.php @@ -1,6 +1,6 @@ setCommand(array_shift($arguments)); } elseif (!empty($_SERVER['_'])) { + // Plain PHP version if ($_SERVER['argv'][0] !== $_SERVER['_']) { $this->setCommand($_SERVER['_']); $this->addArgument(reset($arguments)); diff --git a/src/app/CliTools/Console/Shell/Executor.php b/src/app/CliTools/Shell/Executor.php similarity index 73% rename from src/app/CliTools/Console/Shell/Executor.php rename to src/app/CliTools/Shell/Executor.php index 93d0903..f57fcbf 100644 --- a/src/app/CliTools/Console/Shell/Executor.php +++ b/src/app/CliTools/Shell/Executor.php @@ -21,8 +21,7 @@ */ use CliTools\Exception\CommandExecutionException; -use CliTools\Console\Builder\CommandBuilder; -use CliTools\Console\Builder\CommandBuilderInterface; +use CliTools\Shell\CommandBuilder\CommandBuilderInterface; use CliTools\Utility\ConsoleUtility; class Executor { @@ -64,6 +63,13 @@ class Executor { */ protected $strictMode = true; + /** + * Finisher callback list + * + * @var array + */ + protected $finishers = array(); + // ########################################## // Methods // ########################################## @@ -152,6 +158,15 @@ public function setStrictMode($strictMode) { return $this; } + /** + * Clear state + */ + public function clear() { + $this->output = null; + $this->returnCode = null; + $this->finishers = array(); + } + /** * Execute command @@ -166,6 +181,8 @@ public function execute() { exec($this->command->build(), $this->output, $this->returnCode); + $this->runFinishers(); + if ($this->strictMode && $this->returnCode !== 0) { throw $this->generateException('Process ' . $this->command->getCommand() . ' did not finished successfully'); } @@ -176,10 +193,11 @@ public function execute() { /** * Execute interactive * + * @param array $opts Option array * @return $this * @throws \Exception */ - public function execInteractive() { + public function execInteractive(array $opts = null) { $this->checkCommand(); ConsoleUtility::verboseWriteln('EXEC::INTERACTIVE', $this->command->build()); @@ -193,9 +211,34 @@ public function execInteractive() { $process = proc_open($this->command->build(), $descriptorSpec, $pipes); if (is_resource($process)) { - $this->returnCode = proc_close($process); + if (!empty($opts['startupCallback']) && is_callable($opts['startupCallback'])) { + $opts['startupCallback']($process); + } + + do { + if (is_resource($process)) { + $status = proc_get_status($process); + if (!empty($status) && !empty($opts['runningCallback']) && is_callable($opts['runningCallback'])) { + $opts['runningCallback']($process, $status); + } + } else { + break; + } + usleep(100 * 1000); + } while (!empty($status) && is_array($status) && $status['running'] === true); + + if (is_resource($process)) { + proc_close($process); + } + + $this->returnCode = $status['exitcode']; - if ($this->strictMode && $this->returnCode !== 0) { + $this->runFinishers(); + + if ($status['signaled'] === true && $status['exitcode'] === -1) { + // user may hit CTRL+C + ConsoleUtility::getOutput()->writeln('Processed stopped by signal'); + } elseif ($this->strictMode && $this->returnCode !== 0) { throw $this->generateException('Process ' . $this->command->getCommand() . ' did not finished successfully'); } } else { @@ -243,4 +286,24 @@ protected function generateException($msg) { return $e; } + + /** + * Add finisher callback (will run after command execution) + * + * @param callable $callback + */ + public function addFinisherCallback(callable $callback) { + $this->finishers[] = $callback; + } + + /** + * Run finisher commands + */ + public function runFinishers() { + foreach ($this->finishers as $call) { + if (is_callable($call)) { + $call($this); + } + } + } } diff --git a/src/app/CliTools/Utility/CommandExecutionUtility.php b/src/app/CliTools/Utility/CommandExecutionUtility.php deleted file mode 100644 index 74e5b4b..0000000 --- a/src/app/CliTools/Utility/CommandExecutionUtility.php +++ /dev/null @@ -1,169 +0,0 @@ - - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -use CliTools\Exception\CommandExecutionException; - -/** - * Class CommandExecutionUtility - * - * @package CliTools\Utility - * @deprecated - */ -class CommandExecutionUtility { - - /** - * Build raw command - * - * @param string $command Command - * @param string|null $parameterTemplate Parameter Template - * @param array|null $parameter Parameter List - * - * @return string - */ - public static function buildCommand($command, $parameterTemplate = null, $parameter = null) { - // Escape command - $execCommand = escapeshellcmd($command); - - // Escape args - if ($parameter !== null && is_array($parameter) && count($parameter) >= 1) { - // dynamic paramter - $parameter = array_map('escapeshellarg', $parameter); - - // Just add parameter if template is empty - if ($parameterTemplate === null) { - $parameterTemplate = str_repeat('%s ', count($parameter)); - } - - $execCommand .= ' ' . vsprintf($parameterTemplate, $parameter); - } elseif ($parameterTemplate !== null && $parameter === null) { - // only template specified, use as static parameter - $execCommand .= ' ' . $parameterTemplate; - } - - return $execCommand; - } - - - /** - * Build argument list as string - * - * @param array $parameter Parameter List - * - * @return string - */ - public static function buildArgumentString(array $parameter) { - $parameter = array_map('escapeshellarg', $parameter); - $ret = implode(' ', $parameter); - return $ret; - } - - /** - * Exec raw command - * - * @param string $command Command - * @param string $output Output - * - * @return integer - * @throws CommandExecutionException - */ - public static function execRaw($command, &$output = null) { - ConsoleUtility::verboseWriteln('EXEC::RAW', $command); - - exec($command, $output, $execStatus); - - if ($execStatus !== 0) { - $e = new CommandExecutionException('Process ' . $command . ' did not finished successfully'); - $e->setReturnCode($execStatus); - throw $e; - } - - return $execStatus; - } - - /** - * Exec command - * - * @param string $command Command - * @param string $output Output - * @param string|null $parameterTemplate Parameter Template - * @param array|null $parameter Parameter List - * - * @return integer - * @throws CommandExecutionException - */ - public static function exec($command, &$output, $parameterTemplate, $parameter = null) { - $execCommand = self::buildCommand($command, $parameterTemplate, $parameter); - - ConsoleUtility::verboseWriteln('EXEC::EXEC', $execCommand); - - exec($execCommand, $output, $execStatus); - - if ($execStatus !== 0) { - $e = new CommandExecutionException('Process ' . $execCommand . ' did not finished successfully'); - $e->setReturnCode($execStatus); - throw $e; - } - - return $execStatus; - } - - /** - * Execute command (via passthru) - * - * @param string $command Command - * @param string|null $parameterTemplate Parameter Template - * @param array|null $parameter Parameter List - * - * @return integer - * @throws CommandExecutionException - */ - public static function execInteractive($command, $parameterTemplate = null, $parameter = null) { - $execCommand = self::buildCommand($command, $parameterTemplate, $parameter); - - ConsoleUtility::verboseWriteln('EXEC::INTERACTIVE', $execCommand); - - $descriptorSpec = array( - 0 => array('file', 'php://stdin', 'r'), // stdin is a file that the child will read from - 1 => array('file', 'php://stdout', 'w'), // stdout is a file that the child will write to - 2 => array('file', 'php://stderr', 'w') // stderr is a file that the child will write to - ); - - $process = proc_open($execCommand, $descriptorSpec, $pipes); - - if (is_resource($process)) { - $execStatus = proc_close($process); - $execStatus = pcntl_wexitstatus($execStatus); - - if ($execStatus !== 0) { - $e = new CommandExecutionException('Process ' . $execCommand . ' did not finished successfully [return code: ' . $execStatus . ']'); - $e->setReturnCode($execStatus); - throw $e; - } - } else { - $e = new CommandExecutionException('Process ' . $execCommand . ' could not be started'); - $e->setReturnCode(-1); - throw $e; - } - - return $execStatus; - } -} diff --git a/src/app/CliTools/Utility/ConsoleUtility.php b/src/app/CliTools/Utility/ConsoleUtility.php index 1c4dc93..1dad09e 100644 --- a/src/app/CliTools/Utility/ConsoleUtility.php +++ b/src/app/CliTools/Utility/ConsoleUtility.php @@ -20,6 +20,8 @@ * along with this program. If not, see . */ +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -108,4 +110,32 @@ public static function verboseWriteln($area, $line) { self::$output->writeln($line); } } + + /** + * Ask question with yes/no detection + * + * @param string $question Question + * @param string $default Default + * + * @return bool + */ + public static function questionYesNo($message, $default) { + $ret = false; + + while (1) { + $question = new Question(' >>> ' . $message . ' [yes/no] ', $default); + $questionDialog = new QuestionHelper(); + $answer = $questionDialog->ask(self::$input, self::$output, $question); + + if (stripos($answer, 'n') === 0) { + $ret = false; + break; + } elseif (stripos($answer, 'y') === 0) { + $ret = true; + break; + } + } + + return $ret; + } } diff --git a/src/app/CliTools/Utility/DockerUtility.php b/src/app/CliTools/Utility/DockerUtility.php index 57d22f0..980f7e7 100644 --- a/src/app/CliTools/Utility/DockerUtility.php +++ b/src/app/CliTools/Utility/DockerUtility.php @@ -20,7 +20,7 @@ * along with this program. If not, see . */ -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; use CliTools\Console\Shell\Executor; class DockerUtility { @@ -73,27 +73,7 @@ public static function getDockerConfiguration($container) { * @return bool|string */ public static function searchDockerDirectoryRecursive($path = null) { - $ret = false; - - // Set path to current path (if not specified) - if ($path === null) { - $path = getcwd(); - } - - if (!empty($path) && $path !== '/') { - // Check if current path is docker directory - if (self::isDockerDirectory($path)) { - // Docker found - $ret = $path; - } else { - // go up in directory - $path .= '/../'; - $path = realpath($path); - $ret = self::searchDockerDirectoryRecursive($path); - } - } - - return $ret; + return UnixUtility::findFileInDirectortyTree('docker-compose.yml', $path); } /** @@ -109,8 +89,7 @@ public static function isDockerDirectory($path = null) { } $dockerFileList = array( - 'docker-compose.yml', - 'fig.yml', + 'docker-compose.yml' ); foreach ($dockerFileList as $dockerFile) { diff --git a/src/app/CliTools/Utility/FilterUtility.php b/src/app/CliTools/Utility/FilterUtility.php new file mode 100644 index 0000000..09d169e --- /dev/null +++ b/src/app/CliTools/Utility/FilterUtility.php @@ -0,0 +1,76 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +class FilterUtility { + + /** + * Filter mysql table list by filter + * + * @param array $tables List of tables + * @param array $filters List of filters + * + * @return array + */ + public static function mysqlTableFilter(array $tables, array $filters) { + $ret = array(); + + foreach ($tables as $table) { + foreach ($filters as $filter) { + if (preg_match($filter, $table)) { + continue 2; + } + } + $ret[] = $table; + } + + return $ret; + } + + /** + * Filter mysql table list by filter + * + * @param array $tables List of tables + * @param array $filters List of filters + * @param string|null $database Database + * + * @return array + */ + public static function mysqlIgnoredTableFilter(array $tables, array $filters, $database = null) { + $ret = array(); + + foreach ($tables as $table) { + foreach ($filters as $filter) { + if (preg_match($filter, $table)) { + + if ($database !== null) { + $ret[] = $database . '.' . $table; + } else { + $ret[] = $table; + } + continue 2; + } + } + } + + return $ret; + } +} diff --git a/src/app/CliTools/Utility/PhpUtility.php b/src/app/CliTools/Utility/PhpUtility.php index 97ede9b..e5656c8 100644 --- a/src/app/CliTools/Utility/PhpUtility.php +++ b/src/app/CliTools/Utility/PhpUtility.php @@ -22,6 +22,47 @@ class PhpUtility { + /** + * Get content of file + * + * @param string $file Filename + * @return string + */ + public static function fileGetContents($file) { + if (!is_file($file) || !is_readable($file)) { + throw new \RuntimeException('Could not read "' . $file . '"'); + } + + return file_get_contents($file); + } + + /** + * Get content of file (array) + * + * @param string $file Filename + * @return array + */ + public static function fileGetContentsArray($file) { + $content = self::fileGetContents($file); + $content = str_replace("/r/n", "/n", $content); + + $ret = explode("/n", $content); + + return $ret; + } + + /** + * Get content of file + * + * @param string $file Filename + * @param string $content Content + */ + public static function filePutContents($file, $content) { + if (file_put_contents($file, $content) === false) { + throw new \RuntimeException('Could not write "' . $file . '"'); + } + } + /** * Change current working directory * @@ -29,11 +70,32 @@ class PhpUtility { * @throws \RuntimeException */ public static function chdir($path) { - if (!chdir($path)) { + if (!is_dir($path) || !chdir($path)) { throw new \RuntimeException('Could not change working directory to "' . $path . '"'); } } + /** + * Create new directory + * + * @param string $path Directory + * @param integer $mode Perms + * @param boolean $recursive Creation of nested directories + * @param resource $context Context + * @throws \RuntimeException + */ + public static function mkdir($path, $mode = 0777, $recursive = false, $context = null) { + if ($context !== null) { + $res = mkdir($path, $mode, $recursive, $context); + } else { + $res = mkdir($path, $mode, $recursive); + } + + if (!$res) { + throw new \RuntimeException('Could not create directory "' . $path . '"'); + } + } + /** * Remove file * @@ -45,4 +107,64 @@ public static function unlink($path) { throw new \RuntimeException('Could not change working directory to "' . $path . '"'); } } + + /** + * Fetch content from url using curl + * + * @param string $url Url + * @param callable $progress Progress callback + * + * @return mixed + */ + public static function curlFetch($url, callable $progress = null) { + $curlHandle = curl_init(); + curl_setopt($curlHandle, CURLOPT_URL, $url); + curl_setopt($curlHandle, CURLOPT_VERBOSE, 0); + curl_setopt($curlHandle, CURLOPT_HEADER, 0); + curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, 1); + curl_setopt($curlHandle, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($curlHandle, CURLOPT_USERAGENT, 'CliTools ' . CLITOOLS_COMMAND_VERSION . '(https://github.com/mblaschke/vagrant-clitools)'); + + if($progress) { + curl_setopt($curlHandle, CURLOPT_NOPROGRESS, false); + curl_setopt($curlHandle, CURLOPT_PROGRESSFUNCTION, $progress); + } + + $ret = curl_exec($curlHandle); + if (curl_errno($curlHandle) || empty($ret)) { + throw new \RuntimeException('Could not fetch url "' . $url . '", error: ' . curl_error($curlHandle)); + } + curl_close($curlHandle); + + return $ret; + } + + + /** + * Get MIME type for file + * + * @param string $file Path to file + * + * @return string + */ + public static function getMimeType($file) { + // Get mime type from file + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $ret = finfo_file($finfo, $file); + finfo_close($finfo); + + if ($ret === 'application/octet-stream') { + $finfo = finfo_open(); + $dumpFileInfo = finfo_file($finfo, $file); + finfo_close($finfo); + + if (strpos($dumpFileInfo, 'LZMA compressed data') !== false) { + $ret = 'application/x-lzma'; + } + } + + return $ret; + } } diff --git a/src/app/CliTools/Utility/UnixUtility.php b/src/app/CliTools/Utility/UnixUtility.php index 1abd1fa..135f059 100644 --- a/src/app/CliTools/Utility/UnixUtility.php +++ b/src/app/CliTools/Utility/UnixUtility.php @@ -20,7 +20,7 @@ * along with this program. If not, see . */ -use CliTools\Console\Builder\CommandBuilder; +use CliTools\Shell\CommandBuilder\CommandBuilder; abstract class UnixUtility { @@ -49,13 +49,13 @@ public static function lsbSystemDescription() { /** * Get CPU Count * - * @return string + * @return integer */ public static function cpuCount() { $command = new CommandBuilder('nproc'); $ret = $command->execute()->getOutputString(); - $ret = trim($ret); + $ret = (int)trim($ret); return $ret; } @@ -63,7 +63,7 @@ public static function cpuCount() { /** * Get Memory Count * - * @return string + * @return integer */ public static function memorySize() { $command = new CommandBuilder('cat', '/proc/meminfo'); @@ -112,7 +112,7 @@ public static function dockerVersion() { /** * Get mount info list * - * @return string + * @return array */ public static function mountInfoList() { $command = new CommandBuilder('df', '-a --type=ext3 --type=ext4 --type vmhgfs --type vboxsf --portability'); @@ -205,12 +205,12 @@ public static function defaultGateway() { * @param string $message Message */ public static function sendWallMessage($message) { - $commandWall = new CommandBuilder('wall'); - $commandWall->setOutputRedirect(CommandBuilder::OUTPUT_REDIRECT_NULL); + $wall = new CommandBuilder('wall'); + $wall->setOutputRedirect(CommandBuilder::OUTPUT_REDIRECT_NULL); $command = new CommandBuilder('echo'); $command->addArgument($message) - ->addPipeCommand($commandWall); + ->addPipeCommand($wall); $command->execute(); } @@ -256,4 +256,73 @@ public static function checkExecutable($command) { return false; } + + /** + * Search directory upwards for a file + * + * @param string|array $file Filename + * @param string $path Path + * @return boolean|string + */ + public static function findFileInDirectortyTree($file, $path = null) { + $ret = false; + + $fileList = (array)$file; + + // Set path to current path (if not specified) + if ($path === null) { + $path = getcwd(); + } + + if (!empty($path) && $path !== '/') { + // Check if file exists in path + foreach ($fileList as $file) { + if (file_exists($path . '/' . $file)) { + // File found + $ret = $path . '/' . $file; + break; + } + } + + if ($ret === false) { + // go up in directory + $path .= '/../'; + $path = realpath($path); + $ret = self::findFileInDirectortyTree($fileList, $path); + } + } + + return $ret; + } + + /** + * Reload tty + */ + public static function reloadTtyBanner($ttyName) { + // Check if we can reload tty + try { + $who = new CommandBuilder('who'); + $who->addPipeCommand( new CommandBuilder('grep', '%s', array($ttyName))); + $who->execute(); + + // if there is no exception -> there is a logged in user + } catch (\Exception $e) { + // if there is an exception -> there is NO logged in user + + try { + $ps = new CommandBuilder('ps', 'h -o pid,comm,args -C getty'); + $ps->addPipeCommand( new CommandBuilder('grep', '%s', array($ttyName))); + $output = $ps->execute()->getOutput(); + + if (!empty($output)) { + $outputLine = trim(reset($output)); + $outputLineParts = preg_split('/[\s]+/', $outputLine); + list($pid) = $outputLineParts; + + posix_kill($pid, SIGHUP); + } + + } catch (\Exception $e) {} + } + } } diff --git a/src/command.php b/src/command.php index a70c438..9e191f4 100644 --- a/src/command.php +++ b/src/command.php @@ -19,11 +19,11 @@ * along with this program. If not, see . */ -define('CLITOOLS_COMMAND_VERSION', '1.9.0'); +error_reporting(E_ALL); +define('CLITOOLS_COMMAND_VERSION', '2.0.0'); define('CLITOOLS_ROOT_FS', __DIR__); require __DIR__ . '/vendor/autoload.php'; -require __DIR__ . '/app/bootstrap.php'; $app = new CliTools\Console\Application('CliTools :: Development Console Utility', CLITOOLS_COMMAND_VERSION); diff --git a/src/composer.json b/src/composer.json index 4d2d5ce..c2e61bb 100644 --- a/src/composer.json +++ b/src/composer.json @@ -1,10 +1,16 @@ { + "name": "Clitools", "repositories": [ { "type": "vcs", "url": "https://github.com/jamiebicknell/Growl-GNTP.git" } ], "require": { "symfony/console": "2.*", - "symfony/class-loader": "2.*", - "jamiebicknell/Growl-GNTP": "dev-master" + "jamiebicknell/Growl-GNTP": "dev-master", + "symfony/yaml": "^2.6" + }, + "autoload": { + "psr-0": { + "CliTools": "./app" + } } } diff --git a/src/composer.lock b/src/composer.lock index fd5c2cf..6487db0 100644 --- a/src/composer.lock +++ b/src/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "4585abd5cf3956b5044b2156049a1fe9", + "hash": "7b023eac532864cff6d957dd51e5999c", "packages": [ { "name": "jamiebicknell/Growl-GNTP", @@ -47,36 +47,42 @@ "time": "2014-06-21 19:14:27" }, { - "name": "symfony/class-loader", - "version": "v2.6.6", - "target-dir": "Symfony/Component/ClassLoader", + "name": "symfony/console", + "version": "v2.7.1", "source": { "type": "git", - "url": "https://github.com/symfony/ClassLoader.git", - "reference": "861765b3e5f32979de5bd19ad2577cbb830a29d5" + "url": "https://github.com/symfony/Console.git", + "reference": "564398bc1f33faf92fc2ec86859983d30eb81806" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ClassLoader/zipball/861765b3e5f32979de5bd19ad2577cbb830a29d5", - "reference": "861765b3e5f32979de5bd19ad2577cbb830a29d5", + "url": "https://api.github.com/repos/symfony/Console/zipball/564398bc1f33faf92fc2ec86859983d30eb81806", + "reference": "564398bc1f33faf92fc2ec86859983d30eb81806", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=5.3.9" }, "require-dev": { - "symfony/finder": "~2.0,>=2.0.5", - "symfony/phpunit-bridge": "~2.7" + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.1", + "symfony/phpunit-bridge": "~2.7", + "symfony/process": "~2.1" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/process": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6-dev" + "dev-master": "2.7-dev" } }, "autoload": { - "psr-0": { - "Symfony\\Component\\ClassLoader\\": "" + "psr-4": { + "Symfony\\Component\\Console\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -84,57 +90,48 @@ "MIT" ], "authors": [ - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - }, { "name": "Fabien Potencier", "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony ClassLoader Component", - "homepage": "http://symfony.com", - "time": "2015-03-27 10:19:51" + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2015-06-10 15:30:22" }, { - "name": "symfony/console", - "version": "v2.6.6", - "target-dir": "Symfony/Component/Console", + "name": "symfony/yaml", + "version": "v2.7.1", "source": { "type": "git", - "url": "https://github.com/symfony/Console.git", - "reference": "5b91dc4ed5eb08553f57f6df04c4730a73992667" + "url": "https://github.com/symfony/Yaml.git", + "reference": "9808e75c609a14f6db02f70fccf4ca4aab53c160" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/5b91dc4ed5eb08553f57f6df04c4730a73992667", - "reference": "5b91dc4ed5eb08553f57f6df04c4730a73992667", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/9808e75c609a14f6db02f70fccf4ca4aab53c160", + "reference": "9808e75c609a14f6db02f70fccf4ca4aab53c160", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=5.3.9" }, "require-dev": { - "psr/log": "~1.0", - "symfony/event-dispatcher": "~2.1", - "symfony/phpunit-bridge": "~2.7", - "symfony/process": "~2.1" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/process": "" + "symfony/phpunit-bridge": "~2.7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6-dev" + "dev-master": "2.7-dev" } }, "autoload": { - "psr-0": { - "Symfony\\Component\\Console\\": "" + "psr-4": { + "Symfony\\Component\\Yaml\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -142,18 +139,18 @@ "MIT" ], "authors": [ - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - }, { "name": "Fabien Potencier", "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Console Component", - "homepage": "http://symfony.com", - "time": "2015-03-30 15:54:10" + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2015-06-10 15:30:22" } ], "packages-dev": [], diff --git a/src/conf/.gitkeep b/src/conf/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/config.ini b/src/config.ini index 4409b59..dd808c0 100644 --- a/src/config.ini +++ b/src/config.ini @@ -1,8 +1,9 @@ [config] -ssh_conf_path = "/opt/conf/ssh" -www_base_path = "/var/www" -domain_dev[] = "dev" -self_update_url = "https://www.achenar.net/clicommand/clitools.phar" +ssh_conf_path = "/opt/conf/ssh" +www_base_path = "/var/www" +domain_dev = "vm" +github_repo = "mblaschke/clitools" +update_fallback_url = "https://www.achenar.net/clicommand/clitools.phar" [db] dsn = "mysql:host=localhost" @@ -10,6 +11,9 @@ username = "root" password = "" debug_log_dir = "/tmp/" +[bin] +composer = "composer" + [syscheck] enabled = 1 wall = 1 @@ -34,9 +38,17 @@ typo3[] = "/^sys_log$/i" typo3[] = "/^sys_history$/i" typo3[] = "/^tx_extbase_cache.*/i" + [commands] ; load following classes class[] = "CliTools\Console\Command\Common\SelfUpdateCommand" +class[] = "CliTools\Console\Command\Common\MakeCommand" + +class[] = "CliTools\Console\Command\Sync\InitCommand" +class[] = "CliTools\Console\Command\Sync\ServerCommand" +class[] = "CliTools\Console\Command\Sync\BackupCommand" +class[] = "CliTools\Console\Command\Sync\RestoreCommand" +class[] = "CliTools\Console\Command\Sync\DeployCommand" class[] = "CliTools\Console\Command\TYPO3\BeUserCommand" class[] = "CliTools\Console\Command\TYPO3\InstallerCommand" @@ -63,9 +75,11 @@ class[] = "CliTools\Console\Command\Mysql\RestartCommand" class[] = "CliTools\Console\Command\Mysql\DebugCommand" class[] = "CliTools\Console\Command\Mysql\SlowLogCommand" class[] = "CliTools\Console\Command\Mysql\DropCommand" +class[] = "CliTools\Console\Command\Mysql\ConvertCommand" class[] = "CliTools\Console\Command\Php\TraceCommand" class[] = "CliTools\Console\Command\Php\RestartCommand" +class[] = "CliTools\Console\Command\Php\ComposerCommand" class[] = "CliTools\Console\Command\Samba\RestartCommand" @@ -93,6 +107,8 @@ class[] = "CliTools\Console\Command\System\CrontaskCommand" class[] = "CliTools\Console\Command\User\RebuildSshConfigCommand" +class[] = "CliTools\Console\Command\Vagrant\ShareCommand" + exclude[] = "CliTools\Console\Command\*\RestartCommand" ; exclude this class (example)