Shell

通過前台終端訪問在後台執行命令

  • March 31, 2019

我正在嘗試創建一個可以執行任意命令的函式,與子程序互動(省略細節),然後等待它退出。如果成功,打字run <command>將表現得就像一個裸露的<command>.

如果我不與子程序互動,我會簡單地寫:

run() {
   "$@"
}

但是因為我需要在它執行時與之互動,所以我使用coprocand進行了更複雜的設置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 "$@"在“前台”中執行並且父shell成為“後台”?“與孩子互動”部分既不讀取也不寫入終端,所以我不需要它在前台執行。我很高興將 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; }

我得到與 just 相同的行為&。但在我的案例中,我使用的是 FIFO coproc 設置,我認為最好不要過度簡化問題,以防cmd &和之間存在差異coproc cmd

為什麼要避免 ptys?

run()可以在自動化環境中使用。如果它用於管道或重定向,則不會有任何終端可以模擬;建立一個 pty 將是一個錯誤。

為什麼不使用期望?

我不是想自動化 vim,向它發送任何輸入或類似的東西。

在您的範常式式碼中,一旦 Vim 嘗試從 tty 讀取,或者可能為其設置一些屬性,它就會通過 SIGTTIN 信號被核心掛起。

這是因為互動式 shell 將其生成在不同的程序組中,而(尚未)將 tty 移交給該組,即將其置於“後台”。這是正常的作業控制行為,正常交出 tty 的方式是使用fg. 然後當然是外殼進入後台並因此被掛起。

當 shell 是互動式的時,所有這些都是故意的,否則就好像你被允許在提示符下繼續輸入命令,例如使用 Vim 編輯文件。

run您可以通過將整個函式改為腳本來輕鬆解決這個問題。這樣,它將由互動式 shell 同步執行,而不會與 tty 競爭。如果您這樣做,您自己的範常式式碼已經完成了您所要求的一切,包括您的run (然後是腳本)和 coproc 之間的並發互動。

如果不能在腳本中使用它,那麼您可能會看到 Bash 以外的 shell 是否允許更好地控制將互動式 tty 傳遞給子程序。我個人不是更高級外殼的專家。

如果你真的必須使用 Bash 並且真的必須通過一個由互動式 shell 執行的函式來擁有這個功能,那麼恐怕唯一的出路就是用一種允許你訪問 tcsetpgrp(3) 的語言製作一個幫助程序和 sigprocmask(2)。

目的是在子程序(你的 coproc)中做在父程序(互動式 shell)中沒有做的事情,以便強行抓取 tty。

請記住,儘管這被明確認為是不好的做法。

但是,如果您在孩子仍然擁有它時努力不使用父 shell 中的 tty,那麼可能不會造成任何傷害。“不要使用”我的意思是不要不要echo去/從 tty,當然不要執行其他可能在孩子仍在執行時訪問 tty 的程序。printf``read

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 中的等價物只會更長一點。

這個幫助程序需要由你的 coproc 和一個 exec 執行,如下所示:

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 的所有範例案例,通過我的主要互動式 shell 獲取函式並run從命令提示符執行。

如果您需要從 coproc 執行腳本,則可能需要使用 執行它bash -i,否則 Bash 可能會從管道或 stdin/stdout/stderr 上的 /dev/null 開始,而不是繼承 Python 腳本獲取的 tty。此外,無論您在 coproc 中(或在其下方)執行什麼,都最好不要呼叫額外run()的 s。(實際上不確定,沒有測試過這種情況,但我想它至少需要仔細封裝)。


為了回答您的具體(子)問題,我需要介紹一些理論。

每個 tty 都有一個,也只有一個,所謂的“會話”。(雖然不是每個會話都有一個 tty,例如典型的守護程序的情況,但我想這與這裡無關)。

基本上,每個會話都是程序的集合,並通過與“會話領導者”的 pid 對應的 id 來標識。因此,“會話領導者”是屬於該會話的那些程序之一,並且正是第一個啟動該特定會話的程序。

特定會話的所有程序(領導者和非領導者)都可以訪問與它們所屬的會話關聯的 tty。但是這裡有第一個區別:在任何特定時刻只有一個程序可以是所謂的“前台程序”,而在此期間的所有其他程序都是“後台程序”。“前台”程序可以自由訪問 tty。相反,如果“後台”程序敢於訪問他們的 tty,就會被核心中斷。並不是說根本不允許後台程序,而是核心向它們發出信號,表明“輪不到它們說話”。

所以,去你的具體問題:

“前景”和“背景”究竟是什麼意思?

“前台”的意思是“在那一刻合法地使用 tty”

“背景”的意思是“當時沒有使用 tty”

或者,換句話說,再次引用您的問題:

