Skip to content

Commit

Permalink
[Process] Fix stopping a process on Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed Dec 5, 2015
1 parent d9b8d0c commit 80fb51c
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 24 deletions.
14 changes: 11 additions & 3 deletions src/Symfony/Component/Process/Process.php
Expand Up @@ -157,8 +157,16 @@ public function __construct($commandline, $cwd = null, array $env = null, $stdin

public function __destruct()
{
// stop() will check if we have a process running.
$this->stop();
if ($this->isRunning()) {
$this->doSignal(15, false);
usleep(10000);
}
if ($this->isRunning()) {
usleep(100000);
$this->doSignal(9, false);
}

// Don't call ->stop() nor ->close() since we don't want to wait for the subprocess here
}

public function __clone()
Expand Down Expand Up @@ -1190,7 +1198,7 @@ private function doSignal($signal, $throwException)

if ('\\' === DIRECTORY_SEPARATOR) {
exec(sprintf('taskkill /F /T /PID %d 2>&1', $this->getPid()), $output, $exitCode);
if ($exitCode) {
if ($exitCode && $this->isRunning()) {
if ($throwException) {
throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output)));
}
Expand Down
48 changes: 35 additions & 13 deletions src/Symfony/Component/Process/Tests/AbstractProcessTest.php
Expand Up @@ -167,6 +167,10 @@ public function testProcessPipes($code, $size)
$this->assertEquals($expectedLength, strlen($p->getErrorOutput()));
}

/**
* @expectedException Symfony\Component\Process\Exception\LogicException
* @expectedExceptionMessage STDIN can not be set while the process is running.
*/
public function testSetStdinWhileRunningThrowsAnException()
{
$process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"');
Expand All @@ -176,9 +180,10 @@ public function testSetStdinWhileRunningThrowsAnException()
$process->stop();
$this->fail('A LogicException should have been raised.');
} catch (LogicException $e) {
$this->assertEquals('STDIN can not be set while the process is running.', $e->getMessage());
}
$process->stop();

throw $e;
}

/**
Expand Down Expand Up @@ -659,6 +664,10 @@ public function testRestart()
$this->assertNotEquals($process1->getOutput(), $process2->getOutput());
}

/**
* @expectedException Symfony\Component\Process\Exception\RuntimeException
* @expectedExceptionMessage The process timed-out.
*/
public function testRunProcessWithTimeout()
{
$timeout = 0.5;
Expand All @@ -672,14 +681,13 @@ public function testRunProcessWithTimeout()
}
$duration = microtime(true) - $start;

if ('\\' === DIRECTORY_SEPARATOR) {
// Windows is a bit slower as it read file handles, then allow twice the precision
$maxDuration = $timeout + 2 * Process::TIMEOUT_PRECISION;
} else {
if ('\\' !== DIRECTORY_SEPARATOR) {
// On Windows, timers are too transient
$maxDuration = $timeout + Process::TIMEOUT_PRECISION;
$this->assertLessThan($maxDuration, $duration);
}

$this->assertLessThan($maxDuration, $duration);
throw $e;
}

public function testCheckTimeoutOnNonStartedProcess()
Expand All @@ -695,6 +703,10 @@ public function testCheckTimeoutOnTerminatedProcess()
$process->checkTimeout();
}

/**
* @expectedException Symfony\Component\Process\Exception\RuntimeException
* @expectedExceptionMessage The process timed-out.
*/
public function testCheckTimeoutOnStartedProcess()
{
$timeout = 0.5;
Expand All @@ -717,8 +729,14 @@ public function testCheckTimeoutOnStartedProcess()

$this->assertLessThan($timeout + $precision, $duration);
$this->assertFalse($process->isSuccessful());

throw $e;
}

/**
* @expectedException Symfony\Component\Process\Exception\RuntimeException
* @expectedExceptionMessage The process timed-out.
*/
public function testStartAfterATimeout()
{
$process = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 1000; while ($n--) {echo \'\'; usleep(1000); }')));
Expand All @@ -731,6 +749,8 @@ public function testStartAfterATimeout()
$process->start();
usleep(10000);
$process->stop();

throw $e;
}

public function testGetPid()
Expand Down Expand Up @@ -760,14 +780,14 @@ public function testSignal()
$this->markTestSkipped('Extension pcntl is required.');
}

$process = $this->getProcess('exec php -f '.__DIR__.'/SignalListener.php');
$process = $this->getProcess('exec '.self::$phpBin.' '.__DIR__.'/SignalListener.php');
$process->start();
usleep(500000);
$process->signal(SIGUSR1);

while ($process->isRunning() && false === strpos($process->getOutput(), 'Caught SIGUSR1')) {
usleep(10000);
while (false === strpos($process->getOutput(), 'Caught')) {
usleep(1000);
}
$process->signal(SIGUSR1);
$process->wait();

$this->assertEquals('Caught SIGUSR1', $process->getOutput());
}
Expand Down Expand Up @@ -828,6 +848,8 @@ public function provideMethodsThatNeedARunningProcess()

/**
* @dataProvider provideMethodsThatNeedATerminatedProcess
* @expectedException Symfony\Component\Process\Exception\LogicException
* @expectedExceptionMessage Process must be terminated before calling
*/
public function testMethodsThatNeedATerminatedProcess($method)
{
Expand All @@ -838,10 +860,10 @@ public function testMethodsThatNeedATerminatedProcess($method)
$process->stop(0);
$this->fail('A LogicException must have been thrown');
} catch (\Exception $e) {
$this->assertInstanceOf('Symfony\Component\Process\Exception\LogicException', $e);
$this->assertEquals(sprintf('Process must be terminated before calling %s.', $method), $e->getMessage());
}
$process->stop(0);

throw $e;
}

public function provideMethodsThatNeedATerminatedProcess()
Expand Down
Expand Up @@ -112,6 +112,10 @@ public function testExitCodeIsAvailableAfterSignal()
$this->markTestSkipped('Signal is not supported in sigchild environment');
}

/**
* @expectedException Symfony\Component\Process\Exception\RuntimeException
* @expectedExceptionMessage The process timed-out.
*/
public function testStartAfterATimeout()
{
if ('\\' === DIRECTORY_SEPARATOR) {
Expand Down
12 changes: 4 additions & 8 deletions src/Symfony/Component/Process/Tests/SignalListener.php
Expand Up @@ -9,17 +9,13 @@
* file that was distributed with this source code.
*/

// required for signal handling
declare (ticks = 1);
pcntl_signal(SIGUSR1, function () {echo 'SIGUSR1'; exit;});

pcntl_signal(SIGUSR1, function () {echo 'Caught SIGUSR1'; exit;});
echo 'Caught ';

$n = 0;

// ticks require activity to work - sleep(4); does not work
while ($n < 400) {
while ($n++ < 400) {
usleep(10000);
++$n;
pcntl_signal_dispatch();
}

return;

0 comments on commit 80fb51c

Please sign in to comment.