Script does not respond to signals when its infinite while loop does nothing
I am experimenting with making a reusable generic cli server that I can control (start / pause / resume / stop) from a terminal session.
So far, my approach is that I have a single script acting independently on both the console (parent loop) and server (child loop), not pcntl_fork()
-ing but proc_open()
-ing as a child process, so to speak.
The console loop then acts on the server loop, signaling it with posix_kill()
.
Ignoring, for now, whether this is a sane approach, I stumbled upon something strange: namely, when the outline loop pauses the server loop with a signal SIGTSTP
, the server loop will not respond to the SIGCONT
signal unless its while
-loop actually does something useful.
What can be done here?
change
As requested in the comments, I've simplified my example code. However, as I feared, this code works very well.
I may be missing something in the code with classes, but I just can't see how both examples differ in their subroutine - to me it looks like both examples follow the same procedure.
And as an important side of a note: in my more complex example, I was constantly trying to write a file to loop()
, which actually works, even when paused. So this tells me that the loop continues to run correctly. The server just just doesn't want to respond to signals anymore after I paused it.
Anyway, here's a simplified version of my previous example I showed below:
$lockPath = '.lock';
if( file_exists( $lockPath ) ) {
echo 'Process already running; exiting...' . PHP_EOL;
exit( 1 );
}
else if( $argc == 2 && 'child' == $argv[ 1 ] ) {
/* child process */
if( false === ( $lock = fopen( $lockPath, 'x' ) ) ) {
echo 'Unable to acquire lock; exiting...' . PHP_EOL;
exit( 1 );
}
else if( false !== flock( $lock, LOCK_EX ) ) {
echo 'Process started...' . PHP_EOL;
$state = 1;
declare( ticks = 1 );
pcntl_signal( SIGTSTP, function( $signo ) use ( &$state ) {
echo 'pcntl_signal SIGTSTP' . PHP_EOL;
$state = 0;
} );
pcntl_signal( SIGCONT, function( $signo ) use ( &$state ) {
echo 'pcntl_signal SIGCONT' . PHP_EOL;
$state = 1;
} );
pcntl_signal( SIGTERM, function( $signo ) use ( &$state ) {
echo 'pcntl_signal SIGTERM' . PHP_EOL;
$state = -1;
} );
while( $state !== -1 ) {
/**
* It doesn't matter whether I leave the first echo out
* and/or whether I put either echo in functions,
* Any combination simply works as expected here
*/
echo 'Server state: ' . $state . PHP_EOL;
if( $state !== 0 ) {
echo 'Server tick.' . PHP_EOL;
}
usleep( 1000000 );
}
flock( $lock, LOCK_UN ) && fclose( $lock ) && unlink( $lockPath );
echo 'Process ended; unlocked, closed and deleted lock file; exiting...' . PHP_EOL;
exit( 0 );
}
}
else {
/* parent process */
function consoleRead() {
$fd = STDIN;
$read = array( $fd );
$write = array();
$except = array();
$result = stream_select( $read, $write, $except, 0 );
if( $result === false ) {
throw new RuntimeException( 'stream_select() failed' );
}
if( $result === 0 ) {
return false;
}
return stream_get_line( $fd, 1024, PHP_EOL );
}
$decriptors = array(
0 => STDIN,
1 => STDOUT,
2 => STDERR
);
$childProcess = proc_open( sprintf( 'exec %s child', __FILE__ ), $decriptors, $pipes );
while( 1 ) {
$childStatus = proc_get_status( $childProcess );
$childPid = $childStatus[ 'pid' ];
if( false !== ( $command = consoleRead() ) ) {
switch( $command ) {
case 'status':
var_export( $childStatus );
break;
case 'run':
case 'start':
// nothing?
break;
case 'pause':
case 'suspend':
// SIGTSTP
if( false !== $childPid ) {
posix_kill( $childPid, SIGTSTP );
}
break;
case 'resume':
case 'continue':
// SIGCONT
if( false !== $childPid ) {
posix_kill( $childPid, SIGCONT );
}
break;
case 'halt':
case 'quit':
case 'stop':
// SIGTERM
if( false !== $childPid ) {
posix_kill( $childPid, SIGTERM );
}
break;
}
}
usleep( 1000000 );
}
exit( 0 );
}
When running any example (top and bottom) in the console, type pause<enter>
and then resume<enter>
. The expected behavior is that after resuming this thread, you will see this thread again (among other things):
Server tick.
Server tick.
Server tick.
/ change
This is what I am using:
Both the console and the server are instances of my abstract class LoopedProcess
:
abstract class LoopedProcess
{
const STOPPED = -1;
const PAUSED = 0;
const RUNNING = 1;
private $state = self::STOPPED;
private $throttle = 50;
final protected function getState() {
return $this->state;
}
final public function isStopped() {
return self::STOPPED === $this->getState();
}
final public function isPaused() {
return self::PAUSED === $this->getState();
}
final public function isRunning() {
return self::RUNNING === $this->getState();
}
protected function onBeforeRun() {}
protected function onRun() {}
final public function run() {
if( $this->isStopped() && false !== $this->onBeforeRun() ) {
$this->state = self::RUNNING;
$this->onRun();
$this->loop();
}
}
protected function onBeforePause() {}
protected function onPause() {}
final public function pause() {
if( $this->isRunning() && false !== $this->onBeforePause() ) {
$this->state = self::PAUSED;
$this->onPause();
}
}
protected function onBeforeResume() {}
protected function onResume() {}
final public function resume() {
if( $this->isPaused() && false !== $this->onBeforeResume() ) {
$this->state = self::RUNNING;
$this->onResume();
}
}
protected function onBeforeStop() {}
protected function onStop() {}
final public function stop() {
if( !$this->isStopped() && false !== $this->onBeforeStop() ) {
$this->state = self::STOPPED;
$this->onStop();
}
}
final protected function setThrottle( $throttle ) {
$this->throttle = (int) $throttle;
}
protected function onLoopStart() {}
protected function onLoopEnd() {}
final private function loop() {
while( !$this->isStopped() ) {
$this->onLoopStart();
if( !$this->isPaused() ) {
$this->tick();
}
$this->onLoopEnd();
usleep( $this->throttle );
}
}
abstract protected function tick();
}
Here's a very rudimentary abstract console class based on LoopedProcess
:
abstract class Console
extends LoopedProcess
{
public function __construct() {
$this->setThrottle( 1000000 ); // 1 sec
}
public function consoleRead() {
$fd = STDIN;
$read = array( $fd );
$write = array();
$except = array();
$result = stream_select( $read, $write, $except, 0 );
if( $result === false ) {
throw new RuntimeException( 'stream_select() failed' );
}
if( $result === 0 ) {
return false;
}
return stream_get_line( $fd, 1024, PHP_EOL );
}
public function consoleWrite( $data ) {
echo "\r$data\n";
}
}
The following server console extends the above abstract console class. Internally, ServerConsole::tick()
you will find that it is responding to commands entered from the terminal and sending signals to the child process (the actual server).
class ServerConsole
extends Console
{
private $childProcess;
private $childProcessId;
public function __construct() {
declare( ticks = 1 );
$self = $this;
pcntl_signal( SIGINT, function( $signo ) use ( $self ) {
$self->consoleWrite( 'Console received SIGINT' );
$self->stop();
} );
parent::__construct();
}
protected function onBeforeRun() {
$decriptors = array( /*
0 => STDIN,
1 => STDOUT,
2 => STDERR
*/ );
$this->childProcess = proc_open( sprintf( 'exec %s child', __FILE__ ), $decriptors, $pipes );
if( !is_resource( $this->childProcess ) ) {
$this->consoleWrite( 'Unable to create child process; exiting...' );
return false;
}
else {
$this->consoleWrite( 'Child process created...' );
}
}
protected function onStop() {
$this->consoleWrite( 'Parent process ended; exiting...' );
$childPid = proc_get_status( $this->childProcess )[ 'pid' ];
if( false !== $childPid ) {
posix_kill( $childPid, SIGTERM );
}
}
protected function tick() {
$childStatus = proc_get_status( $this->childProcess );
$childPid = $childStatus[ 'pid' ];
if( false !== ( $command = $this->consoleRead() ) ) {
var_dump( $childPid, $command );
switch( $command ) {
case 'run':
case 'start':
// nothing, for now
break;
case 'pause':
case 'suspend':
// SIGTSTP
if( false !== $childPid ) {
posix_kill( $childPid, SIGTSTP );
}
break;
case 'resume':
case 'continue':
// SIGCONT
if( false !== $childPid ) {
posix_kill( $childPid, SIGCONT );
}
break;
case 'halt':
case 'quit':
case 'stop':
// SIGTERM
if( false !== $childPid ) {
posix_kill( $childPid, SIGTERM );
}
break;
}
}
}
}
And here is the server implementation. Weird behavior is happening here. If you don't override the hook LoopedProcess::onLoopStart()
, it will no longer respond to signals as soon as it pauses. So, if I remove the hook, LoopedProcess::loop()
it doesn't actually do anything important.
class Server
extends LoopedProcess
{
public function __construct() {
declare( ticks = 1 );
$self = $this;
// install the signal handlers
pcntl_signal( SIGTSTP, function( $signo ) use ( $self ) {
echo 'pcntl_signal SIGTSTP' . PHP_EOL;
$self->pause();
} );
pcntl_signal( SIGCONT, function( $signo ) use ( $self ) {
echo 'pcntl_signal SIGCONT' . PHP_EOL;
$self->resume();
} );
pcntl_signal( SIGTERM, function( $signo ) use ( $self ) {
echo 'pcntl_signal SIGTERM' . PHP_EOL;
$self->stop();
} );
$this->setThrottle( 2000000 ); // 2 sec
}
protected function tick() {
echo 'Server tick.' . PHP_EOL;
}
protected function onBeforePause() {
echo 'Server pausing.' . PHP_EOL;
}
protected function onPause() {
echo 'Server paused.' . PHP_EOL;
}
protected function onBeforeResume() {
echo 'Server resuming.' . PHP_EOL;
}
protected function onResume() {
echo 'Server resumed.' . PHP_EOL;
}
/**
* if I remove this hook, Server becomes unresponsive
* to signals, after it has been paused
*/
protected function onLoopStart() {
echo 'Server state: ' . ( $this->getState() ) . PHP_EOL;
}
}
And here's a script that ties it all together:
$lockPath = '.lock';
if( file_exists( $lockPath ) ) {
echo 'Process already running; exiting...' . PHP_EOL;
exit( 1 );
}
else if( $argc == 2 && 'child' == $argv[ 1 ] ) {
/* child process */
if( false === ( $lock = fopen( $lockPath, 'x' ) ) ) {
echo 'Unable to acquire lock; exiting...' . PHP_EOL;
exit( 1 );
}
else if( false !== flock( $lock, LOCK_EX ) ) {
echo 'Process started...' . PHP_EOL;
$server = new Server();
$server->run();
flock( $lock, LOCK_UN ) && fclose( $lock ) && unlink( $lockPath );
echo 'Process ended; unlocked, closed and deleted lock file; exiting...' . PHP_EOL;
exit( 0 );
}
}
else {
/* parent process */
$console = new ServerConsole();
$console->run();
exit( 0 );
}
So, let's summarize:
When Server
paused and actually doesn't do anything important internally loop()
because I don't have an intercept that outputs anything, it becomes immune to new signals. However, when the hook is implemented, it responds to signals as expected.
What can be done here?
source to share
I got it to work by adding a call pcntl_signal_dispatch()
inside loop()
, as per this comment 1 on the PHP documentation website, for example:
final private function loop() {
while( !$this->isStopped() ) {
$this->onLoopStart();
if( !$this->isPaused() ) {
$this->tick();
}
$this->onLoopEnd();
pcntl_signal_dispatch(); // adding this worked
// (I actually need to put it in onLoopEnd() though, this was just a temporary hack)
usleep( $this->throttle );
}
}
However, my simplified example script doesn't need this. Therefore, I would still be interested to know in what cases it is necessary to name pcntl_signal_dispatch()
and the reason for this, if anyone has any ideas.
1) The comment is currently hidden behind the site title, so you may need to scroll down a bit.
source to share