web-dev-qa-db-ja.com

フォアグラウンドターミナルアクセスでバックグラウンドでコマンドを実行する

任意のコマンドを実行し、子プロセス(詳細は省略)と対話して、終了するのを待つことができる関数を作成しようとしています。成功した場合、_run <command>_と入力すると、_<command>_のように動作するように見えます。

子プロセスと対話していない場合は、次のように記述します。

_run() {
    "$@"
}
_

しかし、実行中にそれを操作する必要があるため、coprocwaitを使用して、このより複雑なセットアップを行っています。

_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固有のソリューションを探しているわけではないことに注意してください。そして、私は疑似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_の間に違いがある場合は、質問を単純化しない方がいいと考えました。

なぜptyを避けるのですか?

run()は、自動化されたコンテキストで使用できます。パイプラインまたはリダイレクトで使用されている場合、エミュレートする端末はありません。 ptyの設定は間違いでしょう。

Expectを使用しないのはなぜですか?

私はvimを自動化しようとしているのではなく、それに入力やそのようなものを送信しています。

6
John Kugelman

あなたのコード例では、Vimはttyから読み込もうとするか、またはいくつかの属性をそれに設定しようとするとすぐに、SIGTTINシグナルを介してカーネルによって中断されます。

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

これはすべて、シェルがインタラクティブである場合の意図的なものです。それ以外の場合は、たとえばVimでファイルを編集している間、プロンプトでコマンドを入力し続けることが許可されているかのようになります。

代わりに、run関数全体をスクリプトにするだけで、簡単に回避できます。そうすれば、ttyと競合することなく、対話型シェルによって同期的に実行されます。これを行うと、run(スクリプト)とcoprocの間の同時対話を含め、独自のサンプルコードで要求どおりの処理がすべて実行されます。

スクリプトに含めることができない場合は、Bash以外のシェルでインタラクティブttyを子プロセスに渡すことをより細かく制御できるかどうかを確認できます。私は個人的にはより高度なシェルの専門家ではありません。

本当にBashを使用する必要があり、対話型シェルによって実行される関数を通じてこの機能が本当に必要な場合、tcsetpgrp(3)にアクセスできる言語でヘルパープログラムを作成するのが唯一の方法だと思います。およびsigprocmask(2)。

目的は、ttyを強制的に取得するために、親(インタラクティブシェル)で行われていないことを子(コプロ)で行うことです。

ただし、これは明らかに悪い習慣と見なされています。

ただし、子がまだ持っているときに親シェルのttyをこまめに使用しない場合、害はない可能性があります。 「使用しない」とは、ttyとの間でechoしない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は、パイプで取得されたttyを継承するのではなく、パイプまたはstdin/stdout/stderrの/ dev/nullで始まる場合があります。 Pythonスクリプト。また、coproc内(またはその下)で​​実行するものは、追加のrun() sを呼び出さない方がよいでしょう(実際には、そのシナリオをテストしていませんが、ただし、少なくとも慎重にカプセル化する必要があると思います)。


特定の(サブ)質問に答えるために、少し理論を紹介する必要があります。

すべてのttyには、いわゆる「セッション」が1つだけあります。 (ただし、典型的なデーモンプロセスの場合のように、すべてのセッションにttyがあるわけではありませんが、ここでは関係がないと思います)。

基本的に、すべてのセッションはプロセスのコレクションであり、「セッションリーダー」のPIDに対応するIDによって識別されます。したがって、「セッションリーダー」は、そのセッションに属するプロセスの1つであり、まさにその特定のセッションを最初に開始したプロセスです。

特定のセッションのすべてのプロセス(リーダーではなく)は、それらが属するセッションに関連付けられているttyにアクセスできます。しかし、ここで最初の違いがあります。ある特定の瞬間のoneプロセスのみが、いわゆる「フォアグラウンドプロセス」になることができますが、その間、他のすべては「バックグラウンドプロセス」です。 「フォアグラウンド」プロセスは、ttyに無料でアクセスできます。逆に、「バックグラウンド」プロセスは、ttyにアクセスしようとすると、カーネルによって中断されます。バックグラウンドプロセスがまったく許可されていないのではなく、「発言する番ではない」という事実の核心からシグナルが送られるのです。

それで、あなたの特定の質問に行きます:

「前景」と「背景」は正確にはどういう意味ですか?

「前景」とは、「その時点でttyを使用して正当にいる」ことを意味します

「背景」は単に「その時点でttyを使用していない」ことを意味します

または、言い換えると、もう一度質問を引用してください。

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

ttyへの正当なアクセス。

親が実行を続けている間にバックグラウンドプロセスをフォアグラウンドに持ってくることは可能ですか?

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

ただし、対話型シェルの場合は、(wait =(2)またはselect(2)で)自分自身を一時停止することを選択するのはShellですシステムコールをブロックすると、その瞬間に最も適切であると見なされます)。バックグラウンドで動作していた子の1人にttyを渡した後。

これから、あなたの特定の質問に対する正確な答えは次のとおりです:シェルアプリケーションを使用する場合使用しているシェルがfgコマンドを発行した後、それ自体を停止しないようにするメソッド(組み込みコマンドまたは何)。 AFAIK Bashでは、このような選択はできません。他のシェルアプリケーションについては知りません。

