Browse Source

Moved process library out of project

master
Adam Pippin 4 years ago
parent
commit
cf6965c70f
  1. 93
      app/System/Command.php
  2. 4
      app/System/Command/Xrandr.php
  3. 25
      app/System/CommandArgument.php
  4. 585
      app/System/Process.php
  5. 18
      app/System/RawCommandArgument.php
  6. 13
      app/System/SensitiveCommandArgument.php
  7. 6
      composer.json
  8. 49
      composer.lock

93
app/System/Command.php

@ -1,93 +0,0 @@
<?php
declare(strict_types=1);
namespace App\System;
class Command
{
protected $command;
protected $defaultArguments;
protected $path;
public function __construct(string $command, array $defaultArguments = [])
{
$this->command = $command;
$this->defaultArguments = $defaultArguments;
$this->path = static::find($command);
if (!isset($this->path))
{
throw new \Exception('Command not found: '.$command);
}
}
public function getPath()
{
return $this->path;
}
public function create(array $arguments)
{
$arguments = array_merge($this->defaultArguments, $arguments);
foreach ($arguments as &$argument)
{
if (!is_object($argument) || !($argument instanceof CommandArgument))
{
$argument = new CommandArgument($argument);
}
}
$command = [];
if (strpos($this->path, ' ') === false)
{
$command[] = '"'.$this->path.'"';
}
else
{
$command[] = $this->path;
}
$command = array_merge($command, $arguments);
$process = app()->make('App\\System\\Process', ['command' => $command]);
$process->setStartModeSync();
return $process;
}
public function __invoke(array $arguments)
{
return $this->create($arguments)->start()->getStdout();
}
protected static function find($command)
{
if (PHP_OS == 'WINNT')
{
$result = shell_exec('where '.escapeshellarg($command));
// No idea why Windows is using unix line endings here
// Trim the trailing newline, last result is always empty
$result = explode("\n", trim($result));
$result = end($result);
}
else
{
$result = shell_exec('command -v '.escapeshellarg($command));
}
if (!is_string($result))
{
return;
}
$result = trim($result);
if (empty($result))
{
return;
}
return $result;
}
}

4
app/System/Command/Xrandr.php

