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, "�") } 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() }