英文:
Exec external program/script and detect if it requests user input
问题
给定以下示例:
// test.go
package main
import (
"fmt"
"os/exec"
)
func main() {
cmd := exec.Command("login")
in, _ := cmd.StdinPipe()
in.Write([]byte("user"))
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Println("error:", err)
}
fmt.Printf("%s", out)
}
我如何检测进程不会完成,因为它正在等待用户输入?
我正在尝试能够运行任何脚本,但如果由于某种原因它尝试从标准输入读取,则中止它。
谢谢!
英文:
given the following example:
// test.go
package main
import (
"fmt"
"os/exec"
)
func main() {
cmd := exec.Command("login")
in, _ := cmd.StdinPipe()
in.Write([]byte("user"))
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Println("error:", err)
}
fmt.Printf("%s", out)
}
How can I detect that the process is not going to finish, because it is waiting for user input?
I'm trying to be able to run any script, but abort it if for some reason it tries to read from stdin.
Thanks!
答案1
得分: 4
检测进程是否无法完成是一个困难的问题。实际上,这是计算机科学中经典的“不可解”问题之一:停机问题。
通常情况下,当你调用exec.Command并且不会传递任何输入时,它会导致程序从操作系统的空设备中读取(请参阅exec.Cmd字段中的文档)。在你的代码(以及下面的代码)中,你明确地创建了一个管道(尽管你应该检查StdinPipe
的错误返回,以防它没有正确创建),所以你应该随后调用in.Close()
。无论哪种情况,子进程都会收到EOF并且应该在自己退出后清理。
为了帮助处理不正确处理输入或其他原因导致自己陷入困境的进程,通常的解决方案是使用超时。在Go中,你可以使用goroutine来实现:
// 设置超时时间
const CommandTimeout = 5 * time.Second
func main() {
cmd := exec.Command("login")
// 设置输入
in, err := cmd.StdinPipe()
if err != nil {
log.Fatalf("无法为STDIN创建管道:%s", err)
}
// 写入输入并关闭
go func() {
defer in.Close()
fmt.Fprintln(in, "user")
}()
// 捕获输出
var b bytes.Buffer
cmd.Stdout, cmd.Stderr = &b, &b
// 启动进程
if err := cmd.Start(); err != nil {
log.Fatalf("无法启动命令:%s", err)
}
// 如果进程没有及时退出,则终止进程
defer time.AfterFunc(CommandTimeout, func() {
log.Printf("命令超时")
cmd.Process.Kill()
}).Stop()
// 等待进程完成
if err := cmd.Wait(); err != nil {
log.Fatalf("命令失败:%s", err)
}
// 打印输出
fmt.Printf("输出:\n%s", b.String())
}
在上面的代码中,实际上有三个主要的goroutine:主goroutine生成子进程并等待其退出;一个计时器goroutine在后台发送,如果进程没有及时停止,则会终止进程;还有一个goroutine,在程序准备好读取输出时将其写入程序中。
英文:
Detecting that the process is not going to finish is a difficult problem. In fact, it is one of the classic "unsolvable" problems in Computer Science: the Halting Problem.
In general, when you are calling exec.Command and will not be passing it any input, it will cause the program to read from your OS's null device (see documentation in the exec.Cmd fields). In your code (and mine below), you explicitly create a pipe (though you should check the error return of StdinPipe
in case it is not created correctly), so you should subsequently call in.Close()
. In either case, the subprocess will get an EOF and should clean up after itself and exit.
To help with processes that don't handle input correctly or otherwise get themselves stuck, the general solution is to use a timeout. In Go, you can use goroutines for this:
// Set your timeout
const CommandTimeout = 5 * time.Second
func main() {
cmd := exec.Command("login")
// Set up the input
in, err := cmd.StdinPipe()
if err != nil {
log.Fatalf("failed to create pipe for STDIN: %s", err)
}
// Write the input and close
go func() {
defer in.Close()
fmt.Fprintln(in, "user")
}()
// Capture the output
var b bytes.Buffer
cmd.Stdout, cmd.Stderr = &b, &b
// Start the process
if err := cmd.Start(); err != nil {
log.Fatalf("failed to start command: %s", err)
}
// Kill the process if it doesn't exit in time
defer time.AfterFunc(CommandTimeout, func() {
log.Printf("command timed out")
cmd.Process.Kill()
}).Stop()
// Wait for the process to finish
if err := cmd.Wait(); err != nil {
log.Fatalf("command failed: %s", err)
}
// Print out the output
fmt.Printf("Output:\n%s", b.String())
}
In the code above, there are actually three main goroutines of interest: the main goroutine spawns the subprocess and waits for it to exit; a timer goroutine is sent off in the background to kill the process if it's not Stopped in time; and a goroutine that writes the output to the program when it's ready to read it.
答案2
得分: 2
尽管这样做不能让你“检测”试图从stdin读取的程序,但我会关闭stdin。这样,子进程在尝试读取时只会收到EOF。大多数程序知道如何处理关闭的stdin。
// 所有错误处理都被排除在外
cmd := exec.Command("login")
in, _ := cmd.StdinPipe()
cmd.Start()
in.Close()
cmd.Wait()
不幸的是,这意味着你不能使用合并的输出,以下代码可以让你做同样的事情。它需要你导入bytes
包。
var buf = new(bytes.Buffer)
cmd.Stdout = buf
cmd.Stderr = buf
在cmd.Wait()
之后,你可以这样做:
out := buf.Bytes()
英文:
Although this would not allow you to "detect" the program trying to read from stdin, I would just close stdin. This way, the child process will just receive an EOF when it tried to read. Most programs know how to handle a closed stdin.
// All error handling excluded
cmd := exec.Command("login")
in, _ := cmd.StdinPipe()
cmd.Start()
in.Close()
cmd.Wait()
Unfortunately, this means you can't use combined output, the following code should allow you to do the same thing. It requires you to import the bytes
package.
var buf = new(bytes.Buffer)
cmd.Stdout = buf
cmd.Stderr = buf
After cmd.Wait()
, you can then do:
out := buf.Bytes()
答案3
得分: 1
我认为解决方案是通过适当调整Cmd.Stdin并在之后运行它,而不是使用CombinedOutput()来运行子进程并关闭stdin。
英文:
I think the solution is to run the child process with closed stdin - by adjusting the Cmd.Stdin appropriately and then Runinng it afterwards instead of using CombinedOutput().
答案4
得分: 0
最后,我将实现Kyle Lemons的答案,并强制新进程拥有自己的会话,而不附加终端,以便执行的命令意识到没有终端可读取。
// test.go
package main
import (
"log"
"os/exec"
"syscall"
)
func main() {
cmd := exec.Command("./test.sh")
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
out, err := cmd.CombinedOutput()
if err != nil {
log.Fatal("error:", err)
}
log.Printf("%s", out)
}
英文:
Finally, I'm going to implement a combination of Kyle Lemons answer and forcing the new process have it's own session without a terminal attached to it, so that the executed comand will be aware that there is no terminal to read from.
// test.go
package main
import (
"log"
"os/exec"
"syscall"
)
func main() {
cmd := exec.Command("./test.sh")
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
out, err := cmd.CombinedOutput()
if err != nil {
log.Fatal("error:", err)
}
log.Printf("%s", out)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论