shellcommand.class.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. <?php
  2. /**
  3. * Command
  4. *
  5. * This class represents a shell command.
  6. *
  7. * @author Michael Härtl <haertl.mike@gmail.com>
  8. * @license http://www.opensource.org/licenses/MIT
  9. */
  10. class ShellCommand
  11. {
  12. /**
  13. * @var bool whether to escape any argument passed through `addArg()`.
  14. * Default is `true`.
  15. */
  16. public $escapeArgs = true;
  17. /**
  18. * @var bool whether to escape the command passed to `setCommand()` or the
  19. * constructor. This is only useful if `$escapeArgs` is `false`. Default
  20. * is `false`.
  21. */
  22. public $escapeCommand = false;
  23. /**
  24. * @var bool whether to use `exec()` instead of `proc_open()`. This can be
  25. * used on Windows system to workaround some quirks there. Note, that any
  26. * errors from your command will be output directly to the PHP output
  27. * stream. `getStdErr()` will also not work anymore and thus you also won't
  28. * get the error output from `getError()` in this case. You also can't pass
  29. * any environment variables to the command if this is enabled. Default is
  30. * `false`.
  31. */
  32. public $useExec = false;
  33. /**
  34. * @var bool whether to capture stderr (2>&1) when `useExec` is true. This
  35. * will try to redirect the stderr to stdout and provide the complete
  36. * output of both in `getStdErr()` and `getError()`. Default is `true`.
  37. */
  38. public $captureStdErr = true;
  39. /**
  40. * @var string|null the initial working dir for `proc_open()`. Default is
  41. * `null` for current PHP working dir.
  42. */
  43. public $procCwd;
  44. /**
  45. * @var array|null an array with environment variables to pass to
  46. * `proc_open()`. Default is `null` for none.
  47. */
  48. public $procEnv;
  49. /**
  50. * @var array|null an array of other_options for `proc_open()`. Default is
  51. * `null` for none.
  52. */
  53. public $procOptions;
  54. /**
  55. * @var bool|null whether to set the stdin/stdout/stderr streams to
  56. * non-blocking mode when `proc_open()` is used. This allows to have huge
  57. * inputs/outputs without making the process hang. The default is `null`
  58. * which will enable the feature on Non-Windows systems. Set it to `true`
  59. * or `false` to manually enable/disable it. It does not work on Windows.
  60. */
  61. public $nonBlockingMode;
  62. /**
  63. * @var int the time in seconds after which a command should be terminated.
  64. * This only works in non-blocking mode. Default is `null` which means the
  65. * process is never terminated.
  66. */
  67. public $timeout;
  68. /**
  69. * @var null|string the locale to temporarily set before calling
  70. * `escapeshellargs()`. Default is `null` for none.
  71. */
  72. public $locale;
  73. /**
  74. * @var null|string|resource to pipe to standard input
  75. */
  76. protected $_stdIn;
  77. /**
  78. * @var string the command to execute
  79. */
  80. protected $_command;
  81. /**
  82. * @var array the list of command arguments
  83. */
  84. protected $_args = array();
  85. /**
  86. * @var string the full command string to execute
  87. */
  88. protected $_execCommand;
  89. /**
  90. * @var string the stdout output
  91. */
  92. protected $_stdOut = '';
  93. /**
  94. * @var string the stderr output
  95. */
  96. protected $_stdErr = '';
  97. /**
  98. * @var int the exit code
  99. */
  100. protected $_exitCode;
  101. /**
  102. * @var string the error message
  103. */
  104. protected $_error = '';
  105. /**
  106. * @var bool whether the command was successfully executed
  107. */
  108. protected $_executed = false;
  109. /**
  110. * @param string|array $options either a command string or an options array
  111. * @see setOptions
  112. */
  113. public function __construct($options = null)
  114. {
  115. if (is_array($options)) {
  116. $this->setOptions($options);
  117. } elseif (is_string($options)) {
  118. $this->setCommand($options);
  119. }
  120. }
  121. /**
  122. * @param array $options array of name => value options that should be
  123. * applied to the object You can also pass options that use a setter, e.g.
  124. * you can pass a `fileName` option which will be passed to
  125. * `setFileName()`.
  126. * @throws \Exception
  127. * @return static for method chaining
  128. */
  129. public function setOptions($options)
  130. {
  131. foreach ($options as $key => $value) {
  132. if (property_exists($this, $key)) {
  133. $this->$key = $value;
  134. } else {
  135. $method = 'set'.ucfirst($key);
  136. if (method_exists($this, $method)) {
  137. call_user_func(array($this,$method), $value);
  138. } else {
  139. throw new \Exception("Unknown configuration option '$key'");
  140. }
  141. }
  142. }
  143. return $this;
  144. }
  145. /**
  146. * @param string $command the command or full command string to execute,
  147. * like 'gzip' or 'gzip -d'. You can still call addArg() to add more
  148. * arguments to the command. If $escapeCommand was set to true, the command
  149. * gets escaped with escapeshellcmd().
  150. * @return static for method chaining
  151. */
  152. public function setCommand($command)
  153. {
  154. if ($this->escapeCommand) {
  155. $command = escapeshellcmd($command);
  156. }
  157. if ($this->getIsWindows()) {
  158. // Make sure to switch to correct drive like "E:" first if we have
  159. // a full path in command
  160. if (isset($command[1]) && $command[1]===':') {
  161. $position = 1;
  162. // Could be a quoted absolute path because of spaces.
  163. // i.e. "C:\Program Files (x86)\file.exe"
  164. } elseif (isset($command[2]) && $command[2]===':') {
  165. $position = 2;
  166. } else {
  167. $position = false;
  168. }
  169. // Absolute path. If it's a relative path, let it slide.
  170. if ($position) {
  171. $command = sprintf(
  172. $command[$position - 1] . ': && cd %s && %s',
  173. escapeshellarg(dirname($command)),
  174. escapeshellarg(basename($command))
  175. );
  176. }
  177. }
  178. $this->_command = $command;
  179. return $this;
  180. }
  181. /**
  182. * @param string|resource $stdIn If set, the string will be piped to the
  183. * command via standard input. This enables the same functionality as
  184. * piping on the command line. It can also be a resource like a file
  185. * handle or a stream in which case its content will be piped into the
  186. * command like an input redirection.
  187. * @return static for method chaining
  188. */
  189. public function setStdIn($stdIn) {
  190. $this->_stdIn = $stdIn;
  191. return $this;
  192. }
  193. /**
  194. * @return string|null the command that was set through setCommand() or
  195. * passed to the constructor. `null` if none.
  196. */
  197. public function getCommand()
  198. {
  199. return $this->_command;
  200. }
  201. /**
  202. * @return string|bool the full command string to execute. If no command
  203. * was set with setCommand() or passed to the constructor it will return
  204. * `false`.
  205. */
  206. public function getExecCommand()
  207. {
  208. if ($this->_execCommand===null) {
  209. $command = $this->getCommand();
  210. if (!$command) {
  211. $this->_error = 'Could not locate any executable command';
  212. return false;
  213. }
  214. $args = $this->getArgs();
  215. $this->_execCommand = $args ? $command.' '.$args : $command;
  216. }
  217. return $this->_execCommand;
  218. }
  219. /**
  220. * @param string $args the command arguments as string. Note that these
  221. * will not get escaped!
  222. * @return static for method chaining
  223. */
  224. public function setArgs($args)
  225. {
  226. $this->_args = array($args);
  227. return $this;
  228. }
  229. /**
  230. * @return string the command args that where set with setArgs() or added
  231. * with addArg() separated by spaces
  232. */
  233. public function getArgs()
  234. {
  235. return implode(' ', $this->_args);
  236. }
  237. /**
  238. * @param string $key the argument key to add e.g. `--feature` or
  239. * `--name=`. If the key does not end with and `=`, the $value will be
  240. * separated by a space, if any. Keys are not escaped unless $value is null
  241. * and $escape is `true`.
  242. * @param string|array|null $value the optional argument value which will
  243. * get escaped if $escapeArgs is true. An array can be passed to add more
  244. * than one value for a key, e.g. `addArg('--exclude',
  245. * array('val1','val2'))` which will create the option `'--exclude' 'val1'
  246. * 'val2'`.
  247. * @param bool|null $escape if set, this overrides the $escapeArgs setting
  248. * and enforces escaping/no escaping
  249. * @return static for method chaining
  250. */
  251. public function addArg($key, $value = null, $escape = null)
  252. {
  253. $doEscape = $escape !== null ? $escape : $this->escapeArgs;
  254. $useLocale = $doEscape && $this->locale !== null;
  255. if ($useLocale) {
  256. $locale = setlocale(LC_CTYPE, 0); // Returns current locale setting
  257. setlocale(LC_CTYPE, $this->locale);
  258. }
  259. if ($value === null) {
  260. $this->_args[] = $doEscape ? escapeshellarg($key) : $key;
  261. } else {
  262. if (substr($key, -1) === '=') {
  263. $separator = '=';
  264. $argKey = substr($key, 0, -1);
  265. } else {
  266. $separator = ' ';
  267. $argKey = $key;
  268. }
  269. $argKey = $doEscape ? escapeshellarg($argKey) : $argKey;
  270. if (is_array($value)) {
  271. $params = array();
  272. foreach ($value as $v) {
  273. $params[] = $doEscape ? escapeshellarg($v) : $v;
  274. }
  275. $this->_args[] = $argKey . $separator . implode(' ', $params);
  276. } else {
  277. $this->_args[] = $argKey . $separator .
  278. ($doEscape ? escapeshellarg($value) : $value);
  279. }
  280. }
  281. if ($useLocale) {
  282. setlocale(LC_CTYPE, $locale);
  283. }
  284. return $this;
  285. }
  286. /**
  287. * @param bool $trim whether to `trim()` the return value. The default is `true`.
  288. * @return string the command output (stdout). Empty if none.
  289. */
  290. public function getOutput($trim = true)
  291. {
  292. return $trim ? trim($this->_stdOut) : $this->_stdOut;
  293. }
  294. /**
  295. * @param bool $trim whether to `trim()` the return value. The default is `true`.
  296. * @return string the error message, either stderr or an internal message.
  297. * Empty string if none.
  298. */
  299. public function getError($trim = true)
  300. {
  301. return $trim ? trim($this->_error) : $this->_error;
  302. }
  303. /**
  304. * @param bool $trim whether to `trim()` the return value. The default is `true`.
  305. * @return string the stderr output. Empty if none.
  306. */
  307. public function getStdErr($trim = true)
  308. {
  309. return $trim ? trim($this->_stdErr) : $this->_stdErr;
  310. }
  311. /**
  312. * @return int|null the exit code or null if command was not executed yet
  313. */
  314. public function getExitCode()
  315. {
  316. return $this->_exitCode;
  317. }
  318. /**
  319. * @return string whether the command was successfully executed
  320. */
  321. public function getExecuted()
  322. {
  323. return $this->_executed;
  324. }
  325. /**
  326. * Execute the command
  327. *
  328. * @return bool whether execution was successful. If `false`, error details
  329. * can be obtained from getError(), getStdErr() and getExitCode().
  330. */
  331. public function execute()
  332. {
  333. $command = $this->getExecCommand();
  334. if (!$command) {
  335. return false;
  336. }
  337. if ($this->useExec) {
  338. $execCommand = $this->captureStdErr ? "$command 2>&1" : $command;
  339. exec($execCommand, $output, $this->_exitCode);
  340. $this->_stdOut = implode("\n", $output);
  341. if ($this->_exitCode !== 0) {
  342. $this->_stdErr = $this->_stdOut;
  343. $this->_error = empty($this->_stdErr) ? 'Command failed' : $this->_stdErr;
  344. return false;
  345. }
  346. } else {
  347. $isInputStream = $this->_stdIn !== null &&
  348. is_resource($this->_stdIn) &&
  349. in_array(get_resource_type($this->_stdIn), array('file', 'stream'));
  350. $isInputString = is_string($this->_stdIn);
  351. $hasInput = $isInputStream || $isInputString;
  352. $hasTimeout = $this->timeout !== null && $this->timeout > 0;
  353. $descriptors = array(
  354. 1 => array('pipe','w'),
  355. 2 => array('pipe', $this->getIsWindows() ? 'a' : 'w'),
  356. );
  357. if ($hasInput) {
  358. $descriptors[0] = array('pipe', 'r');
  359. }
  360. // Issue #20 Set non-blocking mode to fix hanging processes
  361. $nonBlocking = $this->nonBlockingMode === null ?
  362. !$this->getIsWindows() : $this->nonBlockingMode;
  363. $startTime = $hasTimeout ? time() : 0;
  364. $process = proc_open($command, $descriptors, $pipes, $this->procCwd, $this->procEnv, $this->procOptions);
  365. if (is_resource($process)) {
  366. if ($nonBlocking) {
  367. stream_set_blocking($pipes[1], false);
  368. stream_set_blocking($pipes[2], false);
  369. if ($hasInput) {
  370. $writtenBytes = 0;
  371. $isInputOpen = true;
  372. stream_set_blocking($pipes[0], false);
  373. if ($isInputStream) {
  374. stream_set_blocking($this->_stdIn, false);
  375. }
  376. }
  377. // Due to the non-blocking streams we now have to check in
  378. // a loop if the process is still running. We also need to
  379. // ensure that all the pipes are written/read alternately
  380. // until there's nothing left to write/read.
  381. $isRunning = true;
  382. while ($isRunning) {
  383. $status = proc_get_status($process);
  384. $isRunning = $status['running'];
  385. // We first write to stdIn if we have an input. For big
  386. // inputs it will only write until the input buffer of
  387. // the command is full (the command may now wait that
  388. // we read the output buffers - see below). So we may
  389. // have to continue writing in another cycle.
  390. //
  391. // After everything is written it's safe to close the
  392. // input pipe.
  393. if ($isRunning && $hasInput && $isInputOpen) {
  394. if ($isInputStream) {
  395. $written = stream_copy_to_stream($this->_stdIn, $pipes[0], 16 * 1024, $writtenBytes);
  396. if ($written === false || $written === 0) {
  397. $isInputOpen = false;
  398. fclose($pipes[0]);
  399. } else {
  400. $writtenBytes += $written;
  401. }
  402. } else {
  403. if ($writtenBytes < strlen($this->_stdIn)) {
  404. $writtenBytes += fwrite($pipes[0], substr($this->_stdIn, $writtenBytes));
  405. } else {
  406. $isInputOpen = false;
  407. fclose($pipes[0]);
  408. }
  409. }
  410. }
  411. // Read out the output buffers because if they are full
  412. // the command may block execution. We do this even if
  413. // $isRunning is `false`, because there could be output
  414. // left in the buffers.
  415. //
  416. // The latter is only an assumption and needs to be
  417. // verified - but it does not hurt either and works as
  418. // expected.
  419. //
  420. while (($out = fgets($pipes[1])) !== false) {
  421. $this->_stdOut .= $out;
  422. }
  423. while (($err = fgets($pipes[2])) !== false) {
  424. $this->_stdErr .= $err;
  425. }
  426. $runTime = $hasTimeout ? time() - $startTime : 0;
  427. if ($isRunning && $hasTimeout && $runTime >= $this->timeout) {
  428. // Only send a SIGTERM and handle status in the next cycle
  429. proc_terminate($process);
  430. }
  431. if (!$isRunning) {
  432. $this->_exitCode = $status['exitcode'];
  433. if ($this->_exitCode !== 0 && empty($this->_stdErr)) {
  434. if ($status['stopped']) {
  435. $signal = $status['stopsig'];
  436. $this->_stdErr = "Command stopped by signal $signal";
  437. } elseif ($status['signaled']) {
  438. $signal = $status['termsig'];
  439. $this->_stdErr = "Command terminated by signal $signal";
  440. } else {
  441. $this->_stdErr = 'Command unexpectedly terminated without error message';
  442. }
  443. }
  444. fclose($pipes[1]);
  445. fclose($pipes[2]);
  446. proc_close($process);
  447. } else {
  448. // The command is still running. Let's wait some
  449. // time before we start the next cycle.
  450. usleep(10000);
  451. }
  452. }
  453. } else {
  454. if ($hasInput) {
  455. if ($isInputStream) {
  456. stream_copy_to_stream($this->_stdIn, $pipes[0]);
  457. } elseif ($isInputString) {
  458. fwrite($pipes[0], $this->_stdIn);
  459. }
  460. fclose($pipes[0]);
  461. }
  462. $this->_stdOut = stream_get_contents($pipes[1]);
  463. $this->_stdErr = stream_get_contents($pipes[2]);
  464. fclose($pipes[1]);
  465. fclose($pipes[2]);
  466. $this->_exitCode = proc_close($process);
  467. }
  468. if ($this->_exitCode !== 0) {
  469. $this->_error = $this->_stdErr ?
  470. $this->_stdErr :
  471. "Failed without error message: $command (Exit code: {$this->_exitCode})";
  472. return false;
  473. }
  474. } else {
  475. $this->_error = "Could not run command $command";
  476. return false;
  477. }
  478. }
  479. $this->_executed = true;
  480. return true;
  481. }
  482. /**
  483. * @return bool whether we are on a Windows OS
  484. */
  485. public function getIsWindows()
  486. {
  487. return strncasecmp(PHP_OS, 'WIN', 3)===0;
  488. }
  489. /**
  490. * @return string the current command string to execute
  491. */
  492. public function __toString()
  493. {
  494. return (string) $this->getExecCommand();
  495. }
  496. }