1

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!

4

4 に答える 4

4

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.

于 2012-11-18T04:12:09.787 に答える
2

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()
于 2012-11-17T18:53:05.123 に答える
1

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().

于 2012-11-17T18:45:24.317 に答える
0

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)
}
于 2012-11-18T20:01:32.747 に答える