PageRenderTime 58ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/inc/MaintenanceScript.php

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