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