您好,欢迎访问沃航(武汉)科技有限公司官方网站
自研的提供web更新程序的守护进程
2024-07-08 12:46:59

由于项目需要,小沃抽空开发了一个的提供web更新程序的守护进程程序,功能如下:

1、提供web页面用于上传可执行文件,方便客户自己更新程序。

2、将可执行文件的标准输出与标准错误重定向到文件。

3、每小时将标准输出与标准错误的文件归档,并清空当前文件,保证日志文件不会过大。

源代码如下:go语言后台为

package main

import (
    "archive/zip"
    _ "embed"
    "fmt"
    "io"
    "mime"
    "net/http"
    "os"
    "os/exec"
    "time"
)

func main() {
    if len(os.Args) < 3 {
        fmt.Println("too low args.")
        fmt.Println("usb like this:\"bootloader 59120 server.exe\"")
        return
    }
    go RunProcess()
    http.HandleFunc("/", HttpHandle)
    err := http.ListenAndServe(":"+os.Args[1], nil)
    fmt.Println(err)
}

//go:embed index.html
var html []byte

var cmd *exec.Cmd

func RunProcess() {
    for {
        time.Sleep(5 * time.Second)
        src, err := os.Open("execFile")
        if err == nil { // 存在execFile文件,将其替换成正式文件后再运行。
            dst, err := os.Create(os.Args[2])
            if err == nil {
                io.Copy(dst, src)
                dst.Close()
                os.Chmod(os.Args[2], 0755)
            }
            src.Close()
            os.Remove("execFile")
        }
        os.MkdirAll("logs/out", 0755)
        os.MkdirAll("logs/err", 0755)
        stdout, err := os.Create("logs/out/out.log")
        if err != nil {
            fmt.Println(err)
            continue
        }
        stderr, err := os.Create("logs/err/err.log")
        if err != nil {
            fmt.Println(err)
            stdout.Close()
            continue
        }
        cmd = exec.Command(os.Args[2], os.Args[3:]...)
        cmd.Stdin = os.Stdin
        cmd.Stdout = stdout
        cmd.Stderr = stderr
        err = cmd.Start()
        if err != nil {
            fmt.Println(err)
            continue
        }
        errch := make(chan error, 1)
        go func() {
            errch <- cmd.Wait()
        }()
        loop := true
        for loop {
            select {
            case <-errch: // 进程退出后,退出本次循环,进入下次循环
                loop = false
            case <-time.After(time.Hour): // 1小时后,更换存储fmt的文件,但是进程不重启
                now := time.Now()
                os.MkdirAll("logs/out/"+now.Format("2006/01"), 0755)
                oldstdout, err := os.Open("logs/out/out.log")
                if err != nil {
                    fmt.Println(err)
                    continue
                }
                newstdout, err := os.Create("logs/out/" + time.Now().Format("2006/01/02_15_04_05") + ".log")
                if err != nil {
                    fmt.Println(err)
                    oldstdout.Close()
                    continue
                }
                io.Copy(newstdout, oldstdout)
                oldstdout.Close()
                newstdout.Close()
                stdout.Truncate(0) // 清空输出文件
                stdout.Seek(0, io.SeekStart)
                os.MkdirAll("logs/err/"+now.Format("2006/01"), 0755)
                oldstderr, err := os.Open("logs/err/err.log")
                if err != nil {
                    fmt.Println(err)
                    continue
                }
                newstderr, err := os.Create("logs/err/" + time.Now().Format("2006/01/02_15_04_05") + ".log")
                if err != nil {
                    fmt.Println(err)
                    oldstderr.Close()
                    continue
                }
                io.Copy(newstderr, oldstderr)
                oldstderr.Close()
                newstderr.Close()
                stderr.Truncate(0) // 清空错误文件
                stderr.Seek(0, io.SeekStart)
            }
        }
        stdout.Close()
        stderr.Close()
        now := time.Now()
        os.MkdirAll("logs/out/"+now.Format("2006/01"), 0755)
        os.Rename("logs/out/out.log", "logs/out/"+now.Format("2006/01/02_15_04_05")+".log")
        os.MkdirAll("logs/err/"+now.Format("2006/01"), 0755)
        os.Rename("logs/err/err.log", "logs/err/"+now.Format("2006/01/02_15_04_05")+".log")
    }
}

