Search code examples
gosignalsstdout

child process receives SIGINT which should be handled only by parent process, resulting in abrupt termination of child


I am trying to manage an application (which needs to be closed with a certain procedure, in this case saving the world) in golang using stdpipes.
This is a bare-bone example of what I'm trying to achieve but I have a problem which is quite specific for me but it might be interesting also for others (maybe you can suggest how to generalize it).

I also added a function called interruptListener that creates a goroutine and manages the stop of the program when a kill signal is sent

Normal function of the script:

  • launch the minecraft server
  • waits 40 seconds and then issues through stdIn the "stop" command
    (in this case it works as expected printing all the logs about the saving process)

Test case (to demonstrate where is the problem):

  • launch the minecraft server
  • before the script issues the stop command the user send ctrl+c
    (in this case it should finish printing the logs about the saving process and then exit, but it does not... it seems that after receiving the kill signal the scanner.Scan() returns false so it just exits)

Do you have an idea of why this could be happening? what should I research to get to the solution?

I'm really lost I already spent 8+ hours with all the possible combination of codes...

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
    "os/exec"
    "os/signal"
    "strings"
    "sync"
    "syscall"
    "time"
)

var cmd *exec.Cmd

var wg sync.WaitGroup

var stdOut io.ReadCloser
var stdErr io.ReadCloser
var stdIn io.WriteCloser

func main() {
    interruptListener()

    cSplit := strings.Split("java -Xmx1024M -Xms1024M -jar server.jar nogui", " ")
    cmd = exec.Command(cSplit[0], cSplit[1:]...)
    cmd.Dir = "path/to/server/folder"
    stdOut, _ = cmd.StdoutPipe()
    stdErr, _ = cmd.StderrPipe()
    stdIn, _ = cmd.StdinPipe()
    wg.Add(2)
    go printer(stdOut)
    go printer(stdErr)
    err := cmd.Start()
    if err != nil {
        fmt.Println(err)
    }

    // test 1: wait 40 seconds for it to send the stop command and observe the output
    // test 2: in the 40 seconds (after the server has loaded press ctrl+c), there is no output
    time.Sleep(40 * time.Second)
    execute("stop")

    err = cmd.Wait()
    if err != nil {
        fmt.Println(err.Error())
    }
}

func printer(stdP io.ReadCloser) {
    defer func() {
        wg.Done()
        fmt.Println("printer is out")
    }()

    var line string

    scanner := bufio.NewScanner(stdP)

    for scanner.Scan() {
        line = scanner.Text()

        fmt.Println(line)
    }

    fmt.Printf("scanner error: %v\n", scanner.Err())
}

func execute(com string) {
    fmt.Println("sending", com, "to terminal")

    // needs to be added otherwise the virtual "enter" button is not pressed
    com += "\n"

    // write to cmd
    _, err := stdIn.Write([]byte(com))
    if err != nil {
        fmt.Println(err.Error())
    }
}

func interruptListener() {
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
    go func() {
        select {
        case <-c:
            execute("stop")
            wg.Wait()
            os.Exit(0)
        }
    }()
}

Solution

  • thanks to the people that answered in the comments this is an acceptable solution (still not perfect as explained in the comments)

    just add this few lines before executing cmd.Start():

        // launch as new process group so that signals (ex: SIGINT) are not sent also the the child process
        cmd.SysProcAttr = &syscall.SysProcAttr{
            CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, // windows
            // Setpgid: true, // linux
        }