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?

+3


source to share


1 answer


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.

+1


source







All Articles