golang进阶 后台进程的启动和停止


原文链接: golang进阶 后台进程的启动和停止

https://github.com/tim1020/godaemon

package godaemon

import (
	//"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"strconv"
	"syscall"
	"time"
)

var (
	TimeDeadLine = 10 * time.Second
	srv          *server
	appName      string
	pidFile      string
	pidVal       int
)

//improvement http.Server
type server struct {
	http.Server
	listener *listener
	cm       *ConnectionManager
}

//用来重载net.Listener的方法
type listener struct {
	net.Listener
	server *server
}

func init() {
	file, _ := filepath.Abs(os.Args[0])
	appPath := filepath.Dir(file)
	appName = filepath.Base(file)
	pidFile = appPath + "/" + appName + ".pid"
	if os.Getenv("__Daemon") != "true" { //master
		cmd := "start" //缺省为start
		if l := len(os.Args); l > 1 {
			cmd = os.Args[l-1]
		}
		switch cmd {
		case "start":
			if isRunning() {
				log.Printf("[%d] %s is running\n", pidVal, appName)
			} else { //fork daemon进程
				if err := forkDaemon(); err != nil {
					log.Fatal(err)
				}
			}
		case "restart": //重启:
			if !isRunning() {
				log.Printf("%s not running\n", appName)
			} else {
				log.Printf("[%d] %s restart now\n", pidVal, appName)
				restart(pidVal)
			}
		case "stop": //停止
			if !isRunning() {
				log.Printf("%s not running\n", appName)
			} else {

				syscall.Kill(pidVal, syscall.SIGTERM) //kill
			}
		case "-h":
			fmt.Printf("Usage: %s start|restart|stop\n", appName)
		default: //其它不识别的参数
			return //返回至调用方
		}
		//主进程退出
		os.Exit(0)
	}
	go handleSignals()
}

//检查pidFile是否存在以及文件里的pid是否存活
func isRunning() bool {
	if mf, err := os.Open(pidFile); err == nil {
		pid, _ := ioutil.ReadAll(mf)
		pidVal, _ = strconv.Atoi(string(pid))
	}
	running := false
	if pidVal > 0 {
		if err := syscall.Kill(pidVal, 0); err == nil { //发一个信号为0到指定进程ID,如果没有错误发生,表示进程存活
			running = true
		}
	}
	return running
}

//保存pid
func savePid(pid int) error {
	file, err := os.OpenFile(pidFile, os.O_CREATE|os.O_WRONLY, os.ModePerm)
	if err != nil {
		return err
	}
	defer file.Close()
	file.WriteString(strconv.Itoa(pid))
	return nil
}

