Search code examples
goiogoroutine

How to set a timeout for a bufio scanner that scans in a loop?


I am using golang to start a process and monitor the output. The process will run for a long time, and I need to be able to send a signal to end it.

I have the following code which works well in most time. However, the process may have no output for some reason, and the for loop would be blocked by Scan() method and failed to receive from processFinishChan.

Is there a easy way to set a timeout for Scan() method? I tried a solution that run Scan() in another goroutine, and use another select to receive from the new goroutine and a timeout channel, but considering the outer for loop, would there be more and more goroutines that blocked by Scan?

// code that start the process...
scanner := bufio.NewScanner(stdout)

for {
    select {
    case <-processFinishChan: // send to this channel to terminate the process
        log.Println("Killing Process")
        err := cmdObject.Process.Kill()
        if err != nil {
            log.Printf("Error Killing: %v", err)
        } else {
            return
        }
    default:
        // default case, read the output of process and send to user.
        if !scanner.Scan() && scanner.Err() == nil {
            // reach EOF
            return
        }
        m := scanner.Bytes()

        WSOutChanHolder.mu.Lock()
        for _, ch := range WSOutChanHolder.data {
            ch <- m
        }
        WSOutChanHolder.mu.Unlock()
    }
}

Solution

  • Assuming stdout is the result of cmdObject.StdoutPipe(), the reader should close the reader and interrupt any ongoing reads when waiting after the process exits.

    Wait will close the pipe after seeing the command exit, so most callers need not close the pipe themselves.

    So, we need to kill the process in a separate goroutine, then Wait after killing the process to observe it happen and close the reader.

    // code that start the process...
    scanner := bufio.NewScanner(stdout)
    
    go func() {
        <-processFinishChan: // send to this channel to terminate the process
        log.Println("Killing Process")
        err := cmdObject.Process.Kill()
        if err != nil {
            log.Printf("Error Killing: %v", err)
        } 
    
        cmdObject.Wait()
    } ()
    
    for {
        // default case, read the output of process and send to user.
        if !scanner.Scan() && scanner.Err() == nil {
            // reach EOF
            return
        }
        m := scanner.Bytes()
    
        WSOutChanHolder.mu.Lock()
        for _, ch := range WSOutChanHolder.data {
            ch <- m
        }
        WSOutChanHolder.mu.Unlock()
    }