PHP CLI 中 STDIN 的非阻塞实现

21
有没有一种方式可以使用 PHP 从 STDIN 中进行非阻塞读取:
我尝试过这个方法:
stream_set_blocking(STDIN, false);
echo fread(STDIN, 1);

还有这个:

$stdin = fopen('php://stdin', 'r');
stream_set_blocking($stdin, false);
echo 'Press enter to force run command...' . PHP_EOL;
echo fread($stdin, 1);

但它仍会一直阻塞,直到fread获得一些数据。

我注意到有几个关于此问题的未解决的bug报告(已有7年历史)。如果无法解决此问题,是否有人知道任何可以实现此功能的简单方法(适用于Windows和Linux)?


3
我不确定我理解非阻塞行为的含义。如果没有等待输入进入stdin,那么fread()应该返回FALSE吗?它如何区分这一点与EOF?在我看来,您需要某种除fread()之外的测试*来确定stdin是否有等待数据,因为没有明确的失败信息。 - Graham
@Graham fread函数在没有从STDIN接收到输入之前,即暂停脚本的执行。基本上我想要的是检查是否有用户输入到STDIN,如果没有,则继续执行,否则运行其他一些操作。 - Petah
@Graham "Non blocking" 的意思是,即使 fread() 无法读取应该读取的数据量(第二个参数),但它仍应立即返回,但读取的数据量少于此。在这种情况下,它应返回一个空字符串。@Petah 你尝试过 fopen('php://stdin) 吗?我模糊地记得我之前遇到过 STDIN 的问题。 - KingCrunch
@KingCrunch,我刚试了一下(根据我的更新问题),但是结果还是一样的。 - Petah
所以... stream_set_blocking(STDIN, false) 应该发生的行为是,fread() 不会等待,并且如果 if (strlen(fread($stdin,1)) == 0),那么没有任何等待输入? - Graham
显示剩余9条评论
6个回答

16

这是我能想到的代码。在Linux中它运行良好,但在Windows上,只要我按下一个键,输入就会被缓冲直到按回车键。我不知道如何在流上禁用缓冲。

<?php

function non_block_read($fd, &$data) {
    $read = array($fd);
    $write = array();
    $except = array();
    $result = stream_select($read, $write, $except, 0);
    if($result === false) throw new Exception('stream_select failed');
    if($result === 0) return false;
    $data = stream_get_line($fd, 1);
    return true;
}

while(1) {
    $x = "";
    if(non_block_read(STDIN, $x)) {
        echo "Input: " . $x . "\n";
        // handle your input here
    } else {
        echo ".";
        // perform your processing here
    }
}

?>

刚刚偶然看到这个问题,如果你正在使用stream_set_read_buffer(STDIN, 0);并且像你已经提到的那样,stream_set_blocking(STDIN, false);可能会起作用。第一个函数应该将stdin设置为无缓冲读取,第二个函数将其设置为非阻塞,这与无缓冲有些不同。 - John Doe
这对我不起作用...它一直让我输入才能继续。 - Loenix
在Windows上它并不完美,参见:https://stackoverflow.com/questions/21464457/why-stream-select-on-stdin-becomes-blocking-when-cmd-exe-loses-focus - gouchaoer
更进一步...如果你使用system('stty cbreak -echo');而不是普通的system('stty cbreak');,你就不会得到按键的回显。 - Xavi Montero
当你把数据通过管道传输到脚本时,如何判断stdin已经结束? - Petah
显示剩余4条评论

5

只是一个提醒,非阻塞的 STDIN 现在可以工作了。


stream_set_blocking(STDIN, 0); 确实有效。 - Tofandel

2
system('stty cbreak');
while(true){
    if($char = fread(STDIN, 1)) {
        echo chr(8) . mb_strtoupper($char);
    }
}

我不知道为什么这个有效,但是对我来说绝对有效!(Ubuntu Linux 18.04 GNU Bash 4.4)谢谢 - hanshenrik
这个解决方案需要与Martin的解决方案结合使用,否则在“等待字符”时将没有“处理循环”。我的意思是:这可以立即起作用,但只响应按键。Martin的允许在没有按键按下时进行处理。对于这个stty技巧来说是一个好点。 - Xavi Montero

1
Petah,我无法直接帮助你处理PHP方面的问题,但是我可以向你推荐一篇文章,其中有人通过在shell脚本中测试命名管道中是否存在待处理数据来模拟晶体管。这是一篇非常有趣的阅读材料,将shell脚本提升到了一个全新的极客水平。 :-)
文章链接在这里:http://www.linusakesson.net/programming/pipelogic/ 所以...回答你的“粗糙的hack”请求,我想你可以通过命名管道将stdio传输,然后使用上述URL中包含源代码的工具进行exec(),以测试是否有任何内容等待通过管道发送。你可能需要开发一些包装函数来帮助处理相关事项。
我怀疑pipelogic解决方案仅适用于Linux,或者至少需要类Unix操作系统。不知道如何在Windows上实现这一点。