//捕获系统信号
func handleSignals() {
	signals := make(chan os.Signal)
	signal.Notify(signals, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
	var err error
	for {
		sig := <-signals
		switch sig {
		case syscall.SIGHUP: //重启
			if srv != nil {
				err = srv.fork()
			} else { //only deamon时不支持kill -HUP,因为可能监听地址会占用
				log.Printf("[%d] %s stopped.", os.Getpid(), appName)
				os.Remove(pidFile)
				os.Exit(2)
			}
			if err != nil {
				log.Fatalln(err)
			}
		case syscall.SIGINT:
			fallthrough
		case syscall.SIGTERM:
			log.Printf("[%d] %s stop graceful", os.Getpid(), appName)
			if srv != nil {
				srv.shutdown()
			} else {
				log.Printf("[%d] %s stopped.", os.Getpid(), appName)
			}
			os.Exit(1)
		}
	}
}

//forkDaemon,当checkPid为true时,检查是否有存活的,有则不执行
func forkDaemon() error {
	args := os.Args
	os.Setenv("__Daemon", "true")
	procAttr := &syscall.ProcAttr{
		Env:   os.Environ(),
		Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
	}
	pid, err := syscall.ForkExec(os.Args[0], args, procAttr)
	if err != nil {
		return err
	}
	log.Printf("[%d] %s start daemon\n", pid, appName)
	savePid(pid)
	return nil
}

//重启(先发送kill -HUP到运行进程,手工重启daemon ...当有运行的进程时,daemon不启动)
func restart(pid int) {
	syscall.Kill(pid, syscall.SIGHUP) //kill -HUP, daemon only时,会直接退出
	fork := make(chan bool, 1)
	go func() { //循环,查看pidFile是否存在,不存在或值已改变,发送消息
		for {
			f, err := os.Open(pidFile)
			if err != nil || os.IsNotExist(err) { //文件已不存在
				fork <- true
				break
			} else {
				pidVal, _ := ioutil.ReadAll(f)
				if strconv.Itoa(pid) != string(pidVal) {
					fork <- false
					break
				}
			}
			time.Sleep(500 * time.Millisecond)
		}
	}()
	//处理结果
	select {
	case r := <-fork:
		if r {
			forkDaemon()
		}
	case <-time.After(time.Second * 5):
		log.Fatalln("restart timeout")
	}

}

//处理http.Server,使支持graceful stop/restart
func Graceful(s http.Server) error {
	os.Setenv("__GRACEFUL", "true")
	srv = &server{
		cm:     newConnectionManager(),
		Server: s,
	}
	srv.ConnState = func(conn net.Conn, state http.ConnState) {
		switch state {
		case http.StateNew:
			srv.cm.add(1)
		case http.StateActive:
			srv.cm.rmIdleConns(conn.LocalAddr().String())
		case http.StateIdle:
			srv.cm.addIdleConns(conn.LocalAddr().String(), conn)
		case http.StateHijacked, http.StateClosed:
			srv.cm.done()
		}
	}
	l, err := srv.getListener()
	if err == nil {
		err = srv.Server.Serve(l)
	}
	return err
}

//使用addr和handler来启动一个支持graceful的服务
func GracefulServe(addr string, handler http.Handler) error {
	s := http.Server{
		Addr:    addr,
		Handler: handler,
	}
	return Graceful(s)
}

//获取listener
func (this *server) getListener() (*listener, error) {
	var l net.Listener
	var err error
	if os.Getenv("_GRACEFUL_RESTART") == "true" { //grace restart出来的进程,从FD FILE获取
		f := os.NewFile(3, "")
		l, err = net.FileListener(f)
		syscall.Kill(syscall.Getppid(), syscall.SIGTERM) //发信号给父进程,让父进程停止服务
	} else { //初始启动,监听addr
		l, err = net.Listen("tcp", this.Addr)
	}
	if err == nil {
		this.listener = &listener{
			Listener: l,
			server:   this,
		}
	}
	return this.listener, err
}

//fork一个新的进程
func (this *server) fork() error {
	os.Setenv("_GRACEFUL_RESTART", "true")
	lFd, err := this.listener.File()
	if err != nil {
		return err
	}
	execSpec := &syscall.ProcAttr{
		Env:   os.Environ(),
		Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), lFd},
	}
	pid, err := syscall.ForkExec(os.Args[0], os.Args, execSpec)
	if err != nil {
		return err
	}
	savePid(pid)
	log.Printf("[%d] %s fork ok\n", pid, appName)
	return nil
}

//关闭服务
func (this *server) shutdown() {
	this.SetKeepAlivesEnabled(false)
	this.cm.close(TimeDeadLine)
	this.listener.Close()
	log.Printf("[%d] %s stopped.", os.Getpid(), appName)
}

参考平滑重启
https://github.com/StevenACoffman/grace


package main

import (
	"flag"
	"io/ioutil"
	"log"
	"os"
	"strconv"
	"syscall"
	"time"

	"github.com/pytool/chaos/beat"
	"github.com/pytool/chaos/job"
	"github.com/sevlyar/go-daemon"
)

var (
	reboot = flag.Bool("r", false, `reboot - restart app`)
	signal = flag.String("s", "", `send signal to the daemon
		quit — graceful shutdown
		stop — fast shutdown
		reload — reloading the configuration file`)
)

const APPNAME = "snapd"

var cntxt = &daemon.Context{
	PidFileName: "/tmp/" + APPNAME + "/" + APPNAME + ".pid",
	PidFilePerm: 0644,
	LogFileName: "/tmp/" + APPNAME + "/" + APPNAME + ".log",
	LogFilePerm: 0640,
	WorkDir:     "./",
	Umask:       027,
	Args:        []string{"[snap-daemon]"},
}

