我正在尝试创建可重用的通用cli服务器,我可以从终端会话控制(启动/暂停/恢复/停止)。
到目前为止,我的方法是,我有一个脚本独立地充当控制台(父循环)和服务器(子循环),而不是pcntl_fork()
- 但是proc_open()
- 可以这么说,就像儿童过程一样。
然后,控制台循环通过posix_kill()
发送信号来对服务器循环起作用。
现在忽略这是否是一种明智的做法,我偶然发现了一些奇怪的事情 - 即,当控制台循环使用SIGTSTP
信号暂停服务器循环时,服务器循环将不会响应{ {1}}信号,除非它的SIGCONT
- 循环实际上正在做一些有用的事情。
这可能会发生什么?
修改
根据评论中的要求,我简化了我的代码示例。但是,正如我已经担心的那样,这个代码工作正常。
也许我在课程中忽略了代码中的某些内容,但我不知道两个例子在例行程序中有何不同 - 对我来说,看起来两个例子都遵循相同的例程。
作为一个重要的旁注:在我更复杂的例子中,我已经尝试不断写入while
中的文件,这实际上有效,即使暂停也是如此。所以,这告诉我循环继续正常运行。在我暂停信号后,服务器只是不想再响应信号。
无论如何,这是我之前的例子的简化版本,我已在下面显示:
loop()
在控制台中运行示例(上方和下方)时,请输入$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's 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 );
}
,然后输入pause<enter>
。预期的行为是,在恢复之后,您将再次看到(除此之外)此流:
resume<enter>
/修改
这是我使用的:
控制台和服务器都是我的抽象Server tick.
Server tick.
Server tick.
类的实例:
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();
}
的非常基本的抽象控制台类:
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";
}
}
内,您会发现它响应从终端输入的命令,并将信号发送到子进程(实际服务器)。
ServerConsole::tick()
这是服务器实现。这是奇怪的行为发生的地方。如果不覆盖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;
}
}
}
}
挂钩,一旦暂停,它将不再响应信号。所以,如果我删除钩子,LoopedProcess::onLoopStart()
实际上就没有任何重要性。
LoopedProcess::loop()
以下是将所有内容联系在一起的脚本:
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;
}
}
所以,总结一下:
当$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 );
}
暂停并在Server
内有效地执行任何重要操作时,因为我没有实现任何输出的钩子,它对新信号没有反应。但是,当实现钩子 时,它会按预期响应信号。
这可能会发生什么?
答案 0 :(得分:2)
我已经通过在PHP文档网站上按pcntl_signal_dispatch()
1 在loop()
内添加对this comment的调用来实现它,如下所示:
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 );
}
}
我的简化示例脚本确实不需要这个。所以我仍然有兴趣知道在什么情况下有必要致电pcntl_signal_dispatch()
以及背后的原因,如果有人对此有任何见解。
1)评论目前隐藏在网站的标题后面,因此您可能需要向上滚动一点。