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