フロントエンド端末アクセスを介してバックグラウンドでコマンドを実行する

フロントエンド端末アクセスを介してバックグラウンドでコマンドを実行する

任意のコマンドを実行し、子プロセスと対話(具体的な詳細は省略)してから、終了するのを待つことができる関数を作成しようとしています。成功すると、入力はrun <command>まるで裸のように動作します<command>

子プロセスと対話しなかった場合は、単に次のように書いたことでしょう。

run() {
    "$@"
}

ただし、実行中に対話する必要があるため、coprocおよびを使用してより複雑な設定を使用しますwait

run() {
    exec {in}<&0 {out}>&1 {err}>&2
    { coproc "$@" 0<&$in 1>&$out 2>&$err; } 2>/dev/null
    exec {in}<&- {out}>&- {err}>&-

    # while child running:
    #     status/signal/exchange data with child process

    wait
}

(これは単純化されています。すべてのcoprocリダイレクトが実際に役に立つ作業を実行するわけではありませんが、"$@" &実際のプログラムではリダイレクトが必要です。)

コマンドは"$@"何でも構いません。私の機能は、run lsなどrun makeで動作しますがrun vim。 Vimがバックグラウンドプロセスであり、端末アクセス権がないことを検出して編集ウィンドウを表示するのではなく、それ自体が停止するため、失敗したようです。 Vimが正しく動作するように修正したいと思います。

coproc "$@"親シェルが「バックグラウンド」になっている間に「フォアグラウンド」で実行されるようにするにはどうすればよいですか?「子供との対話」部分は端末で読み書きすることはないので、フォアグラウンドで実行する必要はありません。 ttyの制御権をコルーチンに渡してくれて嬉しいです。

run()私がやっていることでは、親プロセスと"$@"子プロセスにあることが重要です。私はこの役割を変えることはできません。しかし、私はできる前景と背景を変更します。 (ただ何をすべきかわからない。)

私はVim固有のソリューションを探しているわけではありません。私はpseudo-ttyを避けることを好みます。私の理想的な解決策は、stdinとstdoutがtty、パイプに接続されている、またはファイルからリダイレクトされたときに同じようにうまく機能します。

run echo foo                               # should print "foo"
echo foo | run sed 's/foo/bar/' | cat      # should print "bar"
run vim                                    # should open vim normally

なぜコプロセスを使用するのですか?

私はcoprocなしでこの問題を書くことができます。

run() { "$@" & wait; }

私はちょうど同じ行動を使用しました&。しかし、私のユースケースではFIFO coproc設定を使用していますcmd &coproc cmd

なぜptysを避けるべきですか?

run()自動化された環境で使用できます。パイプやリダイレクトに使用されている場合はエミュレートする端末がなく、ptyを設定するとエラーが発生します。

期待を使わないのはなぜですか?

私はvimを自動化したり、vimに入力を送信したり、そのようなものを送信したくありません。

答え1

