26

ユーザーがアップロードした任意の (したがって、最悪の場合、安全でなく、エラーが発生し、クラッシュする) コードを Linux サーバーで実行するための Python プログラムを作成しています。セキュリティの問題はさておき、私の目的は、コード(コンパイルまたは解釈された任意の言語である可能性があります)が正しいものを に書き込むかどうかstdoutstderrおよびプログラムのstdin. この後、結果をユーザーに表示する必要があります。

現在の解決策

現在、私の解決策は、 、およびsubprocess.Popen(...)のファイルハンドルを使用して子プロセスを生成することです。ハンドルの後ろのファイルには、プログラムが操作中に読み取る入力が含まれており、プログラムが終了した後、ファイルとファイルが読み取られて正確性がチェックされます。stdoutstderrstdinstdinstdoutstderr

問題

このアプローチはそれ以外の場合は完全に機能しますが、結果を表示するときに、指定された入力と出力を組み合わせて、端末からプログラムを実行したときと同じ場所に入力が表示されるようにすることはできません。つまり、次のようなプログラムの場合

print "Hello."
name = raw_input("Type your name: ")
print "Nice to meet you, %s!" % (name)

stdout実行後のプログラムの内容を含むファイルの内容は次のようになります。

Hello.
Type your name: 
Nice to meet you, Anonymous!

を含むファイルの内容stdinAnonymous<LF>. したがって、要するに、指定されたサンプルコード (および同等に、のコード) に対して、次のような結果を達成したいと考えています。

Hello.
Type your name: Anonymous
Nice to meet you, Anonymous!

したがって、問題は、プログラムがいつ入力を待っているかを検出することです。

試した方法

問題を解決するために、次の方法を試しました。

Popen.communicate(...)

これにより、親プロセスはパイプに沿って個別にデータを送信できますが、一度しか呼び出すことができないため、ドキュメントから推測できるように、複数の出力と入力を持つプログラムには適していません。

Popen.stdoutPopen.stderrから直接読み取り、Popen.stdinに書き込む

ドキュメントはこれに対して警告しており、プログラムが入力を待機し始めると、Popen.stdouts.read().readline()呼び出しが無限にブロックされるようです。

select.select(...)ファイルハンドルが I/O の準備ができているかどうかを確認するために使用します

これでは何も改善されないようです。どうやら、パイプは常に読み取りまたは書き込みの準備ができているようです。そのため、select.select(...)ここではあまり役に立ちません。

ノンブロッキング読み取りに別のスレッドを使用する

この回答で示唆されているように、からの読み取り結果を Queue() に格納するThread()を作成しようとしました。ユーザー入力を要求する行の前の出力行は適切に表示されますが、プログラムがユーザー入力の待機を開始する行 (上記の例) は読み取られません。stdout"Type your name: "

子プロセスのファイル ハンドルとしてPTYスレーブを使用する

hereの指示に従ってpty.openpty()、マスターとスレーブのファイル記述子を使用して疑似端末を作成しようとしました。その後、スレーブ ファイル記述子をsubprocess.Popen(...)呼び出しのstdout,stderrおよびstdinパラメータの引数として指定しました。で開かれたマスター ファイル記述子を読み取るとos.fdopen(...)、別のスレッドを使用した場合と同じ結果が得られます。入力を要求する行は読み取られません。

編集: @Antti Haapala のpty.fork()子プロセス作成の例を代わりにsubprocess.Popen(...)使用すると、 によって作成された出力も読み取ることができるようですraw_input(...)

pexpect の使用

また、pexpect で生成されたプロセスのメソッド (ドキュメントはこちら) も試しましたが、 で得た最良の結果はread()以前と同じです:read_nonblocking()ユーザーに何かを入力してもらう前の出力の行は読んでください。で作成された PTY と同じです。入力を要求する行読み取られます。readline()read_nonblocking()pty.fork()

編集:子を作成するマスタープログラムでing の代わりにsys.stdout.write(...)andを使用すると、プロンプト行が表示されない問題が修正されたように見えましたが、実際にはどちらの場合も読み取られました。sys.stdout.flush()print

その他

も試しましselect.poll(...)たが、パイプまたは PTY マスター ファイル記述子は常に書き込みの準備ができているようです。

ノート