我想知道前台和後台程序的區別

對 tty 的合法訪問。

是否可以在父程序繼續執行時將後台程序帶到前台?

一般而言:後台程序(無論是否為父程序)確實會繼續執行,只是如果它們嘗試訪問其 tty,它們會(預設情況下)停止。(注意:他們可以忽略或以其他方式處理這些特定信號(SIGTTIN 和 SIGTTOU),但通常情況並非如此,因此預設處置是暫停程序

但是:在互動式 shell 的情況下,它是選擇在它傳遞之後暫停自身的 shell(在 wait(2) 或 select(2) 或任何阻塞系統呼叫中,它認為這是最合適的那個)將 tty 轉移到其背景中的一個孩子。

由此,您的特定問題的確切答案是:使用 shell 應用程序時,這取決於您使用的 shell 是否為您提供了一種方法(內置命令或什麼),在發出fg命令後不會自行停止。AFAIK Bash 不允許您這樣選擇。我不知道其他 shell 應用程序。

有什麼cmd &不同cmd

在 a 上cmd,Bash 生成一個屬於它自己會話的新程序,將 tty 交給它,然後讓自己處於等待狀態。

在 a 上cmd &,Bash 生成一個屬於它自己會話的新程序。

如何將前台控制權交給子程序

一般而言:您需要使用 tcsetpgrp(3)。實際上,這可以由父母或孩子完成,但建議的做法是由父母完成。

在 Bash 的特定情況下:您發出fg命令,並且通過這樣做,Bash 使用 tcsetpgrp(3) 來支持該孩子,然後將自己置於等待狀態。


從這裡,您可能會發現另一個有趣的見解是,實際上,在相當新的 UNIX 系統上,會話的程序之間還有一個額外的層次結構:所謂的“程序組”。

這是相關的,因為到目前為止我所說的“前台”概念實際上並不局限於“單個程序”,而是擴展到“單個程序組”。

也就是說:“前台”的常見常見情況是只有一個程序可以合法訪問 tty,但核心實際上允許更高級的情況,即一整組程序(仍然屬於同一個程序) session) 可以合法訪問 tty。

事實上,為了移交 tty “前景”而呼叫的函式被命名為tcsetpgrp,而不是(例如)tcsetpid 之類的東西,這並不是錯誤的。

然而,實際上,顯然 Bash 並沒有利用這種更高級的可能性,而且是故意的。

不過,您可能想利用它。這完全取決於您的具體應用。

作為程序分組的一個實際範例,我可以在上面的解決方案中選擇使用“重新獲得前台程序組”方法,而不是“移交前台組”方法。

也就是說,我可以讓 Python 腳本使用 os.setpgid() 函式(它包裝 setpgid(2) 系統呼叫),以便將程序重新分配給目前前台程序組(可能是 shell 程序本身,但是不一定如此),從而重新獲得了 Bash 尚未移交的前台狀態。

然而,這對於最終目標來說是一種相當間接的方式,並且可能還會產生不良的副作用,因為有幾個與 tty 控制無關的程序組的其他用途最終可能會涉及您的 coproc。例如,UNIX 信號通常可以傳遞給整個程序組,而不是單個程序。

run()最後,為什麼從 Bash 的命令提示符而不是從腳本(或作為腳本)呼叫您自己的範例函式會如此不同?

因為run()從命令提示符呼叫是由 Bash 自己的程序 (*) 執行的,而當從腳本呼叫時,它是由互動式 Bash 已經愉快地將 tty 移交給的不同程序 (-group) 執行的。

因此,從腳本來看,Bash 為避免與 tty 競爭而實施的最後一個“防禦”很容易被保存和恢復 stdin/stdout/stderr 的文件描述符這一眾所周知的簡單技巧所繞過。

() 或者它可能會產生一個屬於它自己的同一*程序組的新程序。實際上,我從未研究過互動式 Bash 使用什麼確切方法來執行函式,但它在 tty 方面沒有任何區別。

高溫高壓

我添加了程式碼,以便:

  • 它適用於您的三個範例和

  • 互動先於等待。

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根據需要更改並發作業),並且在正常情況下會發生以下兩種情況之一:

  • 如果命令立即退出interact()將立即返回,因為無事可做並且fg將什麼也不做。

  • 如果命令沒有立即退出interact()可以互動(例如,在 5 秒後向協程序發送 HUP)並且fg將使用最初執行的相同 stdin/out/err 將協程序置於前台(你可以檢查這個與ls -l /proc/<pid>/df)。

最後三個命令中對 /dev/null 的重定向是裝飾性的。它們允許run <command>看起來與您自己執行時完全相同command

引用自:https://unix.stackexchange.com/questions/507786