284 lines
6.8 KiB
Go
284 lines
6.8 KiB
Go
package main
|
||
|
||
import (
|
||
"bufio"
|
||
"errors"
|
||
"io"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
"unicode/utf8"
|
||
|
||
"view/platform"
|
||
)
|
||
|
||
// LocalPackTask 本机打包/命令执行记录(本地 SQLite 持久化,不同步云端)。
|
||
type LocalPackTask struct {
|
||
ID string `json:"id"`
|
||
Title string `json:"title"`
|
||
Cmd string `json:"cmd"`
|
||
Dir string `json:"dir"`
|
||
Status string `json:"status"` // running | done | failed
|
||
PID int `json:"pid"`
|
||
StartedAt string `json:"startedAt"`
|
||
EndedAt string `json:"endedAt"`
|
||
Error string `json:"error"`
|
||
Logs []string `json:"logs"` // 控制台行
|
||
LogBytes int `json:"logBytes"`
|
||
}
|
||
|
||
// PackLogEvent 实时日志推送。
|
||
type PackLogEvent struct {
|
||
TaskID string `json:"taskId"`
|
||
Line string `json:"line"`
|
||
}
|
||
|
||
var (
|
||
packTaskMu sync.Mutex
|
||
// packLive 仅缓存进行中任务的内存日志,便于实时推送;历史一律读库。
|
||
packLive = map[string]*LocalPackTask{}
|
||
packSeq int64
|
||
)
|
||
|
||
// RunDirCommand 在指定目录后台无窗口执行一行 shell 命令,捕获控制台输出并持久化。
|
||
func (a *App) RunDirCommand(dir, command, label string) (LocalPackTask, error) {
|
||
if e := a.ready(); e != nil {
|
||
return LocalPackTask{}, e
|
||
}
|
||
command = strings.TrimSpace(command)
|
||
if command == "" {
|
||
return LocalPackTask{}, errors.New("PACK_CMD_REQUIRED")
|
||
}
|
||
title := strings.TrimSpace(label)
|
||
if title == "" {
|
||
title = command
|
||
}
|
||
|
||
cmd := shellCommand(command)
|
||
workDir := strings.TrimSpace(dir)
|
||
if workDir != "" {
|
||
if info, err := os.Stat(workDir); err == nil && info.IsDir() {
|
||
cmd.Dir = workDir
|
||
}
|
||
}
|
||
platform.ConfigureHidden(cmd)
|
||
|
||
stdout, eOut := cmd.StdoutPipe()
|
||
if eOut != nil {
|
||
return LocalPackTask{}, eOut
|
||
}
|
||
stderr, eErr := cmd.StderrPipe()
|
||
if eErr != nil {
|
||
return LocalPackTask{}, eErr
|
||
}
|
||
|
||
packTaskMu.Lock()
|
||
packSeq++
|
||
id := "pack-" + strconv.FormatInt(time.Now().UnixMilli(), 10) + "-" + strconv.FormatInt(packSeq, 10)
|
||
task := &LocalPackTask{
|
||
ID: id,
|
||
Title: title,
|
||
Cmd: command,
|
||
Dir: workDir,
|
||
Status: "running",
|
||
StartedAt: time.Now().Format(time.RFC3339),
|
||
Logs: []string{"▶ " + title, "$ " + command},
|
||
}
|
||
if workDir != "" {
|
||
task.Logs = append(task.Logs, "cwd: "+workDir)
|
||
}
|
||
packLive[id] = task
|
||
packTaskMu.Unlock()
|
||
|
||
if e := a.store.insertPackTask(*task); e != nil {
|
||
packTaskMu.Lock()
|
||
delete(packLive, id)
|
||
packTaskMu.Unlock()
|
||
return LocalPackTask{}, e
|
||
}
|
||
for _, line := range task.Logs {
|
||
a.store.appendPackTaskLog(id, line)
|
||
}
|
||
a.store.pruneOldPackTasks()
|
||
a.emit("pack:task", a.taskSummary(*task))
|
||
|
||
if e := cmd.Start(); e != nil {
|
||
a.appendPackLog(id, "启动失败:"+e.Error())
|
||
a.finishPackTask(id, "failed", e.Error())
|
||
a.store.Log("error", "打包", "命令启动失败", e.Error()+" · "+command)
|
||
return a.getPackTaskLiveOrDB(id), e
|
||
}
|
||
pid := 0
|
||
if cmd.Process != nil {
|
||
pid = cmd.Process.Pid
|
||
}
|
||
a.patchPackTask(id, func(t *LocalPackTask) { t.PID = pid })
|
||
a.appendPackLog(id, "pid="+strconv.Itoa(pid))
|
||
a.store.Log("info", "打包", "已执行 "+title, "dir="+workDir+" pid="+strconv.Itoa(pid)+" cmd="+command)
|
||
|
||
var wg sync.WaitGroup
|
||
wg.Add(2)
|
||
go func() { defer wg.Done(); a.pumpPackOutput(id, stdout) }()
|
||
go func() { defer wg.Done(); a.pumpPackOutput(id, stderr) }()
|
||
|
||
go func() {
|
||
wg.Wait()
|
||
err := cmd.Wait()
|
||
if err != nil {
|
||
a.appendPackLog(id, "✗ "+err.Error())
|
||
a.finishPackTask(id, "failed", err.Error())
|
||
return
|
||
}
|
||
a.appendPackLog(id, "✓ 完成")
|
||
a.finishPackTask(id, "done", "")
|
||
}()
|
||
return a.getPackTaskLiveOrDB(id), nil
|
||
}
|
||
|
||
func (a *App) pumpPackOutput(id string, r io.Reader) {
|
||
sc := bufio.NewScanner(r)
|
||
buf := make([]byte, 0, 64*1024)
|
||
sc.Buffer(buf, 1024*1024)
|
||
for sc.Scan() {
|
||
line := strings.TrimRight(sc.Text(), "\r")
|
||
if !utf8.ValidString(line) {
|
||
line = strings.ToValidUTF8(line, "<22>")
|
||
}
|
||
a.appendPackLog(id, line)
|
||
}
|
||
}
|
||
|
||
func (a *App) appendPackLog(id, line string) {
|
||
packTaskMu.Lock()
|
||
if t := packLive[id]; t != nil {
|
||
t.Logs = append(t.Logs, line)
|
||
if len(t.Logs) > packLogKeepLimit {
|
||
t.Logs = t.Logs[len(t.Logs)-packLogKeepLimit:]
|
||
}
|
||
}
|
||
packTaskMu.Unlock()
|
||
if a.store != nil {
|
||
a.store.appendPackTaskLog(id, line)
|
||
}
|
||
a.emit("pack:log", PackLogEvent{TaskID: id, Line: line})
|
||
}
|
||
|
||
// ListLocalPackTasks 列表(不含完整日志,只带末尾预览)。
|
||
func (a *App) ListLocalPackTasks() []LocalPackTask {
|
||
if e := a.ready(); e != nil {
|
||
return nil
|
||
}
|
||
list, e := a.store.listPackTasks(packTaskKeepLimit)
|
||
if e != nil {
|
||
return nil
|
||
}
|
||
out := make([]LocalPackTask, 0, len(list))
|
||
for _, t := range list {
|
||
// 进行中优先用内存预览
|
||
packTaskMu.Lock()
|
||
live := packLive[t.ID]
|
||
packTaskMu.Unlock()
|
||
if live != nil {
|
||
out = append(out, a.taskSummary(*live))
|
||
continue
|
||
}
|
||
tail, _ := a.store.listPackTaskLogTail(t.ID, 3)
|
||
t.Logs = tail
|
||
out = append(out, t)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// GetLocalPackTask 返回含完整控制台日志的任务详情。
|
||
func (a *App) GetLocalPackTask(id string) (LocalPackTask, error) {
|
||
if e := a.ready(); e != nil {
|
||
return LocalPackTask{}, e
|
||
}
|
||
packTaskMu.Lock()
|
||
if live := packLive[id]; live != nil {
|
||
cp := *live
|
||
cp.Logs = append([]string(nil), live.Logs...)
|
||
packTaskMu.Unlock()
|
||
return cp, nil
|
||
}
|
||
packTaskMu.Unlock()
|
||
|
||
t, e := a.store.getPackTask(id)
|
||
if e != nil {
|
||
return LocalPackTask{}, errors.New("PACK_TASK_NOT_FOUND")
|
||
}
|
||
logs, _ := a.store.listPackTaskLogs(id)
|
||
t.Logs = logs
|
||
return t, nil
|
||
}
|
||
|
||
// ClearFinishedLocalPackTasks 清除已完成/失败的任务。
|
||
func (a *App) ClearFinishedLocalPackTasks() []LocalPackTask {
|
||
if e := a.ready(); e != nil {
|
||
return nil
|
||
}
|
||
_ = a.store.deleteFinishedPackTasks()
|
||
a.emit("pack:task", nil)
|
||
return a.ListLocalPackTasks()
|
||
}
|
||
|
||
// DismissLocalPackTask 移除单条非 running 任务。
|
||
func (a *App) DismissLocalPackTask(id string) []LocalPackTask {
|
||
if e := a.ready(); e != nil {
|
||
return nil
|
||
}
|
||
_ = a.store.deletePackTask(id)
|
||
packTaskMu.Lock()
|
||
delete(packLive, id)
|
||
packTaskMu.Unlock()
|
||
a.emit("pack:task", nil)
|
||
return a.ListLocalPackTasks()
|
||
}
|
||
|
||
func (a *App) taskSummary(t LocalPackTask) LocalPackTask {
|
||
cp := t
|
||
cp.Logs = nil
|
||
if n := len(t.Logs); n > 0 {
|
||
start := n - 3
|
||
if start < 0 {
|
||
start = 0
|
||
}
|
||
cp.Logs = append([]string(nil), t.Logs[start:]...)
|
||
}
|
||
return cp
|
||
}
|
||
|
||
func (a *App) getPackTaskLiveOrDB(id string) LocalPackTask {
|
||
t, _ := a.GetLocalPackTask(id)
|
||
return t
|
||
}
|
||
|
||
func (a *App) patchPackTask(id string, fn func(*LocalPackTask)) {
|
||
packTaskMu.Lock()
|
||
var snap LocalPackTask
|
||
if t := packLive[id]; t != nil {
|
||
fn(t)
|
||
snap = a.taskSummary(*t)
|
||
if a.store != nil {
|
||
a.store.updatePackTaskMeta(*t)
|
||
}
|
||
}
|
||
packTaskMu.Unlock()
|
||
if snap.ID != "" {
|
||
a.emit("pack:task", snap)
|
||
}
|
||
}
|
||
|
||
func (a *App) finishPackTask(id, status, errMsg string) {
|
||
a.patchPackTask(id, func(t *LocalPackTask) {
|
||
t.Status = status
|
||
t.Error = errMsg
|
||
t.EndedAt = time.Now().Format(time.RFC3339)
|
||
})
|
||
packTaskMu.Lock()
|
||
delete(packLive, id)
|
||
packTaskMu.Unlock()
|
||
}
|