如何从Perl中启动交互式命令,同时允许Perl解析命令输出

huangapple go评论92阅读模式
英文:

How to launch interactive commands from Perl while allowing Perl to parse the command output

问题

以下是要翻译的内容:

问题

如何更改Perl脚本以允许子进程的无人值守和交互式执行,以便在交互式情景中,用户与GDB进行交互,就像Perl脚本不解析输出一样?并且在用户按下键盘时显示提示和按键,而不是在按下Enter键之后。

详情

我有一个Perl脚本,启动子进程并读取输出,对输出采取各种操作,但始终只将输出打印到标准输出。通常,子进程是一个无人值守的进程,不需要用户输入(即不从标准输入读取)。但是,有时需要将子进程更改为运行GDB,并且GDB确实需要提示用户以获取“(gdb)”提示(_不_打印换行符),读取用户的输入(然后由用户按Enter键终止),处理用户的命令,并发出生成的输出(可能来自其在GDB控制下的应用程序,也可能来自GDB本身)。然而,目前编写的Perl脚本读取了GDB提示,但不会立即打印出该提示,并且还不会在用户键入每个字符时立即打印出用户的输入(包括使用退格键、箭头键等),而是在用户最终按下Enter键后一次性打印出gdb提示和用户的输入。因此,用户必须像被蒙住眼睛一样输入,直到Perl脚本完成读取整行输出并随后打印出整个输入行。强制用户像被蒙住眼睛一样输入问题所在。

约束条件

  1. 不允许更改“真实子进程”中的任何内容,只能更改Perl脚本或Perl脚本可能选择使用的任何中间“辅助脚本”或工具(如果需要),可以通过here-documents或类似机制暂时生成它们。
  2. 不得要求Perl的版本高于v5.14.2。
  3. 除非可以证明以每次读取整行输出的方式与逐字符读取字符输出并发出它一样高效(真实场景中的输出大小庞大),否则不要更改Perl脚本以逐字符输出。
  4. 不要使用非内置于Linux版本中的重量级工具,包括非常旧的Linux版本,例如RHEL6。这排除了使用Expect,也可能排除了使用Expect.pm,如https://stackoverflow.com/q/12244863/257924所示,但我不确定,因为这可能需要安装额外的Perl包,带来风险。
  5. 不要更改调用gdb的工具的使用方式,以便在运行后附加到现有的子进程。在某些情况下,需要在要调试的应用程序的::main()之前涉及到gdb。

可行的可能性

  1. 更改Perl脚本以允许通过重定向使用两个管道或类似的方式与标准输入进行交互。我不确定如何在Perl中操作。
  2. 更改Perl脚本以启动一个中间脚本作为子进程执行重定向,然后Perl脚本从单独的管道读取。与上述类似:我不知道如何编写代码。

最小完整可验证示例(MCVE)

下面的Bash脚本写出了所有脚本并启动它们。您可以在Linux的/tmp目录中执行此操作。我在Linux以外的平台上没有测试过这个脚本,因此不要指望它能在没有修改的情况下正常工作。

英文:

The Question

How do I change a Perl script to allow for both unattended and
interactive execution of the child process, such that in the
interactive scenario, the user interacts with the GDB just like they
would if the Perl script was not also parsing the output? And showing
prompts and keyboard presses as the user presses them, not after they
press the Enter key.

Details

I have a Perl script that launches child processes and reads the
output, taking various actions on the output, but always just printing
the output to standard output. Normally, the child process is an
unattended process that does not require user input (i.e., does not
read from standard input). However, sometimes that child process needs
to be changed to run GDB, and GDB does need to both prompt the user
with a prompt of "(gdb) " (without printing a newline character),
read the users input (which is then terminated by the user pressing
the Enter key), process the users command, and emit the resulting
output (which may be from its application under GDB control, or from
GDB itself). However, the Perl script as currently written reads the
GDB prompt but does not print out that prompt immediately, and also
does not immediately print out each character of the users input as
they type each character (and including any usage of backspace, arrow
key stroke handling, etc.), but prints out both the gdb prompt and the
users input all at once after the user finally presses the Enter
key. Thus, the user has to literally type as if they were
blind-folded, and cannot immediately see what they have typed until
the Perl script completes the reading of the entire line of output,
and then prints out the entire input line. Forcing the user to type
blind-folded is the problem.