その他のソリューション

  • また、新しい出力が生成されずに時間が経過したときに入力をフィードしようとすることも頭に浮かびました。ただし、プログラムが重い計算を実行している最中かどうかを知る方法がないため、これは危険です。
  • @Antti Haapala が回答で述べたように、read()glibc のシステム コール ラッパーを置き換えて、入力をマスター プログラムに伝えることができます。ただし、これは静的にリンクされたプログラムやアセンブリ プログラムでは機能しません。(ただし、今考えてみると、そのような呼び出しはソースコードから傍受され、パッチが適用されたバージョンのread()- に置き換えられる可能性がありますが、それでも実装するのは骨の折れる可能性があります。)
  • システムコールをプログラムに伝えるためにLinuxカーネルコードを変更するのread()は、おそらく非常識です...

PTY

PTY は端末を偽装し、あらゆる場所の端末で対話型プログラムが実行されるため、PTY が適していると思います。問題は、どうやって?

4

2 に答える 2

6

stdout が端末 (isatty) の場合、raw_input がプロンプト文字列を stderr に書き込むことに気付きましたか? stdout が端末でない場合、プロンプトも stdout に書き込まれますが、stdout は完全にバッファリングされたモードになります。

tty の stdout を使用

write(1, "Hello.\n", 7)                  = 7
ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
write(2, "Type your name: ", 16)         = 16
fstat(0, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 3), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb114059000
read(0, "abc\n", 1024)                   = 4
write(1, "Nice to meet you, abc!\n", 23) = 23

tty 上ではない stdout を使用

ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fff8d9d3410) = -1 ENOTTY (Inappropriate ioctl for device)
# oops, python noticed that stdout is NOTTY.
fstat(0, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 3), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f29895f0000
read(0, "abc\n", 1024)                     = 4
rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7f29891c4bd0}, {0x451f62, [], SA_RESTORER, 0x7f29891c4bd0}, 8) = 0
write(1, "Hello.\nType your name: Nice to m"..., 46) = 46
# squeeze all output at the same time into stdout... pfft.

したがって、すべての書き込みが同時に stdout にスクイーズされます。さらに悪いことに、入力が読み取られた後です。

したがって、本当の解決策は、pty を使用することです。しかし、あなたはそれを間違っています。pty を機能させるには、サブプロセスではなく pty.fork() コマンドを使用する必要があります。(これは非常にトリッキーになります)。次のような作業コードがあります。

import os
import tty
import pty

program = "python"

# command name in argv[0]
argv = [ "python", "foo.py" ]

pid, master_fd = pty.fork()

# we are in the child process
if pid == pty.CHILD:
    # execute the program
    os.execlp(program, *argv)

# else we are still in the parent, and pty.fork returned the pid of 
# the child. Now you can read, write in master_fd, or use select:
# rfds, wfds, xfds = select.select([master_fd], [], [], timeout)

子プログラムによって設定された端末モードに応じて、さまざまな種類の改行が出てくる可能性があることに注意してください。

「入力待ち」の問題については、疑似端末にいつでも書き込むことができるため、これは本当に助けにはなりません。文字はバッファで待機します。同様に、パイプは常に、ブロックする前に、最大 4K または 32K またはその他の実装で定義された量を書き込むことができます。醜い方法の 1 つは、プログラムを strace し、fd = 0 で read システム コールに入るたびに通知することです。もう 1 つは、代わりに "read()" システム コールを使用して C モジュールを作成し、動的リンカの glibc の前にリンクすることです (実行可能ファイルが静的にリンクされているか、システム コールをアセンブラで直接使用している場合は失敗します...)。次に、 read(0, ...) システム コールが実行されるたびに Python にシグナルを送信します。全体として、おそらく問題を正確に解決する価値はありません。

于 2013-08-07T16:01:20.483 に答える
0

子プロセスがいつ入力を待っているかを検出しようとする代わりに、linuxscriptコマンドを使用できます。スクリプトのマニュアルページから:

scriptユーティリティは、端末に出力されたすべてのタイプスクリプトを作成します。

端末で使用している場合は、次のように使用できます。

$ script -q <outputfile> <command>

したがって、Python では、このコマンドをPopen単に<command>.

編集:次のプログラムを作成しました:

#include <stdio.h>
int main() {
    int i;
    scanf("%d", &i);
    printf("i + 1 = %d\n", i+1);
}

そして、次のように実行しました。

$ echo 9 > infile
$ script -q output ./a.out < infile
$ cat output
9
i + 1 = 10

stdoutしたがって、 の、stderrおよびstdinフラグを使用する代わりに、この方法で Python で実行できると思いますPopen

于 2013-08-08T18:45:50.043 に答える