// Will return true if the process with PID exists.
func checkPid(pid int) bool {
	process, err := os.FindProcess(pid)
	if err != nil {
		log.Printf("Unable to find the process %d", pid)
		return false
	}

	err = process.Signal(syscall.Signal(0))
	// log.Println(err)
	if err != nil {
		// log.Printf("Process %d is dead!", pid)
		return false
	} else {
		// log.Printf("Process %d is alive!", pid)
		return true
	}
	// return true
}

func main() {
	flag.Parse()
	daemon.AddCommand(daemon.StringFlag(signal, "quit"), syscall.SIGQUIT, termHandler)
	daemon.AddCommand(daemon.StringFlag(signal, "stop"), syscall.SIGTERM, termHandler)
	daemon.AddCommand(daemon.StringFlag(signal, "reload"), syscall.SIGHUP, reloadHandler)
	os.Mkdir("/tmp/"+APPNAME, os.ModePerm)
	// fmt.Println(os.Getpid())

	// 重启 snapd -r
	if *reboot {

		restart(cntxt.PidFileName)
		return
    }
    
	// 有守护进程
	if len(daemon.ActiveFlags()) > 0 {
		d, err := cntxt.Search()
		if err != nil {
			// 子进程正常退出
			log.Println("Unable send signal to the daemon:", err)
			log.Println("子进程已正常退出,重新启动子进程")
			forkDaemon()
			return
		}

		if checkPid(d.Pid) {
			daemon.SendCommands(d)
			log.Println("send singal to ", d.Pid)
			return
		}
	}
	// log.Println("1111", os.Getenv(daemon.MARK_NAME))
	// 启动服务进程
	forkDaemon()

}

//重启(先发送kill -HUP到运行进程,手工重启daemon ...当有运行的进程时,daemon不启动)
func restart(PidFileName string) {

	fork := make(chan bool, 1)

	go func() { //循环,查看pidFile是否存在,不存在或值已改变,发送消息
		// log.Println(pid)

		f, err := os.Open(PidFileName)
		defer f.Close()

		if err != nil || os.IsNotExist(err) { //文件已不存在
			fork <- true
			return
		}

		pidVal, _ := ioutil.ReadAll(f)
		pid, _ := strconv.Atoi(string(pidVal))
		syscall.Kill(pid, syscall.SIGQUIT) //kill -HUP, daemon only时,会直接退出

		for {

			if !checkPid(pid) {
				fork <- true
			}
			time.Sleep(100 * time.Millisecond)
		}
	}()
	//处理结果
	select {
	case r := <-fork:
		if r {
			forkDaemon()
		}
	case <-time.After(time.Second * 5):
		log.Fatalln("restart timeout")
	}
	// log.Println("finish restart")

}

func forkDaemon() {
	// log.Println(os.Getpid())
	d, err := cntxt.Reborn()
	if err != nil {
		log.Fatalln(err)
	}

	if d != nil {
		// 1. 父进程 代码逻辑
		log.Println("父进程", os.Getpid(), "退出, 启动子进程", d.Pid)
		// select {}
		return
	}

	// 2. 后台子进程代码实现
	defer cntxt.Release()

	log.Println("-- daemon service started --")

	go worker()

	err = daemon.ServeSignals()
	if err != nil {
		log.Println("Error:", err)
	}
	log.Println("-- daemon service terminated --")
}

var (
	stop = make(chan struct{})
	done = make(chan struct{})
)

func worker() {

	// go app.Run()
	go beat.WSStart()

	for {

		log.Println("loop")
		time.Sleep(time.Second)
		// 接收 控制台退出信号
		if _, ok := <-stop; ok {
			log.Println("recv ch stop")
			break
		}
	}
	beat.WSStop()
	// 等待完全退出
	done <- struct{}{}
}

func termHandler(sig os.Signal) error {
	log.Println("terminating...")

	stop <- struct{}{}
	if sig == syscall.SIGQUIT {
		<-done
	}
	return daemon.ErrStop
}

