由于项目需要,小沃抽空开发了一个的提供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的同级目录。
如要获取最新代码,可前往传送门。
文章作者:沃航科技