Constraints

  1. Changing anything in the "real child process" is not an option, but only the Perl script or any intervening "helper scripts" or utilities (if needed) that the Perl script may choose to use (i.e., temporarily generate via here-documents or similar mechanism.).
  2. Must not require any version of Perl greater than v5.14.2.
  3. Changing the Perl script to read character output one character at a time and emitting it, unless it is provably as performant as reading entire lines of output (the size of the output of the real-world scenarios is voluminous).
  4. Use of heavy-weight tools that are not built into versions of Linux including very old out of date versions of Linux such as RHEL6. This negates the use of Expect, too. It might also negate use of Expect.pm as indicated in https://stackoverflow.com/q/12244863/257924 but am unsure as that might require installation of additional Perl packages, with its attendent risks.
  5. Changing the use of the tools that invoke gdb to instead attach to an existing child process after it is running. In some scenarios, the gdb needs to be involved prior to ::main() in the application to be debugged.

Viable possibilities

  1. Change the Perl script to allow the standard input to be interacted with via redirection using two pipes or something similar. I am unsure how to go about that, in Perl.
  2. Change the Perl script to launch a intermediate script as a child process that does the redirection, and then the Perl script reads from the separate pipe. Similar to the above: I'm unsure how to code that.

The Minimal Complete Verifiable Example (MCVE)

The Bash script below writes out all scripts and launches them. You
can execute this inside /tmp on Linux. I've not tested this script on
any other platform than Linux, so do not expect it to work without
modifications.

#!/bin/bash

# Dump out a perl_script script:
#
#   The perl_script below runs gdb directly on the sleep command, but
#   in the real-world scenario, instead of sleep, there would be a
#   complex script that calls other scripts that finally may or may
#   not invoke gdb.
#
cat > perl_script <<'EOF'
#!/usr/bin/perl
use warnings;
use strict;

# Just for the question, just call gdb directly, but in real-world
# practice, this command would be some other script that eventually
# executes gdb as a child process:
my $shell_command="gdb sleep 999999";

print "perl_script: Executing $shell_command in a pipe ...\n";

my $error_code;  # Leave $error_code undefined here.
open(SUBSHELL_FP, "$shell_command |") || do {
  $error_code = -1;
};
# Avoid reading from a pipe that immediately failed, as indicated by undefined $error_code:
if (!defined($error_code)) {
  while(<SUBSHELL_FP>) {
    print; # print output to the user

    # Not shown here is the parsing of the output of <SUBSHELL_FP> for
    # various strings and taking action upon them that is unrelated to
    # the gdb execution.

    # It is this reading of <SUBSHELL_FP> that "hides" the "(gdb)"
    # prompt and also the keypress output from the user that is typing
    # into the gdb prompt, and only emits it upon the _subsequent_
    # call to print above in the next loop iteration.

  }
  close(SUBSHELL_FP);
  $error_code = $?;
}
print "End of run with error code $error_code\n";
EOF

# Make the perl_script executable:
chmod a+x perl_script

# Execute the perl_script:
#
#   While this perl_script is running, the user can type "info br"
#   (and press the Enter key) to see some output (just to force gdb to
#   emit an answer which is that there are no breakpoints).  But,
#   typing letters into the xterm is NOT displayed, and neither is the
#   "(gdb)" prompt, at least at first. Just as soon as the user
#   presses the Enter key, the Perl script above prints the prompt and
#   the users input (see "print output to the user" above).
#
#   Then the user can press CTRL+d to exit the gdb process, which will
#   then return to the "read" below.
#
./perl_script

# Here, the user has executed gdb and thus exited the
# perl_script. Indicate that the script has finished:
echo Hit enter to continue
# shellcheck disable=SC2034
read -r dont_care

答案1

得分: 1

以下是您要翻译的内容:

How do I change a Perl script to allow for both unattended and interactive execution of the child process, such that in the interactive scenario, the user interacts with the GDB just like they would if the Perl script was not also parsing the output?

好问题。目前我没有找到在不使用Expect的情况下的解决方案,但我正在进一步调查。以下是我的初步调查结果:

只要您不试图读取子进程的输出,它就可以正常工作。来自STDIN的用户输入会实时显示在终端上(按需显示,无需按Enter键查看):

use v5.38;
use experimental 'class';

{
    my $self = Main->new();
    $self->start_process();
    $self->cleanup();
}

class Main;
field $_child_pid;

method cleanup() {
    say "Parent: waiting for child..";
    waitpid($_child_pid, 0);
}

method start_process() { # 参见:https://perldoc.perl.org/perlipc
    if ($_child_pid = fork()) {
        return;
    }
    else {
        die "cannot fork: $!" unless defined $_child_pid;
        # 这是子进程
        exec "gdb", "sleep";
        exit 0;
    }
}

但是,如果我尝试读取子进程的STDOUT(正如您在问题中提到的),则用户输入只有在用户在终端上按下ENTER键后才会显示(所以遗憾的是,下面的脚本不能按要求工作):

use v5.38;
use experimental 'class';

use IO::Select;
{
    my $self = Main->new();
    $self->start_process();
    $self->select_loop();
    $self->cleanup();
}

class Main;
field $_child_pid;
field $_child_rdr;

method cleanup() {
    say "Parent: waiting for child..";
    waitpid($_child_pid, 0);
}

method select_loop() {
    my $timeout = 10;
    my $sel = IO::Select->new( $_child_rdr );
    while (1) {
        local $! = 0; # 清除ERRNO,以便我们可以区分超时和其他错误
        my @ready = $sel->can_read( $timeout );
        if (!@ready) {
            if ($!) {
                die "Select() failed with error: $!";
            }
            else {
                die "Select() timed out";
            }
        }
        my $handle = $ready[0];
        my $result = <$handle>;
        if (!defined $result) {
            last;  #EOF
        }
        print ":: ", $result;
    }
}

method start_process() { # 参见:https://perldoc.perl.org/perlipc
    pipe($_child_rdr,  my $parent_wtr);
    $parent_wtr->autoflush(1);
    if ($_child_pid = fork()) {
        return;
    }
    else {
        die "cannot fork: $!" unless defined $_child_pid;
        # 这是子进程
        close $_child_rdr;
        open (STDOUT, ">&", $parent_wtr) or die $!;
        open (STDERR, ">&", $parent_wtr) or die $!;
        exec "gdb", "sleep";
        exit 0;
    }
}

我不确定为什么尝试从子进程的STDOUT中读取数据会更改终端的行为,并阻止STDIN实时回显(仅在用户按Enter键后才回显)。

更新

使用Expect 似乎可以解决这个问题,但您需要查找“(gdb)”提示,然后调用interact()

use v5.38;
use experimental 'class';
use Expect;
{
    my $self = Main->new();
    $self->start_process();
    $self->expect_loop();
    $self->cleanup();
}

class Main;
field $_exp;

method cleanup() {
    say "Parent: cleaning up..";
    $_exp->hard_close();
}

method expect_loop() {
    my $timeout = 20;
    while (1) {
        my ( $matched_pattern_position, $error, $successfully_matching_string, $before_match, $after_match)
            = $_exp->expect($timeout, '-re', '\(gdb\) ');
        if (defined $error) {
            if ($error eq '1:TIMEOUT' ) {
                say "expect: timeout";
            }
            elsif ($error eq '2:EOF') {
                say "expect: child EOF";
            }
            else {
                say "expect: error";
            }
            last;
        }
        $_exp->interact();
    }
}

method start_process() {
    my $cmd='gdb';
    my @param = qw(sleep);
    $_exp = Expect->new();
    $_exp->spawn($cmd, @param) or die "Cannot spawn $cmd: $!\n";
}

您在问题中提到不能使用Expect,能否澄清为什么不能使用它?

英文:

> How do I change a Perl script to allow for both unattended and interactive execution of the child process, such that in the interactive scenario, the user interacts with the GDB just like they would if the Perl script was not also parsing the output?

Good question. Currently I could not find the solution without using Expect, but I am investigating this further. Here are my preliminary findings:

As long as you are not trying to read the output of the child process it works fine. The user input from STDIN is shown in real time (as required, and you do not have to press enter to see it) in the terminal:

use v5.38;
use experimental &#39;class&#39;;