次のようにコードを追加しました。

  • 3つの例で動作します。
  • 待つ前に相互作用が発生します。
  • interact() {
        pid=$1
        ! ps -p $pid && return
        ls -ld /proc/$pid/fd/*
        sleep 5; kill -1 $pid   # TEST SIGNAL TO PARENT
    }
    
    run() {
        exec {in}<&0 {out}>&1 {err}>&2
        { coproc "$@" 0<&$in 1>&$out 2>&$err; } 2>/dev/null
        exec {in}<&- {out}>&- {err}>&-
        { interact $! <&- >/tmp/whatever.log 2>&1& } 2>/dev/null
        fg %1 >/dev/null 2>&1
        wait 2>/dev/null
    }
    

    fg %1すべてのコマンドに対して実行され(%1同時操作に必要に応じて変更)、一般的な状況では、次の2つのいずれかが発生します。

  • コマンドがすぐに終了すると、何もしないでinteract()fgもしないため、すぐに返されます。
  • コマンドが即座に終了しない場合は、対話することができますinteract()(たとえば、5秒後にHUPをセカンダリプロセスに送信します)、セカンダリfgプロセスは元々実行されたのと同じstdin / out / errを使用してフォアグラウンドにインポートされます(確認できます)。この金額ls -l /proc/<pid>/df)。

    最後の3つのコマンドでは、/ dev / nullへのリダイレクトは外観的です。run <command>個別に実行するときとまったく同じように見えます。command

  • 答え2

    サンプルコードでは、Vimがttyからデータを読み取るか、いくつかのプロパティを設定しようとすると、SIGTTIN信号を介してカーネルによって中断されます。

    これは、対話型シェルがそれを別のプロセスグループに作成しますが(まだ)そのグループにttyを渡さないためです。つまり、「バックグラウンドに配置」します。これは通常のジョブ制御動作であり、ttyを渡す一般的な方法はを使用することですfg。もちろん、シェルはバックグラウンドに移動するので一時停止します。

    これはすべてシェルが対話型であることを意図しています。それ以外の場合は、Vimでファイルを編集しているときにプロンプ​​トにコマンドを入力することができます。

    run関数全体をスクリプトに切り替えると、この問題を簡単に解決できます。これにより、ttyと競合せずに対話型シェルによって同期的に実行されます。これにより、独自のサンプルコードは、すでにrun (当時のスクリプト)とcoprocとの同時相互作用を含む、要求されたすべてのタスクを実行します。

    スクリプトに入れることができない場合は、Bash以外のシェルが対話型ttyを子プロセスに渡すことをより細かく制御できることを確認できます。私は個人的に高級シェルの専門家ではありません。

    Bashを本当に使用し、インタラクティブシェルで実行される関数を介してこの機能を使用する必要がある場合は、残念ながら唯一の方法は、次のことを許可する言語でヘルパープログラムとsigprocmask(2)を作成することです。 tcsetpgrp(3) にアクセスします。

    目的は、ttyを強制的に取得するために、子プロセス(coproc)の親プロセス(対話型シェル)で完了していないタスクを実行することです。

    しかし、これは明らかに悪い習慣と見なされることに注意してください。

    しかし、サブシェルがまだttyを所有している間に親シェルでttyを使用しないと、おそらく何の害もありません。 「使用しない」とは、 echottyとprintf対話しないことを意味readし、子プロセスがまだ実行中にttyにアクセスできる他のプログラムを実行しないことを意味します。

    Pythonのヘルパープログラムは次のとおりです。

    #!/usr/bin/python3
    
    import os
    import sys
    import signal
    
    def main():
        in_fd = sys.stdin.fileno()
        if os.isatty(in_fd):
            oldset = signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGTTIN, signal.SIGTTOU})
            os.tcsetpgrp(in_fd, os.getpid())
            signal.pthread_sigmask(signal.SIG_SETMASK, oldset)
        if len(sys.argv) > 1:
            # Note: here I used execvp for ease of testing. In production
            # you might prefer to use execv passing it the command to run
            # with full path produced by the shell's completion
            # facility
            os.execvp(sys.argv[1], sys.argv[1:])
    
    if __name__ == '__main__':
        main()
    

    Cでの対応する内容は少し長くなります。

    このヘルパーは、次のようにexecを使用してcoprocで実行する必要があります。

    run() {
        exec {in}<&0 {out}>&1 {err}>&2
        { coproc exec grab-tty.py "$@" {side_channel_in}<&0 {side_channel_out}>&1 0<&${in}- 1>&${out}- 2>&${err}- ; } 2>/dev/null
        exec {in}<&- {out}>&- {err}>&-
    
        # while child running:
        #     status/signal/exchange data with child process
    
        wait
    }
    

    すべての例では、この設定はUbuntu 14.04、Bash 4.3、およびPython 3.4で動作し、デフォルトの対話型シェルは関数を取得し、コマンドrunプロンプトで実行します。

    coprocでスクリプトを実行する必要がある場合は、を使用して実行する必要がありますbash -i。それ以外の場合、BashはPythonスクリプトから取得したttyを継承するのではなく、stdin/stdout/stderrまたは/dev/nullのパイプで開始できます。また、coprocの内部(またはその下)で​​何を実行してもextra run()sを呼び出さないことをお勧めします。 (実際には確かではなく、このケースをテストしていませんが、少なくとも慎重なカプセル化が必要になると思います。)


    あなたの特定の(サブ)質問に答えるには、いくつかの理論を紹介する必要があります。

    各ttyにはいわゆる「セッション」が1つしかありません。 (通常のデーモンの場合のように、すべてのセッションにttyがあるわけではありませんが、ここでは関係がないと思います)。

    デフォルトでは、各セッションは「セッションリーダー」のpidに対応するIDによって識別されるプロセスの集まりです。したがって、「セッションリーダー」は、そのセッションに属するプロセスの1つであり、正確にその特定のセッションを開始する最初のプロセスです。

    みんな特定のセッション(リーダーと非リーダー)のプロセスは、自分が属するセッションに関連付けられているttyにアクセスできます。しかし、最初の違いは次のとおりです。一つ特定の瞬間のプロセスは「フォアグラウンドプロセス」と呼ぶことができる。その他すべてこの間は「バックグラウンドプロセス」です。 「フォアグラウンド」プロセスはttyに無料でアクセスできます。代わりに、「バックグラウンド」プロセスが自分のttyにアクセスしようとすると、カーネルによって中断されます。これは、バックグラウンドプロセスがまったく許可されていないという意味ではなく、カーネルが「今は彼らが話す順番ではありません」という信号を送るという意味です。

    したがって、特定の質問に答えるには:

    「前景」と「背景」は正確に何を意味しますか?

    「Prospect」は「存在」を意味する。法的にその瞬間にはttyが使われています。」

    「背景」とは、「当時ttyは使用されていない」を意味する。

    つまり、あなたの質問を再び引用すると、次のようになります。

    フォアグラウンドプロセスとバックグラウンドプロセスの違いを知りたいです。

    正当なttyにアクセスします。

    親プロセスの実行中にバックグラウンドプロセスをフォアグラウンドにインポートできますか?

    一般的に:バックグラウンドプロセス(親または非親)する引き続き実行してください。 ttyにアクセスしようとすると(デフォルトで)停止します。 (注:これらの特定の信号(SIGTTINとSIGTTOU)は無視または処理できますが、通常はそうではないため、デフォルトの設定はプロセスを一時停止することです。)

    ただし、対話型シェルの場合は次のようになります。シェルしたがって、バックグラウンドで子の1つにttyを渡し、それ自体を中断することを選択します(wait(2)またはselect(2)、または当時最も適切であると判断されたブロックシステムコール)。

    これにより、特定の質問に対する正確な回答は次のようになります。シェルアプリケーションを使用する場合これは、使用しているシェルがコマンドを実行した後にそれ自体を停止しない方法(組み込みコマンドまたはその他)を提供するかどうかによって異なりますfg。 AFAIK Bashはそのような選択を許可しません。他のシェルアプリケーションについてはわかりません。

    とどうcmd &違うのcmd

    では、cmdBashは独自のセッションの新しいプロセスを作成し、ttyを渡して待機状態に入ります。

    では、cmd &Bashは独自のセッションである新しいプロセスを作成します。

    子プロセスにフォアグラウンドコントロールを付与する方法

    通常、tcsetpgrp(3) を使用する必要があります。実際、これは親や子供が行うことができますが、推奨されるアプローチは親が行うことです。

    Bashの場合:コマンドをfg実行すると、Bashはtcsetpgrp(3)を使用して子プロセスをバックアップして待機状態に入ります。


    ここで興味深い事実を見つけることができれば、実際にはかなり新しいUNIXシステムにはセッションプロセス間に追加の階層があるということです。プロセスグループ」。

    これは、私がこれまで「フォアグラウンド」の概念について述べたことは、実際には「単一プロセス」に限定されず、「単一プロセスグループ」に拡張されるためです。

    つまり、偶然一般的な「前景」の場合は、1つのプロセスだけがttyに正当にアクセスできますが、カーネルは実際にプロセス全体がttyにアクセスできる高度なケースを受け入れます。プロセスグループ(まだ同じセッションに属しています)はttyへの正当なアクセス権を持っています。

    tty "フォアグラウンド"を渡すために呼び出された関数がエラーではないことを命名tcsetpgrp、次のようなものではなく(例えば)tcsetpid

    しかし、実際には、Bashはこれらのより高い可能性を意図的に活用していないようです。

    あなたそれでも活用したいかもしれません。それはすべて特定のアプリケーションによって異なります。

    プロセスグループ化の実際の例と同様に、上記のソリューションの「フロントグループを引き渡す」アプローチの代わりに「フォアグラウンドプロセスグループを再利用する」アプローチを使用することも選択できました。

    つまり、Pythonスクリプトでos.setpgid()関数(setpgid(2)システムコールをラップします)を使用して、プロセスを現在のフォアグラウンドプロセスグループ(シェルプロセス自体である可能性がありますが必ずしもそうではありません)に再割り当てすることができますあります。これにより、バッシュは降伏していない前景の状態を取り戻しました。

    ただし、これは最終目標を達成するためのやや間接的な方法であり、望ましくない副作用がある可能性があります。なぜなら、プロセスグループのいくつかの他の使用は、tty制御とは何の関係もなく、最終的にcoprocと関連する可能性があるからです。たとえば、UNIX信号は通常、単一プロセスではなくプロセスグループ全体に渡すことができます。

    最後に、run()スクリプト(またはスクリプト)の代わりにBashのコマンドプロンプトでこれを行うのはなぜですか?としてスクリプト)?

    run()コマンドプロンプトでの呼び出しはBashの独自のプロセス(*)によって実行されますが、スクリプトから呼び出されると他のプロセス(グループ)によって実行されるため、対話型Bashは喜んでttyをそのプロセスに渡しました。

    したがって、スクリプトで判断すると、ttyとの競合を避けるためにBashが実装した最後の「防御」は、stdin / stdout / stderrのファイル記述子を保存して復元するよく知られている簡単なトリックによって簡単にバイパスされます。

    (*)または次の新しいプロセスを作成することもできます。それ自体と同じプロセスグループ。私は実際にインタラクティブBashが機能を実行するためにどのような正確な方法を使用しているのかを調べたことはありませんが、ttyの面では違いはありません。

    ファタイ

    答え3

    質問を完全に理解していませんが、ここにあなたが探しているものがあります。

    前景と背景はシェルのコンセプトです。

    • フォアグラウンドタスクはttyにアクセスできます。 ctrl-zを押すと一時停止できます。
    • 待機中のジョブは、前景(fg)または背景(bg)に移動できます。
    • ジョブはバックグラウンドで開始できます«command»&
    • バックグラウンドタスクをフォアグラウンドにインポートできますfg %jobid
    • アクションはプロセスであり、シェルが提供するその他のメタデータヘルプです。ジョブは、そのジョブを開始したシェルからのみジョブとしてアクセスできます。別の観点から見ると、それは1つのプロセスです。

    答え4

    Vimがバックグラウンドプロセスであり、端末アクセス権がないことを検出して編集ウィンドウを表示するのではなく、それ自体が停止するため、失敗したようです。 Vimが正しく動作するように修正したいと思います。

    実際、前景や背景とは何の関係もありません。

    vimがすることは呼び出しだけですisatty()この機能は、端末に接続されていないことを意味します。この問題を解決する唯一の方法は、vimを端末に接続することです。これを行う方法は2つあります。

    • 使用しないでくださいどの標準出力のリダイレクト。リダイレクトがある場合、最終的にターミナルにリダイレクトされても、ターミナルの代わりにパイプをisatty()指し、vimはそれ自体がバックグラウンドで実行されます。
    • 医師ttyを使用してください。はい、あなたがそれをしたくないと言ったことを知っています。しかし、リダイレクトが必要な場合は、擬似ttyを使用しないでください。はい不可能

    関連情報