_cmd &_とcmdの違いは何ですか?

cmdでは、Bashは自身のセッションに属する新しいプロセスを生成し、ttyをそれに渡し、それ自体を待機状態にします。

_cmd &_では、Bashは自身のセッションに属する新しいプロセスを生成します。

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

一般的には、tcsetpgrp(3)を使用する必要があります。実際には、これは親または子のいずれかで実行できますが、推奨される方法は、親が実行することです。

Bashの特定のケースでは、fgコマンドを発行します。そうすることで、Bashはtcsetpgrp(3)を使用して、その子を優先して待機させます。


ここから、興味を引くかもしれないもう1つの洞察は、実際には、ごく最近のUNIXシステムでは、セッションのプロセス間に1つの追加の階層レベル、いわゆる「」があるということです。プロセスグループ」。

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

つまり、「フォアグラウンド」の通常の共通ケースは、ttyへの正当なアクセス権を持つ単一のプロセスのみであるが、カーネルは実際にはプロセスのグループ全体(まだ同じセッションに属している)がttyに正当なアクセス権を持っている、より高度なケースを可能にします。

実際、ttyの「前景」を渡すために呼び出す関数の名前がtcsetpgrpであり、(たとえば)tcsetpid.

ただし、実際には、Bashはこのより高度な可能性を意図的に利用していないことは明らかです。

しかし、あなたはそれを利用したいと思うかもしれません。それはすべて特定のアプリケーションに依存します。

プロセスグループ化の実際の例と同じように、上のソリューションでは、「フォアグラウンドグループを引き継ぐ」アプローチの代わりに、「フォアグラウンドプロセスグループを取り戻す」アプローチを使用することを選択できました。

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

ただし、それは最終目標へのかなり間接的な方法であり、またtty制御に関連しないプロセスグループのいくつかの他の使用法があり、最終的にはcoprocが関与する可能性があるため、望ましくない副作用が生じる可能性もあります。たとえば、UNIXシグナルは一般に、単一のプロセスではなく、プロセスグループ全体に配信できます。

最後に、独自の例run()関数を、スクリプト(またはas aからではなく、Bashのコマンドプロンプトから呼び出す理由がまったく異なる) 脚本) ?

コマンドプロンプトから呼び出されたrun()は、Bashの独自のプロセス(*)によって実行されますが、スクリプトから呼び出された場合、インタラクティブなBashがttyをすでに喜んで渡している別のプロセス(-group)によって実行されます。以上。

したがって、スクリプトから、ttyの競合を回避するためにBashが設定する最後の最後の「防御」は、stdin/stdout/stderrのファイル記述子を保存および復元するという単純なよく知られたトリックによって簡単に回避されます。

(*)または、同じプロセスグループに属する新しいプロセスを生成する可能性があります。インタラクティブなBashが関数を実行するために使用する正確なアプローチを実際に調査したことはありませんが、かなりの違いはありません。

HTH

2
LL3

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

  • それはあなたの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つのいずれかが発生します。

  • コマンドがすぐに終了する場合、何もする必要がなく、fgは何もしないため、interact()はすぐに戻ります。
  • コマンドがすぐに終了しない場合、interact()は相互作用し(たとえば、5秒後にHUPをコプロセスに送信します)、fgは同じstdin/outを使用してコプロセスをフォアグラウンドに配置します最初に実行された/ err(これは_ls -l /proc/<pid>/df_で確認できます)。

    最後の3つのコマンドでの/ dev/nullへのリダイレクトは表面的なものです。 _run <command>_をcommandを単独で実行した場合とまったく同じように表示できます。

  • 4
    Colin Pearse

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

    前景と背景はシェルの概念です。

    • フォアグラウンドジョブはttyにアクセスできます。 ctrl-zを押すと一時停止できます。
    • 一時停止されたジョブは、フォアグラウンド(fg)またはバックグラウンド(bg)に移動できます。
    • ジョブは«command»&を使用してバックグラウンドで開始できます。
    • fg %jobidを使用して、バックグラウンドジョブをフォアグラウンドにすることができます
    • ジョブはプロセスであり、シェルによるその他のメタデータのヘルプです。ジョブは、それを開始したシェルからのジョブとしてのみアクセスできます。他の観点からは、それは単なるプロセスです。
    0
    ctrl-alt-delor

    Vimはバックグラウンドプロセスであり、ターミナルアクセスがないことを検出しているため、失敗します。編集ウィンドウをポップアップするのではなく、それ自体を中断します。 Vimが正常に動作するように修正したいと思います。

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

    Vimが行うことは isatty() 関数を呼び出すことで、これは端末に接続されていないことを示します。修正する唯一の方法は、vimを端末に接続することです。それには2つの方法があります。

    • Stdoutにanyリダイレクトを使用しないようにしてください。リダイレクトがある場合、最後にターミナルにリダイレクトしてしまっても、isatty()はターミナルではなくパイプをポイントし、vimはそれ自体をバックグラウンドにします。
    • 疑似ttyを使用します。はい、あなたはあなたがそれを望まないと言ったのを知っています。しかし、リダイレクトが必要な場合、疑似ttyを回避することは不可能です。
    0
    Wouter Verhelst