func HttpHandle(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Access-Control-Allow-Origin", "*")
    if r.RequestURI == "/api/update" {
        err := r.ParseMultipartForm(100 * 1024 * 1024)
        if err != nil {
            w.Write([]byte("{\"errcode\":3000,\"errmsg\":" + err.Error() + "}"))
            return
        }
        src, _, err := r.FormFile("file")
        if err != nil {
            w.Write([]byte("{\"errcode\":3000,\"errmsg\":" + err.Error() + "}"))
            return
        }
        dst, err := os.Create("execFile")
        if err != nil {
            w.Write([]byte("{\"errcode\":3000,\"errmsg\":" + err.Error() + "}"))
            return
        }
        io.Copy(dst, src)
        src.Close()
        dst.Close()
        if cmd.Process != nil && cmd.Process.Pid > 0 {
            cmd.Process.Kill()
        }
        w.Write([]byte("{\"errcode\":0}"))
    } else if r.RequestURI == "/api/reset" {
        if cmd.Process != nil && cmd.Process.Pid > 0 {
            cmd.Process.Kill()
        }
        w.Write([]byte("{\"errcode\":0}"))
    } else if r.RequestURI == "/api/downlogs" {
        outFile, err := os.Create("logs.zip")
        if err != nil {
            w.Write([]byte(err.Error()))
            return
        }
        zw := zip.NewWriter(outFile)
        walkDir("logs", zw)
        zw.Close()
        w.Header().Set("Content-Type", mime.TypeByExtension(".zip"))
        outFile.Close()
        outFile, err = os.Open("logs.zip")
        if err != nil {
            w.Write([]byte(err.Error()))
            return
        }
        http.ServeContent(w, r, "logs.zip", time.Now(), outFile)
    } else {
        w.Write(html)
    }
}

func walkDir(dir string, zw *zip.Writer) {
    files, err := os.ReadDir(dir)
    if err != nil {
        fmt.Println(err)
        return
    }
    for _, file := range files {
        filename := dir + "/" + file.Name()
        if file.IsDir() {
            walkDir(filename, zw)
        } else {
            fz, err := zw.Create(filename)
            if err != nil {
                fmt.Println(err)
                continue
            }
            fo, err := os.Open(filename)
            if err != nil {
                fmt.Println(err)
                continue
            }
            io.Copy(fz, fo)
            fo.Close()
        }
    }
}

提供一个简单的前端页面,源码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript">
        function updateProcess() {
            var msg = document.getElementById('msg')
            var input = document.createElement('input')
            input.type = 'file'
            input.onchange = function () {
                msg.innerText = ''
                let fd = new FormData()
                fd.append('file', input.files[0])
                let xhr = new XMLHttpRequest()
                xhr.open('POST', '/api/update')
                xhr.upload.onprogress = function (event) {
                    if (event.lengthComputable) {
                        msg.innerText = '上传进度:' + Math.round(event.loaded / event.total * 100) + '%'
                    }
                }
                xhr.onload = function () {
                    msg.innerHTML = xhr.responseText
                }
                xhr.send(fd)
            }
            input.click()
        }
        function resetProcess() {
            var msg = document.getElementById('msg')
            msg.innerHTML = ''
            var xhr = new XMLHttpRequest()
            xhr.open('POST', '/api/reset')
            xhr.onload = function () {
                msg.innerHTML = xhr.responseText
            }
            xhr.send(null)
        }
        function downloadlogs() {
            let ele = document.createElement('a')
            ele.href = '/api/downlogs'
            ele.download = 'logs.zip'
            ele.click()
        }
        function clearMessage() {
            var msg = document.getElementById('msg')
            msg.innerHTML = ''
        }
    </script>
    <title>程序更新</title>
</head>

<body>
    <div>
        <button onclick="updateProcess()">上传更新文件</button>
        <button onclick="resetProcess()">重启当前程序</button>
        <button onclick="downloadlogs()">下载日志</button>
        <button onclick="clearMessage()">清空msg</button>
    </div>
    <div id="msg"></div>
</body>

</html>

注意,编译的时候index.html需要放在go的同级目录。

如要获取最新代码,可前往传送门


文章作者:沃航科技

联系我们
地址:
武汉市洪山区蓝晶国际7栋903
QQ:
932773931
电话:
027-59761089-806
手机:
13397158231
邮箱:
jevian_ma@worldflying.cn
×
物联网组态平台
试用账号:123456
试用密码:123456
如需测试更多功能或者有疑问可发送邮件至:jevian_ma@worldflying.cn
×
积木编程平台
试用方式:试用手机号码注册即可使用
如需测试更多功能或者有疑问可发送邮件至:jevian_ma@worldflying.cn