@ -4,9 +4,7 @@ declare(strict_types=1);
namespace App\System\Command;
use App\System\Command;
class Xrandr extends Command
class Xrandr extends \Processes\Command
{
public function __construct()
{

25
app/System/CommandArgument.php

@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\System;
class CommandArgument
{
protected $value;
public function __construct($val)
{
$this->value = $val;
}
public function get()
{
return escapeshellarg($this->value);
}
public function __toString()
{
return escapeshellarg($this->value);
}
}

585
app/System/Process.php

@ -1,585 +0,0 @@
<?php
declare(strict_types=1);
namespace App\System;
class Process
{
/**
* The full command path to execute.
*
* This may include raw strings as well as instances of CommandArgument. When
* executing, the instances will be resolved with the CommandArgument::get()
* method. When printing for errors/logs/etc, it will instead be cast to string.
*
* @var array
*/
protected $command;
/**
* Whether to raise an exception if the process exits with a non-zero (failure)
* exit code.
*
* @var bool
*/
protected $raiseExceptionOnFailure = true;
/**
* An array of callbacks to call with each line of output.
*
* Each element should be an array with two elements. The first is a callable.
* The second is optional and may contain a preg-compatible regular expression.
* If this is set, then only lines matching this regular expression will trigger
* the callback and each invocation will include the matches array.
*
* @var array[][callable,string|null]
*/
protected $outputCallbacks = [];
/**
* An array of callbacks to call when the process is stopping.
*
* @var array[callable]
*/
protected $exitCallback = [];
/**
* How to start the process -- one of the START_MODE constants.
*
* @var int
*/
protected $startMode = self::START_MODE_ASYNC;
/**
* Start the process asynchronously. start() will return immediately.
*
* This uses PHP's tick functions to continue pumping the stdout/stderr
* streams and monitoring the process status.
*/
protected const START_MODE_ASYNC = 0x01;
/**
* Start the process sychronously. start() will return once the process
* exits.
*
* This uses stream_select to monitor for output on stdout/stderr and calls
* tick() to pump the streams and check the process status whenever that
* changes.
*/
protected const START_MODE_SYNC = 0x02;
/**
* Start the process bound to the parent process's streams.
*/
protected const START_MODE_REPLACE = 0x04;
/**
* Override the working directory of the spawned process.
*
* @var string|null
*/
protected $cwd;
/**
* Associative array of additional environment variables to set for the
* child process.
*
* @var array[string]
*/
protected $env = [];
/**
* The process's current status represented by one of the STATUS_ constants.
*
* @var int
*/
protected $status = self::STATUS_CREATED;
/**
* A new Process object has been created. The handle has not been opened.
*/
protected const STATUS_CREATED = 0x01;
/**
* The process is starting.
*/
protected const STATUS_STARTING = 0x02;
/**
* The process has been started and is now running.
*/
protected const STATUS_RUNNING = 0x04;
/**
* It looks like the process has exited. We'll call the exit callbacks.
*/
protected const STATUS_STOPPING = 0x05;
/**
* All exit callbacks have been completed.
*/
protected const STATUS_STOPPED = 0x06;
/**
* The process has exited, we've completed all our callbacks and
* unregistered the tick callback if applicable.
*/
protected const STATUS_EXITED = 0x07;
/**
* The process handle returned from proc_open.
* @var resource
*/
protected $handle;
/**
* The process's stdin stream.
*/
protected $stream_stdin;
/**
* The process's stdout stream.
*/
protected $stream_stdout;
/**
* The process's stderr stream.
*/
protected $stream_stderr;
/**
* All of the process's stdout output when not running in replace mode.
* @var string
*/
protected $stdout = null;
/**
* All of the process's stderr output when not running in replace mode.
* @var string
*/
protected $stderr = null;
/**
* When registering an output callback, bitwise constant to specify to
* listen to stdout output.
*
* @var int
*/
public const STREAM_STDOUT = 1;
/**
* When registering an output callback, bitwise constant to specify to
* listen to stderr output.
*
* @var int
*/
public const STREAM_STDERR = 2;
/**
* The process's exit status once it has exited.
* @var int|null
*/
protected $exitStatus = null;
public function __construct(array $command)
{
$this->command = $command;
}
/**
* Set an environment variable to pass to the child process.
*
* @param string $key
* @param string|null $value
* @return Process
*/
public function setEnv($key, $value)
{
if (isset($value))
{
$this->env[$key] = $value;
}
else
{
unset($this->env[$key]);
}
return $this;
}
/**
* Set multiple environment variables from an associative array.
*
* @param array $values
* @return Process
*/
public function setEnvRange(array $values)
{
foreach ($values as $k => $v)
{
$this->setEnv($k, $v);
}
return $this;
}
/**
* Whether to raise an exception if the process returns a non-zero exit
* code.
*
* @param bool $raiseException true to raise an exception
* @return void
*/
public function setRaiseExceptionOnFailure($raiseException)
{
$this->raiseExceptionOnFailure = $raiseException;
return $this;
}
/**
* Add an output callback to receive process stdout and stderr output.
*
* Signature is function(Process $process, string $line, array $matches = null)
*
* @param callable $callback the callback to call
* @param string $filter regex to use to filter lines before calling callback
* @param mixed $streams
* @return void
*/
public function addOutputCallback(callable $callback, $filter = null, $streams = Process::STREAM_STDOUT)
{
$this->outputCallbacks[] = [$callback, $filter, $streams];
return $this;
}
/**
* Add a callback to be called when the process transitions into stopping.
*
* @param callable $callback
* @return void
*/
public function addExitCallback(callable $callback)
{
$this->exitCallback[] = $callback;
return $this;
}
public function setStartModeSync()
{
$this->startMode = static::START_MODE_SYNC;
return $this;
}
public function setStartModeAsync()
{
$this->startMode = static::START_MODE_ASYNC;
return $this;
}
public function setStartModeReplace()
{
$this->startMode = static::START_MODE_SYNC | static::START_MODE_REPLACE;
return $this;
}
public function getStdout()
{
return $this->stdout;
}
public function getStdoutString()
{
return isset($this->stdout) ? implode(PHP_EOL, $this->stdout) : null;
}
public function getStderr()
{
return $this->stderr;
}
public function getStderrString()
{
return isset($this->stderr) ? implode(PHP_EOL, $this->stderr) : null;
}
public function getExitStatus()
{
return $this->exitStatus;
}
/**
* Check whether the process is still running. This will return false
* during the times it is transitioning to starting or into exiting.
*
* @return bool true if running
*/
public function isRunning()
{
return $this->status == static::STATUS_RUNNING;
}
public function fork()
{
if (PHP_OS == 'WINNT')
{
$command = sprintf(
'start /B %s',
$this->getCommand()
);
}
else
{
$command = sprintf(
'nohup %s > /dev/null 2>&1 &',
$this->getCommand()
);
}
shell_exec($command);
}
public function start()
{
$this->status = static::STATUS_STARTING;
if ($this->startMode & static::START_MODE_REPLACE)
{
$descriptors = [
0 => STDIN,
1 => STDOUT,
2 => STDERR
];
}
else
{
$descriptors = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
];
}
$pipes = null;
if (PHP_OS == 'WINNT')
{
$commandWrapper = '"';
}
else
{
$commandWrapper = '';
}
$this->handle = $handle = proc_open(
$commandWrapper.$this->getCommand().$commandWrapper,
$descriptors,
$pipes,
$this->cwd,
array_merge(getenv(), $this->env)
);
if (!is_resource($handle))
{
throw new \Exception('Failed to start process');
}
if (sizeof($pipes))
{
$this->stream_stdin = $pipes[0];
$this->stream_stdout = $pipes[1];
$this->stream_stderr = $pipes[2];
stream_set_blocking($pipes[0], false);
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
}
$this->status = static::STATUS_RUNNING;
if ($this->startMode & static::START_MODE_ASYNC)
{
declare(ticks=1);
register_tick_function([&$this, 'tick']);
}
elseif ($this->startMode & static::START_MODE_SYNC)
{
while ($this->status != static::STATUS_EXITED)
{
if ($this->startMode & static::START_MODE_REPLACE)
{
sleep(1);
}
else
{
$read_streams = [$pipes[1], $pipes[2]];
$write_streams = null;
$except_streams = null;
stream_select($read_streams, $write_streams, $except_streams, 1);
}
$this->tick();
}
}
return $this;
}
public function stop()
{
proc_terminate($this->handle);
return $this;
}
protected function tick()
{
if (!($this->startMode & static::START_MODE_REPLACE))
{
$this->tickStreams();
}
$this->tickProcessStatus();
}
protected function tickStreams()
{
// Pump streams if necessary
if (!isset($this->stdout) || !is_array($this->stdout))
{
$this->stdout = [];
}
while ($line = fgets($this->stream_stdout))
{
// This array is initialized a few lines above and not set anywhere else.
// @phan-suppress-next-line PhanTypeMismatchDimEmpty
$this->stdout[] = trim($line);
foreach ($this->outputCallbacks as $callback)
{
if (($callback[2] & static::STREAM_STDOUT) == 0)
{
continue;
}
if (isset($callback[1]))
{
if (preg_match($callback[1], $line, $matches))
{
call_user_func($callback[0], $this, $line, $matches);
}
}
else
{
call_user_func($callback[0], $this, $line);
}
}
}
if (!isset($this->stderr) || !is_array($this->stderr))
{
$this->stderr = [];
}
while ($line = fgets($this->stream_stderr))
{
// This array is initialized a few lines above and not set anywhere else.
// @phan-suppress-next-line PhanTypeMismatchDimEmpty
$this->stderr[] = trim($line);
foreach ($this->outputCallbacks as $callback)
{
if (($callback[2] & static::STREAM_STDERR) == 0)
{
continue;
}
if (isset($callback[1]))
{
if (preg_match($callback[1], $line, $matches))
{
call_user_func($callback[0], $this, $line, $matches);
}
}
else
{
call_user_func($callback[0], $this, $line);
}
}
}
}
protected function tickProcessStatus()
{
// Check current status
$status = proc_get_status($this->handle);
if (isset($status['exitcode']) && $status['exitcode'] !== -1)
{
$this->exitStatus = $status['exitcode'];
}
if ($status['running'] === false && $this->status == static::STATUS_RUNNING)
{
$this->stopping();
}
elseif ($this->status == static::STATUS_STOPPED)
{
$this->status = static::STATUS_EXITED;
if ($this->startMode & static::START_MODE_ASYNC)
{
unregister_tick_function([&$this, 'tick']);
}
$this->exited();
}
}
protected function stopping()
{
$this->status = static::STATUS_STOPPING;
foreach ($this->exitCallback as $exitCallback)
{
call_user_func($exitCallback, $this);
}
$this->status = static::STATUS_STOPPED;
}
protected function exited()
{
if (!($this->startMode & static::START_MODE_REPLACE))
{
fclose($this->stream_stdin);
fclose($this->stream_stdout);
fclose($this->stream_stderr);
}
proc_close($this->handle);
if ($this->raiseExceptionOnFailure && $this->exitStatus != 0)
{
throw new \Exception('Command execution failed ('.$this->exitStatus.'): '.implode(' ', $this->command).PHP_EOL.
'Stdout:'.PHP_EOL.
$this->getStdoutString().PHP_EOL.
'Stderr:'.PHP_EOL.
$this->getStderrString().PHP_EOL);
}
}
public function write($data)
{
fwrite($this->stream_stdin, $data);
return $this;
}
public function close()
{
fclose($this->stream_stdin);
return $this;
}
protected function getCommand()
{
$processed = [];
$command = $this->command;
$commandName = array_shift($command);
$processed[] = strpos($commandName, ' ') === false ? $commandName : '"'.$commandName.'"';
foreach ($command as $part)
{
if (is_object($part) && $part instanceof CommandArgument)
{
$processed[] = $part->get();
}
else
{
$processed[] = $part;
}
}
return implode(' ', $processed);
}
}

18
app/System/RawCommandArgument.php

@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace App\System;
class RawCommandArgument extends CommandArgument
{
public function get()
{
return $this->value;
}
public function __toString()
{
return $this->value;
}
}

13
app/System/SensitiveCommandArgument.php

@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
namespace App\System;
class SensitiveCommandArgument extends CommandArgument
{
public function __toString()
{
return '<'.escapeshellarg(str_repeat('*', strlen($this->value))).'>';
}
}

6
composer.json

@ -17,6 +17,7 @@
"require": {
"php": "^7.2.0",
"laravel-zero/framework": "^7.0",
"nucleardog/processes": "dev-master",
"symfony/yaml": "^5.0"
},
"require-dev": {
@ -48,5 +49,8 @@
},
"minimum-stability": "dev",
"prefer-stable": true,
"bin": ["monitor_layout"]
"bin": ["monitor_layout"],
"repositories": [
{ "type": "composer", "url": "https://composer.nucleardog.ca/" }
]
}

