Showing preview only (213K chars total). Download the full file or copy to clipboard to get everything.
Repository: symfony/process
Branch: 8.1
Commit: 6826d46e277c
Files: 52
Total size: 199.6 KB
Directory structure:
gitextract_euimhexu/
├── .gitattributes
├── .github/
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ └── close-pull-request.yml
├── .gitignore
├── CHANGELOG.md
├── Exception/
│ ├── ExceptionInterface.php
│ ├── InvalidArgumentException.php
│ ├── LogicException.php
│ ├── ProcessFailedException.php
│ ├── ProcessSignaledException.php
│ ├── ProcessStartFailedException.php
│ ├── ProcessTimedOutException.php
│ ├── RunProcessFailedException.php
│ └── RuntimeException.php
├── ExecutableFinder.php
├── InputStream.php
├── LICENSE
├── Messenger/
│ ├── RunProcessContext.php
│ ├── RunProcessMessage.php
│ └── RunProcessMessageHandler.php
├── PhpExecutableFinder.php
├── PhpProcess.php
├── PhpSubprocess.php
├── Pipes/
│ ├── AbstractPipes.php
│ ├── PipesInterface.php
│ ├── UnixPipes.php
│ └── WindowsPipes.php
├── Process.php
├── ProcessUtils.php
├── README.md
├── Tests/
│ ├── CreateNewConsoleTest.php
│ ├── ErrorProcessInitiator.php
│ ├── ExecutableFinderTest.php
│ ├── Fixtures/
│ │ ├── executable_with_added_suffix.foo
│ │ ├── executable_without_suffix
│ │ ├── memory.php
│ │ └── open_basedir.php
│ ├── KillableProcessWithOutput.php
│ ├── Messenger/
│ │ └── RunProcessMessageHandlerTest.php
│ ├── NonStopableProcess.php
│ ├── OutputMemoryLimitProcess.php
│ ├── PhpExecutableFinderTest.php
│ ├── PhpProcessTest.php
│ ├── PhpSubprocessTest.php
│ ├── PipeStdinInStdoutStdErrStreamSelect.php
│ ├── ProcessFailedExceptionTest.php
│ ├── ProcessTest.php
│ ├── ProcessWindowsEnvBlockTest.php
│ ├── SignalListener.php
│ └── ThreeSecondProcess.php
├── composer.json
└── phpunit.xml.dist
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
/Tests export-ignore
/phpunit.xml.dist export-ignore
/.git* export-ignore
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
Please do not submit any Pull Requests here. They will be closed.
---
Please submit your PR here instead:
https://github.com/symfony/symfony
This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!
================================================
FILE: .github/workflows/close-pull-request.yml
================================================
name: Close Pull Request
on:
pull_request_target:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: |
Thanks for your Pull Request! We love contributions.
However, you should instead open your PR on the main repository:
https://github.com/symfony/symfony
This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!
================================================
FILE: .gitignore
================================================
vendor/
composer.lock
phpunit.xml
================================================
FILE: CHANGELOG.md
================================================
CHANGELOG
=========
7.3
---
* Add `RunProcessMessage::fromShellCommandline()` to instantiate a Process via the fromShellCommandline method
7.1
---
* Add `Process::setIgnoredSignals()` to disable signal propagation to the child process
6.4
---
* Add `PhpSubprocess` to handle PHP subprocesses that take over the
configuration from their parent
* Add `RunProcessMessage` and `RunProcessMessageHandler`
5.2.0
-----
* added `Process::setOptions()` to set `Process` specific options
* added option `create_new_console` to allow a subprocess to continue
to run after the main script exited, both on Linux and on Windows
5.1.0
-----
* added `Process::getStartTime()` to retrieve the start time of the process as float
5.0.0
-----
* removed `Process::inheritEnvironmentVariables()`
* removed `PhpProcess::setPhpBinary()`
* `Process` must be instantiated with a command array, use `Process::fromShellCommandline()` when the command should be parsed by the shell
* removed `Process::setCommandLine()`
4.4.0
-----
* deprecated `Process::inheritEnvironmentVariables()`: env variables are always inherited.
* added `Process::getLastOutputTime()` method
4.2.0
-----
* added the `Process::fromShellCommandline()` to run commands in a shell wrapper
* deprecated passing a command as string when creating a `Process` instance
* deprecated the `Process::setCommandline()` and the `PhpProcess::setPhpBinary()` methods
* added the `Process::waitUntil()` method to wait for the process only for a
specific output, then continue the normal execution of your application
4.1.0
-----
* added the `Process::isTtySupported()` method that allows to check for TTY support
* made `PhpExecutableFinder` look for the `PHP_BINARY` env var when searching the php binary
* added the `ProcessSignaledException` class to properly catch signaled process errors
4.0.0
-----
* environment variables will always be inherited
* added a second `array $env = []` argument to the `start()`, `run()`,
`mustRun()`, and `restart()` methods of the `Process` class
* added a second `array $env = []` argument to the `start()` method of the
`PhpProcess` class
* the `ProcessUtils::escapeArgument()` method has been removed
* the `areEnvironmentVariablesInherited()`, `getOptions()`, and `setOptions()`
methods of the `Process` class have been removed
* support for passing `proc_open()` options has been removed
* removed the `ProcessBuilder` class, use the `Process` class instead
* removed the `getEnhanceWindowsCompatibility()` and `setEnhanceWindowsCompatibility()` methods of the `Process` class
* passing a not existing working directory to the constructor of the `Symfony\Component\Process\Process` class is not
supported anymore
3.4.0
-----
* deprecated the ProcessBuilder class
* deprecated calling `Process::start()` without setting a valid working directory beforehand (via `setWorkingDirectory()` or constructor)
3.3.0
-----
* added command line arrays in the `Process` class
* added `$env` argument to `Process::start()`, `run()`, `mustRun()` and `restart()` methods
* deprecated the `ProcessUtils::escapeArgument()` method
* deprecated not inheriting environment variables
* deprecated configuring `proc_open()` options
* deprecated configuring enhanced Windows compatibility
* deprecated configuring enhanced sigchild compatibility
2.5.0
-----
* added support for PTY mode
* added the convenience method "mustRun"
* deprecation: Process::setStdin() is deprecated in favor of Process::setInput()
* deprecation: Process::getStdin() is deprecated in favor of Process::getInput()
* deprecation: Process::setInput() and ProcessBuilder::setInput() do not accept non-scalar types
2.4.0
-----
* added the ability to define an idle timeout
2.3.0
-----
* added ProcessUtils::escapeArgument() to fix the bug in escapeshellarg() function on Windows
* added Process::signal()
* added Process::getPid()
* added support for a TTY mode
2.2.0
-----
* added ProcessBuilder::setArguments() to reset the arguments on a builder
* added a way to retrieve the standard and error output incrementally
* added Process:restart()
2.1.0
-----
* added support for non-blocking processes (start(), wait(), isRunning(), stop())
* enhanced Windows compatibility
* added Process::getExitCodeText() that returns a string representation for
the exit code returned by the process
* added ProcessBuilder
================================================
FILE: Exception/ExceptionInterface.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Exception;
/**
* Marker Interface for the Process Component.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface ExceptionInterface extends \Throwable
{
}
================================================
FILE: Exception/InvalidArgumentException.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Exception;
/**
* InvalidArgumentException for the Process Component.
*
* @author Romain Neutron <imprec@gmail.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}
================================================
FILE: Exception/LogicException.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Exception;
/**
* LogicException for the Process Component.
*
* @author Romain Neutron <imprec@gmail.com>
*/
class LogicException extends \LogicException implements ExceptionInterface
{
}
================================================
FILE: Exception/ProcessFailedException.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Exception;
use Symfony\Component\Process\Process;
/**
* Exception for failed processes.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ProcessFailedException extends RuntimeException
{
public function __construct(
private Process $process,
) {
if ($process->isSuccessful()) {
throw new InvalidArgumentException('Expected a failed process, but the given process was successful.');
}
$error = \sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s",
$process->getCommandLine(),
$process->getExitCode(),
$process->getExitCodeText(),
$process->getWorkingDirectory()
);
if (!$process->isOutputDisabled()) {
$error .= \sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s",
$process->getOutput(),
$process->getErrorOutput()
);
}
parent::__construct($error);
$this->process = $process;
}
public function getProcess(): Process
{
return $this->process;
}
}
================================================
FILE: Exception/ProcessSignaledException.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Exception;
use Symfony\Component\Process\Process;
/**
* Exception that is thrown when a process has been signaled.
*
* @author Sullivan Senechal <soullivaneuh@gmail.com>
*/
final class ProcessSignaledException extends RuntimeException
{
public function __construct(
private Process $process,
) {
parent::__construct(\sprintf('The process has been signaled with signal "%s".', $process->getTermSignal()));
}
public function getProcess(): Process
{
return $this->process;
}
public function getSignal(): int
{
return $this->getProcess()->getTermSignal();
}
}
================================================
FILE: Exception/ProcessStartFailedException.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Exception;
use Symfony\Component\Process\Process;
/**
* Exception for processes failed during startup.
*/
class ProcessStartFailedException extends ProcessFailedException
{
public function __construct(
private Process $process,
?string $message,
) {
if ($process->isStarted()) {
throw new InvalidArgumentException('Expected a process that failed during startup, but the given process was started successfully.');
}
$error = \sprintf('The command "%s" failed.'."\n\nWorking directory: %s\n\nError: %s",
$process->getCommandLine(),
$process->getWorkingDirectory(),
$message ?? 'unknown'
);
// Skip parent constructor
RuntimeException::__construct($error);
}
public function getProcess(): Process
{
return $this->process;
}
}
================================================
FILE: Exception/ProcessTimedOutException.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Exception;
use Symfony\Component\Process\Process;
/**
* Exception that is thrown when a process times out.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ProcessTimedOutException extends RuntimeException
{
public const TYPE_GENERAL = 1;
public const TYPE_IDLE = 2;
public function __construct(
private Process $process,
private int $timeoutType,
) {
parent::__construct(\sprintf(
'The process "%s" exceeded the timeout of %s seconds.',
$process->getCommandLine(),
$this->getExceededTimeout()
));
}
public function getProcess(): Process
{
return $this->process;
}
public function isGeneralTimeout(): bool
{
return self::TYPE_GENERAL === $this->timeoutType;
}
public function isIdleTimeout(): bool
{
return self::TYPE_IDLE === $this->timeoutType;
}
public function getExceededTimeout(): ?float
{
return match ($this->timeoutType) {
self::TYPE_GENERAL => $this->process->getTimeout(),
self::TYPE_IDLE => $this->process->getIdleTimeout(),
default => throw new \LogicException(\sprintf('Unknown timeout type "%d".', $this->timeoutType)),
};
}
}
================================================
FILE: Exception/RunProcessFailedException.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Exception;
use Symfony\Component\Process\Messenger\RunProcessContext;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class RunProcessFailedException extends RuntimeException
{
public function __construct(ProcessFailedException $exception, public readonly RunProcessContext $context)
{
parent::__construct($exception->getMessage(), $exception->getCode());
}
}
================================================
FILE: Exception/RuntimeException.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Exception;
/**
* RuntimeException for the Process Component.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
================================================
FILE: ExecutableFinder.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process;
/**
* Generic executable finder.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ExecutableFinder
{
private const CMD_BUILTINS = [
'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date',
'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto',
'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause',
'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set',
'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol',
];
private array $suffixes = [];
/**
* Replaces default suffixes of executable.
*/
public function setSuffixes(array $suffixes): void
{
$this->suffixes = $suffixes;
}
/**
* Adds new possible suffix to check for executable, including the dot (.).
*
* $finder = new ExecutableFinder();
* $finder->addSuffix('.foo');
*/
public function addSuffix(string $suffix): void
{
$this->suffixes[] = $suffix;
}
/**
* Finds an executable by name.
*
* @param string $name The executable name (without the extension)
* @param string|null $default The default to return if no executable is found
* @param array $extraDirs Additional dirs to check into
*/
public function find(string $name, ?string $default = null, array $extraDirs = []): ?string
{
// windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes
if ('\\' === \DIRECTORY_SEPARATOR && \in_array(strtolower($name), self::CMD_BUILTINS, true)) {
return $name;
}
$dirs = array_merge(
explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path') ?: ''),
$extraDirs
);
$suffixes = $this->suffixes;
if ('\\' === \DIRECTORY_SEPARATOR) {
$pathExt = getenv('PATHEXT') ?: '';
$suffixes = array_merge($suffixes, $pathExt ? explode(\PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com']);
}
$suffixes = '' !== pathinfo($name, \PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']);
foreach ($suffixes as $suffix) {
foreach ($dirs as $dir) {
if ('' === $dir) {
$dir = '.';
}
if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) {
return $file;
}
if (!@is_dir($dir) && basename($dir) === $name.$suffix && @is_executable($dir)) {
return $dir;
}
}
}
if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) {
return $default;
}
$execResult = exec('command -v -- '.escapeshellarg($name));
if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) {
return $executablePath;
}
return $default;
}
}
================================================
FILE: InputStream.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process;
use Symfony\Component\Process\Exception\RuntimeException;
/**
* Provides a way to continuously write to the input of a Process until the InputStream is closed.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @implements \IteratorAggregate<int, string>
*/
class InputStream implements \IteratorAggregate
{
private ?\Closure $onEmpty = null;
private array $input = [];
private bool $open = true;
/**
* Sets a callback that is called when the write buffer becomes empty.
*/
public function onEmpty(?callable $onEmpty = null): void
{
$this->onEmpty = null !== $onEmpty ? $onEmpty(...) : null;
}
/**
* Appends an input to the write buffer.
*
* @param resource|string|int|float|bool|\Traversable|null $input The input to append as scalar,
* stream resource or \Traversable
*/
public function write(mixed $input): void
{
if (null === $input) {
return;
}
if ($this->isClosed()) {
throw new RuntimeException(\sprintf('"%s" is closed.', static::class));
}
$this->input[] = ProcessUtils::validateInput(__METHOD__, $input);
}
/**
* Closes the write buffer.
*/
public function close(): void
{
$this->open = false;
}
/**
* Tells whether the write buffer is closed or not.
*/
public function isClosed(): bool
{
return !$this->open;
}
public function getIterator(): \Traversable
{
$this->open = true;
while ($this->open || $this->input) {
if (!$this->input) {
yield '';
continue;
}
$current = array_shift($this->input);
if ($current instanceof \Iterator) {
yield from $current;
} else {
yield $current;
}
if (!$this->input && $this->open && null !== $onEmpty = $this->onEmpty) {
$this->write($onEmpty($this));
}
}
}
}
================================================
FILE: LICENSE
================================================
Copyright (c) 2004-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: Messenger/RunProcessContext.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Messenger;
use Symfony\Component\Process\Process;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class RunProcessContext
{
public readonly ?int $exitCode;
public readonly ?string $output;
public readonly ?string $errorOutput;
public function __construct(
public readonly RunProcessMessage $message,
Process $process,
) {
$this->exitCode = $process->getExitCode();
$this->output = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getOutput();
$this->errorOutput = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getErrorOutput();
}
}
================================================
FILE: Messenger/RunProcessMessage.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Messenger;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
class RunProcessMessage implements \Stringable
{
public ?string $commandLine = null;
public function __construct(
public readonly array $command,
public readonly ?string $cwd = null,
public readonly ?array $env = null,
public readonly mixed $input = null,
public readonly ?float $timeout = 60.0,
) {
}
public function __toString(): string
{
return $this->commandLine ?? implode(' ', $this->command);
}
/**
* Create a process message instance that will instantiate a Process using the fromShellCommandline method.
*
* @see Process::fromShellCommandline
*/
public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): self
{
$message = new self([], $cwd, $env, $input, $timeout);
$message->commandLine = $command;
return $message;
}
}
================================================
FILE: Messenger/RunProcessMessageHandler.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Messenger;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Exception\RunProcessFailedException;
use Symfony\Component\Process\Process;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class RunProcessMessageHandler
{
public function __invoke(RunProcessMessage $message): RunProcessContext
{
$process = match ($message->commandLine) {
null => new Process($message->command, $message->cwd, $message->env, $message->input, $message->timeout),
default => Process::fromShellCommandline($message->commandLine, $message->cwd, $message->env, $message->input, $message->timeout),
};
try {
return new RunProcessContext($message, $process->mustRun());
} catch (ProcessFailedException $e) {
throw new RunProcessFailedException($e, new RunProcessContext($message, $e->getProcess()));
}
}
}
================================================
FILE: PhpExecutableFinder.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process;
/**
* An executable finder specifically designed for the PHP executable.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class PhpExecutableFinder
{
private ExecutableFinder $executableFinder;
public function __construct()
{
$this->executableFinder = new ExecutableFinder();
}
/**
* Finds The PHP executable.
*/
public function find(bool $includeArgs = true): string|false
{
if ($php = getenv('PHP_BINARY')) {
if (!is_executable($php) && !$php = $this->executableFinder->find($php)) {
return false;
}
if (@is_dir($php)) {
return false;
}
return $php;
}
$args = $this->findArguments();
$args = $includeArgs && $args ? ' '.implode(' ', $args) : '';
// PHP_BINARY return the current sapi executable
if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cli', 'cli-server', 'phpdbg'], true)) {
return \PHP_BINARY.$args;
}
if ($php = getenv('PHP_PATH')) {
if (!@is_executable($php) || @is_dir($php)) {
return false;
}
return $php;
}
if ($php = getenv('PHP_PEAR_PHP_BIN')) {
if (@is_executable($php) && !@is_dir($php)) {
return $php;
}
}
if (@is_executable($php = \PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php')) && !@is_dir($php)) {
return $php;
}
$dirs = [\PHP_BINDIR];
if ('\\' === \DIRECTORY_SEPARATOR) {
$dirs[] = 'C:\xampp\php\\';
}
if ($herdPath = getenv('HERD_HOME')) {
$dirs[] = $herdPath.\DIRECTORY_SEPARATOR.'bin';
}
return $this->executableFinder->find('php', false, $dirs);
}
/**
* Finds the PHP executable arguments.
*
* @return list<non-empty-string>
*/
public function findArguments(): array
{
$arguments = [];
if ('phpdbg' === \PHP_SAPI) {
$arguments[] = '-qrr';
}
return $arguments;
}
}
================================================
FILE: PhpProcess.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process;
use Symfony\Component\Process\Exception\LogicException;
use Symfony\Component\Process\Exception\RuntimeException;
/**
* PhpProcess runs a PHP script in an independent process.
*
* $p = new PhpProcess('<?php echo "foo"; ?>');
* $p->run();
* print $p->getOutput()."\n";
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @psalm-import-type EnvArray from Process
*/
class PhpProcess extends Process
{
/**
* @param string $script The PHP script to run (as a string)
* @param string|null $cwd The working directory or null to use the working dir of the current PHP process
* @param EnvArray|null $env The environment variables or null to use the same environment as the current PHP process
* @param int $timeout The timeout in seconds
* @param array|null $php Path to the PHP binary to use with any additional arguments
*/
public function __construct(string $script, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null)
{
if (null === $php) {
$executableFinder = new PhpExecutableFinder();
$php = $executableFinder->find(false);
$php = false === $php ? null : array_merge([$php], $executableFinder->findArguments());
}
if ('phpdbg' === \PHP_SAPI) {
$file = tempnam(sys_get_temp_dir(), 'dbg');
file_put_contents($file, $script);
register_shutdown_function('unlink', $file);
$php[] = $file;
$script = null;
}
parent::__construct($php, $cwd, $env, $script, $timeout);
}
public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static
{
throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class));
}
/**
* @param (callable('out'|'err', string):void)|null $callback
* @param EnvArray $env
*/
public function start(?callable $callback = null, array $env = []): void
{
if (null === $this->getCommandLine()) {
throw new RuntimeException('Unable to find the PHP executable.');
}
parent::start($callback, $env);
}
}
================================================
FILE: PhpSubprocess.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process;
use Symfony\Component\Process\Exception\LogicException;
use Symfony\Component\Process\Exception\RuntimeException;
/**
* PhpSubprocess runs a PHP command as a subprocess while keeping the original php.ini settings.
*
* For this, it generates a temporary php.ini file taking over all the current settings and disables
* loading additional .ini files. Basically, your command gets prefixed using "php -n -c /tmp/temp.ini".
*
* Given your php.ini contains "memory_limit=-1" and you have a "MemoryTest.php" with the following content:
*
* <?php var_dump(ini_get('memory_limit'));
*
* These are the differences between the regular Process and PhpSubprocess classes:
*
* $p = new Process(['php', '-d', 'memory_limit=256M', 'MemoryTest.php']);
* $p->run();
* print $p->getOutput()."\n";
*
* This will output "string(2) "-1", because the process is started with the default php.ini settings.
*
* $p = new PhpSubprocess(['MemoryTest.php'], null, null, 60, ['php', '-d', 'memory_limit=256M']);
* $p->run();
* print $p->getOutput()."\n";
*
* This will output "string(4) "256M"", because the process is started with the temporarily created php.ini settings.
*
* @author Yanick Witschi <yanick.witschi@terminal42.ch>
* @author Partially copied and heavily inspired from composer/xdebug-handler by John Stevenson <john-stevenson@blueyonder.co.uk>
*
* @psalm-import-type EnvArray from Process
*/
class PhpSubprocess extends Process
{
/**
* @param array $command The command to run and its arguments listed as separate entries. They will automatically
* get prefixed with the PHP binary
* @param string|null $cwd The working directory or null to use the working dir of the current PHP process
* @param EnvArray|null $env The environment variables or null to use the same environment as the current PHP process
* @param int $timeout The timeout in seconds
* @param array|null $php Path to the PHP binary to use with any additional arguments
*/
public function __construct(array $command, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null)
{
if (null === $php) {
$executableFinder = new PhpExecutableFinder();
$php = $executableFinder->find(false);
$php = false === $php ? null : array_merge([$php], $executableFinder->findArguments());
}
if (null === $php) {
throw new RuntimeException('Unable to find PHP binary.');
}
$tmpIni = $this->writeTmpIni($this->getAllIniFiles(), sys_get_temp_dir());
$php = array_merge($php, ['-n', '-c', $tmpIni]);
register_shutdown_function('unlink', $tmpIni);
$command = array_merge($php, $command);
parent::__construct($command, $cwd, $env, null, $timeout);
}
public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static
{
throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class));
}
/**
* @param (callable('out'|'err', string):void)|null $callback
*/
public function start(?callable $callback = null, array $env = []): void
{
if (null === $this->getCommandLine()) {
throw new RuntimeException('Unable to find the PHP executable.');
}
parent::start($callback, $env);
}
private function writeTmpIni(array $iniFiles, string $tmpDir): string
{
if (false === $tmpfile = @tempnam($tmpDir, '')) {
throw new RuntimeException('Unable to create temporary ini file.');
}
// $iniFiles has at least one item and it may be empty
if ('' === $iniFiles[0]) {
array_shift($iniFiles);
}
$content = '';
foreach ($iniFiles as $file) {
// Check for inaccessible ini files
if (($data = @file_get_contents($file)) === false) {
throw new RuntimeException('Unable to read ini: '.$file);
}
// Check and remove directives after HOST and PATH sections
if (preg_match('/^\s*\[(?:PATH|HOST)\s*=/mi', $data, $matches, \PREG_OFFSET_CAPTURE)) {
$data = substr($data, 0, $matches[0][1]);
}
$content .= $data."\n";
}
// Merge loaded settings into our ini content, if it is valid
$config = parse_ini_string($content);
$loaded = ini_get_all(null, false);
if (false === $config || false === $loaded) {
throw new RuntimeException('Unable to parse ini data.');
}
$content .= $this->mergeLoadedConfig($loaded, $config);
// Work-around for https://bugs.php.net/bug.php?id=75932
$content .= "opcache.enable_cli=0\n";
if (false === @file_put_contents($tmpfile, $content)) {
throw new RuntimeException('Unable to write temporary ini file.');
}
return $tmpfile;
}
private function mergeLoadedConfig(array $loadedConfig, array $iniConfig): string
{
$content = '';
foreach ($loadedConfig as $name => $value) {
if (!\is_string($value)) {
continue;
}
if (!isset($iniConfig[$name]) || $iniConfig[$name] !== $value) {
// Double-quote escape each value
$content .= $name.'="'.addcslashes($value, '\\"')."\"\n";
}
}
return $content;
}
private function getAllIniFiles(): array
{
$paths = [(string) php_ini_loaded_file()];
if (false !== $scanned = php_ini_scanned_files()) {
$paths = array_merge($paths, array_map('trim', explode(',', $scanned)));
}
return $paths;
}
}
================================================
FILE: Pipes/AbstractPipes.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Pipes;
use Symfony\Component\Process\Exception\InvalidArgumentException;
/**
* @author Romain Neutron <imprec@gmail.com>
*
* @internal
*/
abstract class AbstractPipes implements PipesInterface
{
public array $pipes = [];
private string $inputBuffer = '';
/** @var resource|string|\Iterator */
private $input;
private bool $blocked = true;
private ?string $lastError = null;
/**
* @param resource|string|\Iterator $input
*/
public function __construct($input)
{
if (\is_resource($input) || $input instanceof \Iterator) {
$this->input = $input;
} else {
$this->inputBuffer = (string) $input;
}
}
public function close(): void
{
foreach ($this->pipes as $pipe) {
if (\is_resource($pipe)) {
fclose($pipe);
}
}
$this->pipes = [];
}
/**
* Returns true if a system call has been interrupted.
*
* stream_select() returns false when the `select` system call is interrupted by an incoming signal.
*/
protected function hasSystemCallBeenInterrupted(): bool
{
$lastError = $this->lastError;
$this->lastError = null;
if (null === $lastError) {
return false;
}
if (false !== stripos($lastError, 'interrupted system call')) {
return true;
}
// on applications with a different locale than english, the message above is not found because
// it's translated. So we also check for the SOCKET_EINTR constant which is defined under
// Windows and UNIX-like platforms (if available on the platform).
return \defined('SOCKET_EINTR') && str_starts_with($lastError, 'stream_select(): Unable to select ['.\SOCKET_EINTR.']');
}
/**
* Unblocks streams.
*/
protected function unblock(): void
{
if (!$this->blocked) {
return;
}
foreach ($this->pipes as $pipe) {
stream_set_blocking($pipe, false);
}
if (\is_resource($this->input)) {
stream_set_blocking($this->input, false);
}
$this->blocked = false;
}
/**
* Writes input to stdin.
*
* @throws InvalidArgumentException When an input iterator yields a non supported value
*/
protected function write(): ?array
{
if (!isset($this->pipes[0])) {
return null;
}
$input = $this->input;
if ($input instanceof \Iterator) {
if (!$input->valid()) {
$input = null;
} elseif (\is_resource($input = $input->current())) {
stream_set_blocking($input, false);
} elseif (!isset($this->inputBuffer[0])) {
if (!\is_string($input)) {
if (!\is_scalar($input)) {
throw new InvalidArgumentException(\sprintf('"%s" yielded a value of type "%s", but only scalars and stream resources are supported.', get_debug_type($this->input), get_debug_type($input)));
}
$input = (string) $input;
}
$this->inputBuffer = $input;
$this->input->next();
$input = null;
} else {
$input = null;
}
}
$r = $e = [];
$w = [$this->pipes[0]];
// let's have a look if something changed in streams
if (false === @stream_select($r, $w, $e, 0, 0)) {
return null;
}
foreach ($w as $stdin) {
if (isset($this->inputBuffer[0])) {
if (false === $written = @fwrite($stdin, $this->inputBuffer)) {
return $this->closeBrokenInputPipe();
}
$this->inputBuffer = substr($this->inputBuffer, $written);
if (isset($this->inputBuffer[0]) && isset($this->pipes[0])) {
return [$this->pipes[0]];
}
}
if ($input) {
while (true) {
$data = fread($input, self::CHUNK_SIZE);
if (!isset($data[0])) {
break;
}
if (false === $written = @fwrite($stdin, $data)) {
return $this->closeBrokenInputPipe();
}
$data = substr($data, $written);
if (isset($data[0])) {
$this->inputBuffer = $data;
return isset($this->pipes[0]) ? [$this->pipes[0]] : null;
}
}
if (feof($input)) {
if ($this->input instanceof \Iterator) {
$this->input->next();
} else {
$this->input = null;
}
}
}
}
// no input to read on resource, buffer is empty
if (!isset($this->inputBuffer[0]) && !($this->input instanceof \Iterator ? $this->input->valid() : $this->input)) {
$this->input = null;
fclose($this->pipes[0]);
unset($this->pipes[0]);
} elseif (!$w) {
return [$this->pipes[0]];
}
return null;
}
private function closeBrokenInputPipe(): void
{
$this->lastError = error_get_last()['message'] ?? null;
if (\is_resource($this->pipes[0] ?? null)) {
fclose($this->pipes[0]);
}
unset($this->pipes[0]);
$this->input = null;
$this->inputBuffer = '';
}
/**
* @internal
*/
public function handleError(int $type, string $msg): void
{
$this->lastError = $msg;
}
}
================================================
FILE: Pipes/PipesInterface.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Pipes;
/**
* PipesInterface manages descriptors and pipes for the use of proc_open.
*
* @author Romain Neutron <imprec@gmail.com>
*
* @internal
*/
interface PipesInterface
{
public const CHUNK_SIZE = 16384;
/**
* Returns an array of descriptors for the use of proc_open.
*/
public function getDescriptors(): array;
/**
* Returns an array of filenames indexed by their related stream in case these pipes use temporary files.
*
* @return string[]
*/
public function getFiles(): array;
/**
* Reads data in file handles and pipes.
*
* @param bool $blocking Whether to use blocking calls or not
* @param bool $close Whether to close pipes if they've reached EOF
*
* @return string[] An array of read data indexed by their fd
*/
public function readAndWrite(bool $blocking, bool $close = false): array;
/**
* Returns if the current state has open file handles or pipes.
*/
public function areOpen(): bool;
/**
* Returns if pipes are able to read output.
*/
public function haveReadSupport(): bool;
/**
* Closes file handles and pipes.
*/
public function close(): void;
}
================================================
FILE: Pipes/UnixPipes.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Pipes;
use Symfony\Component\Process\Process;
/**
* UnixPipes implementation uses unix pipes as handles.
*
* @author Romain Neutron <imprec@gmail.com>
*
* @internal
*/
class UnixPipes extends AbstractPipes
{
public function __construct(
private ?bool $ttyMode,
private bool $ptyMode,
mixed $input,
private bool $haveReadSupport,
) {
parent::__construct($input);
}
public function __serialize(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __unserialize(array $data): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->close();
}
public function getDescriptors(): array
{
if (!$this->haveReadSupport) {
$nullstream = fopen('/dev/null', 'c');
return [
['pipe', 'r'],
$nullstream,
$nullstream,
];
}
if ($this->ttyMode) {
return [
['file', '/dev/tty', 'r'],
['file', '/dev/tty', 'w'],
['file', '/dev/tty', 'w'],
];
}
if ($this->ptyMode && Process::isPtySupported()) {
return [
['pty'],
['pty'],
['pipe', 'w'], // stderr needs to be in a pipe to correctly split error and output, since PHP will use the same stream for both
];
}
return [
['pipe', 'r'],
['pipe', 'w'], // stdout
['pipe', 'w'], // stderr
];
}
public function getFiles(): array
{
return [];
}
public function readAndWrite(bool $blocking, bool $close = false): array
{
$this->unblock();
$w = $this->write();
$read = $e = [];
$r = $this->pipes;
unset($r[0]);
// let's have a look if something changed in streams
set_error_handler($this->handleError(...));
if (($r || $w) && false === stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) {
restore_error_handler();
// if a system call has been interrupted, forget about it, let's try again
// otherwise, an error occurred, let's reset pipes
if (!$this->hasSystemCallBeenInterrupted()) {
$this->pipes = [];
}
return $read;
}
restore_error_handler();
foreach ($r as $pipe) {
// prior PHP 5.4 the array passed to stream_select is modified and
// lose key association, we have to find back the key
$read[$type = array_search($pipe, $this->pipes, true)] = '';
do {
$data = @fread($pipe, self::CHUNK_SIZE);
$read[$type] .= $data;
} while (isset($data[0]) && ($close || isset($data[self::CHUNK_SIZE - 1])));
if (!isset($read[$type][0])) {
unset($read[$type]);
}
if ($close && feof($pipe)) {
fclose($pipe);
unset($this->pipes[$type]);
}
}
return $read;
}
public function haveReadSupport(): bool
{
return $this->haveReadSupport;
}
public function areOpen(): bool
{
return (bool) $this->pipes;
}
}
================================================
FILE: Pipes/WindowsPipes.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Pipes;
use Symfony\Component\Process\Exception\RuntimeException;
use Symfony\Component\Process\Process;
/**
* WindowsPipes implementation uses temporary files as handles.
*
* @see https://bugs.php.net/51800
* @see https://bugs.php.net/65650
*
* @author Romain Neutron <imprec@gmail.com>
*
* @internal
*/
class WindowsPipes extends AbstractPipes
{
private array $files = [];
private array $fileHandles = [];
private array $lockHandles = [];
private array $readBytes = [
Process::STDOUT => 0,
Process::STDERR => 0,
];
public function __construct(
mixed $input,
private bool $haveReadSupport,
) {
if ($this->haveReadSupport) {
// Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big.
// Workaround for this problem is to use temporary files instead of pipes on Windows platform.
//
// @see https://bugs.php.net/51800
$pipes = [
Process::STDOUT => Process::OUT,
Process::STDERR => Process::ERR,
];
$tmpDir = sys_get_temp_dir();
$lastError = 'unknown reason';
set_error_handler(static function ($type, $msg) use (&$lastError) { $lastError = $msg; });
for ($i = 0;; ++$i) {
foreach ($pipes as $pipe => $name) {
$file = \sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name);
if (!$h = fopen($file.'.lock', 'w')) {
if (file_exists($file.'.lock')) {
continue 2;
}
restore_error_handler();
throw new RuntimeException('A temporary file could not be opened to write the process output: '.$lastError);
}
if (!flock($h, \LOCK_EX | \LOCK_NB)) {
continue 2;
}
if (isset($this->lockHandles[$pipe])) {
flock($this->lockHandles[$pipe], \LOCK_UN);
fclose($this->lockHandles[$pipe]);
}
$this->lockHandles[$pipe] = $h;
if (!($h = fopen($file, 'w')) || !fclose($h) || !$h = fopen($file, 'r')) {
flock($this->lockHandles[$pipe], \LOCK_UN);
fclose($this->lockHandles[$pipe]);
unset($this->lockHandles[$pipe]);
continue 2;
}
$this->fileHandles[$pipe] = $h;
$this->files[$pipe] = $file;
}
break;
}
restore_error_handler();
}
parent::__construct($input);
}
public function __serialize(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __unserialize(array $data): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->close();
}
public function getDescriptors(): array
{
if (!$this->haveReadSupport) {
$nullstream = fopen('NUL', 'c');
return [
['pipe', 'r'],
$nullstream,
$nullstream,
];
}
// We're not using pipe on Windows platform as it hangs (https://bugs.php.net/51800)
// We're not using file handles as it can produce corrupted output https://bugs.php.net/65650
// So we redirect output within the commandline and pass the nul device to the process
return [
['pipe', 'r'],
['file', 'NUL', 'w'],
['file', 'NUL', 'w'],
];
}
public function getFiles(): array
{
return $this->files;
}
public function readAndWrite(bool $blocking, bool $close = false): array
{
$this->unblock();
$w = $this->write();
$read = $r = $e = [];
if ($blocking) {
if ($w) {
@stream_select($r, $w, $e, 0, Process::TIMEOUT_PRECISION * 1E6);
} elseif ($this->fileHandles) {
usleep((int) (Process::TIMEOUT_PRECISION * 1E6));
}
}
foreach ($this->fileHandles as $type => $fileHandle) {
$data = stream_get_contents($fileHandle, -1, $this->readBytes[$type]);
if (isset($data[0])) {
$this->readBytes[$type] += \strlen($data);
$read[$type] = $data;
}
if ($close) {
ftruncate($fileHandle, 0);
fclose($fileHandle);
flock($this->lockHandles[$type], \LOCK_UN);
fclose($this->lockHandles[$type]);
unset($this->fileHandles[$type], $this->lockHandles[$type]);
}
}
return $read;
}
public function haveReadSupport(): bool
{
return $this->haveReadSupport;
}
public function areOpen(): bool
{
return $this->pipes && $this->fileHandles;
}
public function close(): void
{
parent::close();
foreach ($this->fileHandles as $type => $handle) {
ftruncate($handle, 0);
fclose($handle);
flock($this->lockHandles[$type], \LOCK_UN);
fclose($this->lockHandles[$type]);
}
$this->fileHandles = $this->lockHandles = [];
}
}
================================================
FILE: Process.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process;
use Symfony\Component\Process\Exception\InvalidArgumentException;
use Symfony\Component\Process\Exception\LogicException;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Exception\ProcessSignaledException;
use Symfony\Component\Process\Exception\ProcessStartFailedException;
use Symfony\Component\Process\Exception\ProcessTimedOutException;
use Symfony\Component\Process\Exception\RuntimeException;
use Symfony\Component\Process\Pipes\UnixPipes;
use Symfony\Component\Process\Pipes\WindowsPipes;
/**
* Process is a thin wrapper around proc_* functions to easily
* start independent PHP processes.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Romain Neutron <imprec@gmail.com>
*
* @implements \IteratorAggregate<string, string>
*
* @psalm-type EnvArray = array<string, string|\Stringable|false>
*/
class Process implements \IteratorAggregate
{
public const ERR = 'err';
public const OUT = 'out';
public const STATUS_READY = 'ready';
public const STATUS_STARTED = 'started';
public const STATUS_TERMINATED = 'terminated';
public const STDIN = 0;
public const STDOUT = 1;
public const STDERR = 2;
// Timeout Precision in seconds.
public const TIMEOUT_PRECISION = 0.2;
public const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking
public const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory
public const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating
public const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating
/**
* Maximum number of UTF-16 code units allowed in the Windows environment block.
*
* The Win32 CreateProcess API encodes env vars as KEY=VALUE\0 in UTF-16LE,
* terminated by an extra \0. Exceeding this limit causes proc_open() to hang
* silently rather than returning false.
*
* @see https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa
*/
private const WINDOWS_ENV_BLOCK_MAX_LENGTH = 32767;
/** @var \Closure('out'|'err', string):bool|null */
private ?\Closure $callback = null;
/** @var string[]|string */
private array|string $commandline;
private ?string $cwd;
/** @var EnvArray */
private array $env = [];
/** @var resource|string|\Iterator|null */
private $input;
private ?float $starttime = null;
private ?float $lastOutputTime = null;
private ?float $timeout = null;
private ?float $idleTimeout = null;
private ?int $exitcode = null;
private array $fallbackStatus = [];
private array $processInformation;
private bool $outputDisabled = false;
/** @var resource */
private $stdout;
/** @var resource */
private $stderr;
/** @var resource|null */
private $process;
private string $status = self::STATUS_READY;
private int $incrementalOutputOffset = 0;
private int $incrementalErrorOutputOffset = 0;
private bool $tty = false;
private bool $pty;
private array $options = ['suppress_errors' => true, 'bypass_shell' => true];
private array $ignoredSignals = [];
private WindowsPipes|UnixPipes $processPipes;
private ?int $latestSignal = null;
private static ?bool $sigchild = null;
private static array $executables = [];
/**
* Exit codes translation table.
*
* User-defined errors must use exit codes in the 64-113 range.
*/
public static array $exitCodes = [
0 => 'OK',
1 => 'General error',
2 => 'Misuse of shell builtins',
126 => 'Invoked command cannot execute',
127 => 'Command not found',
128 => 'Invalid exit argument',
// signals
129 => 'Hangup',
130 => 'Interrupt',
131 => 'Quit and dump core',
132 => 'Illegal instruction',
133 => 'Trace/breakpoint trap',
134 => 'Process aborted',
135 => 'Bus error: "access to undefined portion of memory object"',
136 => 'Floating point exception: "erroneous arithmetic operation"',
137 => 'Kill (terminate immediately)',
138 => 'User-defined 1',
139 => 'Segmentation violation',
140 => 'User-defined 2',
141 => 'Write to pipe with no one reading',
142 => 'Signal raised by alarm',
143 => 'Termination (request to terminate)',
// 144 - not defined
145 => 'Child process terminated, stopped (or continued*)',
146 => 'Continue if stopped',
147 => 'Stop executing temporarily',
148 => 'Terminal stop signal',
149 => 'Background process attempting to read from tty ("in")',
150 => 'Background process attempting to write to tty ("out")',
151 => 'Urgent data available on socket',
152 => 'CPU time limit exceeded',
153 => 'File size limit exceeded',
154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
155 => 'Profiling timer expired',
// 156 - not defined
157 => 'Pollable event',
// 158 - not defined
159 => 'Bad syscall',
];
/**
* @param string[] $command The command to run and its arguments listed as separate entries
* @param string|null $cwd The working directory or null to use the working dir of the current PHP process
* @param EnvArray|null $env The environment variables or null to use the same environment as the current PHP process
* @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input
* @param int|float|null $timeout The timeout in seconds or null to disable
*
* @throws LogicException When proc_open is not installed
*/
public function __construct(array $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60)
{
if (!\function_exists('proc_open')) {
throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.');
}
$this->commandline = $command;
$this->cwd = $cwd;
// on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started
// on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected
// @see : https://bugs.php.net/51800
// @see : https://bugs.php.net/50524
if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) {
$this->cwd = getcwd();
}
if (null !== $env) {
$this->setEnv($env);
}
$this->setInput($input);
$this->setTimeout($timeout);
$this->pty = false;
}
/**
* Creates a Process instance as a command-line to be run in a shell wrapper.
*
* Command-lines are parsed by the shell of your OS (/bin/sh on Unix-like, cmd.exe on Windows.)
* This allows using e.g. pipes or conditional execution. In this mode, signals are sent to the
* shell wrapper and not to your commands.
*
* In order to inject dynamic values into command-lines, we strongly recommend using placeholders.
* This will save escaping values, which is not portable nor secure anyway:
*
* $process = Process::fromShellCommandline('my_command "${:MY_VAR}"');
* $process->run(null, ['MY_VAR' => $theValue]);
*
* @param string $command The command line to pass to the shell of the OS
* @param string|null $cwd The working directory or null to use the working dir of the current PHP process
* @param EnvArray|null $env The environment variables or null to use the same environment as the current PHP process
* @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input
* @param int|float|null $timeout The timeout in seconds or null to disable
*
* @throws LogicException When proc_open is not installed
*/
public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static
{
$process = new static([], $cwd, $env, $input, $timeout);
$process->commandline = $command;
return $process;
}
public function __serialize(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __unserialize(array $data): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
if ($this->options['create_new_console'] ?? false) {
$this->processPipes->close();
} else {
$this->stop(0);
}
}
public function __clone()
{
$this->resetProcessData();
}
/**
* Runs the process.
*
* The callback receives the type of output (out or err) and
* some bytes from the output in real-time. It allows to have feedback
* from the independent process during execution.
*
* The STDOUT and STDERR are also available after the process is finished
* via the getOutput() and getErrorOutput() methods.
*
* @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
* @param EnvArray $env
*
* @return int The exit status code
*
* @throws ProcessStartFailedException When process can't be launched
* @throws RuntimeException When process is already running
* @throws ProcessTimedOutException When process timed out
* @throws ProcessSignaledException When process stopped after receiving signal
* @throws LogicException In case a callback is provided and output has been disabled
*
* @final
*/
public function run(?callable $callback = null, array $env = []): int
{
$this->start($callback, $env);
return $this->wait();
}
/**
* Runs the process.
*
* This is identical to run() except that an exception is thrown if the process
* exits with a non-zero exit code.
*
* @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
* @param EnvArray $env
*
* @return $this
*
* @throws ProcessFailedException When process didn't terminate successfully
* @throws RuntimeException When process can't be launched
* @throws RuntimeException When process is already running
* @throws ProcessTimedOutException When process timed out
* @throws ProcessSignaledException When process stopped after receiving signal
* @throws LogicException In case a callback is provided and output has been disabled
*
* @final
*/
public function mustRun(?callable $callback = null, array $env = []): static
{
if (0 !== $this->run($callback, $env)) {
throw new ProcessFailedException($this);
}
return $this;
}
/**
* Starts the process and returns after writing the input to STDIN.
*
* This method blocks until all STDIN data is sent to the process then it
* returns while the process runs in the background.
*
* The termination of the process can be awaited with wait().
*
* The callback receives the type of output (out or err) and some bytes from
* the output in real-time while writing the standard input to the process.
* It allows to have feedback from the independent process during execution.
*
* @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
* @param EnvArray $env
*
* @throws ProcessStartFailedException When process can't be launched
* @throws RuntimeException When process is already running
* @throws LogicException In case a callback is provided and output has been disabled
*/
public function start(?callable $callback = null, array $env = []): void
{
if ($this->isRunning()) {
throw new RuntimeException('Process is already running.');
}
$this->resetProcessData();
$this->starttime = $this->lastOutputTime = microtime(true);
$this->callback = $this->buildCallback($callback);
$descriptors = $this->getDescriptors(null !== $callback);
if ($this->env) {
$env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env;
}
$env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv();
if (\is_array($commandline = $this->commandline)) {
$commandline = array_values(array_map(strval(...), $commandline));
} else {
$commandline = $this->replacePlaceholders($commandline, $env);
}
if ('\\' === \DIRECTORY_SEPARATOR) {
$commandline = $this->prepareWindowsCommandLine($commandline, $env);
} elseif ($this->isSigchildEnabled()) {
// last exit code is output on the fourth pipe and caught to work around --enable-sigchild
$descriptors[3] = ['pipe', 'w'];
if (\is_array($commandline)) {
// exec is mandatory to deal with sending a signal to the process
$commandline = 'exec '.$this->buildShellCommandline($commandline);
}
// See https://unix.stackexchange.com/questions/71205/background-process-pipe-input
$commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;';
$commandline .= 'pid=$!; echo $pid >&3; wait $pid 2>/dev/null; code=$?; echo $code >&3; exit $code';
}
$envPairs = [];
foreach ($env as $k => $v) {
if (false !== $v && !\in_array($k = (string) $k, ['', 'argc', 'argv', 'ARGC', 'ARGV'], true) && !str_contains($k, '=') && !str_contains($k, "\0")) {
$envPairs[] = $k.'='.$v;
}
}
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->validateWindowsEnvBlockSize($envPairs);
}
if (!is_dir($this->cwd)) {
throw new RuntimeException(\sprintf('The provided cwd "%s" does not exist.', $this->cwd));
}
$lastError = null;
set_error_handler(static function ($type, $msg) use (&$lastError) {
$lastError = $msg;
return true;
});
$oldMask = [];
if ($this->ignoredSignals && \function_exists('pcntl_sigprocmask')) {
// we block signals we want to ignore, as proc_open will use fork / posix_spawn which will copy the signal mask this allow to block
// signals in the child process
pcntl_sigprocmask(\SIG_BLOCK, $this->ignoredSignals, $oldMask);
}
try {
$process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options);
// Ensure array vs string commands behave the same
if (!$process && \is_array($commandline)) {
$process = @proc_open('exec '.$this->buildShellCommandline($commandline), $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options);
}
} finally {
if ($this->ignoredSignals && \function_exists('pcntl_sigprocmask')) {
// we restore the signal mask here to avoid any side effects
pcntl_sigprocmask(\SIG_SETMASK, $oldMask);
}
restore_error_handler();
}
if (!$process) {
throw new ProcessStartFailedException($this, $lastError);
}
$this->process = $process;
$this->status = self::STATUS_STARTED;
if (isset($descriptors[3])) {
$this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]);
}
if ($this->tty) {
return;
}
$this->updateStatus(false);
$this->checkTimeout();
}
/**
* Restarts the process.
*
* Be warned that the process is cloned before being started.
*
* @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
* @param EnvArray $env
*
* @throws ProcessStartFailedException When process can't be launched
* @throws RuntimeException When process is already running
*
* @see start()
*
* @final
*/
public function restart(?callable $callback = null, array $env = []): static
{
if ($this->isRunning()) {
throw new RuntimeException('Process is already running.');
}
$process = clone $this;
$process->start($callback, $env);
return $process;
}
/**
* Waits for the process to terminate.
*
* The callback receives the type of output (out or err) and some bytes
* from the output in real-time while writing the standard input to the process.
* It allows to have feedback from the independent process during execution.
*
* @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
*
* @return int The exitcode of the process
*
* @throws ProcessTimedOutException When process timed out
* @throws ProcessSignaledException When process stopped after receiving signal
* @throws LogicException When process is not yet started
*/
public function wait(?callable $callback = null): int
{
$this->requireProcessIsStarted(__FUNCTION__);
$this->updateStatus(false);
if (null !== $callback) {
if (!$this->processPipes->haveReadSupport()) {
$this->stop(0);
throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait".');
}
$this->callback = $this->buildCallback($callback);
}
do {
$this->checkTimeout();
$running = $this->isRunning() && ('\\' === \DIRECTORY_SEPARATOR || $this->processPipes->areOpen());
$this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
} while ($running);
while ($this->isRunning()) {
$this->checkTimeout();
usleep(1000);
}
if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
throw new ProcessSignaledException($this);
}
return $this->exitcode;
}
/**
* Waits until the callback returns true.
*
* The callback receives the type of output (out or err) and some bytes
* from the output in real-time while writing the standard input to the process.
* It allows to have feedback from the independent process during execution.
*
* @param (callable('out'|'err', string):bool)|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
*
* @throws RuntimeException When process timed out
* @throws LogicException When process is not yet started
* @throws ProcessTimedOutException In case the timeout was reached
*/
public function waitUntil(callable $callback): bool
{
$this->requireProcessIsStarted(__FUNCTION__);
$this->updateStatus(false);
if (!$this->processPipes->haveReadSupport()) {
$this->stop(0);
throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".');
}
$callback = $this->buildCallback($callback);
$ready = false;
while (true) {
$this->checkTimeout();
$running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
$output = $this->processPipes->readAndWrite($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
foreach ($output as $type => $data) {
if (3 !== $type) {
$ready = $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data) || $ready;
} elseif (!isset($this->fallbackStatus['signaled'])) {
$this->fallbackStatus['exitcode'] = (int) $data;
}
}
if ($ready) {
return true;
}
if (!$running) {
return false;
}
usleep(1000);
}
}
/**
* Returns the Pid (process identifier), if applicable.
*
* @return int|null The process id if running, null otherwise
*/
public function getPid(): ?int
{
return $this->isRunning() ? $this->processInformation['pid'] : null;
}
/**
* Sends a POSIX signal to the process.
*
* @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants)
*
* @return $this
*
* @throws LogicException In case the process is not running
* @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
* @throws RuntimeException In case of failure
*/
public function signal(int $signal): static
{
$this->doSignal($signal, true);
return $this;
}
/**
* Disables fetching output and error output from the underlying process.
*
* @return $this
*
* @throws RuntimeException In case the process is already running
* @throws LogicException if an idle timeout is set
*/
public function disableOutput(): static
{
if ($this->isRunning()) {
throw new RuntimeException('Disabling output while the process is running is not possible.');
}
if (null !== $this->idleTimeout) {
throw new LogicException('Output cannot be disabled while an idle timeout is set.');
}
$this->outputDisabled = true;
return $this;
}
/**
* Enables fetching output and error output from the underlying process.
*
* @return $this
*
* @throws RuntimeException In case the process is already running
*/
public function enableOutput(): static
{
if ($this->isRunning()) {
throw new RuntimeException('Enabling output while the process is running is not possible.');
}
$this->outputDisabled = false;
return $this;
}
/**
* Returns true in case the output is disabled, false otherwise.
*/
public function isOutputDisabled(): bool
{
return $this->outputDisabled;
}
/**
* Returns the current output of the process (STDOUT).
*
* @throws LogicException in case the output has been disabled
* @throws LogicException In case the process is not started
*/
public function getOutput(): string
{
$this->readPipesForOutput(__FUNCTION__);
if (false === $ret = stream_get_contents($this->stdout, -1, 0)) {
return '';
}
return $ret;
}
/**
* Returns the output incrementally.
*
* In comparison with the getOutput method which always return the whole
* output, this one returns the new output since the last call.
*
* @throws LogicException in case the output has been disabled
* @throws LogicException In case the process is not started
*/
public function getIncrementalOutput(): string
{
$this->readPipesForOutput(__FUNCTION__);
$latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
$this->incrementalOutputOffset = ftell($this->stdout);
if (false === $latest) {
return '';
}
return $latest;
}
/**
* Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR).
*
* @param int $flags A bit field of Process::ITER_* flags
*
* @return \Generator<string, string>
*
* @throws LogicException in case the output has been disabled
* @throws LogicException In case the process is not started
*/
public function getIterator(int $flags = 0): \Generator
{
$this->readPipesForOutput(__FUNCTION__, false);
$clearOutput = !(self::ITER_KEEP_OUTPUT & $flags);
$blocking = !(self::ITER_NON_BLOCKING & $flags);
$yieldOut = !(self::ITER_SKIP_OUT & $flags);
$yieldErr = !(self::ITER_SKIP_ERR & $flags);
while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) {
if ($yieldOut) {
$out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
if (isset($out[0])) {
if ($clearOutput) {
$this->clearOutput();
} else {
$this->incrementalOutputOffset = ftell($this->stdout);
}
yield self::OUT => $out;
}
}
if ($yieldErr) {
$err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
if (isset($err[0])) {
if ($clearOutput) {
$this->clearErrorOutput();
} else {
$this->incrementalErrorOutputOffset = ftell($this->stderr);
}
yield self::ERR => $err;
}
}
if (!$blocking && !isset($out[0]) && !isset($err[0])) {
yield self::OUT => '';
}
$this->checkTimeout();
$this->readPipesForOutput(__FUNCTION__, $blocking);
}
}
/**
* Clears the process output.
*
* @return $this
*/
public function clearOutput(): static
{
ftruncate($this->stdout, 0);
fseek($this->stdout, 0);
$this->incrementalOutputOffset = 0;
return $this;
}
/**
* Returns the current error output of the process (STDERR).
*
* @throws LogicException in case the output has been disabled
* @throws LogicException In case the process is not started
*/
public function getErrorOutput(): string
{
$this->readPipesForOutput(__FUNCTION__);
if (false === $ret = stream_get_contents($this->stderr, -1, 0)) {
return '';
}
return $ret;
}
/**
* Returns the errorOutput incrementally.
*
* In comparison with the getErrorOutput method which always return the
* whole error output, this one returns the new error output since the last
* call.
*
* @throws LogicException in case the output has been disabled
* @throws LogicException In case the process is not started
*/
public function getIncrementalErrorOutput(): string
{
$this->readPipesForOutput(__FUNCTION__);
$latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
$this->incrementalErrorOutputOffset = ftell($this->stderr);
if (false === $latest) {
return '';
}
return $latest;
}
/**
* Clears the process output.
*
* @return $this
*/
public function clearErrorOutput(): static
{
ftruncate($this->stderr, 0);
fseek($this->stderr, 0);
$this->incrementalErrorOutputOffset = 0;
return $this;
}
/**
* Returns the exit code returned by the process.
*
* @return int|null The exit status code, null if the Process is not terminated
*/
public function getExitCode(): ?int
{
$this->updateStatus(false);
return $this->exitcode;
}
/**
* Returns a string representation for the exit code returned by the process.
*
* This method relies on the Unix exit code status standardization
* and might not be relevant for other operating systems.
*
* @return string|null A string representation for the exit status code, null if the Process is not terminated
*
* @see http://tldp.org/LDP/abs/html/exitcodes.html
* @see http://en.wikipedia.org/wiki/Unix_signal
*/
public function getExitCodeText(): ?string
{
if (null === $exitcode = $this->getExitCode()) {
return null;
}
return self::$exitCodes[$exitcode] ?? 'Unknown error';
}
/**
* Checks if the process ended successfully.
*/
public function isSuccessful(): bool
{
return 0 === $this->getExitCode();
}
/**
* Returns true if the child process has been terminated by an uncaught signal.
*
* It always returns false on Windows.
*
* @throws LogicException In case the process is not terminated
*/
public function hasBeenSignaled(): bool
{
$this->requireProcessIsTerminated(__FUNCTION__);
return $this->processInformation['signaled'];
}
/**
* Returns the number of the signal that caused the child process to terminate its execution.
*
* It is only meaningful if hasBeenSignaled() returns true.
*
* @throws RuntimeException In case --enable-sigchild is activated
* @throws LogicException In case the process is not terminated
*/
public function getTermSignal(): int
{
$this->requireProcessIsTerminated(__FUNCTION__);
if ($this->isSigchildEnabled() && -1 === $this->processInformation['termsig']) {
throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal cannot be retrieved.');
}
return $this->processInformation['termsig'];
}
/**
* Returns true if the child process has been stopped by a signal.
*
* It always returns false on Windows.
*
* @throws LogicException In case the process is not terminated
*/
public function hasBeenStopped(): bool
{
$this->requireProcessIsTerminated(__FUNCTION__);
return $this->processInformation['stopped'];
}
/**
* Returns the number of the signal that caused the child process to stop its execution.
*
* It is only meaningful if hasBeenStopped() returns true.
*
* @throws LogicException In case the process is not terminated
*/
public function getStopSignal(): int
{
$this->requireProcessIsTerminated(__FUNCTION__);
return $this->processInformation['stopsig'];
}
/**
* Checks if the process is currently running.
*/
public function isRunning(): bool
{
if (self::STATUS_STARTED !== $this->status) {
return false;
}
$this->updateStatus(false);
return $this->processInformation['running'];
}
/**
* Checks if the process has been started with no regard to the current state.
*/
public function isStarted(): bool
{
return self::STATUS_READY != $this->status;
}
/**
* Checks if the process is terminated.
*/
public function isTerminated(): bool
{
$this->updateStatus(false);
return self::STATUS_TERMINATED == $this->status;
}
/**
* Gets the process status.
*
* The status is one of: ready, started, terminated.
*/
public function getStatus(): string
{
$this->updateStatus(false);
return $this->status;
}
/**
* Stops the process.
*
* @param int|float $timeout The timeout in seconds
* @param int|null $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
*
* @return int|null The exit-code of the process or null if it's not running
*/
public function stop(float $timeout = 10, ?int $signal = null): ?int
{
$timeoutMicro = microtime(true) + $timeout;
if ($this->isRunning()) {
// given SIGTERM may not be defined and that "proc_terminate" uses the constant value and not the constant itself, we use the same here
$this->doSignal(15, false);
do {
usleep(1000);
} while ($this->isRunning() && microtime(true) < $timeoutMicro);
if ($this->isRunning()) {
// Avoid exception here: process is supposed to be running, but it might have stopped just
// after this line. In any case, let's silently discard the error, we cannot do anything.
$this->doSignal($signal ?: 9, false);
}
}
if ($this->isRunning()) {
if (isset($this->fallbackStatus['pid'])) {
unset($this->fallbackStatus['pid']);
return $this->stop(0, $signal);
}
$this->close();
}
return $this->exitcode;
}
/**
* Adds a line to the STDOUT stream.
*
* @internal
*/
public function addOutput(string $line): void
{
$this->lastOutputTime = microtime(true);
fseek($this->stdout, 0, \SEEK_END);
fwrite($this->stdout, $line);
fseek($this->stdout, $this->incrementalOutputOffset);
}
/**
* Adds a line to the STDERR stream.
*
* @internal
*/
public function addErrorOutput(string $line): void
{
$this->lastOutputTime = microtime(true);
fseek($this->stderr, 0, \SEEK_END);
fwrite($this->stderr, $line);
fseek($this->stderr, $this->incrementalErrorOutputOffset);
}
/**
* Gets the last output time in seconds.
*/
public function getLastOutputTime(): ?float
{
return $this->lastOutputTime;
}
/**
* Gets the command line to be executed.
*/
public function getCommandLine(): string
{
return $this->buildShellCommandline($this->commandline);
}
/**
* Gets the process timeout in seconds (max. runtime).
*/
public function getTimeout(): ?float
{
return $this->timeout;
}
/**
* Gets the process idle timeout in seconds (max. time since last output).
*/
public function getIdleTimeout(): ?float
{
return $this->idleTimeout;
}
/**
* Sets the process timeout (max. runtime) in seconds.
*
* To disable the timeout, set this value to null.
*
* @return $this
*
* @throws InvalidArgumentException if the timeout is negative
*/
public function setTimeout(?float $timeout): static
{
$this->timeout = $this->validateTimeout($timeout);
return $this;
}
/**
* Sets the process idle timeout (max. time since last output) in seconds.
*
* To disable the timeout, set this value to null.
*
* @return $this
*
* @throws LogicException if the output is disabled
* @throws InvalidArgumentException if the timeout is negative
*/
public function setIdleTimeout(?float $timeout): static
{
if (null !== $timeout && $this->outputDisabled) {
throw new LogicException('Idle timeout cannot be set while the output is disabled.');
}
$this->idleTimeout = $this->validateTimeout($timeout);
return $this;
}
/**
* Enables or disables the TTY mode.
*
* @return $this
*
* @throws RuntimeException In case the TTY mode is not supported
*/
public function setTty(bool $tty): static
{
if ('\\' === \DIRECTORY_SEPARATOR && $tty) {
throw new RuntimeException('TTY mode is not supported on Windows platform.');
}
if ($tty && !self::isTtySupported()) {
throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.');
}
$this->tty = $tty;
return $this;
}
/**
* Checks if the TTY mode is enabled.
*/
public function isTty(): bool
{
return $this->tty;
}
/**
* Sets PTY mode.
*
* @return $this
*/
public function setPty(bool $bool): static
{
$this->pty = $bool;
return $this;
}
/**
* Returns PTY state.
*/
public function isPty(): bool
{
return $this->pty;
}
/**
* Gets the working directory.
*/
public function getWorkingDirectory(): ?string
{
if (null === $this->cwd) {
// getcwd() will return false if any one of the parent directories does not have
// the readable or search mode set, even if the current directory does
return getcwd() ?: null;
}
return $this->cwd;
}
/**
* Sets the current working directory.
*
* @return $this
*/
public function setWorkingDirectory(string $cwd): static
{
$this->cwd = $cwd;
return $this;
}
/**
* Gets the environment variables.
*
* @psalm-return EnvArray
*/
public function getEnv(): array
{
return $this->env;
}
/**
* Sets the environment variables.
*
* @param EnvArray $env The new environment variables
*
* @return $this
*/
public function setEnv(array $env): static
{
$this->env = $env;
return $this;
}
/**
* Gets the Process input.
*
* @return resource|string|\Iterator|null
*/
public function getInput()
{
return $this->input;
}
/**
* Sets the input.
*
* This content will be passed to the underlying process standard input.
*
* @param string|resource|\Traversable|self|null $input The content
*
* @return $this
*
* @throws LogicException In case the process is running
*/
public function setInput(mixed $input): static
{
if ($this->isRunning()) {
throw new LogicException('Input cannot be set while the process is running.');
}
$this->input = ProcessUtils::validateInput(__METHOD__, $input);
return $this;
}
/**
* Performs a check between the timeout definition and the time the process started.
*
* In case you run a background process (with the start method), you should
* trigger this method regularly to ensure the process timeout
*
* @throws ProcessTimedOutException In case the timeout was reached
*/
public function checkTimeout(): void
{
if (self::STATUS_STARTED !== $this->status) {
return;
}
if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
$this->stop(0);
throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL);
}
if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
$this->stop(0);
throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE);
}
}
/**
* @throws LogicException in case process is not started
*/
public function getStartTime(): float
{
if (!$this->isStarted()) {
throw new LogicException('Start time is only available after process start.');
}
return $this->starttime;
}
/**
* Defines options to pass to the underlying proc_open().
*
* @see https://php.net/proc_open for the options supported by PHP.
*
* Enabling the "create_new_console" option allows a subprocess to continue
* to run after the main process exited, on both Windows and *nix
*/
public function setOptions(array $options): void
{
if ($this->isRunning()) {
throw new RuntimeException('Setting options while the process is running is not possible.');
}
$defaultOptions = $this->options;
$existingOptions = ['blocking_pipes', 'create_process_group', 'create_new_console'];
foreach ($options as $key => $value) {
if (!\in_array($key, $existingOptions)) {
$this->options = $defaultOptions;
throw new LogicException(\sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions)));
}
$this->options[$key] = $value;
}
}
/**
* Defines a list of posix signals that will not be propagated to the process.
*
* @param list<\SIG*> $signals
*/
public function setIgnoredSignals(array $signals): void
{
if ($this->isRunning()) {
throw new RuntimeException('Setting ignored signals while the process is running is not possible.');
}
$this->ignoredSignals = $signals;
}
/**
* Returns whether TTY is supported on the current operating system.
*/
public static function isTtySupported(): bool
{
static $isTtySupported;
return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT) && @is_writable('/dev/tty'));
}
/**
* Returns whether PTY is supported on the current operating system.
*/
public static function isPtySupported(): bool
{
static $result;
if (null !== $result) {
return $result;
}
if ('\\' === \DIRECTORY_SEPARATOR) {
return $result = false;
}
return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes);
}
/**
* Creates the descriptors needed by the proc_open.
*/
private function getDescriptors(bool $hasCallback): array
{
if ($this->input instanceof \Iterator) {
$this->input->rewind();
}
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $hasCallback);
} else {
$this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $hasCallback);
}
return $this->processPipes->getDescriptors();
}
/**
* Builds up the callback used by wait().
*
* The callbacks adds all occurred output to the specific buffer and calls
* the user callback (if present) with the received output.
*
* @param (callable('out'|'err', string):void)|null $callback
*
* @return \Closure('out'|'err', string):bool
*/
protected function buildCallback(?callable $callback = null): \Closure
{
if ($this->outputDisabled) {
return static fn ($type, $data): bool => null !== $callback && $callback($type, $data);
}
return function ($type, $data) use ($callback): bool {
match ($type) {
self::OUT => $this->addOutput($data),
self::ERR => $this->addErrorOutput($data),
};
return null !== $callback && $callback($type, $data);
};
}
/**
* Updates the status of the process, reads pipes.
*
* @param bool $blocking Whether to use a blocking read call
*/
protected function updateStatus(bool $blocking): void
{
if (self::STATUS_STARTED !== $this->status) {
return;
}
if ($this->processInformation['running'] ?? true) {
$this->processInformation = proc_get_status($this->process);
}
$running = $this->processInformation['running'];
$this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running);
if ($this->fallbackStatus && $this->isSigchildEnabled()) {
$this->processInformation = $this->fallbackStatus + $this->processInformation;
}
if (!$running) {
$this->close();
}
}
/**
* Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
*/
protected function isSigchildEnabled(): bool
{
if (null !== self::$sigchild) {
return self::$sigchild;
}
if (!\function_exists('phpinfo')) {
return self::$sigchild = false;
}
ob_start();
phpinfo(\INFO_GENERAL);
return self::$sigchild = str_contains(ob_get_clean(), '--enable-sigchild');
}
/**
* Reads pipes for the freshest output.
*
* @param string $caller The name of the method that needs fresh outputs
* @param bool $blocking Whether to use blocking calls or not
*
* @throws LogicException in case output has been disabled or process is not started
*/
private function readPipesForOutput(string $caller, bool $blocking = false): void
{
if ($this->outputDisabled) {
throw new LogicException('Output has been disabled.');
}
$this->requireProcessIsStarted($caller);
$this->updateStatus($blocking);
}
/**
* Validates and returns the filtered timeout.
*
* @throws InvalidArgumentException if the given timeout is a negative number
*/
private function validateTimeout(?float $timeout): ?float
{
$timeout = (float) $timeout;
if (0.0 === $timeout) {
$timeout = null;
} elseif ($timeout < 0) {
throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
}
return $timeout;
}
/**
* Reads pipes, executes callback.
*
* @param bool $blocking Whether to use blocking calls or not
* @param bool $close Whether to close file handles or not
*/
private function readPipes(bool $blocking, bool $close): void
{
$result = $this->processPipes->readAndWrite($blocking, $close);
$callback = $this->callback;
foreach ($result as $type => $data) {
if (3 !== $type) {
$callback(self::STDOUT === $type ? self::OUT : self::ERR, $data);
} elseif (!isset($this->fallbackStatus['signaled'])) {
$this->fallbackStatus['exitcode'] = (int) $data;
}
}
}
/**
* Closes process resource, closes file handles, sets the exitcode.
*
* @return int The exitcode
*/
private function close(): int
{
$this->processPipes->close();
if ($this->process) {
proc_close($this->process);
$this->process = null;
}
$this->exitcode = $this->processInformation['exitcode'];
$this->status = self::STATUS_TERMINATED;
if (-1 === $this->exitcode) {
if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) {
// if process has been signaled, no exitcode but a valid termsig, apply Unix convention
$this->exitcode = 128 + $this->processInformation['termsig'];
} elseif ($this->isSigchildEnabled()) {
$this->processInformation['signaled'] = true;
$this->processInformation['termsig'] = -1;
}
}
// Free memory from self-reference callback created by buildCallback
// Doing so in other contexts like __destruct or by garbage collector is ineffective
// Now pipes are closed, so the callback is no longer necessary
$this->callback = null;
return $this->exitcode;
}
/**
* Resets data related to the latest run of the process.
*/
private function resetProcessData(): void
{
$this->starttime = null;
$this->callback = null;
$this->exitcode = null;
$this->fallbackStatus = [];
$this->processInformation = [];
$this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+');
$this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+');
$this->process = null;
$this->latestSignal = null;
$this->status = self::STATUS_READY;
$this->incrementalOutputOffset = 0;
$this->incrementalErrorOutputOffset = 0;
}
/**
* Sends a POSIX signal to the process.
*
* @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants)
* @param bool $throwException Whether to throw exception in case signal failed
*
* @throws LogicException In case the process is not running
* @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
* @throws RuntimeException In case of failure
*/
private function doSignal(int $signal, bool $throwException): bool
{
// Signal seems to be send when sigchild is enable, this allow blocking the signal correctly in this case
if ($this->isSigchildEnabled() && \in_array($signal, $this->ignoredSignals)) {
return false;
}
if (null === $pid = $this->getPid()) {
if ($throwException) {
throw new LogicException('Cannot send signal on a non running process.');
}
return false;
}
if ('\\' === \DIRECTORY_SEPARATOR) {
exec(\sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode);
if ($exitCode && $this->isRunning()) {
if ($throwException) {
throw new RuntimeException(\sprintf('Unable to kill the process (%s).', implode(' ', $output)));
}
return false;
}
} else {
if (!$this->isSigchildEnabled()) {
$ok = @proc_terminate($this->process, $signal);
} elseif (\function_exists('posix_kill')) {
$ok = @posix_kill($pid, $signal);
} elseif ($ok = proc_open(\sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) {
$ok = false === fgets($pipes[2]);
}
if (!$ok) {
if ($throwException) {
throw new RuntimeException(\sprintf('Error while sending signal "%s".', $signal));
}
return false;
}
}
$this->latestSignal = $signal;
$this->fallbackStatus['signaled'] = true;
$this->fallbackStatus['exitcode'] = -1;
$this->fallbackStatus['termsig'] = $this->latestSignal;
return true;
}
/**
* @param string|list<string> $commandline
*/
private function buildShellCommandline(string|array $commandline): string
{
if (\is_string($commandline)) {
return $commandline;
}
if ('\\' === \DIRECTORY_SEPARATOR && isset($commandline[0][0]) && \strlen($commandline[0]) === strcspn($commandline[0], ':/\\')) {
// On Windows, we don't rely on the OS to find the executable if possible to avoid lookups
// in the current directory which could be untrusted. Instead we use the ExecutableFinder.
$commandline[0] = (self::$executables[$commandline[0]] ??= (new ExecutableFinder())->find($commandline[0])) ?? $commandline[0];
}
return implode(' ', array_map($this->escapeArgument(...), $commandline));
}
/**
* @param string|list<string> $cmd
* @param EnvArray $env
*
* @param-out EnvArray $env
*/
private function prepareWindowsCommandLine(string|array $cmd, array &$env): string
{
$cmd = $this->buildShellCommandline($cmd);
$uid = bin2hex(random_bytes(4));
$cmd = preg_replace_callback(
'/"(?:(
[^"%!^]*+
(?:
(?: !LF! | "(?:\^[%!^])?+" )
[^"%!^]*+
)++
) | [^"]*+ )"/x',
static function ($m) use (&$env, $uid) {
static $varCount = 0;
static $varCache = [];
if (!isset($m[1])) {
return $m[0];
}
if (isset($varCache[$m[0]])) {
return $varCache[$m[0]];
}
if (str_contains($value = $m[1], "\0")) {
$value = str_replace("\0", '?', $value);
}
if (false === strpbrk($value, "\"%!\n")) {
return '"'.$value.'"';
}
$value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value);
$value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"';
$var = $uid.++$varCount;
$env[$var] = $value;
return $varCache[$m[0]] = '!'.$var.'!';
},
$cmd
);
static $comSpec;
if (!$comSpec && $comSpec = (new ExecutableFinder())->find('cmd.exe')) {
// Escape according to CommandLineToArgvW rules
$comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec).'"';
}
$cmd = ($comSpec ?? 'cmd').' /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')';
foreach ($this->processPipes->getFiles() as $offset => $filename) {
$cmd .= ' '.$offset.'>"'.$filename.'"';
}
return $cmd;
}
/**
* Ensures the process is running or terminated, throws a LogicException if the process has a not started.
*
* @throws LogicException if the process has not run
*/
private function requireProcessIsStarted(string $functionName): void
{
if (!$this->isStarted()) {
throw new LogicException(\sprintf('Process must be started before calling "%s()".', $functionName));
}
}
/**
* Ensures the process is terminated, throws a LogicException if the process has a status different than "terminated".
*
* @throws LogicException if the process is not yet terminated
*/
private function requireProcessIsTerminated(string $functionName): void
{
if (!$this->isTerminated()) {
throw new LogicException(\sprintf('Process must be terminated before calling "%s()".', $functionName));
}
}
/**
* Escapes a string to be used as a shell argument.
*/
private function escapeArgument(?string $argument): string
{
if ('' === $argument || null === $argument) {
return '""';
}
if ('\\' !== \DIRECTORY_SEPARATOR) {
return "'".str_replace("'", "'\\''", $argument)."'";
}
if (str_contains($argument, "\0")) {
$argument = str_replace("\0", '?', $argument);
}
if (!preg_match('/[()%!^"<>&|\s[\]=;*?\'$]/', $argument)) {
return $argument;
}
$argument = preg_replace('/(\\\\+)$/', '$1$1', $argument);
return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"';
}
/**
* @param EnvArray $env
*/
private function replacePlaceholders(string $commandline, array $env): string
{
return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) {
if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) {
throw new InvalidArgumentException(\sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline);
}
return $this->escapeArgument($env[$matches[1]]);
}, $commandline);
}
/**
* @return EnvArray
*/
private function getDefaultEnv(): array
{
$env = getenv();
$env = ('\\' === \DIRECTORY_SEPARATOR ? array_intersect_ukey($env, $_SERVER, 'strcasecmp') : array_intersect_key($env, $_SERVER)) ?: $env;
return $_ENV + ('\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($env, $_ENV, 'strcasecmp') : $env);
}
private function validateWindowsEnvBlockSize(array $envPairs): void
{
$block = implode("\0", $envPairs)."\0";
@preg_replace('/./u', '', $block, -1, $blockLength)
?? preg_replace('/./', '', $block, -1, $blockLength);
$blockLength += 1 + preg_match_all('/[\xF0-\xF4][\x80-\xBF]{3}/', $block);
if ($blockLength > self::WINDOWS_ENV_BLOCK_MAX_LENGTH) {
throw new InvalidArgumentException(\sprintf('The environment block size (%d) exceeds the Windows limit of %d UTF-16 code units.', $blockLength, self::WINDOWS_ENV_BLOCK_MAX_LENGTH));
}
}
}
================================================
FILE: ProcessUtils.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process;
use Symfony\Component\Process\Exception\InvalidArgumentException;
/**
* ProcessUtils is a bunch of utility methods.
*
* This class contains static methods only and is not meant to be instantiated.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
class ProcessUtils
{
/**
* This class should not be instantiated.
*/
private function __construct()
{
}
/**
* Validates and normalizes a Process input.
*
* @param string $caller The name of method call that validates the input
* @param mixed $input The input to validate
*
* @throws InvalidArgumentException In case the input is not valid
*/
public static function validateInput(string $caller, mixed $input): mixed
{
if (null !== $input) {
if (\is_resource($input)) {
return $input;
}
if (\is_scalar($input)) {
return (string) $input;
}
if ($input instanceof Process) {
return $input->getIterator($input::ITER_SKIP_ERR);
}
if ($input instanceof \Iterator) {
return $input;
}
if ($input instanceof \Traversable) {
return new \IteratorIterator($input);
}
throw new InvalidArgumentException(\sprintf('"%s" only accepts strings, Traversable objects or stream resources.', $caller));
}
return $input;
}
}
================================================
FILE: README.md
================================================
Process Component
=================
The Process component executes commands in sub-processes.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/process.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
================================================
FILE: Tests/CreateNewConsoleTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\Process;
/**
* @author Andrei Olteanu <andrei@flashsoft.eu>
*/
class CreateNewConsoleTest extends TestCase
{
public function testOptionCreateNewConsole()
{
$this->expectNotToPerformAssertions();
try {
$process = new Process(['php', __DIR__.'/ThreeSecondProcess.php']);
$process->setOptions(['create_new_console' => true]);
$process->disableOutput();
$process->start();
} catch (\Exception $e) {
$this->fail($e);
}
}
public function testItReturnsFastAfterStart()
{
// The started process must run in background after the main has finished but that can't be tested with PHPUnit
$startTime = microtime(true);
$process = new Process(['php', __DIR__.'/ThreeSecondProcess.php']);
$process->setOptions(['create_new_console' => true]);
$process->disableOutput();
$process->start();
$this->assertLessThan(3000, $startTime - microtime(true));
}
}
================================================
FILE: Tests/ErrorProcessInitiator.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Tests;
use Symfony\Component\Process\Exception\ProcessTimedOutException;
use Symfony\Component\Process\Process;
require is_file(\dirname(__DIR__).'/vendor/autoload.php') ? \dirname(__DIR__).'/vendor/autoload.php' : \dirname(__DIR__, 5).'/vendor/autoload.php';
['e' => $php] = getopt('e:') + ['e' => 'php'];
try {
$process = new Process([$php, '-r', "echo 'ready'; trigger_error('error', E_USER_ERROR);"]);
$process->start();
$process->setTimeout(0.5);
while (!str_contains($process->getOutput(), 'ready')) {
usleep(1000);
}
$process->isRunning() && $process->signal(\SIGSTOP);
$process->wait();
return $process->getExitCode();
} catch (ProcessTimedOutException $t) {
echo $t->getMessage().\PHP_EOL;
return 1;
}
================================================
FILE: Tests/ExecutableFinderTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Tests;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;
/**
* @author Chris Smith <chris@cs278.org>
*/
class ExecutableFinderTest extends TestCase
{
protected function tearDown(): void
{
putenv('PATH='.($_SERVER['PATH'] ?? $_SERVER['Path']));
}
public function testFind()
{
if (\ini_get('open_basedir')) {
$this->markTestSkipped('Cannot test when open_basedir is set');
}
putenv('PATH='.\dirname(\PHP_BINARY));
$finder = new ExecutableFinder();
$result = $finder->find($this->getPhpBinaryName());
$this->assertSamePath(\PHP_BINARY, $result);
}
public function testFindWithDefault()
{
if (\ini_get('open_basedir')) {
$this->markTestSkipped('Cannot test when open_basedir is set');
}
$expected = 'defaultValue';
putenv('PATH=');
$finder = new ExecutableFinder();
$result = $finder->find('foo', $expected);
$this->assertEquals($expected, $result);
}
public function testFindWithNullAsDefault()
{
if (\ini_get('open_basedir')) {
$this->markTestSkipped('Cannot test when open_basedir is set');
}
putenv('PATH=');
$finder = new ExecutableFinder();
$result = $finder->find('foo');
$this->assertNull($result);
}
public function testFindWithExtraDirs()
{
if (\ini_get('open_basedir')) {
$this->markTestSkipped('Cannot test when open_basedir is set');
}
putenv('PATH=');
$extraDirs = [\dirname(\PHP_BINARY)];
$finder = new ExecutableFinder();
$result = $finder->find($this->getPhpBinaryName(), null, $extraDirs);
$this->assertSamePath(\PHP_BINARY, $result);
}
public function testFindWithoutSuffix()
{
$fixturesDir = __DIR__.\DIRECTORY_SEPARATOR.'Fixtures';
$name = 'executable_without_suffix';
$finder = new ExecutableFinder();
$result = $finder->find($name, null, [$fixturesDir]);
$this->assertSamePath($fixturesDir.\DIRECTORY_SEPARATOR.$name, $result);
}
public function testFindWithAddedSuffixes()
{
$fixturesDir = __DIR__.\DIRECTORY_SEPARATOR.'Fixtures';
$name = 'executable_with_added_suffix';
$suffix = '.foo';
$finder = new ExecutableFinder();
$finder->addSuffix($suffix);
$result = $finder->find($name, null, [$fixturesDir]);
$this->assertSamePath($fixturesDir.\DIRECTORY_SEPARATOR.$name.$suffix, $result);
}
#[RunInSeparateProcess]
public function testFindWithOpenBaseDir()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Cannot run test on windows');
}
if (\ini_get('open_basedir')) {
$this->markTestSkipped('Cannot test when open_basedir is set');
}
$process = new Process([\PHP_BINARY, '-d', 'open_basedir='.\dirname(\PHP_BINARY).\PATH_SEPARATOR.'/', __DIR__.'/Fixtures/open_basedir.php']);
$process->run();
$result = $process->getOutput();
$this->assertSamePath(\PHP_BINARY, $result);
}
#[RunInSeparateProcess]
public function testFindBatchExecutableOnWindows()
{
if (\ini_get('open_basedir')) {
$this->markTestSkipped('Cannot test when open_basedir is set');
}
if ('\\' !== \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Can be only tested on windows');
}
$tempDir = realpath(sys_get_temp_dir());
$target = str_replace('.tmp', '_tmp', tempnam($tempDir, 'example-windows-executable'));
try {
touch($target);
touch($target.'.BAT');
$this->assertFalse(is_executable($target));
putenv('PATH='.$tempDir);
$finder = new ExecutableFinder();
$result = $finder->find(basename($target), false);
} finally {
unlink($target);
unlink($target.'.BAT');
}
$this->assertSamePath($target.'.BAT', $result);
}
#[RunInSeparateProcess]
public function testEmptyDirInPath()
{
putenv(\sprintf('PATH=%s%s', \dirname(\PHP_BINARY), \PATH_SEPARATOR));
try {
touch('executable');
chmod('executable', 0o700);
$finder = new ExecutableFinder();
$result = $finder->find('executable');
$this->assertSame(\sprintf('.%sexecutable', \DIRECTORY_SEPARATOR), $result);
} finally {
unlink('executable');
}
}
public function testFindBuiltInCommandOnWindows()
{
if ('\\' !== \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Can be only tested on windows');
}
$finder = new ExecutableFinder();
$this->assertSame('rmdir', strtolower($finder->find('RMDIR')));
$this->assertSame('cd', strtolower($finder->find('cd')));
$this->assertSame('move', strtolower($finder->find('MoVe')));
}
private function assertSamePath($expected, $tested)
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->assertEquals(strtolower($expected), strtolower($tested));
} else {
$this->assertEquals($expected, $tested);
}
}
private function getPhpBinaryName()
{
return basename(\PHP_BINARY, '\\' === \DIRECTORY_SEPARATOR ? '.exe' : '');
}
}
================================================
FILE: Tests/Fixtures/executable_with_added_suffix.foo
================================================
See \Symfony\Component\Process\Tests\ExecutableFinderTest::testFindWithAddedSuffixes()
================================================
FILE: Tests/Fixtures/executable_without_suffix
================================================
See \Symfony\Component\Process\Tests\ExecutableFinderTest::testFindWithoutSuffix()
================================================
FILE: Tests/Fixtures/memory.php
================================================
<?php
echo ini_get('memory_limit');
================================================
FILE: Tests/Fixtures/open_basedir.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
require_once __DIR__.'/../../ExecutableFinder.php';
use Symfony\Component\Process\ExecutableFinder;
putenv('PATH='.dirname(PHP_BINARY));
function getPhpBinaryName(): string
{
return basename(PHP_BINARY, '\\' === DIRECTORY_SEPARATOR ? '.exe' : '');
}
echo (new ExecutableFinder())->find(getPhpBinaryName());
================================================
FILE: Tests/KillableProcessWithOutput.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
$outputs = [
'First iteration output',
'Second iteration output',
'One more iteration output',
'This took more time',
];
$iterationTime = 10000;
foreach ($outputs as $output) {
usleep($iterationTime);
$iterationTime *= 10;
echo $output."\n";
}
================================================
FILE: Tests/Messenger/RunProcessMessageHandlerTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Tests\Messenger;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\Exception\RunProcessFailedException;
use Symfony\Component\Process\Messenger\RunProcessMessage;
use Symfony\Component\Process\Messenger\RunProcessMessageHandler;
class RunProcessMessageHandlerTest extends TestCase
{
public function testRunSuccessfulProcess()
{
$context = (new RunProcessMessageHandler())(new RunProcessMessage(['ls'], cwd: __DIR__));
$this->assertSame(['ls'], $context->message->command);
$this->assertSame(0, $context->exitCode);
$this->assertStringContainsString(basename(__FILE__), $context->output);
}
public function testRunFailedProcess()
{
try {
(new RunProcessMessageHandler())(new RunProcessMessage(['invalid']));
} catch (RunProcessFailedException $e) {
$this->assertSame(['invalid'], $e->context->message->command);
$this->assertContains(
$e->context->exitCode,
[null, '\\' === \DIRECTORY_SEPARATOR ? 1 : 127],
'Exit code should be 1 on Windows, 127 on other systems, or null',
);
return;
}
$this->fail('Exception not thrown');
}
public function testRunSuccessfulProcessFromShellCommandline()
{
$context = (new RunProcessMessageHandler())(RunProcessMessage::fromShellCommandline('ls | grep Test', cwd: __DIR__));
$this->assertSame('ls | grep Test', $context->message->commandLine);
$this->assertSame(0, $context->exitCode);
$this->assertStringContainsString(basename(__FILE__), $context->output);
}
public function testRunFailedProcessFromShellCommandline()
{
try {
(new RunProcessMessageHandler())(RunProcessMessage::fromShellCommandline('invalid'));
$this->fail('Exception not thrown');
} catch (RunProcessFailedException $e) {
$this->assertSame('invalid', $e->context->message->commandLine);
$this->assertContains(
$e->context->exitCode,
[null, '\\' === \DIRECTORY_SEPARATOR ? 1 : 127],
'Exit code should be 1 on Windows, 127 on other systems, or null',
);
}
}
}
================================================
FILE: Tests/NonStopableProcess.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Runs a PHP script that can be stopped only with a SIGKILL (9) signal for 3 seconds.
*
* @args duration Run this script with a custom duration
*
* @example `php NonStopableProcess.php 42` will run the script for 42 seconds
*/
function handleSignal($signal)
{
$name = match ($signal) {
\SIGTERM => 'SIGTERM',
\SIGINT => 'SIGINT',
default => $signal.' (unknown)',
};
echo "signal $name\n";
}
pcntl_signal(\SIGTERM, 'handleSignal');
pcntl_signal(\SIGINT, 'handleSignal');
echo 'received ';
$duration = isset($argv[1]) ? (int) $argv[1] : 3;
$start = microtime(true);
while ($duration > (microtime(true) - $start)) {
usleep(10000);
pcntl_signal_dispatch();
}
================================================
FILE: Tests/OutputMemoryLimitProcess.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Tests;
use Symfony\Component\Process\PhpSubprocess;
use Symfony\Component\Process\Process;
require is_file(\dirname(__DIR__).'/vendor/autoload.php') ? \dirname(__DIR__).'/vendor/autoload.php' : \dirname(__DIR__, 5).'/vendor/autoload.php';
['e' => $php, 'p' => $process] = getopt('e:p:') + ['e' => 'php', 'p' => 'Process'];
if ('Process' === $process) {
$p = new Process([$php, __DIR__.'/Fixtures/memory.php']);
} else {
$p = new PhpSubprocess([__DIR__.'/Fixtures/memory.php'], null, null, 60, [$php]);
}
$p->mustRun();
echo $p->getOutput();
================================================
FILE: Tests/PhpExecutableFinderTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\PhpExecutableFinder;
/**
* @author Robert Schönthal <seroscho@googlemail.com>
*/
class PhpExecutableFinderTest extends TestCase
{
/**
* tests find() with the constant PHP_BINARY.
*/
public function testFind()
{
$f = new PhpExecutableFinder();
$current = \PHP_BINARY;
$args = 'phpdbg' === \PHP_SAPI ? ' -qrr' : '';
$this->assertEquals($current.$args, $f->find(), '::find() returns the executable PHP');
$this->assertEquals($current, $f->find(false), '::find() returns the executable PHP');
}
/**
* tests find() with the env var PHP_PATH.
*/
public function testFindArguments()
{
$f = new PhpExecutableFinder();
if ('phpdbg' === \PHP_SAPI) {
$this->assertEquals(['-qrr'], $f->findArguments(), '::findArguments() returns phpdbg arguments');
} else {
$this->assertEquals([], $f->findArguments(), '::findArguments() returns no arguments');
}
}
public function testNotExitsBinaryFile()
{
$f = new PhpExecutableFinder();
$originalPhpBinary = getenv('PHP_BINARY');
try {
putenv('PHP_BINARY=/usr/local/php/bin/php-invalid');
$this->assertFalse($f->find(), '::find() returns false because of not exist file');
$this->assertFalse($f->find(false), '::find(false) returns false because of not exist file');
} finally {
putenv('PHP_BINARY='.$originalPhpBinary);
}
}
public function testFindWithExecutableDirectory()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Directories are not executable on Windows');
}
$originalPhpBinary = getenv('PHP_BINARY');
try {
$executableDirectoryPath = sys_get_temp_dir().'/PhpExecutableFinderTest_testFindWithExecutableDirectory';
@mkdir($executableDirectoryPath);
$this->assertTrue(is_executable($executableDirectoryPath));
putenv('PHP_BINARY='.$executableDirectoryPath);
$this->assertFalse((new PhpExecutableFinder())->find());
} finally {
putenv('PHP_BINARY='.$originalPhpBinary);
}
}
}
================================================
FILE: Tests/PhpProcessTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\Exception\LogicException;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\PhpProcess;
class PhpProcessTest extends TestCase
{
public function testNonBlockingWorks()
{
$expected = 'hello world!';
$process = new PhpProcess(<<<PHP
<?php echo '$expected';
PHP
);
$process->start();
$process->wait();
$this->assertEquals($expected, $process->getOutput());
}
public function testCommandLine()
{
$process = new PhpProcess(<<<'PHP'
<?php echo phpversion().PHP_SAPI;
PHP
);
$commandLine = $process->getCommandLine();
$process->start();
$this->assertStringContainsString($commandLine, $process->getCommandLine(), '::getCommandLine() returns the command line of PHP after start');
$process->wait();
$this->assertStringContainsString($commandLine, $process->getCommandLine(), '::getCommandLine() returns the command line of PHP after wait');
$this->assertSame(\PHP_VERSION.\PHP_SAPI, $process->getOutput());
}
public function testPassingPhpExplicitly()
{
$finder = new PhpExecutableFinder();
$php = array_merge([$finder->find(false)], $finder->findArguments());
$expected = 'hello world!';
$script = <<<PHP
<?php echo '$expected';
PHP;
$process = new PhpProcess($script, null, null, 60, $php);
$process->run();
$this->assertEquals($expected, $process->getOutput());
}
public function testProcessCannotBeCreatedUsingFromShellCommandLine()
{
static::expectException(LogicException::class);
static::expectExceptionMessage('The "Symfony\Component\Process\PhpProcess::fromShellCommandline()" method cannot be called when using "Symfony\Component\Process\PhpProcess".');
PhpProcess::fromShellCommandline(<<<PHP
<?php echo 'Hello World!';
PHP
);
}
}
================================================
FILE: Tests/PhpSubprocessTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Tests;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
class PhpSubprocessTest extends TestCase
{
private static $phpBin;
public static function setUpBeforeClass(): void
{
$phpBin = new PhpExecutableFinder();
self::$phpBin = getenv('SYMFONY_PROCESS_PHP_TEST_BINARY') ?: ('phpdbg' === \PHP_SAPI ? 'php' : $phpBin->find());
}
#[DataProvider('subprocessProvider')]
public function testSubprocess(string $processClass, string $memoryLimit, string $expectedMemoryLimit)
{
$process = new Process([self::$phpBin,
'-d',
'memory_limit='.$memoryLimit,
__DIR__.'/OutputMemoryLimitProcess.php',
'-e', self::$phpBin,
'-p', $processClass,
]);
$process->mustRun();
$this->assertEquals($expectedMemoryLimit, trim($process->getOutput()));
}
public static function subprocessProvider(): \Generator
{
yield 'Process does ignore dynamic memory_limit' => [
'Process',
self::getRandomMemoryLimit(),
self::getDefaultMemoryLimit(),
];
yield 'PhpSubprocess does not ignore dynamic memory_limit' => [
'PhpSubprocess',
self::getRandomMemoryLimit(),
self::getRandomMemoryLimit(),
];
}
private static function getDefaultMemoryLimit(): string
{
return trim(ini_get_all()['memory_limit']['global_value']);
}
private static function getRandomMemoryLimit(): string
{
$memoryLimit = 123; // Take something that's really unlikely to be configured on a user system.
while (($formatted = $memoryLimit.'M') === self::getDefaultMemoryLimit()) {
++$memoryLimit;
}
return $formatted;
}
}
================================================
FILE: Tests/PipeStdinInStdoutStdErrStreamSelect.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
define('ERR_SELECT_FAILED', 1);
define('ERR_TIMEOUT', 2);
define('ERR_READ_FAILED', 3);
define('ERR_WRITE_FAILED', 4);
$read = [\STDIN];
$write = [\STDOUT, \STDERR];
stream_set_blocking(\STDIN, false);
stream_set_blocking(\STDOUT, false);
stream_set_blocking(\STDERR, false);
$out = $err = '';
while ($read || $write) {
$r = $read;
$w = $write;
$e = null;
$n = stream_select($r, $w, $e, 5);
if (false === $n) {
exit(ERR_SELECT_FAILED);
} elseif ($n < 1) {
exit(ERR_TIMEOUT);
}
if (in_array(\STDOUT, $w) && '' !== $out) {
$written = fwrite(\STDOUT, (string) $out, 32768);
if (false === $written) {
exit(ERR_WRITE_FAILED);
}
$out = (string) substr($out, $written);
}
if (null === $read && '' === $out) {
$write = array_diff($write, [\STDOUT]);
}
if (in_array(\STDERR, $w) && '' !== $err) {
$written = fwrite(\STDERR, (string) $err, 32768);
if (false === $written) {
exit(ERR_WRITE_FAILED);
}
$err = (string) substr($err, $written);
}
if (null === $read && '' === $err) {
$write = array_diff($write, [\STDERR]);
}
if ($r) {
$str = fread(\STDIN, 32768);
if (false !== $str) {
$out .= $str;
$err .= $str;
}
if (false === $str || feof(\STDIN)) {
$read = null;
if (!feof(\STDIN)) {
exit(ERR_READ_FAILED);
}
}
}
}
================================================
FILE: Tests/ProcessFailedExceptionTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
/**
* @author Sebastian Marek <proofek@gmail.com>
*/
class ProcessFailedExceptionTest extends TestCase
{
/**
* tests ProcessFailedException throws exception if the process was successful.
*/
public function testProcessFailedExceptionThrowsException()
{
$process = $this->getMockBuilder(Process::class)->onlyMethods(['isSuccessful'])->setConstructorArgs([['php']])->getMock();
$process->expects($this->once())
->method('isSuccessful')
->willReturn(true);
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Expected a failed process, but the given process was successful.');
new ProcessFailedException($process);
}
/**
* tests ProcessFailedException uses information from process output
* to generate exception message.
*/
public function testProcessFailedExceptionPopulatesInformationFromProcessOutput()
{
$cmd = 'php';
$exitCode = 1;
$exitText = 'General error';
$output = 'Command output';
$errorOutput = 'FATAL: Unexpected error';
$workingDirectory = getcwd();
$process = $this->getMockBuilder(Process::class)->onlyMethods(['isSuccessful', 'getOutput', 'getErrorOutput', 'getExitCode', 'getExitCodeText', 'isOutputDisabled', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock();
$process->expects($this->once())
->method('isSuccessful')
->willReturn(false);
$process->expects($this->once())
->method('getOutput')
->willReturn($output);
$process->expects($this->once())
->method('getErrorOutput')
->willReturn($errorOutput);
$process->expects($this->once())
->method('getExitCode')
->willReturn($exitCode);
$process->expects($this->once())
->method('getExitCodeText')
->willReturn($exitText);
$process->expects($this->once())
->method('isOutputDisabled')
->willReturn(false);
$process->expects($this->once())
->method('getWorkingDirectory')
->willReturn($workingDirectory);
$exception = new ProcessFailedException($process);
$this->assertStringMatchesFormat(
"The command \"%s\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}\n\nOutput:\n================\n{$output}\n\nError Output:\n================\n{$errorOutput}",
str_replace("'php'", 'php', $exception->getMessage())
);
}
/**
* Tests that ProcessFailedException does not extract information from
* process output if it was previously disabled.
*/
public function testDisabledOutputInFailedExceptionDoesNotPopulateOutput()
{
$cmd = 'php';
$exitCode = 1;
$exitText = 'General error';
$workingDirectory = getcwd();
$process = $this->getMockBuilder(Process::class)->onlyMethods(['isSuccessful', 'isOutputDisabled', 'getExitCode', 'getExitCodeText', 'getOutput', 'getErrorOutput', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock();
$process->expects($this->once())
->method('isSuccessful')
->willReturn(false);
$process->expects($this->never())
->method('getOutput');
$process->expects($this->never())
->method('getErrorOutput');
$process->expects($this->once())
->method('getExitCode')
->willReturn($exitCode);
$process->expects($this->once())
->method('getExitCodeText')
->willReturn($exitText);
$process->expects($this->once())
->method('isOutputDisabled')
->willReturn(true);
$process->expects($this->once())
->method('getWorkingDirectory')
->willReturn($workingDirectory);
$exception = new ProcessFailedException($process);
$this->assertStringMatchesFormat(
"The command \"%s\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}",
$exception->getMessage()
);
}
}
================================================
FILE: Tests/ProcessTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Tests;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\Exception\InvalidArgumentException;
use Symfony\Component\Process\Exception\LogicException;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Exception\ProcessSignaledException;
use Symfony\Component\Process\Exception\ProcessTimedOutException;
use Symfony\Component\Process\Exception\RuntimeException;
use Symfony\Component\Process\InputStream;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Pipes\PipesInterface;
use Symfony\Component\Process\Process;
/**
* @author Robert Schönthal <seroscho@googlemail.com>
*/
class ProcessTest extends TestCase
{
private static string $phpBin;
private static ?Process $process = null;
private static bool $sigchild;
public static function setUpBeforeClass(): void
{
$phpBin = new PhpExecutableFinder();
self::$phpBin = getenv('SYMFONY_PROCESS_PHP_TEST_BINARY') ?: ('phpdbg' === \PHP_SAPI ? 'php' : $phpBin->find());
ob_start();
phpinfo(\INFO_GENERAL);
self::$sigchild = str_contains(ob_get_clean(), '--enable-sigchild');
}
protected function tearDown(): void
{
if (self::$process) {
self::$process->stop(0);
self::$process = null;
}
}
public function testInvalidCwd()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessageMatches('/The provided cwd ".*" does not exist\./');
try {
// Check that it works fine if the CWD exists
$cmd = new Process(['echo', 'test'], __DIR__);
$cmd->run();
} catch (\Exception $e) {
$this->fail($e);
}
$cmd = new Process(['echo', 'test'], __DIR__.'/notfound/');
$cmd->run();
}
#[DataProvider('invalidProcessProvider')]
public function testInvalidCommand(Process $process)
{
// An invalid command should not fail during start
$this->assertSame('\\' === \DIRECTORY_SEPARATOR ? 1 : 127, $process->run());
}
public static function invalidProcessProvider(): array
{
return [
[new Process(['invalid'])],
[Process::fromShellCommandline('invalid')],
];
}
#[Group('transient-on-windows')]
public function testThatProcessDoesNotThrowWarningDuringRun()
{
@trigger_error('Test Error', \E_USER_NOTICE);
$process = $this->getProcessForCode('sleep(3)');
$process->run();
$actualError = error_get_last();
$this->assertEquals('Test Error', $actualError['message']);
$this->assertEquals(\E_USER_NOTICE, $actualError['type']);
}
public function testNegativeTimeoutFromConstructor()
{
$this->expectException(InvalidArgumentException::class);
$this->getProcess('', null, null, null, -1);
}
public function testNegativeTimeoutFromSetter()
{
$this->expectException(InvalidArgumentException::class);
$p = $this->getProcess('');
$p->setTimeout(-1);
}
public function testFloatAndNullTimeout()
{
$p = $this->getProcess('');
$p->setTimeout(10);
$this->assertSame(10.0, $p->getTimeout());
$p->setTimeout(null);
$this->assertNull($p->getTimeout());
$p->setTimeout(0.0);
$this->assertNull($p->getTimeout());
}
#[RequiresPhpExtension('pcntl')]
public function testStopWithTimeoutIsActuallyWorking()
{
$p = $this->getProcess([self::$phpBin, __DIR__.'/NonStopableProcess.php', 30]);
$p->start();
while ($p->isRunning() && !str_contains($p->getOutput(), 'received')) {
usleep(1000);
}
if (!$p->isRunning()) {
throw new \LogicException('Process is not running: '.$p->getErrorOutput());
}
$start = microtime(true);
$p->stop(0.1);
$p->wait();
$this->assertLessThan(15, microtime(true) - $start);
}
#[Group('transient-on-windows')]
public function testWaitUntilSpecificOutput()
{
$p = $this->getProcess([self::$phpBin, __DIR__.'/KillableProcessWithOutput.php']);
$p->start();
$start = microtime(true);
$completeOutput = '';
$result = $p->waitUntil(static function ($type, $output) use (&$completeOutput) {
return str_contains($completeOutput .= $output, 'One more');
});
$this->assertTrue($result);
$this->assertLessThan(20, microtime(true) - $start);
$this->assertStringStartsWith("First iteration output\nSecond iteration output\nOne more", $completeOutput);
$p->stop();
}
public function testWaitUntilCanReturnFalse()
{
$p = $this->getProcess('echo foo');
$p->start();
$this->assertFalse($p->waitUntil(static fn () => false));
}
public function testAllOutputIsActuallyReadOnTermination()
{
// this code will result in a maximum of 2 reads of 8192 bytes by calling
// start() and isRunning(). by the time getOutput() is called the process
// has terminated so the internal pipes array is already empty. normally
// the call to start() will not read any data as the process will not have
// generated output, but this is non-deterministic so we must count it as
// a possibility. therefore we need 2 * PipesInterface::CHUNK_SIZE plus
// another byte which will never be read.
$expectedOutputSize = PipesInterface::CHUNK_SIZE * 2 + 2;
$code = \sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize);
$p = $this->getProcessForCode($code);
$p->start();
// Don't call Process::run nor Process::wait to avoid any read of pipes
$h = new \ReflectionProperty($p, 'process');
$h = $h->getValue($p);
$s = @proc_get_status($h);
while (!empty($s['running'])) {
usleep(1000);
$s = proc_get_status($h);
}
$o = $p->getOutput();
$this->assertEquals($expectedOutputSize, \strlen($o));
}
public function testCallbacksAreExecutedWithStart()
{
$process = $this->getProcess('echo foo');
$process->start(static function ($type, $buffer) use (&$data) {
$data .= $buffer;
});
$process->wait();
$this->assertSame('foo'.\PHP_EOL, $data);
}
public function testReadSupportIsDisabledWithoutCallback()
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait".');
$process = $this->getProcess('echo foo');
// disabling output + not passing a callback to start() => read support disabled
$process->disableOutput();
$process->start();
$process->wait(static function ($type, $buffer) use (&$data) {
$data .= $buffer;
});
}
/**
* tests results from sub processes.
*/
#[DataProvider('responsesCodeProvider')]
public function testProcessResponses($expected, $getter, $code)
{
$p = $this->getProcessForCode($code);
$p->run();
$this->assertSame($expected, $p->$getter());
}
/**
* tests results from sub processes.
*/
#[DataProvider('pipesCodeProvider')]
public function testProcessPipes($code, $size)
{
$expected = str_repeat(str_repeat('*', 1024), $size).'!';
$expectedLength = (1024 * $size) + 1;
$p = $this->getProcessForCode($code);
$p->setInput($expected);
$p->run();
$this->assertEquals($expectedLength, \strlen($p->getOutput()));
$this->assertEquals($expectedLength, \strlen($p->getErrorOutput()));
}
#[DataProvider('pipesCodeProvider')]
public function testSetStreamAsInput($code, $size)
{
$expected = str_repeat(str_repeat('*', 1024), $size).'!';
$expectedLength = (1024 * $size) + 1;
$stream = fopen('php://temporary', 'w+');
fwrite($stream, $expected);
rewind($stream);
$p = $this->getProcessForCode($code);
$p->setInput($stream);
$p->run();
fclose($stream);
$this->assertEquals($expectedLength, \strlen($p->getOutput()));
$this->assertEquals($expectedLength, \strlen($p->getErrorOutput()));
}
public function testLiveStreamAsInput()
{
$stream = fopen('php://memory', 'r+');
fwrite($stream, 'hello');
rewind($stream);
$p = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);');
$p->setInput($stream);
$p->start(static function ($type, $data) use ($stream) {
if ('hello' === $data) {
fclose($stream);
}
});
$p->wait();
$this->assertSame('hello', $p->getOutput());
}
public function testSetInputWhileRunningThrowsAnException()
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Input cannot be set while the process is running.');
$process = $this->getProcessForCode('sleep(30);');
$process->start();
try {
$process->setInput('foobar');
$process->stop();
$this->fail('A LogicException should have been raised.');
} catch (LogicException $e) {
}
$process->stop();
throw $e;
}
#[DataProvider('provideInvalidInputValues')]
public function testInvalidInput(array|object $value)
{
$process = $this->getProcess('foo');
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('"Symfony\Component\Process\Process::setInput" only accepts strings, Traversable objects or stream resources.');
$process->setInput($value);
}
public static function provideInvalidInputValues()
{
return [
[[]],
[new NonStringifiable()],
];
}
#[DataProvider('provideInputValues')]
public function testValidInput(?string $expected, float|string|null $value)
{
$process = $this->getProcess('foo');
$process->setInput($value);
$this->assertSame($expected, $process->getInput());
}
public static function provideInputValues()
{
return [
[null, null],
['24.5', 24.5],
['input data', 'input data'],
];
}
public static function chainedCommandsOutputProvider()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
return [
["2 \r\n2\r\n", '&&', '2'],
];
}
return [
["1\n1\n", ';', '1'],
["2\n2\n", '&&', '2'],
];
}
#[DataProvider('chainedCommandsOutputProvider')]
public function testChainedCommandsOutput($expected, $operator, $input)
{
$process = $this->getProcess(\sprintf('echo %s %s echo %s', $input, $operator, $input));
$process->run();
$this->assertEquals($expected, $process->getOutput());
}
public function testCallbackIsExecutedForOutput()
{
$p = $this->getProcessForCode('echo \'foo\';');
$called = false;
$p->run(static function ($type, $buffer) use (&$called) {
$called = 'foo' === $buffer;
});
$this->assertTrue($called, 'The callback should be executed with the output');
}
public function testCallbackIsExecutedForOutputWheneverOutputIsDisabled()
{
$p = $this->getProcessForCode('echo \'foo\';');
$p->disableOutput();
$called = false;
$p->run(static function ($type, $buffer) use (&$called) {
$called = 'foo' === $buffer;
});
$this->assertTrue($called, 'The callback should be executed with the output');
}
public function testGetErrorOutput()
{
$p = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }');
$p->run();
$this->assertEquals(3, preg_match_all('/ERROR/', $p->getErrorOutput(), $matches));
}
public function testFlushErrorOutput()
{
$p = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }');
$p->run();
$p->clearErrorOutput();
$this->assertSame('', $p->getErrorOutput());
}
#[DataProvider('provideIncrementalOutput')]
public function testIncrementalOutput($getOutput, $getIncrementalOutput, $uri)
{
$lock = tempnam(sys_get_temp_dir(), __FUNCTION__);
$p = $this->getProcessForCode('file_put_contents($s = \''.$uri.'\', \'foo\'); flock(fopen('.var_export($lock, true).', \'r\'), LOCK_EX); file_put_contents($s, \'bar\');');
$h = fopen($lock, 'w');
flock($h, \LOCK_EX);
$p->start();
foreach (['foo', 'bar'] as $s) {
while (!str_contains($p->$getOutput(), $s)) {
usleep(1000);
}
$this->assertSame($s, $p->$getIncrementalOutput());
$this->assertSame('', $p->$getIncrementalOutput());
flock($h, \LOCK_UN);
}
fclose($h);
}
public static function provideIncrementalOutput()
{
return [
['getOutput', 'getIncrementalOutput', 'php://stdout'],
['getErrorOutput', 'getIncrementalErrorOutput', 'php://stderr'],
];
}
public function testGetOutput()
{
$p = $this->getProcessForCode('$n = 0; while ($n < 3) { echo \' foo \'; $n++; }');
$p->run();
$this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches));
}
public function testFlushOutput()
{
$p = $this->getProcessForCode('$n=0;while ($n<3) {echo \' foo \';$n++;}');
$p->run();
$p->clearOutput();
$this->assertSame('', $p->getOutput());
}
public function testZeroAsOutput()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
// see http://stackoverflow.com/questions/7105433/windows-batch-echo-without-new-line
$p = $this->getProcess('echo | set /p dummyName=0');
} else {
$p = $this->getProcess('printf 0');
}
$p->run();
$this->assertSame('0', $p->getOutput());
}
public function testExitCodeCommandFailed()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Windows does not support POSIX exit code');
}
// such command run in bash return an exitcode 127
$process = $this->getProcess('nonexistingcommandIhopeneversomeonewouldnameacommandlikethis');
$process->run();
$this->assertGreaterThan(0, $process->getExitCode());
}
public function testTTYCommand()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Windows does not have /dev/tty support');
}
if (!Process::isTtySupported()) {
$this->markTestSkipped('There is no TTY support');
}
$process = $this->getProcess('echo "foo" >> /dev/null && '.$this->getProcessForCode('usleep(100000);')->getCommandLine());
$process->setTty(true);
$process->start();
$this->assertTrue($process->isRunning());
$process->wait();
$this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
}
public function testTTYCommandExitCode()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Windows does have /dev/tty support');
}
if (!Process::isTtySupported()) {
$this->markTestSkipped('There is no TTY support');
}
$process = $this->getProcess('echo "foo" >> /dev/null');
$process->setTty(true);
$process->run();
$this->assertTrue($process->isSuccessful());
}
public function testTTYInWindowsEnvironment()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('TTY mode is not supported on Windows platform.');
if ('\\' !== \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('This test is for Windows platform only');
}
$process = $this->getProcess('echo "foo" >> /dev/null');
$process->setTty(false);
$process->setTty(true);
}
public function testExitCodeTextIsNullWhenExitCodeIsNull()
{
$process = $this->getProcess('');
$this->assertNull($process->getExitCodeText());
}
public function testStderrNotMixedWithStdout()
{
if (!Process::isPtySupported()) {
$this->markTestSkipped('PTY is not supported on this operating system.');
}
$process = $this->getProcess('echo "foo" && echo "bar" >&2');
$process->setPty(true);
$process->run();
$this->assertSame("foo\r\n", $process->getOutput());
$this->assertSame("bar\n", $process->getErrorOutput());
}
public function testPTYCommand()
{
if (!Process::isPtySupported()) {
$this->markTestSkipped('PTY is not supported on this operating system.');
}
$process = $this->getProcess('echo "foo"');
$process->setPty(true);
$process->run();
$this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
$this->assertEquals("foo\r\n", $process->getOutput());
}
public function testMustRun()
{
$process = $this->getProcess('echo foo');
$this->assertSame($process, $process->mustRun());
$this->assertEquals('foo'.\PHP_EOL, $process->getOutput());
}
public function testSuccessfulMustRunHasCorrectExitCode()
{
$process = $this->getProcess('echo foo')->mustRun();
$this->assertEquals(0, $process->getExitCode());
}
public function testMustRunThrowsException()
{
$process = $this->getProcess('exit 1');
$this->expectException(ProcessFailedException::class);
$process->mustRun();
}
public function testExitCodeText()
{
$process = $this->getProcess('');
$r = new \ReflectionObject($process);
$p = $r->getProperty('exitcode');
$p->setValue($process, 2);
$this->assertEquals('Misuse of shell builtins', $process->getExitCodeText());
}
public function testStartIsNonBlocking()
{
$process = $this->getProcessForCode('usleep(500000);');
$start = microtime(true);
$process->start();
$end = microtime(true);
$this->assertLessThan(0.4, $end - $start);
$process->stop();
}
public function testUpdateStatus()
{
$process = $this->getProcess('echo foo');
$process->run();
$this->assertGreaterThan(0, \strlen($process->getOutput()));
}
public function testGetExitCodeIsNullOnStart()
{
$process = $this->getProcessForCode('usleep(100000);');
$this->assertNull($process->getExitCode());
$process->start();
$this->assertNull($process->getExitCode());
$process->wait();
$this->assertEquals(0, $process->getExitCode());
}
public function testGetExitCodeIsNullOnWhenStartingAgain()
{
$process = $this->getProcessForCode('usleep(100000);');
$process->run();
$this->assertEquals(0, $process->getExitCode());
$process->start();
$this->assertNull($process->getExitCode());
$process->wait();
$this->assertEquals(0, $process->getExitCode());
}
public function testGetExitCode()
{
$process = $this->getProcess('echo foo');
$process->run();
$this->assertSame(0, $process->getExitCode());
}
public function testStatus()
{
$process = $this->getProcessForCode('usleep(100000);');
$this->assertFalse($process->isRunning());
$this->assertFalse($process->isStarted());
$this->assertFalse($process->isTerminated());
$this->assertSame(Process::STATUS_READY, $process->getStatus());
$process->start();
$this->assertTrue($process->isRunning());
$this->assertTrue($process->isStarted());
$this->assertFalse($process->isTerminated());
$this->assertSame(Process::STATUS_STARTED, $process->getStatus());
$process->wait();
$this->assertFalse($process->isRunning());
$this->assertTrue($process->isStarted());
$this->assertTrue($process->isTerminated());
$this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
}
public function testStop()
{
$process = $this->getProcessForCode('sleep(31);');
$process->start();
$this->assertTrue($process->isRunning());
$process->stop();
$this->assertFalse($process->isRunning());
}
public function testStopDoesNotThrowAfterBrokenPipe()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Broken pipe notices are specific to Unix-like platforms.');
}
$process = $this->getProcess([self::$phpBin, '-r', 'exit(0);'], null, null, str_repeat('*', PipesInterface::CHUNK_SIZE * 32));
$process->run();
$this->assertSame(0, $process->getExitCode());
$process->stop(0);
// __destruct() should not trigger a broken pipe notice
self::$process = $process = null;
gc_collect_cycles();
}
public function testIsSuccessful()
{
$process = $this->getProcess('echo foo');
$process->run();
$this->assertTrue($process->isSuccessful());
}
public function testIsSuccessfulOnlyAfterTerminated()
{
$process = $this->getProcessForCode('usleep(100000);');
$process->start();
$this->assertFalse($process->isSuccessful());
$process->wait();
$this->assertTrue($process->isSuccessful());
}
public function testIsNotSuccessful()
{
$process = $this->getProcessForCode('throw new \Exception(\'BOUM\');');
$process->run();
$this->assertFalse($process->isSuccessful());
}
public function testProcessIsNotSignaled()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Windows does not support POSIX signals');
}
$process = $this->getProcess('echo foo');
$process->run();
$this->assertFalse($process->hasBeenSignaled());
}
public function testProcessWithoutTermSignal()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Windows does not support POSIX signals');
}
$process = $this->getProcess('echo foo');
$process->run();
$this->assertEquals(0, $process->getTermSignal());
}
public function testProcessIsSignaledIfStopped()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Windows does not support POSIX signals');
}
$process = $this->getProcessForCode('sleep(32);');
$process->start();
$process->stop();
$this->assertTrue($process->hasBeenSignaled());
$this->assertEquals(15, $process->getTermSignal()); // SIGTERM
}
public function testProcessThrowsExceptionWhenExternallySignaled()
{
$this->expectException(ProcessSignaledException::class);
$this->expectExceptionMessage('The process has been signaled with signal "9".');
if (!\function_exists('posix_kill')) {
$this->markTestSkipped('Function posix_kill is required.');
}
if (self::$sigchild) {
$this->markTestSkipped('PHP is compiled with --enable-sigchild.');
}
$process = $this->getProcessForCode('sleep(32.1);');
$process->start();
posix_kill($process->getPid(), 9); // SIGKILL
$process->wait();
}
public function testRestart()
{
$process1 = $this->getProcessForCode('echo getmypid();');
$process1->run();
$process2 = $process1->restart();
$process2->wait(); // wait for output
// Ensure that both processed finished and the output is numeric
$this->assertFalse($process1->isRunning());
$this->assertFalse($process2->isRunning());
$this->assertIsNumeric($process1->getOutput());
$this->assertIsNumeric($process2->getOutput());
// Ensure that restart returned a new process by check that the output is different
$this->assertNotEquals($process1->getOutput(), $process2->getOutput());
}
public function testRunProcessWithTimeout()
{
$this->expectException(ProcessTimedOutException::class);
$this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.');
$process = $this->getProcessForCode('sleep(30);');
$process->setTimeout(0.1);
$start = microtime(true);
try {
$process->run();
$this->fail('A RuntimeException should have been raised');
} catch (RuntimeException $e) {
}
$this->assertLessThan(15, microtime(true) - $start);
throw $e;
}
public function testIterateOverProcessWithTimeout()
{
$this->expectException(ProcessTimedOutException::class);
$this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.');
$process = $this->getProcessForCode('sleep(30);');
$process->setTimeout(0.1);
$start = microtime(true);
try {
$process->start();
foreach ($process as $buffer) {
}
$this->fail('A RuntimeException should have been raised');
} catch (RuntimeException $e) {
}
$this->assertLessThan(15, microtime(true) - $start);
throw $e;
}
public function testCheckTimeoutOnNonStartedProcess()
{
$process = $this->getProcess('echo foo');
$this->assertNull($process->checkTimeout());
}
public function testCheckTimeoutOnTerminatedProcess()
{
$process = $this->getProcess('echo foo');
$process->run();
$this->assertNull($process->checkTimeout());
}
public function testCheckTimeoutOnStartedProcess()
{
$this->expectException(ProcessTimedOutException::class);
$this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.');
$process = $this->getProcessForCode('sleep(33);');
$process->setTimeout(0.1);
$process->start();
$start = microtime(true);
try {
while ($process->isRunning()) {
$process->checkTimeout();
usleep(100000);
}
$this->fail('A ProcessTimedOutException should have been raised');
} catch (ProcessTimedOutException $e) {
}
$this->assertLessThan(15, microtime(true) - $start);
throw $e;
}
public function testIdleTimeout()
{
$process = $this->getProcessForCode('sleep(34);');
$process->setTimeout(60);
$process->setIdleTimeout(0.1);
try {
$process->run();
$this->fail('A timeout exception was expected.');
} catch (ProcessTimedOutException $e) {
$this->assertTrue($e->isIdleTimeout());
$this->assertFalse($e->isGeneralTimeout());
$this->assertEquals(0.1, $e->getExceededTimeout());
}
}
public function testIdleTimeoutNotExceededWhenOutputIsSent()
{
$process = $this->getProcessForCode('while (true) {echo \'foo \'; usleep(1000);}');
$process->setTimeout(1);
$process->start();
while (!str_contains($process->getOutput(), 'foo')) {
usleep(1000);
}
$process->setIdleTimeout(0.5);
try {
$process->wait();
$this->fail('A timeout exception was expected.');
} catch (ProcessTimedOutException $e) {
$this->assertTrue($e->isGeneralTimeout(), 'A general timeout is expected.');
$this->assertFalse($e->isIdleTimeout(), 'No idle timeout is expected.');
$this->assertEquals(1, $e->getExceededTimeout());
}
}
public function testStartAfterATimeout()
{
$this->expectException(ProcessTimedOutException::class);
$this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.');
$process = $this->getProcessForCode('sleep(35);');
$process->setTimeout(0.1);
try {
$process->run();
$this->fail('A ProcessTimedOutException should have been raised.');
} catch (ProcessTimedOutException $e) {
}
$this->assertFalse($process->isRunning());
$process->start();
$this->assertTrue($process->isRunning());
$process->stop(0);
throw $e;
}
public function testGetPid()
{
$process = $this->getProcessForCode('sleep(36);');
$process->start();
$this->assertGreaterThan(0, $process->getPid());
$process->stop(0);
}
public function testGetPidIsNullBeforeStart()
{
$process = $this->getProcess('foo');
$this->assertNull($process->getPid());
}
public function testGetPidIsNullAfterRun()
{
$process = $this->getProcess('echo foo');
$process->run();
$this->assertNull($process->getPid());
}
#[RequiresPhpExtension('pcntl')]
public function testSignal()
{
$process = $this->getProcess([self::$phpBin, __DIR__.'/SignalListener.php']);
$process->start();
while (!str_contains($process->getOutput(), 'Caught')) {
usleep(1000);
}
$process->signal(\SIGUSR1);
$process->wait();
$this->assertEquals('Caught SIGUSR1', $process->getOutput());
}
#[RequiresPhpExtension('pcntl')]
public function testExitCodeIsAvailableAfterSignal()
{
$process = $this->getProcess('sleep 4');
$process->start();
$process->signal(\SIGKILL);
while ($process->isRunning()) {
usleep(10000);
}
$this->assertFalse($process->isRunning());
$this->assertTrue($process->hasBeenSignaled());
$this->assertFalse($process->isSuccessful());
$this->assertEquals(137, $process->getExitCode());
}
public function testSignalProcessNotRunning()
{
$process = $this->getProcess('foo');
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Cannot send signal on a non running process.');
$process->signal(1); // SIGHUP
}
#[DataProvider('provideMethodsThatNeedARunningProcess')]
public function testMethodsThatNeedARunningProcess($method)
{
$process = $this->getProcess('foo');
$this->expectException(LogicException::class);
$this->expectExceptionMessage(\sprintf('Process must be started before calling "%s()".', $method));
$process->{$method}();
}
public static function provideMethodsThatNeedARunningProcess()
{
return [
['getOutput'],
['getIncrementalOutput'],
['getErrorOutput'],
['getIncrementalErrorOutput'],
['wait'],
];
}
#[DataProvider('provideMethodsThatNeedATerminatedProcess')]
public function testMethodsThatNeedATerminatedProcess($method)
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Process must be terminated before calling');
$process = $this->getProcessForCode('sleep(37);');
$process->start();
try {
$process->{$method}();
$process->stop(0);
$this->fail('A LogicException must have been thrown');
} catch (\Exception $e) {
}
$process->stop(0);
throw $e;
}
public static function provideMethodsThatNeedATerminatedProcess()
{
return [
['hasBeenSignaled'],
['getTermSignal'],
['hasBeenStopped'],
['getStopSignal'],
];
}
public function testWrongSignal()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('POSIX signals do not work on Windows');
}
$this->expectException(RuntimeException::class);
$process = $this->getProcessForCode('sleep(38);');
$process->start();
try {
$process->signal(-4);
$this->fail('A RuntimeException must have been thrown');
} finally {
$process->stop(0);
}
}
public function testDisableOutputDisablesTheOutput()
{
$p = $this->getProcess('foo');
$this->assertFalse($p->isOutputDisabled());
$p->disableOutput();
$this->assertTrue($p->isOutputDisabled());
$p->enableOutput();
$this->assertFalse($p->isOutputDisabled());
}
public function testDisableOutputWhileRunningThrowsException()
{
$p = $this->getProcessForCode('sleep(39);');
$p->start();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Disabling output while the process is running is not possible.');
$p->disableOutput();
}
public function testEnableOutputWhileRunningThrowsException()
{
$p = $this->getProcessForCode('sleep(40);');
$p->disableOutput();
$p->start();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Enabling output while the process is running is not possible.');
$p->enableOutput();
}
public function testEnableOrDisableOutputAfterRunDoesNotThrowException()
{
$p = $this->getProcess('echo foo');
$p->disableOutput();
$p->run();
$p->enableOutput();
$p->disableOutput();
$this->assertTrue($p->isOutputDisabled());
}
public function testDisableOutputWhileIdleTimeoutIsSet()
{
$process = $this->getProcess('foo');
$process->setIdleTimeout(1);
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Output cannot be disabled while an idle timeout is set.');
$process->disableOutput();
}
public function testSetIdleTimeoutWhileOutputIsDisabled()
{
$process = $this->getProcess('foo');
$process->disableOutput();
$this->expectException(LogicException::class);
$this->expectExceptionMessage('timeout cannot be set while the output is disabled.');
$process->setIdleTimeout(1);
}
public function testSetNullIdleTimeoutWhileOutputIsDisabled()
{
$process = $this->getProcess('foo');
$process->disableOutput();
$this->assertSame($process, $process->setIdleTimeout(null));
}
#[DataProvider('provideOutputFetchingMethods')]
public function testGetOutputWhileDisabled($fetchMethod)
{
$p = $this->getProcessForCode('sleep(41);');
$p->disableOutput();
$p->start();
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Output has been disabled.');
$p->{$fetchMethod}();
}
public static function provideOutputFetchingMethods()
{
return [
['getOutput'],
['getIncrementalOutput'],
['getErrorOutput'],
['getIncrementalErrorOutput'],
];
}
public function testStopTerminatesProcessCleanly()
{
$process = $this->getProcessForCode('echo 123; sleep(42);');
$process->run(static function () use ($process) {
$process->stop();
});
$this->assertTrue(true, 'A call to stop() is not expected to cause wait() to throw a RuntimeException');
}
public function testKillSignalTerminatesProcessCleanly()
{
$process = $this->getProcessForCode('echo 123; sleep(43);');
$process->run(static function () use ($process) {
$process->signal(9); // SIGKILL
});
$this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
}
public function testTermSignalTerminatesProcessCleanly()
{
$process = $this->getProcessForCode('echo 123; sleep(44);');
$process->run(static function () use ($process) {
$process->signal(15); // SIGTERM
});
$this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
}
public static function responsesCodeProvider()
{
return [
// expected output / getter / code to execute
// [1,'getExitCode','exit(1);'],
// [true,'isSuccessful','exit();'],
['output', 'getOutput', 'echo \'output\';'],
];
}
public static function pipesCodeProvider()
{
$variations = [
'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);',
'include \''.__DIR__.'/PipeStdinInStdoutStdErrStreamSelect.php\';',
];
if ('\\' === \DIRECTORY_SEPARATOR) {
// Avoid XL buffers on Windows because of https://bugs.php.net/65650
$sizes = [1, 2, 4, 8];
} else {
$sizes = [1, 16, 64, 1024, 4096];
}
$codes = [];
foreach ($sizes as $size) {
foreach ($variations as $code) {
$codes[] = [$code, $size];
}
}
return $codes;
}
#[DataProvider('provideVariousIncrementals')]
public function testIncrementalOutputDoesNotRequireAnotherCall($stream, $method)
{
$process = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\''.$stream.'\', $n, 1); $n++; usleep(1000); }', null, null, null, null);
$process->start();
$result = '';
$limit = microtime(true) + 3;
$expected = '012';
while ($result !== $expected && microtime(true) < $limit) {
$result .= $process->$method();
}
$this->assertSame($expected, $result);
$process->stop();
}
public static function provideVariousIncrementals()
{
return [
['php://stdout', 'getIncrementalOutput'],
['php://stderr', 'getIncrementalErrorOutput'],
];
}
public function testIteratorInput()
{
$input = static function () {
yield 'ping';
yield 'pong';
};
$process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);', null, null, $input());
$process->run();
$this->assertSame('pingpong', $process->getOutput());
}
public function testSimpleInputStream()
{
$input = new InputStream();
$process = $this->getProcessForCode('echo \'ping\'; echo fread(STDIN, 4); echo fread(STDIN, 4);');
$process->setInput($input);
$process->start(static function ($type, $data) use ($input) {
if ('ping' === $data) {
$input->write('pang');
} elseif (!$input->isClosed()) {
$input->write('pong');
$input->close();
}
});
$process->wait();
$this->assertSame('pingpangpong', $process->getOutput());
}
public function testInputStreamWithCallable()
{
$i = 0;
$stream = fopen('php://memory', 'w+');
$stream = static function () use ($stream, &$i) {
if ($i < 3) {
rewind($stream);
fwrite($stream, ++$i);
rewind($stream);
return $stream;
}
return null;
};
$input = new InputStream();
$input->onEmpty($stream);
$input->write($stream());
$process = $this->getProcessForCode('echo fread(STDIN, 3);');
$process->setInput($input);
$process->start(static function ($type, $data) use ($input) {
$input->close();
});
$process->wait();
$this->assertSame('123', $process->getOutput());
}
public function testInputStreamWithGenerator()
{
$input = new InputStream();
$input->onEmpty(static function ($input) {
yield 'pong';
$input->close();
});
$process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);');
$process->setInput($input);
$process->start();
$input->write('ping');
$process->wait();
$this->assertSame('pingpong', $process->getOutput());
}
public function testInputStreamOnEmpty()
{
$i = 0;
$input = new InputStream();
$input->onEmpty(static function () use (&$i) { ++$i; });
$process = $this->getProcessForCode('echo 123; echo fread(STDIN, 1); echo 456;');
$process->setInput($input);
$process->start(static function ($type, $data) use ($input) {
if ('123' === $data) {
$input->close();
}
});
$process->wait();
$this->assertSame(0, $i, 'InputStream->onEmpty callback should be called only when the input *becomes* empty');
$this->assertSame('123456', $process->getOutput());
}
public function testIteratorOutput()
{
$input = new InputStream();
$process = $this->getProcessForCode('fwrite(STDOUT, 123); fwrite(STDERR, 234); flush(); usleep(10000); fwrite(STDOUT, fread(STDIN, 3)); fwrite(STDERR, 456);');
$process->setInput($input);
$process->start();
$output = [];
foreach ($process as $type => $data) {
$output[] = [$type, $data];
break;
}
$expectedOutput = [
[$process::OUT, '123'],
];
$this->assertSame($expectedOutput, $output);
$input->write(345);
foreach ($process as $type => $data) {
$output[] = [$type, $data];
}
$this->assertSame('', $process->getOutput());
$this->assertFalse($process->isRunning());
$expectedOutput = [
[$process::OUT, '123'],
[$process::ERR, '234'],
[$process::OUT, '345'],
[$process::ERR, '456'],
];
$this->assertSame($expectedOutput, $output);
}
public function testNonBlockingNorClearingIteratorOutput()
{
$input = new InputStream();
$process = $this->getProcessForCode('fwrite(STDOUT, fread(STDIN, 3));');
$process->setInput($input);
$process->start();
$output = [];
foreach ($process->getIterator($process::ITER_NON_BLOCKING | $process::ITER_KEEP_OUTPUT) as $type => $data) {
$output[] = [$type, $data];
break;
}
$expectedOutput = [
[$process::OUT, ''],
];
$this->assertSame($expectedOutput, $output);
$input->write(123);
foreach ($process->getIterator($process::ITER_NON_BLOCKING | $process::ITER_KEEP_OUTPUT) as $type => $data) {
if ('' !== $data) {
$output[] = [$type, $data];
}
}
$this->assertSame('123', $process->getOutput());
$this->assertFalse($process->isRunning());
$expectedOutput = [
[$process::OUT, ''],
[$process::OUT, '123'],
];
$this->assertSame($expectedOutput, $output);
}
public function testChainedProcesses()
{
$p1 = $this->getProcessForCode('fwrite(STDERR, 123); fwrite(STDOUT, 456);');
$p2 = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);');
$p2->setInput($p1);
$p1->start();
$p2->run();
$this->assertSame('123', $p1->getErrorOutput());
$this->assertSame('', $p1->getOutput());
$this->assertSame('', $p2->getErrorOutput());
$this->assertSame('456', $p2->getOutput());
}
public function testSetBadEnv()
{
$process = $this->getProcess('echo hello');
$process->setEnv(['bad%%' => '123']);
$process->run();
$this->assertSame('hello'.\PHP_EOL, $process->getOutput());
$this->assertSame('', $process->getErrorOutput());
}
public function testEnvBackupDoesNotDeleteExistingVars()
{
putenv('existing_var=foo');
$_ENV['existing_var'] = 'foo';
$process = $this->getProcess('php -r "echo getenv(\'new_test_var\');"');
$process->setEnv(['existing_var' => 'bar', 'new_test_var' => 'foo']);
$process->run();
$this->assertSame('foo', $process->getOutput());
$this->assertSame('foo', getenv('existing_var'));
$this->assertFalse(getenv('new_test_var'));
putenv('existing_var');
unset($_ENV['existing_var']);
}
public function testEnvIsInherited()
{
$process = $this->getProcessForCode('echo serialize($_SERVER);', null, ['BAR' => 'BAZ', 'EMPTY' => '']);
putenv('FOO=BAR');
$_ENV['FOO'] = 'BAR';
$process->run();
$expected = ['BAR' => 'BAZ', 'EMPTY' => '', 'FOO' => 'BAR'];
$env = array_intersect_key(unserialize($process->getOutput()), $expected);
$this->assertEquals($expected, $env);
putenv('FOO');
unset($_ENV['FOO']);
}
public function testGetCommandLine()
{
$p = new Process(['/usr/bin/php']);
$expected = '\\' === \DIRECTORY_SEPARATOR ? '/usr/bin/php' : "'/usr/bin/php'";
$this->assertSame($expected, $p->getCommandLine());
$p = new Process(['cd', '/d']);
$expected = '\\' === \DIRECTORY_SEPARATOR ? 'cd /d' : "'cd' '/d'";
$this->assertSame($expected, $p->getCommandLine());
}
#[DataProvider('provideEscapeArgument')]
public function testEscapeArgument($arg)
{
$p = new Process([self::$phpBin, '-r', 'echo $argv[1];', $arg]);
$p->run();
$this->assertSame((string) $arg, $p->getOutput());
}
public function testRawCommandLine()
{
$p = Process::fromShellCommandline(\sprintf('"%s" -r %s "a" "" "b"', self::$phpBin, escapeshellarg('print_r($argv);')));
$p->run();
$expected = "Array\n(\n [0] => -\n [1] => a\n [2] => \n [3] => b\n)\n";
$this->assertSame($expected, str_replace('Standard input code', '-', $p->getOutput()));
}
public static function provideEscapeArgument()
{
yield ['a"b%c%'];
yield ['a"b^c^'];
yield ["a\nb'c"];
yield ['a^b c!'];
yield ["a!b\tc"];
yield ['a\\\\"\\"'];
yield ['éÉèÈàÀöä'];
yield [null];
yield [1];
yield [1.1];
}
public function testMsysEscapingOnWindows()
{
if ('\\' !== \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('This test is for Windows platform only');
}
file_put_contents('=foo.txt', 'This is a test file.');
try {
$p = $this->getProcess(['type', substr_replace(getcwd(), '=foo.txt', 2)]);
$p->mustRun();
$this->assertSame('This is a test file.', $p->getOutput());
} finally {
unlink('=foo.txt');
}
$this->assertSame(\sprintf('type "%s=foo.txt"', substr(getcwd(), 0, 2)), $p->getCommandLine());
}
public function testPreparedCommand()
{
$p = Process::fromShellCommandline('echo "${:abc}"DEF');
$p->run(null, ['abc' => 'ABC']);
$this->assertSame('ABCDEF', rtrim($p->getOutput()));
}
public function testPreparedCommandMulti()
{
$p = Process::fromShellCommandline('echo "${:abc}""${:def}"');
$p->run(null, ['abc' => 'ABC', 'def' => 'DEF']);
$this->assertSame('ABCDEF', rtrim($p->getOutput()));
}
public function testPreparedCommandWithQuoteInIt()
{
$p = Process::fromShellCommandline('php -r "${:code}" "${:def}"');
$p->run(null, ['code' => 'echo $argv[1];', 'def' => '"DEF"']);
$this->assertSame('"DEF"', rtrim($p->getOutput()));
}
public function testPreparedCommandWithMissingValue()
{
$p = Process::fromShellCommandline('echo "${:abc}"');
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Command line is missing a value for parameter "abc": echo "${:abc}"');
$p->run(null, ['bcd' => 'BCD']);
}
public function testPreparedCommandWithNoValues()
{
$p = Process::fromShellCommandline('echo "${:abc}"');
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Command line is missing a value for parameter "abc": echo "${:abc}"');
$p->run(null, []);
}
public function testEnvArgument()
{
$cmd = '\\' === \DIRECTORY_SEPARATOR ? 'echo !FOO! !BAR! !BAZ!' : 'echo $FOO $BAR $BAZ';
$p = Process::fromShellCommandline($cmd);
$this->assertSame([], $p->getEnv());
$env = ['FOO' => 'Foo', 'BAR' => 'Bar'];
$p = Process::fromShellCommandline($cmd, null, $env);
$p->run(null, ['BAR' => 'baR', 'BAZ' => 'baZ']);
$this->assertSame('Foo baR baZ', rtrim($p->getOutput()));
$this->assertSame($env, $p->getEnv());
}
public function testEnvVarNamesCastToString()
{
$process = $this->getProcess('echo hello');
$process->setEnv([123 => 'value']);
$process->run();
$this->assertSame('hello'.\PHP_EOL, $process->getOutput());
}
public function testEnvVarNamesWithEqualsSigns()
{
$process = $this->getProcess('echo hello');
$process->setEnv(['VAR=NAME' => 'value']);
$process->run();
$this->assertSame('hello'.\PHP_EOL, $process->getOutput());
}
public function testEnvVarNamesWithNullBytes()
{
$process = $this->getProcess('echo hello');
$process->setEnv(["VAR\0NAME" => 'value']);
$process->run();
$this->assertSame('hello'.\PHP_EOL, $process->getOutput());
}
public function testEnvVarNamesEmpty()
{
$process = $this->getProcess('echo hello');
$process->setEnv(['' => 'value']);
$process->run();
$this->assertSame('hello'.\PHP_EOL, $process->getOutput());
}
public function testWaitStoppedDeadProcess()
{
$process = $this->getProcess(self::$phpBin.' '.__DIR__.'/ErrorProcessInitiator.php -e '.self::$phpBin);
$process->start();
$process->setTimeout(2);
$process->wait();
$this->assertFalse($process->isRunning());
if ('\\' !== \DIRECTORY_SEPARATOR && !\Closure::bind(fn () => $this->isSigchildEnabled(), $process, $process)()) {
$this->assertSame(0, $process->getExitCode());
}
}
public function testEnvCaseInsensitiveOnWindows()
{
$p = $this->getProcessForCode('print_r([$_SERVER[\'PATH\'] ?? 1, $_SERVER[\'Path\'] ?? 2]);', null, ['PATH' => 'bar/baz']);
$p->run(null, ['Path' => 'foo/bar']);
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->assertSame('Array ( [0] => 1 [1] => foo/bar )', preg_replace('/\s++/', ' ', trim($p->getOutput())));
} else {
$this->assertSame('Array ( [0] => bar/baz [1] => foo/bar )', preg_replace('/\s++/', ' ', trim($p->getOutput())));
}
}
public function testMultipleCallsToProcGetStatus()
{
$process = $this->getProcess('echo foo');
$process->start(static fn () => $process->isRunning());
while ($process->isRunning()) {
usleep(1000);
}
$this->assertSame(0, $process->getExitCode());
}
public function testFailingProcessWithMultipleCallsToProcGetStatus()
{
$process = $this->getProcess('exit 123');
$process->start(static fn () => $process->isRunning());
while ($process->isRunning()) {
usleep(1000);
}
$this->assertSame(123, $process->getExitCode());
}
#[Group('slow')]
public function testLongRunningProcessWithMultipleCallsToProcGetStatus()
{
$process = $this->getProcess('sleep 1 && echo "done" && php -r "exit(0);"');
$process->start(static fn () => $process->isRunning());
while ($process->isRunning()) {
usleep(1000);
}
$this->assertSame(0, $process->getExitCode());
}
#[Group('slow')]
public function testLongRunningProcessWithMultipleCallsToProcGetStatusError()
{
$process = $this->getProcess('sleep 1 && echo "failure" && php -r "exit(123);"');
$process->start(static fn () => $process->isRunning());
while ($process->isRunning()) {
usleep(1000);
}
$this->assertSame(123, $process->getExitCode());
}
#[Group('transient-on-windows')]
public function testNotTerminableInputPipe()
{
$process = $this->getProcess('echo foo');
$process->setInput(\STDIN);
$process->start();
$process->setTimeout(2);
$process->wait();
$this->assertFalse($process->isRunning());
}
public function testIgnoringSignal()
{
if (!\function_exists('pcntl_signal')) {
$this->markTestSkipped('pnctl extension is required.'
gitextract_euimhexu/ ├── .gitattributes ├── .github/ │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ └── close-pull-request.yml ├── .gitignore ├── CHANGELOG.md ├── Exception/ │ ├── ExceptionInterface.php │ ├── InvalidArgumentException.php │ ├── LogicException.php │ ├── ProcessFailedException.php │ ├── ProcessSignaledException.php │ ├── ProcessStartFailedException.php │ ├── ProcessTimedOutException.php │ ├── RunProcessFailedException.php │ └── RuntimeException.php ├── ExecutableFinder.php ├── InputStream.php ├── LICENSE ├── Messenger/ │ ├── RunProcessContext.php │ ├── RunProcessMessage.php │ └── RunProcessMessageHandler.php ├── PhpExecutableFinder.php ├── PhpProcess.php ├── PhpSubprocess.php ├── Pipes/ │ ├── AbstractPipes.php │ ├── PipesInterface.php │ ├── UnixPipes.php │ └── WindowsPipes.php ├── Process.php ├── ProcessUtils.php ├── README.md ├── Tests/ │ ├── CreateNewConsoleTest.php │ ├── ErrorProcessInitiator.php │ ├── ExecutableFinderTest.php │ ├── Fixtures/ │ │ ├── executable_with_added_suffix.foo │ │ ├── executable_without_suffix │ │ ├── memory.php │ │ └── open_basedir.php │ ├── KillableProcessWithOutput.php │ ├── Messenger/ │ │ └── RunProcessMessageHandlerTest.php │ ├── NonStopableProcess.php │ ├── OutputMemoryLimitProcess.php │ ├── PhpExecutableFinderTest.php │ ├── PhpProcessTest.php │ ├── PhpSubprocessTest.php │ ├── PipeStdinInStdoutStdErrStreamSelect.php │ ├── ProcessFailedExceptionTest.php │ ├── ProcessTest.php │ ├── ProcessWindowsEnvBlockTest.php │ ├── SignalListener.php │ └── ThreeSecondProcess.php ├── composer.json └── phpunit.xml.dist
SYMBOL INDEX (369 symbols across 34 files)
FILE: Exception/ExceptionInterface.php
type ExceptionInterface (line 19) | interface ExceptionInterface extends \Throwable
FILE: Exception/InvalidArgumentException.php
class InvalidArgumentException (line 19) | class InvalidArgumentException extends \InvalidArgumentException impleme...
FILE: Exception/LogicException.php
class LogicException (line 19) | class LogicException extends \LogicException implements ExceptionInterface
FILE: Exception/ProcessFailedException.php
class ProcessFailedException (line 21) | class ProcessFailedException extends RuntimeException
method __construct (line 23) | public function __construct(
method getProcess (line 49) | public function getProcess(): Process
FILE: Exception/ProcessSignaledException.php
class ProcessSignaledException (line 21) | final class ProcessSignaledException extends RuntimeException
method __construct (line 23) | public function __construct(
method getProcess (line 29) | public function getProcess(): Process
method getSignal (line 34) | public function getSignal(): int
FILE: Exception/ProcessStartFailedException.php
class ProcessStartFailedException (line 19) | class ProcessStartFailedException extends ProcessFailedException
method __construct (line 21) | public function __construct(
method getProcess (line 39) | public function getProcess(): Process
FILE: Exception/ProcessTimedOutException.php
class ProcessTimedOutException (line 21) | class ProcessTimedOutException extends RuntimeException
method __construct (line 26) | public function __construct(
method getProcess (line 37) | public function getProcess(): Process
method isGeneralTimeout (line 42) | public function isGeneralTimeout(): bool
method isIdleTimeout (line 47) | public function isIdleTimeout(): bool
method getExceededTimeout (line 52) | public function getExceededTimeout(): ?float
FILE: Exception/RunProcessFailedException.php
class RunProcessFailedException (line 19) | final class RunProcessFailedException extends RuntimeException
method __construct (line 21) | public function __construct(ProcessFailedException $exception, public ...
FILE: Exception/RuntimeException.php
class RuntimeException (line 19) | class RuntimeException extends \RuntimeException implements ExceptionInt...
FILE: ExecutableFinder.php
class ExecutableFinder (line 20) | class ExecutableFinder
method setSuffixes (line 35) | public function setSuffixes(array $suffixes): void
method addSuffix (line 46) | public function addSuffix(string $suffix): void
method find (line 58) | public function find(string $name, ?string $default = null, array $ext...
FILE: InputStream.php
class InputStream (line 23) | class InputStream implements \IteratorAggregate
method onEmpty (line 32) | public function onEmpty(?callable $onEmpty = null): void
method write (line 43) | public function write(mixed $input): void
method close (line 57) | public function close(): void
method isClosed (line 65) | public function isClosed(): bool
method getIterator (line 70) | public function getIterator(): \Traversable
FILE: Messenger/RunProcessContext.php
class RunProcessContext (line 19) | final class RunProcessContext
method __construct (line 25) | public function __construct(
FILE: Messenger/RunProcessMessage.php
class RunProcessMessage (line 17) | class RunProcessMessage implements \Stringable
method __construct (line 21) | public function __construct(
method __toString (line 30) | public function __toString(): string
method fromShellCommandline (line 40) | public static function fromShellCommandline(string $command, ?string $...
FILE: Messenger/RunProcessMessageHandler.php
class RunProcessMessageHandler (line 21) | final class RunProcessMessageHandler
method __invoke (line 23) | public function __invoke(RunProcessMessage $message): RunProcessContext
FILE: PhpExecutableFinder.php
class PhpExecutableFinder (line 20) | class PhpExecutableFinder
method __construct (line 24) | public function __construct()
method find (line 32) | public function find(bool $includeArgs = true): string|false
method findArguments (line 89) | public function findArguments(): array
FILE: PhpProcess.php
class PhpProcess (line 28) | class PhpProcess extends Process
method __construct (line 37) | public function __construct(string $script, ?string $cwd = null, ?arra...
method fromShellCommandline (line 55) | public static function fromShellCommandline(string $command, ?string $...
method start (line 64) | public function start(?callable $callback = null, array $env = []): void
FILE: PhpSubprocess.php
class PhpSubprocess (line 46) | class PhpSubprocess extends Process
method __construct (line 56) | public function __construct(array $command, ?string $cwd = null, ?arra...
method fromShellCommandline (line 78) | public static function fromShellCommandline(string $command, ?string $...
method start (line 86) | public function start(?callable $callback = null, array $env = []): void
method writeTmpIni (line 95) | private function writeTmpIni(array $iniFiles, string $tmpDir): string
method mergeLoadedConfig (line 141) | private function mergeLoadedConfig(array $loadedConfig, array $iniConf...
method getAllIniFiles (line 159) | private function getAllIniFiles(): array
FILE: Pipes/AbstractPipes.php
class AbstractPipes (line 21) | abstract class AbstractPipes implements PipesInterface
method __construct (line 34) | public function __construct($input)
method close (line 43) | public function close(): void
method hasSystemCallBeenInterrupted (line 58) | protected function hasSystemCallBeenInterrupted(): bool
method unblock (line 80) | protected function unblock(): void
method write (line 101) | protected function write(): ?array
method closeBrokenInputPipe (line 185) | private function closeBrokenInputPipe(): void
method handleError (line 200) | public function handleError(int $type, string $msg): void
FILE: Pipes/PipesInterface.php
type PipesInterface (line 21) | interface PipesInterface
method getDescriptors (line 28) | public function getDescriptors(): array;
method getFiles (line 35) | public function getFiles(): array;
method readAndWrite (line 45) | public function readAndWrite(bool $blocking, bool $close = false): array;
method areOpen (line 50) | public function areOpen(): bool;
method haveReadSupport (line 55) | public function haveReadSupport(): bool;
method close (line 60) | public function close(): void;
FILE: Pipes/UnixPipes.php
class UnixPipes (line 23) | class UnixPipes extends AbstractPipes
method __construct (line 25) | public function __construct(
method __serialize (line 34) | public function __serialize(): array
method __unserialize (line 39) | public function __unserialize(array $data): void
method __destruct (line 44) | public function __destruct()
method getDescriptors (line 49) | public function getDescriptors(): array
method getFiles (line 84) | public function getFiles(): array
method readAndWrite (line 89) | public function readAndWrite(bool $blocking, bool $close = false): array
method haveReadSupport (line 135) | public function haveReadSupport(): bool
method areOpen (line 140) | public function areOpen(): bool
FILE: Pipes/WindowsPipes.php
class WindowsPipes (line 27) | class WindowsPipes extends AbstractPipes
method __construct (line 37) | public function __construct(
method __serialize (line 90) | public function __serialize(): array
method __unserialize (line 95) | public function __unserialize(array $data): void
method __destruct (line 100) | public function __destruct()
method getDescriptors (line 105) | public function getDescriptors(): array
method getFiles (line 127) | public function getFiles(): array
method readAndWrite (line 132) | public function readAndWrite(bool $blocking, bool $close = false): array
method haveReadSupport (line 164) | public function haveReadSupport(): bool
method areOpen (line 169) | public function areOpen(): bool
method close (line 174) | public function close(): void
FILE: Process.php
class Process (line 35) | class Process implements \IteratorAggregate
method __construct (line 162) | public function __construct(array $command, ?string $cwd = null, ?arra...
method fromShellCommandline (line 208) | public static function fromShellCommandline(string $command, ?string $...
method __serialize (line 216) | public function __serialize(): array
method __unserialize (line 221) | public function __unserialize(array $data): void
method __destruct (line 226) | public function __destruct()
method __clone (line 235) | public function __clone()
method run (line 264) | public function run(?callable $callback = null, array $env = []): int
method mustRun (line 292) | public function mustRun(?callable $callback = null, array $env = []): ...
method start (line 321) | public function start(?callable $callback = null, array $env = []): void
method restart (line 440) | public function restart(?callable $callback = null, array $env = []): ...
method wait (line 468) | public function wait(?callable $callback = null): int
method waitUntil (line 514) | public function waitUntil(callable $callback): bool
method getPid (line 554) | public function getPid(): ?int
method signal (line 570) | public function signal(int $signal): static
method disableOutput (line 585) | public function disableOutput(): static
method enableOutput (line 606) | public function enableOutput(): static
method isOutputDisabled (line 620) | public function isOutputDisabled(): bool
method getOutput (line 631) | public function getOutput(): string
method getIncrementalOutput (line 651) | public function getIncrementalOutput(): string
method getIterator (line 675) | public function getIterator(int $flags = 0): \Generator
method clearOutput (line 727) | public function clearOutput(): static
method getErrorOutput (line 742) | public function getErrorOutput(): string
method getIncrementalErrorOutput (line 763) | public function getIncrementalErrorOutput(): string
method clearErrorOutput (line 782) | public function clearErrorOutput(): static
method getExitCode (line 796) | public function getExitCode(): ?int
method getExitCodeText (line 814) | public function getExitCodeText(): ?string
method isSuccessful (line 826) | public function isSuccessful(): bool
method hasBeenSignaled (line 838) | public function hasBeenSignaled(): bool
method getTermSignal (line 853) | public function getTermSignal(): int
method hasBeenStopped (line 871) | public function hasBeenStopped(): bool
method getStopSignal (line 885) | public function getStopSignal(): int
method isRunning (line 895) | public function isRunning(): bool
method isStarted (line 909) | public function isStarted(): bool
method isTerminated (line 917) | public function isTerminated(): bool
method getStatus (line 929) | public function getStatus(): string
method stop (line 944) | public function stop(float $timeout = 10, ?int $signal = null): ?int
method addOutput (line 978) | public function addOutput(string $line): void
method addErrorOutput (line 992) | public function addErrorOutput(string $line): void
method getLastOutputTime (line 1004) | public function getLastOutputTime(): ?float
method getCommandLine (line 1012) | public function getCommandLine(): string
method getTimeout (line 1020) | public function getTimeout(): ?float
method getIdleTimeout (line 1028) | public function getIdleTimeout(): ?float
method setTimeout (line 1042) | public function setTimeout(?float $timeout): static
method setIdleTimeout (line 1059) | public function setIdleTimeout(?float $timeout): static
method setTty (line 1077) | public function setTty(bool $tty): static
method isTty (line 1095) | public function isTty(): bool
method setPty (line 1105) | public function setPty(bool $bool): static
method isPty (line 1115) | public function isPty(): bool
method getWorkingDirectory (line 1123) | public function getWorkingDirectory(): ?string
method setWorkingDirectory (line 1139) | public function setWorkingDirectory(string $cwd): static
method getEnv (line 1151) | public function getEnv(): array
method setEnv (line 1163) | public function setEnv(array $env): static
method getInput (line 1175) | public function getInput()
method setInput (line 1191) | public function setInput(mixed $input): static
method checkTimeout (line 1210) | public function checkTimeout(): void
method getStartTime (line 1232) | public function getStartTime(): float
method setOptions (line 1249) | public function setOptions(array $options): void
method setIgnoredSignals (line 1272) | public function setIgnoredSignals(array $signals): void
method isTtySupported (line 1284) | public static function isTtySupported(): bool
method isPtySupported (line 1294) | public static function isPtySupported(): bool
method getDescriptors (line 1312) | private function getDescriptors(bool $hasCallback): array
method buildCallback (line 1336) | protected function buildCallback(?callable $callback = null): \Closure
method updateStatus (line 1357) | protected function updateStatus(bool $blocking): void
method isSigchildEnabled (line 1382) | protected function isSigchildEnabled(): bool
method readPipesForOutput (line 1406) | private function readPipesForOutput(string $caller, bool $blocking = f...
method validateTimeout (line 1422) | private function validateTimeout(?float $timeout): ?float
method readPipes (line 1441) | private function readPipes(bool $blocking, bool $close): void
method close (line 1460) | private function close(): int
method resetProcessData (line 1491) | private function resetProcessData(): void
method doSignal (line 1517) | private function doSignal(int $signal, bool $throwException): bool
method buildShellCommandline (line 1569) | private function buildShellCommandline(string|array $commandline): string
method prepareWindowsCommandLine (line 1590) | private function prepareWindowsCommandLine(string|array $cmd, array &$...
method requireProcessIsStarted (line 1649) | private function requireProcessIsStarted(string $functionName): void
method requireProcessIsTerminated (line 1661) | private function requireProcessIsTerminated(string $functionName): void
method escapeArgument (line 1671) | private function escapeArgument(?string $argument): string
method replacePlaceholders (line 1693) | private function replacePlaceholders(string $commandline, array $env):...
method getDefaultEnv (line 1707) | private function getDefaultEnv(): array
method validateWindowsEnvBlockSize (line 1715) | private function validateWindowsEnvBlockSize(array $envPairs): void
FILE: ProcessUtils.php
class ProcessUtils (line 23) | class ProcessUtils
method __construct (line 28) | private function __construct()
method validateInput (line 40) | public static function validateInput(string $caller, mixed $input): mixed
FILE: Tests/CreateNewConsoleTest.php
class CreateNewConsoleTest (line 20) | class CreateNewConsoleTest extends TestCase
method testOptionCreateNewConsole (line 22) | public function testOptionCreateNewConsole()
method testItReturnsFastAfterStart (line 35) | public function testItReturnsFastAfterStart()
FILE: Tests/ExecutableFinderTest.php
class ExecutableFinderTest (line 22) | class ExecutableFinderTest extends TestCase
method tearDown (line 24) | protected function tearDown(): void
method testFind (line 29) | public function testFind()
method testFindWithDefault (line 43) | public function testFindWithDefault()
method testFindWithNullAsDefault (line 59) | public function testFindWithNullAsDefault()
method testFindWithExtraDirs (line 74) | public function testFindWithExtraDirs()
method testFindWithoutSuffix (line 90) | public function testFindWithoutSuffix()
method testFindWithAddedSuffixes (line 101) | public function testFindWithAddedSuffixes()
method testFindWithOpenBaseDir (line 115) | #[RunInSeparateProcess]
method testFindBatchExecutableOnWindows (line 133) | #[RunInSeparateProcess]
method testEmptyDirInPath (line 164) | #[RunInSeparateProcess]
method testFindBuiltInCommandOnWindows (line 182) | public function testFindBuiltInCommandOnWindows()
method assertSamePath (line 194) | private function assertSamePath($expected, $tested)
method getPhpBinaryName (line 203) | private function getPhpBinaryName()
FILE: Tests/Fixtures/open_basedir.php
function getPhpBinaryName (line 18) | function getPhpBinaryName(): string
FILE: Tests/Messenger/RunProcessMessageHandlerTest.php
class RunProcessMessageHandlerTest (line 19) | class RunProcessMessageHandlerTest extends TestCase
method testRunSuccessfulProcess (line 21) | public function testRunSuccessfulProcess()
method testRunFailedProcess (line 30) | public function testRunFailedProcess()
method testRunSuccessfulProcessFromShellCommandline (line 48) | public function testRunSuccessfulProcessFromShellCommandline()
method testRunFailedProcessFromShellCommandline (line 57) | public function testRunFailedProcessFromShellCommandline()
FILE: Tests/NonStopableProcess.php
function handleSignal (line 19) | function handleSignal($signal)
FILE: Tests/PhpExecutableFinderTest.php
class PhpExecutableFinderTest (line 20) | class PhpExecutableFinderTest extends TestCase
method testFind (line 25) | public function testFind()
method testFindArguments (line 39) | public function testFindArguments()
method testNotExitsBinaryFile (line 50) | public function testNotExitsBinaryFile()
method testFindWithExecutableDirectory (line 66) | public function testFindWithExecutableDirectory()
FILE: Tests/PhpProcessTest.php
class PhpProcessTest (line 19) | class PhpProcessTest extends TestCase
method testNonBlockingWorks (line 21) | public function testNonBlockingWorks()
method testCommandLine (line 33) | public function testCommandLine()
method testPassingPhpExplicitly (line 51) | public function testPassingPhpExplicitly()
method testProcessCannotBeCreatedUsingFromShellCommandLine (line 65) | public function testProcessCannotBeCreatedUsingFromShellCommandLine()
FILE: Tests/PhpSubprocessTest.php
class PhpSubprocessTest (line 19) | class PhpSubprocessTest extends TestCase
method setUpBeforeClass (line 23) | public static function setUpBeforeClass(): void
method testSubprocess (line 29) | #[DataProvider('subprocessProvider')]
method subprocessProvider (line 44) | public static function subprocessProvider(): \Generator
method getDefaultMemoryLimit (line 59) | private static function getDefaultMemoryLimit(): string
method getRandomMemoryLimit (line 64) | private static function getRandomMemoryLimit(): string
FILE: Tests/ProcessFailedExceptionTest.php
class ProcessFailedExceptionTest (line 21) | class ProcessFailedExceptionTest extends TestCase
method testProcessFailedExceptionThrowsException (line 26) | public function testProcessFailedExceptionThrowsException()
method testProcessFailedExceptionPopulatesInformationFromProcessOutput (line 43) | public function testProcessFailedExceptionPopulatesInformationFromProc...
method testDisabledOutputInFailedExceptionDoesNotPopulateOutput (line 93) | public function testDisabledOutputInFailedExceptionDoesNotPopulateOutp...
FILE: Tests/ProcessTest.php
class ProcessTest (line 32) | class ProcessTest extends TestCase
method setUpBeforeClass (line 38) | public static function setUpBeforeClass(): void
method tearDown (line 48) | protected function tearDown(): void
method testInvalidCwd (line 56) | public function testInvalidCwd()
method testInvalidCommand (line 72) | #[DataProvider('invalidProcessProvider')]
method invalidProcessProvider (line 79) | public static function invalidProcessProvider(): array
method testThatProcessDoesNotThrowWarningDuringRun (line 87) | #[Group('transient-on-windows')]
method testNegativeTimeoutFromConstructor (line 98) | public function testNegativeTimeoutFromConstructor()
method testNegativeTimeoutFromSetter (line 104) | public function testNegativeTimeoutFromSetter()
method testFloatAndNullTimeout (line 111) | public function testFloatAndNullTimeout()
method testStopWithTimeoutIsActuallyWorking (line 125) | #[RequiresPhpExtension('pcntl')]
method testWaitUntilSpecificOutput (line 147) | #[Group('transient-on-windows')]
method testWaitUntilCanReturnFalse (line 165) | public function testWaitUntilCanReturnFalse()
method testAllOutputIsActuallyReadOnTermination (line 172) | public function testAllOutputIsActuallyReadOnTermination()
method testCallbacksAreExecutedWithStart (line 203) | public function testCallbacksAreExecutedWithStart()
method testReadSupportIsDisabledWithoutCallback (line 215) | public function testReadSupportIsDisabledWithoutCallback()
method testProcessResponses (line 232) | #[DataProvider('responsesCodeProvider')]
method testProcessPipes (line 244) | #[DataProvider('pipesCodeProvider')]
method testSetStreamAsInput (line 258) | #[DataProvider('pipesCodeProvider')]
method testLiveStreamAsInput (line 278) | public function testLiveStreamAsInput()
method testSetInputWhileRunningThrowsAnException (line 296) | public function testSetInputWhileRunningThrowsAnException()
method testInvalidInput (line 313) | #[DataProvider('provideInvalidInputValues')]
method provideInvalidInputValues (line 324) | public static function provideInvalidInputValues()
method testValidInput (line 332) | #[DataProvider('provideInputValues')]
method provideInputValues (line 340) | public static function provideInputValues()
method chainedCommandsOutputProvider (line 349) | public static function chainedCommandsOutputProvider()
method testChainedCommandsOutput (line 363) | #[DataProvider('chainedCommandsOutputProvider')]
method testCallbackIsExecutedForOutput (line 371) | public function testCallbackIsExecutedForOutput()
method testCallbackIsExecutedForOutputWheneverOutputIsDisabled (line 383) | public function testCallbackIsExecutedForOutputWheneverOutputIsDisabled()
method testGetErrorOutput (line 396) | public function testGetErrorOutput()
method testFlushErrorOutput (line 404) | public function testFlushErrorOutput()
method testIncrementalOutput (line 413) | #[DataProvider('provideIncrementalOutput')]
method provideIncrementalOutput (line 439) | public static function provideIncrementalOutput()
method testGetOutput (line 447) | public function testGetOutput()
method testFlushOutput (line 455) | public function testFlushOutput()
method testZeroAsOutput (line 464) | public function testZeroAsOutput()
method testExitCodeCommandFailed (line 477) | public function testExitCodeCommandFailed()
method testTTYCommand (line 490) | public function testTTYCommand()
method testTTYCommandExitCode (line 509) | public function testTTYCommandExitCode()
method testTTYInWindowsEnvironment (line 526) | public function testTTYInWindowsEnvironment()
method testExitCodeTextIsNullWhenExitCodeIsNull (line 539) | public function testExitCodeTextIsNullWhenExitCodeIsNull()
method testStderrNotMixedWithStdout (line 545) | public function testStderrNotMixedWithStdout()
method testPTYCommand (line 559) | public function testPTYCommand()
method testMustRun (line 573) | public function testMustRun()
method testSuccessfulMustRunHasCorrectExitCode (line 581) | public function testSuccessfulMustRunHasCorrectExitCode()
method testMustRunThrowsException (line 587) | public function testMustRunThrowsException()
method testExitCodeText (line 596) | public function testExitCodeText()
method testStartIsNonBlocking (line 606) | public function testStartIsNonBlocking()
method testUpdateStatus (line 616) | public function testUpdateStatus()
method testGetExitCodeIsNullOnStart (line 623) | public function testGetExitCodeIsNullOnStart()
method testGetExitCodeIsNullOnWhenStartingAgain (line 633) | public function testGetExitCodeIsNullOnWhenStartingAgain()
method testGetExitCode (line 644) | public function testGetExitCode()
method testStatus (line 651) | public function testStatus()
method testStop (line 670) | public function testStop()
method testStopDoesNotThrowAfterBrokenPipe (line 679) | public function testStopDoesNotThrowAfterBrokenPipe()
method testIsSuccessful (line 697) | public function testIsSuccessful()
method testIsSuccessfulOnlyAfterTerminated (line 704) | public function testIsSuccessfulOnlyAfterTerminated()
method testIsNotSuccessful (line 716) | public function testIsNotSuccessful()
method testProcessIsNotSignaled (line 723) | public function testProcessIsNotSignaled()
method testProcessWithoutTermSignal (line 734) | public function testProcessWithoutTermSignal()
method testProcessIsSignaledIfStopped (line 745) | public function testProcessIsSignaledIfStopped()
method testProcessThrowsExceptionWhenExternallySignaled (line 758) | public function testProcessThrowsExceptionWhenExternallySignaled()
method testRestart (line 777) | public function testRestart()
method testRunProcessWithTimeout (line 795) | public function testRunProcessWithTimeout()
method testIterateOverProcessWithTimeout (line 813) | public function testIterateOverProcessWithTimeout()
method testCheckTimeoutOnNonStartedProcess (line 833) | public function testCheckTimeoutOnNonStartedProcess()
method testCheckTimeoutOnTerminatedProcess (line 839) | public function testCheckTimeoutOnTerminatedProcess()
method testCheckTimeoutOnStartedProcess (line 846) | public function testCheckTimeoutOnStartedProcess()
method testIdleTimeout (line 870) | public function testIdleTimeout()
method testIdleTimeoutNotExceededWhenOutputIsSent (line 887) | public function testIdleTimeoutNotExceededWhenOutputIsSent()
method testStartAfterATimeout (line 909) | public function testStartAfterATimeout()
method testGetPid (line 929) | public function testGetPid()
method testGetPidIsNullBeforeStart (line 937) | public function testGetPidIsNullBeforeStart()
method testGetPidIsNullAfterRun (line 943) | public function testGetPidIsNullAfterRun()
method testSignal (line 950) | #[RequiresPhpExtension('pcntl')]
method testExitCodeIsAvailableAfterSignal (line 965) | #[RequiresPhpExtension('pcntl')]
method testSignalProcessNotRunning (line 982) | public function testSignalProcessNotRunning()
method testMethodsThatNeedARunningProcess (line 992) | #[DataProvider('provideMethodsThatNeedARunningProcess')]
method provideMethodsThatNeedARunningProcess (line 1003) | public static function provideMethodsThatNeedARunningProcess()
method testMethodsThatNeedATerminatedProcess (line 1014) | #[DataProvider('provideMethodsThatNeedATerminatedProcess')]
method provideMethodsThatNeedATerminatedProcess (line 1032) | public static function provideMethodsThatNeedATerminatedProcess()
method testWrongSignal (line 1042) | public function testWrongSignal()
method testDisableOutputDisablesTheOutput (line 1060) | public function testDisableOutputDisablesTheOutput()
method testDisableOutputWhileRunningThrowsException (line 1070) | public function testDisableOutputWhileRunningThrowsException()
method testEnableOutputWhileRunningThrowsException (line 1081) | public function testEnableOutputWhileRunningThrowsException()
method testEnableOrDisableOutputAfterRunDoesNotThrowException (line 1093) | public function testEnableOrDisableOutputAfterRunDoesNotThrowException()
method testDisableOutputWhileIdleTimeoutIsSet (line 1103) | public function testDisableOutputWhileIdleTimeoutIsSet()
method testSetIdleTimeoutWhileOutputIsDisabled (line 1114) | public function testSetIdleTimeoutWhileOutputIsDisabled()
method testSetNullIdleTimeoutWhileOutputIsDisabled (line 1125) | public function testSetNullIdleTimeoutWhileOutputIsDisabled()
method testGetOutputWhileDisabled (line 1132) | #[DataProvider('provideOutputFetchingMethods')]
method provideOutputFetchingMethods (line 1145) | public static function provideOutputFetchingMethods()
method testStopTerminatesProcessCleanly (line 1155) | public function testStopTerminatesProcessCleanly()
method testKillSignalTerminatesProcessCleanly (line 1164) | public function testKillSignalTerminatesProcessCleanly()
method testTermSignalTerminatesProcessCleanly (line 1173) | public function testTermSignalTerminatesProcessCleanly()
method responsesCodeProvider (line 1182) | public static function responsesCodeProvider()
method pipesCodeProvider (line 1192) | public static function pipesCodeProvider()
method testIncrementalOutputDoesNotRequireAnotherCall (line 1216) | #[DataProvider('provideVariousIncrementals')]
method provideVariousIncrementals (line 1233) | public static function provideVariousIncrementals()
method testIteratorInput (line 1241) | public function testIteratorInput()
method testSimpleInputStream (line 1253) | public function testSimpleInputStream()
method testInputStreamWithCallable (line 1273) | public function testInputStreamWithCallable()
method testInputStreamWithGenerator (line 1303) | public function testInputStreamWithGenerator()
method testInputStreamOnEmpty (line 1319) | public function testInputStreamOnEmpty()
method testIteratorOutput (line 1338) | public function testIteratorOutput()
method testNonBlockingNorClearingIteratorOutput (line 1374) | public function testNonBlockingNorClearingIteratorOutput()
method testChainedProcesses (line 1410) | public function testChainedProcesses()
method testSetBadEnv (line 1425) | public function testSetBadEnv()
method testEnvBackupDoesNotDeleteExistingVars (line 1436) | public function testEnvBackupDoesNotDeleteExistingVars()
method testEnvIsInherited (line 1453) | public function testEnvIsInherited()
method testGetCommandLine (line 1471) | public function testGetCommandLine()
method testEscapeArgument (line 1484) | #[DataProvider('provideEscapeArgument')]
method testRawCommandLine (line 1493) | public function testRawCommandLine()
method provideEscapeArgument (line 1502) | public static function provideEscapeArgument()
method testMsysEscapingOnWindows (line 1516) | public function testMsysEscapingOnWindows()
method testPreparedCommand (line 1536) | public function testPreparedCommand()
method testPreparedCommandMulti (line 1544) | public function testPreparedCommandMulti()
method testPreparedCommandWithQuoteInIt (line 1552) | public function testPreparedCommandWithQuoteInIt()
method testPreparedCommandWithMissingValue (line 1560) | public function testPreparedCommandWithMissingValue()
method testPreparedCommandWithNoValues (line 1570) | public function testPreparedCommandWithNoValues()
method testEnvArgument (line 1580) | public function testEnvArgument()
method testEnvVarNamesCastToString (line 1594) | public function testEnvVarNamesCastToString()
method testEnvVarNamesWithEqualsSigns (line 1604) | public function testEnvVarNamesWithEqualsSigns()
method testEnvVarNamesWithNullBytes (line 1614) | public function testEnvVarNamesWithNullBytes()
method testEnvVarNamesEmpty (line 1624) | public function testEnvVarNamesEmpty()
method testWaitStoppedDeadProcess (line 1634) | public function testWaitStoppedDeadProcess()
method testEnvCaseInsensitiveOnWindows (line 1647) | public function testEnvCaseInsensitiveOnWindows()
method testMultipleCallsToProcGetStatus (line 1659) | public function testMultipleCallsToProcGetStatus()
method testFailingProcessWithMultipleCallsToProcGetStatus (line 1669) | public function testFailingProcessWithMultipleCallsToProcGetStatus()
method testLongRunningProcessWithMultipleCallsToProcGetStatus (line 1679) | #[Group('slow')]
method testLongRunningProcessWithMultipleCallsToProcGetStatusError (line 1690) | #[Group('slow')]
method testNotTerminableInputPipe (line 1701) | #[Group('transient-on-windows')]
method testIgnoringSignal (line 1712) | public function testIgnoringSignal()
method testNotIgnoringSignal (line 1728) | public function testNotIgnoringSignal()
method testPathResolutionOnWindows (line 1742) | public function testPathResolutionOnWindows()
method getProcess (line 1753) | private function getProcess(string|array $commandline, ?string $cwd = ...
method getProcessForCode (line 1766) | private function getProcessForCode(string $code, ?string $cwd = null, ...
class NonStringifiable (line 1772) | class NonStringifiable
FILE: Tests/ProcessWindowsEnvBlockTest.php
class ProcessWindowsEnvBlockTest (line 19) | class ProcessWindowsEnvBlockTest extends TestCase
method setUpBeforeClass (line 23) | public static function setUpBeforeClass(): void
method testStartThrowsWhenSingleEnvValueExceedsWindowsLimit (line 29) | public function testStartThrowsWhenSingleEnvValueExceedsWindowsLimit()
method testStartThrowsWhenMultipleEntriesCollectivelyExceedWindowsLimit (line 42) | public function testStartThrowsWhenMultipleEntriesCollectivelyExceedWi...
method testStartDoesNotThrowForSmallEnv (line 59) | public function testStartDoesNotThrowForSmallEnv()
method testStartDoesNotThrowForEmptyEnv (line 68) | public function testStartDoesNotThrowForEmptyEnv()
method testStartDoesNotThrowForNullEnv (line 77) | public function testStartDoesNotThrowForNullEnv()
method testStartDoesNotThrowWhenEnvBlockIsExactlyAtWindowsLimit (line 86) | public function testStartDoesNotThrowWhenEnvBlockIsExactlyAtWindowsLim...
method testStartDoesNotThrowWhenFalseEnvValuesExceedLimit (line 100) | public function testStartDoesNotThrowWhenFalseEnvValuesExceedLimit()
method testWindowsEnvBlockValidationThrowsViaReflection (line 114) | public function testWindowsEnvBlockValidationThrowsViaReflection()
method testWindowsEnvBlockValidationPassesAtExactLimitViaReflection (line 124) | public function testWindowsEnvBlockValidationPassesAtExactLimitViaRefl...
method testWindowsEnvBlockValidationCountsMultibyteInCodeUnitsViaReflection (line 134) | public function testWindowsEnvBlockValidationCountsMultibyteInCodeUnit...
method testWindowsEnvBlockValidationCountsSupplementaryCharsAsTwoCodeUnitsViaReflection (line 145) | public function testWindowsEnvBlockValidationCountsSupplementaryCharsA...
method testWindowsEnvBlockValidationThrowsWhenSupplementaryCharsPushOverLimitViaReflection (line 156) | public function testWindowsEnvBlockValidationThrowsWhenSupplementaryCh...
method getProcess (line 167) | private function getProcess(array $command, ?string $cwd = null, ?arra...
Condensed preview — 52 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (215K chars).
[
{
"path": ".gitattributes",
"chars": 74,
"preview": "/Tests export-ignore\n/phpunit.xml.dist export-ignore\n/.git* export-ignore\n"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 278,
"preview": "Please do not submit any Pull Requests here. They will be closed.\n---\n\nPlease submit your PR here instead:\nhttps://githu"
},
{
"path": ".github/workflows/close-pull-request.yml",
"chars": 544,
"preview": "name: Close Pull Request\n\non:\n pull_request_target:\n types: [opened]\n\njobs:\n run:\n runs-on: ubuntu-latest\n st"
},
{
"path": ".gitignore",
"chars": 34,
"preview": "vendor/\ncomposer.lock\nphpunit.xml\n"
},
{
"path": "CHANGELOG.md",
"chars": 4446,
"preview": "CHANGELOG\n=========\n\n\n7.3\n---\n\n * Add `RunProcessMessage::fromShellCommandline()` to instantiate a Process via the fromS"
},
{
"path": "Exception/ExceptionInterface.php",
"chars": 450,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Exception/InvalidArgumentException.php",
"chars": 496,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Exception/LogicException.php",
"chars": 466,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Exception/ProcessFailedException.php",
"chars": 1428,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Exception/ProcessSignaledException.php",
"chars": 906,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Exception/ProcessStartFailedException.php",
"chars": 1148,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Exception/ProcessTimedOutException.php",
"chars": 1554,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Exception/RunProcessFailedException.php",
"chars": 666,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Exception/RuntimeException.php",
"chars": 481,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "ExecutableFinder.php",
"chars": 3575,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "InputStream.php",
"chars": 2385,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "LICENSE",
"chars": 1068,
"preview": "Copyright (c) 2004-present Fabien Potencier\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "Messenger/RunProcessContext.php",
"chars": 936,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Messenger/RunProcessMessage.php",
"chars": 1296,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Messenger/RunProcessMessageHandler.php",
"chars": 1209,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "PhpExecutableFinder.php",
"chars": 2474,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "PhpProcess.php",
"chars": 2615,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "PhpSubprocess.php",
"chars": 6205,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Pipes/AbstractPipes.php",
"chars": 6113,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Pipes/PipesInterface.php",
"chars": 1502,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Pipes/UnixPipes.php",
"chars": 3739,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Pipes/WindowsPipes.php",
"chars": 5849,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Process.php",
"chars": 58054,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "ProcessUtils.php",
"chars": 1752,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "README.md",
"chars": 472,
"preview": "Process Component\n=================\n\nThe Process component executes commands in sub-processes.\n\nResources\n---------\n\n * "
},
{
"path": "Tests/CreateNewConsoleTest.php",
"chars": 1357,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/ErrorProcessInitiator.php",
"chars": 1039,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/ExecutableFinderTest.php",
"chars": 5867,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Fixtures/executable_with_added_suffix.foo",
"chars": 87,
"preview": "See \\Symfony\\Component\\Process\\Tests\\ExecutableFinderTest::testFindWithAddedSuffixes()\n"
},
{
"path": "Tests/Fixtures/executable_without_suffix",
"chars": 83,
"preview": "See \\Symfony\\Component\\Process\\Tests\\ExecutableFinderTest::testFindWithoutSuffix()\n"
},
{
"path": "Tests/Fixtures/memory.php",
"chars": 37,
"preview": "<?php\n\necho ini_get('memory_limit');\n"
},
{
"path": "Tests/Fixtures/open_basedir.php",
"chars": 553,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/KillableProcessWithOutput.php",
"chars": 512,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Messenger/RunProcessMessageHandlerTest.php",
"chars": 2540,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/NonStopableProcess.php",
"chars": 952,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/OutputMemoryLimitProcess.php",
"chars": 828,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/PhpExecutableFinderTest.php",
"chars": 2577,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/PhpProcessTest.php",
"chars": 2361,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/PhpSubprocessTest.php",
"chars": 2185,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/PipeStdinInStdoutStdErrStreamSelect.php",
"chars": 1755,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/ProcessFailedExceptionTest.php",
"chars": 4667,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/ProcessTest.php",
"chars": 55890,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/ProcessWindowsEnvBlockTest.php",
"chars": 6510,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/SignalListener.php",
"chars": 414,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/ThreeSecondProcess.php",
"chars": 291,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "composer.json",
"chars": 671,
"preview": "{\n \"name\": \"symfony/process\",\n \"type\": \"library\",\n \"description\": \"Executes commands in sub-processes\",\n \"ke"
},
{
"path": "phpunit.xml.dist",
"chars": 995,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:noNa"
}
]
About this extraction
This page contains the full source code of the symfony/process GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 52 files (199.6 KB), approximately 50.0k tokens, and a symbol index with 369 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.