PageRenderTime 46ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/inc/MaintenanceScript.php

https://github.com/richardudovich/testswarm
PHP | 293 lines | 219 code | 28 blank | 46 comment | 44 complexity | 1c2e4375b63cefe973dd38dd5813f484 MD5 | raw file
  1. <?php
  2. /**
  3. * This is the central file for maintenance scripts.
  4. * It is not the entry point however.
  5. *
  6. * @author Timo Tijhof, 2012
  7. * @since 1.0.0
  8. * @package TestSwarm
  9. */
  10. abstract class MaintenanceScript {
  11. private $context, $name, $description;
  12. private $flags = array();
  13. private $options = array();
  14. private $generalArgKeys = array();
  15. private $requiredKeys = array();
  16. private $parsed = array();
  17. abstract protected function init();
  18. abstract protected function execute();
  19. final protected function setDescription( $description ) {
  20. $this->description = $description;
  21. }
  22. /**
  23. * Register a flag, usage any of these 4:
  24. * - php script.php -a -b value -c "value" -d="value"
  25. * @param string $key: one single character.
  26. * @param string $type: one of "boolean", "value"
  27. * @param string $description
  28. */
  29. protected function registerFlag( $key, $type, $description ) {
  30. static $types = array( 'boolean', 'value' );
  31. if ( !is_string( $key ) || strlen ( $key ) !== 1 || !in_array( $type, $types ) ) {
  32. $this->error( 'Illegal flag registration' );
  33. }
  34. $this->flags[$key] = array(
  35. 'type' => $type,
  36. 'description' => $description,
  37. );
  38. }
  39. /**
  40. * Register an option, usage any of these 4:
  41. * - php script.php --foo --bar=value --quux="value"
  42. * @param string $name: at least 2 characters
  43. * @param string $type: one of "boolean", "value"
  44. * @param string $description
  45. * @param bool $required
  46. */
  47. protected function registerOption( $name, $type, $description, $required = false ) {
  48. static $types = array( 'boolean', 'value' );
  49. if ( !is_string( $name ) || strlen ( $name ) < 2 || !in_array( $type, $types ) ) {
  50. $this->error( 'Illegal option registration' );
  51. }
  52. $this->options[$name] = array(
  53. 'type' => $type,
  54. 'description' => $description,
  55. 'required' => (bool) $required
  56. );
  57. }
  58. protected function parseCliArguments() {
  59. // Prepare for getopt(). Note that it supports "require value" for cli args,
  60. // but we use our own format instead (see also php.net/getopt).
  61. $getoptShort = '';
  62. $getoptLong = array();
  63. $requiredKeys = array();
  64. foreach ( $this->flags as $flagKey => $flagInfo ) {
  65. switch ( $flagInfo['type'] ) {
  66. case 'value':
  67. $getoptShort .= $flagKey . '::';
  68. break;
  69. case 'boolean':
  70. $getoptShort .= $flagKey;
  71. break;
  72. }
  73. }
  74. foreach ( $this->options as $optionName => $optionInfo ) {
  75. if ( $optionInfo['required'] ) {
  76. $requiredKeys[] = $optionName;
  77. }
  78. switch ( $optionInfo['type'] ) {
  79. case 'value':
  80. $getoptLong[] = $optionName . '::';
  81. break;
  82. case 'boolean':
  83. $getoptLong[] = $optionName;
  84. break;
  85. }
  86. }
  87. $parsed = getopt( $getoptShort, $getoptLong );
  88. if ( !is_array( $parsed ) ) {
  89. $this->error( 'Parsing command line arguments failed.' );
  90. }
  91. $this->parsed = $parsed;
  92. if ( !$this->getOption( 'help' ) ) {
  93. foreach ( $requiredKeys as $requiredKey ) {
  94. if ( !isset( $parsed[$requiredKey] ) || !$parsed[$requiredKey] ) {
  95. $this->error( 'Option "' . $requiredKey . '" is required.' );
  96. }
  97. }
  98. }
  99. }
  100. protected function getFlag( $key ) {
  101. if ( !isset( $this->flags[$key] ) || !isset( $this->parsed[$key] ) ) {
  102. return false;
  103. }
  104. return $this->flags[$key]['type'] === 'boolean' ? true : $this->parsed[$key];
  105. }
  106. protected function getOption( $name ) {
  107. if ( !isset( $this->options[$name] ) || !isset( $this->parsed[$name] ) ) {
  108. return false;
  109. }
  110. return $this->options[$name]['type'] === 'boolean' ? true :$this->parsed[$name];
  111. }
  112. public function run() {
  113. if ( !defined( 'SWARM_ENTRY' ) || SWARM_ENTRY !== 'SCRIPT' ) {
  114. $this->error( 'MaintenanceScript instances may only be run as part of a maintenace script.' );
  115. }
  116. if ( php_sapi_name() !== 'cli' ) {
  117. $this->error( 'Maintenance scripts may only be run from the command-line interface.' );
  118. }
  119. if ( !ini_get( 'register_argc_argv' ) || ini_get( 'register_argc_argv' ) == '0' ) {
  120. $this->error( 'Maintenance scripts require `register_argc_argv` to be enabled in php.ini.' );
  121. }
  122. // General options for all scripts
  123. $this->registerOption( 'help', 'boolean', 'Display this message' );
  124. // E.g. to allow puppet to run a script without it facing "(Y/N)" or something.
  125. $this->registerOption( 'quiet', 'boolean', 'Surpress standard output and requests for cli input' );
  126. $this->generalArgKeys = array_merge( array_keys( $this->options ), array_keys( $this->flags ) );
  127. $this->init();
  128. $name = get_class( $this );
  129. // "class FooBarScript extends .."
  130. if ( substr( $name, -6 ) === 'Script' ) {
  131. $name = substr( $name, 0, -6 );
  132. }
  133. $this->name = $name;
  134. if ( !isset( $this->description ) ) {
  135. $this->error( "{$this->name} is missing a description." );
  136. }
  137. $this->parseCliArguments();
  138. // Generate header
  139. $version = $this->getContext()->getVersionInfo( 'bypass-cache' );
  140. $versionText = $version['TestSwarm'];
  141. if ( $version['devInfo'] ) {
  142. $versionText .= ' (' . $version['devInfo']['branch'] . ' ' . substr( $version['devInfo']['SHA1'], 0, 7 ) . ')';
  143. }
  144. $description = wordwrap( $this->description, 72, "\n", true );
  145. $description = explode( "\n", $description );
  146. $description[] = str_repeat( '-', 72 );
  147. $label = "{$this->name}: ";
  148. $prefix = str_repeat( ' ', strlen( $label ) );
  149. $description = $label . implode( "\n" . $prefix, $description );
  150. $this->out("[TestSwarm $versionText] Maintenance script
  151. $description
  152. ");
  153. // Help or continue
  154. if ( $this->getOption( 'help' ) ) {
  155. $this->displayHelp();
  156. } else {
  157. $this->execute();
  158. }
  159. $this->out( '' );
  160. exit;
  161. }
  162. protected function displayHelp() {
  163. $helpScript = '';
  164. $helpGeneral = '';
  165. $placeholder = array(
  166. 'boolean' => '',
  167. 'value' => ' <value>',
  168. );
  169. foreach ( $this->flags as $flagKey => $flagInfo ) {
  170. $help = "\n -{$flagKey}" . $placeholder[$flagInfo['type']] . ": {$flagInfo['description']}";
  171. if ( in_array( $flagKey, $this->generalArgKeys ) ) {
  172. $helpGeneral .= $help;
  173. } else {
  174. $helpScript .= $help;
  175. }
  176. }
  177. foreach ( $this->options as $optionName => $optionInfo ) {
  178. $help = "\n --{$optionName}" . $placeholder[$optionInfo['type']] . ": {$optionInfo['description']}";
  179. if ( in_array( $optionName, $this->generalArgKeys ) ) {
  180. $helpGeneral .= $help;
  181. } else {
  182. $helpScript .= $help;
  183. }
  184. }
  185. print ($helpScript ? "Parameters to this script:$helpScript" : '')
  186. . ($helpScript && $helpGeneral ? "\n\n" : '')
  187. . ($helpGeneral ? "General parameters:$helpGeneral" : '');
  188. }
  189. /** @param $seconds int */
  190. protected function wait( $seconds, $message = '' ) {
  191. print $message;
  192. $backspace = chr(8);
  193. for ( $i = $seconds; $i >= 0; $i-- ) {
  194. if ( $i != $seconds ) {
  195. $prevNrLen = strlen( $i + 1 );
  196. // We have to both print backspaces, spaces and then backspaces again. On
  197. // MacOSX, backspaces only move the cursor, it doesn't hide the characters.
  198. // So we overwrite with spaces and then back up (10|->9| instead of 10|->9|0)
  199. print str_repeat( $backspace, $prevNrLen ) . str_repeat( ' ', $prevNrLen )
  200. . str_repeat( $backspace, $prevNrLen );
  201. }
  202. print $i;
  203. flush();
  204. if ( $i > 0 ) {
  205. sleep( 1 );
  206. }
  207. }
  208. print "\n";
  209. }
  210. /**
  211. * @param string $action: Correct grammar "This script will $action!"
  212. */
  213. protected function timeWarningForScriptWill( $action, $seconds = 10 ) {
  214. $this->wait( 10, "WARNING: This script will $action! You can abort now with Control-C. Starting in " );
  215. }
  216. /**
  217. * @param string $message: [optional] text before the input.
  218. */
  219. protected function cliInput( $prefix = '> ' ) {
  220. static $isatty = null;
  221. if ( $this->getOption( 'quiet' ) ) {
  222. return '';
  223. }
  224. if ( $isatty === null ) {
  225. // Both `echo "foo" | php script.php` and `php script.php > foo.txt`
  226. // are being prevented.
  227. $isatty = posix_isatty( STDIN ) && posix_isatty( STDOUT );
  228. // No need to re-run this check each time, we abort within the if-null check
  229. if ( !$isatty ) {
  230. $this->error( 'This script requires an interactive terminal for output and input. Use --quiet to skip input requests.' );
  231. }
  232. }
  233. if ( function_exists( 'readline' ) ) {
  234. // Use readline if available, it's much nicer to work with for the user
  235. // (autocompletion of filenames and no weird characters when using arrow keys)
  236. return readline( $prefix );
  237. } else {
  238. // Readline is not available on Windows platforms (php.net/intro.readline)
  239. $this->outRaw( $prefix );
  240. return rtrim( fgets( STDIN ), "\n" );
  241. }
  242. }
  243. protected function out( $text ) {
  244. $this->outRaw( "$text\n" );
  245. }
  246. protected function outRaw( $text ) {
  247. if ( !$this->getOption( 'quiet' ) ) {
  248. print $text;
  249. }
  250. }
  251. protected function error( $msg ) {
  252. $msg = "Error: $msg\n";
  253. fwrite( STDERR, $msg );
  254. exit( E_ERROR );
  255. }
  256. public static function newFromContext( TestSwarmContext $context ) {
  257. $script = new static();
  258. $script->context = $context;
  259. return $script;
  260. }
  261. final protected function getContext() {
  262. return $this->context;
  263. }
  264. final private function __construct() {}
  265. }