Files
code-utils/pack_cmd.go
2026-08-15 17:18:00 +08:00

284 lines
6.8 KiB
Go
Raw Blame History

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()
}