go 检查进程是否存在


原文链接: go 检查进程是否存在

golang程序优雅关闭与重启 - 知乎

golang中 os.FindProcess(pid)并会做进程存在性检查,查看源码我们发现,他只是创建了一个进程id

func findProcess(pid int) (p *Process, err error) {
	// NOOP for unix.
	return newProcess(pid, 0), nil
}

所以我通过对进程发送一个signal 来判断进程是否存活


// 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
}
  1. 以守护进程启动服务器

当使用 bingo run daemon或者 bingo run daemon start的时候,会触发 DaemonInit()函数,内容如下:

func DaemonInit() {
    // 得到存放pid文件的路径
    dir, _ := os.Getwd()
    pidFile = dir + "/" + Env.Get("PID_FILE")
    if os.Getenv("__Daemon") != "true" { //master
        cmd := "start" //缺省为start
        if l := len(os.Args); l > 2 {
            cmd = os.Args[l-1]
        }
        switch cmd {
        case "start":
            if isRunning() {
                fmt.Printf("\n %c[0;48;34m%s%c[0m", 0x1B, "["+strconv.Itoa(pidVal)+"] Bingo is running", 0x1B)
            } else { //fork daemon进程
                if err := forkDaemon(); err != nil {
                    fmt.Println(err)
                }
            }
        case "restart": //重启:
            if !isRunning() {
                fmt.Printf("\n %c[0;48;31m%s%c[0m", 0x1B, "[Warning]bingo not running", 0x1B)
                restart(pidVal)
            } else {
                fmt.Printf("\n %c[0;48;34m%s%c[0m", 0x1B, "["+strconv.Itoa(pidVal)+"] Bingo restart now", 0x1B)
                restart(pidVal)
            }
        case "stop": //停止
            if !isRunning() {
                fmt.Printf("\n %c[0;48;31m%s%c[0m", 0x1B, "[Warning]bingo not running", 0x1B)
            } else {
                syscall.Kill(pidVal, syscall.SIGTERM) //kill
            }
        case "-h":
            fmt.Println("Usage: " + appName + " start|restart|stop")
        default:   //其它不识别的参数
            return //返回至调用方
        }
        //主进程退出
        os.Exit(0)
    }
    go handleSignals()
}

首先要获取pidFile 这个文件主要是存储令程序运行时候的进程pid,为什么要持久化pid呢?是为了让多次程序运行过程中,判定是否有相同程序启动等操作

之后要获取对应的操作 (start|restart|stop),一个一个说

case start:

首先使用 isRunning()方法判断当前程序是否在运行,如何判断?就是从上面提到的 pidFile 中取出进程号

然后判断当前系统是否运行令这个进程,如果有,证明正在运行,返回 true,反之返回 false

如果没有运行的话,调用 forkDaemon() 函数启动程序,这个函数是整个功能的核心

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(args[0], []string{args[0], "dev"}, procAttr)
    if err != nil {
        panic(err)
    }
    savePid(pid)
    fmt.Printf("\n %c[0;48;32m%s%c[0m", 0x1B, "["+strconv.Itoa(pid)+"] Bingo running...", 0x1B)
    fmt.Println()
    return nil
}

syscall包不支持win系统,也就意味着如果想在 windows上做开发的话,只能使用虚拟机或者 docker

这里的主要功能就是,使用 syscall.ForkExec()fork 一个进程出来

运行这个进程所执行的命令就是这里的参数(因为我们的原始命令是 go run start.go dev,所以这里的args[0]实际上是 start.go编译之后的二进制文件)

然后再把 fork出来的进程号保存在 pidFile

所以最终运行的效果就是我们第一步时候说到的 bingo run dev 达到的效果

case restart:

这个比较简单,通过 pidFile判定程序是否正在运行,如果正在运行,才会继续向下执行

函数体也比较简单,只有两行

syscall.Kill(pid, syscall.SIGHUP) //kill -HUP, daemon only时,会直接退出
forkDaemon()

第一行杀死这个进程 第二行开启一个新进程

case stop:

这里就一行代码,就是杀死这个进程

额外的想法

在开发过程中,每当有一丁点变动(比如更改来一丁点控制器),就需要再次执行一次 bingo run daemon restart 命令,让新的改动生效,十分麻烦

所以我又开发了 bingo run watch 命令,监听改动,自动重启server服务器

