Golang : Daemonizing a simple web server process example




Problem:

You use the previous tutorial on how to put your Golang program into background but somehow found that using the -d=true parameter is troublesome.

Not only that, you want to make your Golang program similar to launching a daemon process on Unix/Linux OS. Such as having different behavior with start, install, reload or stop parameters.

You also want to store the process ID into a file in /tmp folder.

How to do that?

Solution:

We will use os/exec.Command() function to launch a child process of the parent program but with a different parameter. Typical Unix/Linux daemon process supports start and stop parameters. In our program start block, it will launch a daemon process but with main parameter instead. It can be kinda mysterious operation for some readers initially.

To get a better understanding, run the code below.


 package main

 import (
 "fmt"
 "io/ioutil"
 "log"
 "net/http"
 "os"
 "os/exec"
 "strconv"
 "strings"
 "os/signal"
 "syscall"
 )

 var PIDFile = "/tmp/daemonize.pid"

 func savePID(pid int) {

 file, err := os.Create(PIDFile)
 if err != nil {
 log.Printf("Unable to create pid file : %v\n", err)
 os.Exit(1)
 }

 defer file.Close()

 _, err = file.WriteString(strconv.Itoa(pid))

 if err != nil {
 log.Printf("Unable to create pid file : %v\n", err)
 os.Exit(1)
 }

 file.Sync() // flush to disk

 }

 func SayHelloWorld(w http.ResponseWriter, r *http.Request) {
 html := "Hello World"

 w.Write([]byte(html))
 }

 func main() {
 if len(os.Args) != 2 {
 fmt.Printf("Usage : %s [start|stop] \n ", os.Args[0]) // return the program name back to %s
 os.Exit(0) // graceful exit
 }

 if strings.ToLower(os.Args[1]) == "main" {

 // Make arrangement to remove PID file upon receiving the SIGTERM from kill command
 ch := make(chan os.Signal, 1)
 signal.Notify(ch, os.Interrupt, os.Kill, syscall.SIGTERM)

 go func() {
 signalType := <-ch
 signal.Stop(ch)
 fmt.Println("Exit command received. Exiting...")

 // this is a good place to flush everything to disk
 // before terminating.
 fmt.Println("Received signal type : ", signalType)

 // remove PID file
 os.Remove(PIDFile)

 os.Exit(0)

 }()

 mux := http.NewServeMux()
 mux.HandleFunc("/", SayHelloWorld)
 log.Fatalln(http.ListenAndServe(":8080", mux))
 }

 if strings.ToLower(os.Args[1]) == "start" {

 // check if daemon already running.
 if _, err := os.Stat(PIDFile); err == nil {
 fmt.Println("Already running or /tmp/daemonize.pid file exist.")
 os.Exit(1)
 }

 cmd := exec.Command(os.Args[0], "main")
 cmd.Start()
 fmt.Println("Daemon process ID is : ", cmd.Process.Pid)
 savePID(cmd.Process.Pid)
 os.Exit(0)

 }

 // upon receiving the stop command
 // read the Process ID stored in PIDfile
 // kill the process using the Process ID
 // and exit. If Process ID does not exist, prompt error and quit

 if strings.ToLower(os.Args[1]) == "stop" {
 if _, err := os.Stat(PIDFile); err == nil {
 data, err := ioutil.ReadFile(PIDFile)
 if err != nil {
 fmt.Println("Not running")
 os.Exit(1)
 }
 ProcessID, err := strconv.Atoi(string(data))

 if err != nil {
 fmt.Println("Unable to read and parse process id found in ", PIDFile)
 os.Exit(1)
 }

 process, err := os.FindProcess(ProcessID)

 if err != nil {
 fmt.Printf("Unable to find process ID [%v] with error %v \n", ProcessID, err)
 os.Exit(1)
 }
 // remove PID file
 os.Remove(PIDFile)

 fmt.Printf("Killing process ID [%v] now.\n", ProcessID)
 // kill process and exit immediately
 err = process.Kill()

 if err != nil {
 fmt.Printf("Unable to kill process ID [%v] with error %v \n", ProcessID, err)
 os.Exit(1)
 } else {
 fmt.Printf("Killed process ID [%v]\n", ProcessID)
 os.Exit(0)
 }

 } else {

 fmt.Println("Not running.")
 os.Exit(1)
 }
 } else {
 fmt.Printf("Unknown command : %v\n", os.Args[1])
 fmt.Printf("Usage : %s [start|stop]\n", os.Args[0]) // return the program name back to %s
 os.Exit(1)
 }

 }