49
composer.lock

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "c029b92d27ab874462d9886d60030953",
"content-hash": "7979201718112ab89d5243eed39c7ec2",
"packages": [
{
"name": "doctrine/inflector",
@ -1021,6 +1021,49 @@
],
"time": "2020-04-20T15:05:43+00:00"
},
{
"name": "nucleardog/processes",
"version": "dev-master",
"source": {
"type": "git",
"url": "ssh://git@git.nucleardog.ca/nucleardog/processes",
"reference": "8995dbb5f0de7eab45ca9fa1f7d6beed6622ad1a"
},
"require": {
"php": "^7.2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.16",
"phan/phan": "^2.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Processes\\": "src/"
}
},
"scripts": {
"post-autoload-dump": [
"if [ -d .githooks ]; then cp .githooks/* .git/hooks/; fi"
]
},
"license": [
"proprietary"
],
"authors": [
{
"name": "Adam Pippin",
"email": "hello@adampippin.ca"
}
],
"description": "Library for invoking external commands and doing useful things with them",
"homepage": "https://adampippin.ca/",
"support": {
"issues": "https://git.nucleardog.ca/nucleardog/processes/issues",
"source": "https://git.nucleardog.ca/nucleardog/processes"
},
"time": "2020-05-01T23:23:11+00:00"
},
{
"name": "nunomaduro/collision",
"version": "v4.2.0",
@ -5099,7 +5142,9 @@
],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": [],
"stability-flags": {
"nucleardog/processes": 20
},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {

Loading…
Cancel
Save