我使用了github.com/fsnotify/fsnotify包来实现监听

func startWatchServer(port string, handler http.Handler) {
    // 监听目录变化,如果有变化,重启服务
    // 守护进程开启服务,主进程阻塞不断扫描当前目录,有任何更新,向守护进程传递信号,守护进程重启服务
    // 开启一个协程运行服务
    // 监听目录变化,有变化运行 bingo run daemon restart
    f, err := fsnotify.NewWatcher()
    if err != nil {
        panic(err)
    }
    defer f.Close()
    dir, _ := os.Getwd()
    wdDir = dir
    fileWatcher = f
    f.Add(dir)

    done := make(chan bool)

    go func() {
        procAttr := &syscall.ProcAttr{
            Env:   os.Environ(),
            Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
        }
        _, err := syscall.ForkExec(os.Args[0], []string{os.Args[0], "daemon", "start"}, procAttr)
        if err != nil {
            fmt.Println(err)
        }
    }()

    go func() {
        for {
            select {
            case ev := <-f.Events:
                if ev.Op&fsnotify.Create == fsnotify.Create {
                    fmt.Printf("\n %c[0;48;33m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]created file:"+ev.Name, 0x1B)
                }
                if ev.Op&fsnotify.Remove == fsnotify.Remove {
                    fmt.Printf("\n %c[0;48;31m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]deleted file:"+ev.Name, 0x1B)
                }
                if ev.Op&fsnotify.Rename == fsnotify.Rename {
                    fmt.Printf("\n %c[0;48;34m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]renamed file:"+ev.Name, 0x1B)
                } else {
                    fmt.Printf("\n %c[0;48;32m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]modified file:"+ev.Name, 0x1B)
                }
                // 有变化,放入重启数组中
                restartSlice = append(restartSlice, 1)
            case err := <-f.Errors:
                fmt.Println("error:", err)
            }
        }
    }()

    // 准备重启守护进程
    go restartDaemonServer()

    <-done
}

首先按照 fsnotify的文档,创建一个 watcher,然后添加监听目录(这里只是监听目录下的文件,不能监听子目录)

然后开启两个协程:

  1. 监听文件变化,如果有文件变化,把变化的个数写入一个 slice 里,这是一个阻塞的 for循环
  2. 每隔1s中查看一次记录文件变化的 slice, 如果有的话,就重启服务器,并重新设置监听目录,然后清空 slice ,否则跳过

递归遍历子目录,达到监听整个工程目录的效果:

func listeningWatcherDir(dir string) {
    filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
        dir, _ := os.Getwd()
        pidFile = dir + "/" + Env.Get("PID_FILE")
        fileWatcher.Add(path)

        // 这里不能监听 pidFile,否则每次重启都会导致pidFile有更新,会不断的触发重启功能
        fileWatcher.Remove(pidFile)
        return nil
    })
}

这里这个 slice 的作用也就是为了避免当一次保存更新了多个文件的时候,也重启了多次服务器

下面看看重启服务器的代码:

go func() {
                // 执行重启命令
                cmd := exec.Command("bingo", "run", "daemon", "restart")
                stdout, err := cmd.StdoutPipe()
                if err != nil {
                    fmt.Println(err)
                }
                defer stdout.Close()

                if err := cmd.Start(); err != nil {
                    panic(err)
                }
                reader := bufio.NewReader(stdout)
                //实时循环读取输出流中的一行内容
                for {
                    line, err2 := reader.ReadString('\n')
                    if err2 != nil || io.EOF == err2 {
                        break
                    }
                    fmt.Print(line)
                }

                if err := cmd.Wait(); err != nil {
                    fmt.Println(err)
                }
                opBytes, _ := ioutil.ReadAll(stdout)
                fmt.Print(string(opBytes))

            }()

使用 exec.Command() 方法得到一个 cmd

调用 cmd.Stdoutput() 得到一个输出管道,命令打印出来的数据都会从这个管道流出来

然后使用 reader := bufio.NewReader(stdout) 从管道中读出数据

用一个阻塞的for循环,不断的从管道中读出数据,以 \n 为一行,一行一行的读

并打印在控制台里,达到输出的效果,如果这几行不写的话,在新的进程里的 fmt.Println()方法打印出来的数据将无法显示在控制台上.

就酱,最后贴下项目链接 silsuer/bingo ,欢迎star,欢迎PR,欢迎提意见

`