First, use go build instead of go run

go build daemonize.go

./daemonize start

Sample output

Daemon process ID is : 2050

at this stage, the program exited and running in the background.

ps -ef | grep daemon

will show the process running at the background.

now, point your browser to localhost:8080 and you will see the expected output of "Hello World"

finally, execute

./daemonize stop

Killing process ID [2050] now.

Killed process ID [2050]

and this will stop the background process.

In case the process is killed by kill command instead of ./daemonize stop. It will still be able to remove the /tmp/daemonize.pid file


NOTE:

Golang does not have support for forking and even though I've managed to daemonize a Golang program via the traditional method... it is kinda useless because nothing else can be executed after os.Exit(0). Why? See the explanation in https://habrahabr.ru/post/187668/ (in Russian language)

Below is the code example for daemonizing Golang program in traditional way that you can try out just for fun and perhaps learn something. Happy coding!

 package main

 import (
 "fmt"
 "io/ioutil"
 "log"
 "net/http"
 "os"
 "runtime"
 "strconv"
 "strings"
 "syscall"
 )

 var PIDFile = "/tmp/daemonize.pid"

 func savePID(pid string) {

 file, err := os.Create(PIDFile)
 if err != nil {
 log.Printf("Unable to create pid file : %v\n", err)
 os.Exit(1)
 }

 defer file.Close()

 _, err = file.WriteString(pid)

 if err != nil {
 log.Printf("Unable to create pid file : %v\n", err)
 os.Exit(1)
 }

 file.Sync() // flush to disk

 }

 func daemonize(stdin, stdout, stderr string) {

 var ret, ret2 uintptr
 var syserr syscall.Errno

 darwin := runtime.GOOS == "darwin"

 // already a daemon
 if syscall.Getppid() == 1 {
 fmt.Println("Already a daemon.")
 os.Exit(1)
 }

 // detaches from the parent process
 // Golang does not have os.Fork()...
 // so we will use syscall.SYS_FORK

 // ret is the child process ID
 ret, ret2, syserr = syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
 if syserr != 0 {
 fmt.Println("syscall.SYS_FORK error number: %v", syserr)
 os.Exit(1)
 }

 // failure
 if ret2 < 0 {
 fmt.Println("syscall.SYS_FORK failed")
 os.Exit(-1)
 }

 // handle exception for darwin
 if darwin && ret2 == 1 {
 ret = 0
 }

 // if we got a good PID, then we save the child process ID
 // to /tmp/daemonize.pid
 // and exit the parent process.
 if ret > 0 {
 log.Println("Detached process from parent. Child process ID is  : ", ret)

 // convert uintptr(pointer) value to string
 childPID := fmt.Sprint(ret)
 savePID(childPID)

 os.Chdir("/")

 // replace file descriptors for stdin, stdout and stderr
 // default value is /dev/null

 infile, err := os.OpenFile(stdin, os.O_RDWR, 0)
 if err == nil {
 infileDescriptor := infile.Fd()
 syscall.Dup2(int(infileDescriptor), int(os.Stdin.Fd()))
 }

 // remove the output files
 os.Remove(stdout)
 os.Remove(stderr)

 // with correct permissions
 outfile, err := os.OpenFile(stdout, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
 if err == nil {
 outfileDescriptor := outfile.Fd()
 syscall.Dup2(int(outfileDescriptor), int(os.Stdout.Fd()))
 }

 errfile, err := os.OpenFile(stderr, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
 if err == nil {
 errfileDescriptor := errfile.Fd()
 syscall.Dup2(int(errfileDescriptor), int(os.Stderr.Fd()))
 }

 // debug
 //syscall.Write(int(outfile.Fd()), []byte("test output to outfile.\n"))
 //syscall.Write(int(os.Stdout.Fd()), []byte("test output to os.Stdout.\n"))

 os.Exit(0)

 // debug
 // will not work after os.Exit(0)
 //syscall.Write(int(os.Stdout.Fd()), []byte("test output to os.Stdout after os.Exit.\n"))
 }

 // Change the file mode mask
 _ = syscall.Umask(0)

 // create a new SID for the child process(relinquish the session leadership)
 // i.e we do not want the child process to die after the parent process is killed
 // see http://unix.stackexchange.com/questions/240646/why-we-use-setsid-while-daemonizing-a-process
 // just for fun, try commenting out the Setsid() lines and run this program. The daemonized child will die
 // together with the parent process.

 s_ret, s_errno := syscall.Setsid()
 if s_errno.Error() != strconv.Itoa(0) {
 log.Printf("Error: syscall.Setsid errno: %d", s_errno)
 // we already exit the program....
 }

 if s_ret < 0 {
 log.Printf("Error: Unable to set new SID")
 // we already exit the program....
 }

 }

 func main() {
 if len(os.Args) != 2 {
 fmt.Printf("Usage : %s [start|stop] \n ", os.Args[0]) // return the program name back to %s
 os.Exit(0) // graceful exit
 }

 if strings.ToLower(os.Args[1]) == "start" {

 // check if daemon already running.
 if _, err := os.Stat(PIDFile); err == nil {
 fmt.Println("Already running or /tmp/daemonize.pid file exist.")
 os.Exit(1)
 }

 daemonize("/dev/null", "/tmp/daemonize.log", "/tmp/daemonize.log")

 }

 // upon receiving the stop command
 // read the Process ID stored in PIDfile
 // kill the process using the Process ID
 // and exit. If Process ID does not exist, prompt error and quit

 if strings.ToLower(os.Args[1]) == "stop" {
 if _, err := os.Stat(PIDFile); err == nil {
 data, err := ioutil.ReadFile(PIDFile)
 if err != nil {
 fmt.Println("Not running")
 os.Exit(1)
 }
 ProcessID, err := strconv.Atoi(string(data))

 if err != nil {
 fmt.Println("Unable to read and parse process id found in ", PIDFile)
 os.Exit(1)
 }

 process, err := os.FindProcess(ProcessID)

 if err != nil {
 fmt.Printf("Unable to find process ID [%v] with error %v \n", ProcessID, err)
 os.Exit(1)
 }
 // remove PID file
 os.Remove(PIDFile)

 fmt.Printf("Killing process ID [%v] now.\n", ProcessID)
 // kill process and exist immediately
 err = process.Kill()

 if err != nil {
 fmt.Printf("Unable to kill process ID [%v] with error %v \n", ProcessID, err)
 os.Exit(1)
 } else {
 fmt.Printf("Killed process ID [%v]\n", ProcessID)
 os.Exit(0)
 }

 } else {

 fmt.Println("Not running.")
 os.Exit(1)
 }
 } else {
 fmt.Printf("Unknown command : %v\n", os.Args[1])
 fmt.Printf("Usage : %s [start|stop]\n", os.Args[0]) // return the program name back to %s
 os.Exit(1)
 }

 }

References:

https://habrahabr.ru/post/187668/

https://gist.github.com/wofeiwo/3634357

https://www.socketloop.com/tutorials/golang-intercept-and-process-unix-signals-example

https://www.socketloop.com/tutorials/golang-get-command-line-arguments

https://www.socketloop.com/tutorials/golang-check-if-a-file-exist-or-not

  See also : Golang : Terminate-stay-resident or daemonize your program?





By Adam Ng

IF you gain some knowledge or the information here solved your programming problem. Please consider donating to the less fortunate or some charities that you like. Apart from donation, planting trees, volunteering or reducing your carbon footprint will be great too.


Advertisement