{
    my $self = Main-&gt;new();
    $self-&gt;start_process();
    $self-&gt;cleanup();
}

class Main;
field $_child_pid;

method cleanup() {
    say &quot;Parent: waiting for child..&quot;;
    waitpid($_child_pid, 0);
}

method start_process() { # See: https://perldoc.perl.org/perlipc
    if ($_child_pid = fork()) {
        return;
    }
    else {
        die &quot;cannot fork: $!&quot; unless defined $_child_pid;
        # This is the child process
        exec &quot;gdb&quot;, &quot;sleep&quot;;
        exit 0;
    }
}

However, if I try to read the child STDOUT (as you mentioned in your question that you would do), the user input is not shown until the user presses ENTER in the terminal (so unfortunately, the below script does not work as required):

use v5.38;
use experimental &#39;class&#39;;

use IO::Select;
{
    my $self = Main-&gt;new();
    $self-&gt;start_process();
    $self-&gt;select_loop();
    $self-&gt;cleanup();
}

class Main;
field $_child_pid;
field $_child_rdr;

method cleanup() {
    say &quot;Parent: waiting for child..&quot;;
    waitpid($_child_pid, 0);
}

method select_loop() {
    my $timeout = 10;
    my $sel = IO::Select-&gt;new( $_child_rdr );
    while (1) {
        local $! = 0; # Clear ERRNO such that we can differentiate between timeout and other errors
        my @ready = $sel-&gt;can_read( $timeout );
        if (!@ready) {
            if ($!) {
                die &quot;Select() failed with error: $!&quot;;
            }
            else {
                die &quot;Select() timed out&quot;;
            }
        }
        my $handle = $ready[0];
        my $result = &lt;$handle&gt;;
        if (!defined $result) {
            last;  #EOF
        }
        print &quot;:: &quot;, $result;
    }
}

method start_process() { # See: https://perldoc.perl.org/perlipc
    pipe($_child_rdr,  my $parent_wtr);
    $parent_wtr-&gt;autoflush(1);
    if ($_child_pid = fork()) {
        return;
    }
    else {
        die &quot;cannot fork: $!&quot; unless defined $_child_pid;
        # This is the child process
        close $_child_rdr;
        open (STDOUT, &quot;&gt;&amp;&quot;, $parent_wtr) or die $!;
        open (STDERR, &quot;&gt;&amp;&quot;, $parent_wtr) or die $!;
        exec &quot;gdb&quot;, &quot;sleep&quot;;
        exit 0;
    }
}

I am not sure why this simple change of trying to read from the child's STDOUT changes the behavior of the terminal and prevents the STDIN from being echoed in real time (only echoed after the user presses enter).

Update:

Using Expect seems to work though, but you need to look for the "(gdb)" prompt and then call interact():

use v5.38;
use experimental &#39;class&#39;;
use Expect;
{
    my $self = Main-&gt;new();
    $self-&gt;start_process();
    $self-&gt;expect_loop();
    $self-&gt;cleanup();
}

class Main;
field $_exp;

method cleanup() {
    say &quot;Parent: cleaning up..&quot;;
    $_exp-&gt;hard_close();
}

method expect_loop() {
    my $timeout = 20;
    while (1) {
        my ( $matched_pattern_position, $error, $successfully_matching_string, $before_match, $after_match)
            = $_exp-&gt;expect($timeout, &#39;-re&#39;, &#39;\\(gdb\\) &#39;);
        if (defined $error) {
            if ($error eq &#39;1:TIMEOUT&#39; ) {
                say &quot;expect: timeout&quot;;
            }
            elsif ($error eq &#39;2:EOF&#39;) {
                say &quot;expect: child EOF&quot;;
            }
            else {
                say &quot;expect: error&quot;;
            }
            last;
        }
        $_exp-&gt;interact();
    }
}

method start_process() {
    my $cmd=&#39;gdb&#39;;
    my @param = qw(sleep);
    $_exp = Expect-&gt;new();
    $_exp-&gt;spawn($cmd, @param) or die &quot;Cannot spawn $cmd: $!\n&quot;;
}

You mentioned in your question that you could not use Expect, can you clarify why not?

huangapple
  • 本文由 发表于 2023年7月18日 01:35:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/76706865.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定