Using Go lang with Linux (ubuntu).
A service (running as deamon, can be found with systemctl
but not ps
for example) runs a command (see code) that runs an executable (not sure if worth mentioning but – they are actually the same executable in sense of code).
The services are managed with the github.com/kardianos/service
package.
As part of the code that the child service runs – i need to stop the parent service (service.control("stop")
). At this oment the child process fails as well, instead of proceeding with the rest of the flow.
I tried:
-
set session id (
Setsid=true
) -
set group id (
Setpgid-true
) -
call
Release()
on the underlying process after callingStart()
-
combinations of the above (as suggested here https://groups.google.com/g/golang-nuts/c/Jx-ZsdQIMJA)
Not sure what other options i have and would love your advice.
code of the function that inits the process (note: this is a mixture of what i tried. i also tried them separately etc):
func ExecuteCommandAsync(command, args string, detach bool) error {
logger.Notify("Running cli command:", command, args)
cmd := exec.Command(command, args)
if detach {
logger.Notify("Detaching from parent process")
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true, // new session id prevents sigkill to parent kill the child
//Setpgid: true, // new group id prevents sigkill to parent kill the child
}
}
err := cmd.Start()
if err != nil {
logger.Error("Failed running cli command:", command, err)
return err
}
if detach {
logger.Notify("Detached from parent process - releasing child process")
err = cmd.Process.Release()
if err != nil {
return err
}
} else {
go func() {
logger.Notify("Waiting for cli command to finish")
waitErr := cmd.Wait()
if waitErr != nil {
logger.Error("Error waiting for cli command to finish:", waitErr)
} else {
logger.Notify("Command finished successfully")
}
}()
}
return nil
}
code of the function that stops the service:
func systemStopService(s service.Service) error {
if s == nil {
return errors.New("stopService - service is nil")
}
logger.Notify(s.String() + " Stopping service")
err := service.Control(s, "stop")
if err == nil {
logger.Notify(s.String() + " Service stopped")
} else if strings.Contains(err.Error(), "service does not exist") ||
strings.Contains(err.Error(), "service has not been started") {
logger.Notify(s.String() + err.Error())
err = nil // not an error
} else {
logger.Error(s.String()+" Failed to stop service", err)
}
return err
}
2
Answers
The issue was that other than relating to the parent and being a part of its group process id - it is also maintained by the control group (
cgroup
) and signals are sent based on that. when theSIGHUP
fails (if we handled it) - the mellowSIGKILL
is activated and we have no escape.I had 2 solutions:
1 - I could try to change the cgroup as part of my code (there are a few libs that support that)
2 - find someone to run the child process instead of the parent - so they are not related any more. this was achieved using the
cmd := exec.Command("systemd-run", args...)
(see: https://www.freedesktop.org/software/systemd/man/latest/systemd-run.html).I chose the latter - as it looked cleaner - and it worked fine (other than that not all linux OSs use systemd, so they need to be tailored)
The child process receives a SIGHUP signal when the process-leader process exits. If that signal is neither ignored, not handles, by default the child exist.
You can also evade the SIGHUP by starting a new session for the daemon.
In C you might call setsid (man 2 setsid).