执行外部程序/脚本并检测是否需要用户输入

huangapple go评论93阅读模式
英文:

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)
}

huangapple
  • 本文由 发表于 2012年11月18日 01:29:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/13432947.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定