1
抱歉,我真的不知道这如何解决我的问题。 - Petah
@Petah - 从管道读取的非阻塞fread()可以通过一个函数复制,该函数只会在检查到管道上有任何数据等待时运行阻塞fread()。我在发布的链接中提供的工具可以让您测试这个条件。我相当确定这种方法在Windows中不起作用,因为缺少命名管道,所以我不会进一步处理它,因为Windows兼容性是您问题的一个条件。 - ghoti

0
使用马丁的示例,并添加了用户3684669的代码,稍作修改。这是一个小程序,显示一个时钟并每秒更新一次。每当按下一个键时,我将其回显到屏幕上。我还隐藏了字符默认回显到屏幕上的功能。在Linux中运行正常。
<?php
//-- Save the keyboard state
//-- then use cbreak and turn off echo
$stty_orig = '';
function init_keyboard() {
  global $stty_orig;

  $stty_orig = shell_exec("stty -g");
  system('stty cbreak -echo');
}

//-- Return keyboard state
function close_keyboard() {
  global $stty_orig;

  system('stty '.$stty_orig);
}

//-- clear the screen
function clear() {
  echo "\x1B[2J\x1B[0m";
}

//-- move cursor to x,y on the screen
function gotoxy($x, $y) {
  echo "\x1B[".$y.";".$x."H";
}

//-- print a string at x,y on the screen
function printxy($x, $y, $str) {
  gotoxy($x,$y);
  echo $str;
}

//-- Check if we have a character and return it if we do.
function non_block_read($fd) {
    $read = array($fd);
    $write = array();
    $except = array();
    $result = stream_select($read, $write, $except, 0);
    if($result === false) throw new Exception('stream_select failed');
    if($result === 0) return false;
    $data = stream_get_line($fd, 1);
    return $data;
}

//-- main program starts here.
init_keyboard();
clear();
printxy(20,15,'Press Q to quit!');
while(1) {
    $x = "";
    if($x = non_block_read(STDIN)) {
        //echo "Input: " . $x . "\n";
        // handle your input here
        printxy(20,13,'Last Key Pressed: ['.$x."]  ");
        if ($x == 'Q') {
            printxy(1,20,"bye!\n");
            break;
        }
    } else {
        // perform your processing here
        $date_time = date('F j,Y h:i:sa');
        printxy(20,12,$date_time."   ");
    }
    sleep(1);
}
close_keyboard();

0

在使用PHP构建CLI实用程序时,我希望我的实用程序既可以利用参数,也可以利用管道。
为了解决这个问题,我想出了这个片段,可以放在您的脚本顶部。

$stdin = null;
$is_stdin = fstat(STDIN)["size"] > 0;

if ($is_stdin) {
    while (true) {
        $c = fgetc(STDIN);

        // Why? Read this: https://www.php.net/manual/en/function.feof.php#67261
        if ($c === false)
            break;

        $stdin .= $c;
    }
}

这样,您就可以检查脚本是否提供了STDIN或参数。
如果需要,您可以同时利用两者。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接