func reloadHandler(sig os.Signal) error {
	log.Println("configuration reloaded")
	// 原来的进程中运行
	job.Reset()
	beat.Reset()

	log.Println("configuration restart finish")
	// 返回nil 保证后台进程不退出
	return nil
}


启动命令

我们先来个非后台运行的启动命令

func init() {
    startCmd := &cobra.Command{
        Use:   "start",
        Short: "Start Gonne",
        Run: func(cmd *cobra.Command, args []string) {
            startHttp()
        },
    }
    startCmd.Flags().BoolVarP(&daemon, "deamon", "d", false, "is daemon?")
    RootCmd.AddCommand(startCmd)

}

startHttp方法启动一个http的web服务

func startHttp() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello cmd!")
    })
    if err := http.ListenAndServe(":9090", nil); err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

现在通过gonne start便可以启动一个web服务了,但是程序停留在命令行,如果ctrl+C程序也会终止了

命令行参数

如果想要后台启动,那么得让start命令知道是要后台运行的,参照docker命令行的方式就是加上-d,给一个命令添加参数的判断只需很少的代码

改造一下代码

func init() {
    var daemon bool
    startCmd := &cobra.Command{
        Use:   "start",
        Short: "Start Gonne",
        Run: func(cmd *cobra.Command, args []string) {
            if daemon {
        fmt.Println("gonne start",daemon)        
            }
            startHttp()
        },
    }
    startCmd.Flags().BoolVarP(&daemon, "deamon", "d", false, "is daemon?")
    RootCmd.AddCommand(startCmd)

}

命令行输入

gonne start -d

这样就可以接收到-d参数了,这里要说明一下,第一个参数取值,第二个参数代码--deamon,第三个参数代表-d
,第四个参数代码不加-d时候的默认值,第五参数是描述

后台运行

后台运行其实这里使用的是一个巧妙的方法,就是使用系统的command命令行启动自己的命令行输入,是不是有点绕,再看看看改造后的代码

    Run: func(cmd *cobra.Command, args []string) {
      if daemon {
        command := exec.Command("gonne", "start")
        command.Start()
        fmt.Printf("gonne start, [PID] %d running...\n", command.Process.Pid)
        ioutil.WriteFile("gonne.lock", []byte(fmt.Sprintf("%d", command.Process.Pid)), 0666)
        daemon = false
        os.Exit(0)
      } else {
        fmt.Println("gonne start")
      }
      startHttp()
    },

用exec的Command启动刚输入的gonne start -d,就会拦截到这条请求然后通过gonne start,但是程序就不会停留在命令行了,然后发现http服务还在,还可以访问。

还有一点就是把pid输出到gonne.lock文件,给停止的程序调用

终止后台程序

有了之前的操作后,停止就简单多了

    func init() {
        RootCmd.AddCommand(stopCmd)
    }

    var stopCmd = &cobra.Command{
        Use:   "stop",
        Short: "Stop Gonne",
        Run: func(cmd *cobra.Command, args []string) {
            strb, _ := ioutil.ReadFile("gonne.lock")
            command := exec.Command("kill", string(strb))
            command.Start()
            println("gonne stop")
        },
    }

执行 gonne stop 即可终止之前启动的http服务

help命令

好了,关于命令的操作讲完了,再看看cobra给的福利,自动生成的help命令

这个不需要你做什么操作,只需要输入gonne help,相关信息已经帮你生产好了。

appletekiMacBook-Pro:andev apple$ gonne help
Usage:
  gonne [flags]
  gonne [command]

Available Commands:
  help        Help about any command
  start       Start Gonne
  stop        Stop Gonne
  version     Print the version number of Gonne

Flags:
  -h, --help   help for gonne

Use "gonne [command] --help" for more information about a command.

当然,子命令也有

appletekiMacBook-Pro:andev apple$ gonne start -h
Start Gonne

Usage:
  gonne start [flags]

Flags:
  -d, --deamon   is daemon?
  -h, --help     help for start

自此告别各种脚本
golang

`