How can one know, that the process started with `cmd.Start` has been finished?

Problem: I start a system command using cmd.Start. It’s a long-running process, and I check its output for monitoring progress. How can I check, whether the process has been completed/finished?

I’m not calling cmd.Wait, since I do not want to wait until the process finishes (Wait blocks until the process is finished, and I don’t want any blocking.)

I have tried to send the signal 0 to the process, but even after all the process’ output is completed, it returns still the same value.

Hi, I’m just guessing, when you don’t like the blocking, why don’t you let it cmd.Wait in another goroutine?

I guess like this: First make the waiter goroutine, then monitor the process, then wait on a channel that the waiter sends to.

Or maybe, instead of the waiter goroutine, do the monitoring in the extra goroutine and serialize dataflow coming from there.

Anyway I didn’t fully understand the fundamental problem.
Kind regards

That’s right, the problem is not understood.

The point is: blocking (cmd.Wait) = waiting until the external process is 100% complete. What I want is information, whether the process has made 10% progress, then 30% progress, and so on.

I got a solution, and it’s quite simple: I use ps -q <pid> -o state --no-headers in order to get the process status, and then simply wait, until the status becomes ‘Z’ (Z = Zombie, the process has been completed, but not finished by it’s parent process). In the meantime, I can get the progress from running process’ stdout/stderr.

I just thought there was a simpler solution, using the cmd process properties/functions…

I got no idea so I asked Copilot and it basically told me that if you read the exit status, using waitpid, then the process shouldn’t be in the Z state any longer than necessary (depends on what freeing resources means I guess?)

It suggested that code

        var ws syscall.WaitStatus
        _, err := syscall.Wait4(int(pid), &ws, 0, nil)
        if err != nil {
            fmt.Println("Error during waitpid:", err)
            return
        }
        if ws.Exited() {
            fmt.Println("Child exited with status:", ws.ExitStatus())
        }

That blocks, so it is supposed to happen in a goroutine I guess, and that should be the easiest way and should not make it linger according to the AI friend.

Though, I’m sorry, I am on Windows currently and I also don’t know what problems you are confronted with. Last time I used Linux and multithreading (not even processes) made me reconsidering my choice after some while, I remember. Maybe look for ready made code that does monitoring processes in Go.

Anyway, didn’t fully understand it (it lingers in Z state? it’s slow and unpredictable?), but good luck :slight_smile:
Kind regards

Imagine you start a long-running process. It may take several minutes to complete. The process provides output (stdout/stderr), that you can analyse, and provide your user some info/report on the process’ progress (eg. how much job has been already done). You want (1) to monitor the process, and to provide such progress status, and (2) detect when the process has been finished (in any way: it’s no longer active). That’s all I want/need to do.

I made a simple experiment. I wrote the following Bash script:

for i in `seq 10`; do
  sleep 5
  echo $i
done

It’s a simple process, that counts up to 10, with some delay before counting up each number. It simulates a long-running process, which will be completed without any interruptions.

Now, I wanted a Go program, that:
(1) starts the process (the Bash script),
(2) reports the last number counted,
(3) detects, that the started process has been completed.

And all of this, without any blocking, since reporting can be send to other services/goroutines.

I ended up with the following code:

func processStatus(pid int) string {
  str_pid := strconv.Itoa(pid)
  cmd := exec.Command("ps", "-q", str_pid, "-o", "state", "--no-headers")
  var out strings.Builder
  cmd.Stdout = &out
  err := cmd.Run()
  if err != nil {
    log.Fatal(err)
  }
  return out.String()
}

func main() {
  // set up the command
  cmd := exec.Command("./test1.sh")
  var out strings.Builder
  cmd.Stdout = &out

  // start the command
  err := cmd.Start()
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println("Command has been started...")
  
  // wait for the command/process to finish
  for strings.HasPrefix(processStatus(cmd.Process.Pid), "S") {
    fmt.Println("...command still running")

    // report the progress
    arr := strings.Split(out.String(), "\n")
    if len(arr) > 1 {
      fmt.Println( arr[ len(arr) - 2 ] )
    } else if len(arr) == 1 {
      fmt.Println( arr[0] )
    }

    // just to clean up the output a little
    out.Reset()

    // we don't know 
    time.Sleep(5 * time.Second)
  }

  err = cmd.Wait()
  if err != nil {
    log.Fatal(err)
  }

What I found out, is the fact, that if you start the command using the cmd.Start(), after it completes, it doesn’t create the cmd.ProcessState struct/field. So I had to check for the process status with the ps system command.

Dunno, but isn’t that a race condition when you just String() the string builder while the other goroutine/process writes to it? Normally you would read from the pipe, which is no string builder, and thus the os makes sure you can read and the process can write without a problem.

Either you do that or what copilot suggests (?), which is a safe string builder that serializes Write() and String(). Seems kind of reasonable?

type SafeStringBuilder struct {
    mu       sync.RWMutex
    builder  strings.Builder
}

func (sb *SafeStringBuilder) Write(s string) {
    sb.mu.Lock()
    defer sb.mu.Unlock()
    sb.builder.WriteString(s)
}

func (sb *SafeStringBuilder) String() string {
    sb.mu.RLock()
    defer sb.mu.RUnlock()
    return sb.builder.String()
}

I’m more used to calling OS functions on Windows, so, I can’t really help, but the code doesn’t look very correct.

Best regards

This should be quite easy. When the process finishes it should automatically close its stdout channel. So if you just consume all of the process output until you reach EOF, you know the process has exited. As long as STDOUT is still open the process is still running. (You may have to explicitly close STDIN first so the process can exit)