Files
code-utils/launch_run.go

156 lines
3.8 KiB
Go
Raw Normal View History

2026-08-15 17:18:00 +08:00
package main
// launch_run.go启动台启停运行态starting/running/failed与本地进程日志。
// launch_logs 仅存本机 SQLite不参与云同步。
import (
"bufio"
"io"
"strings"
"sync"
"time"
)
type launchRunState struct {
mu sync.Mutex
Status string // starting | running | failed | stopped
Error string
LastLog string
StartedAt time.Time
}
var (
lpRunMu sync.Mutex
lpRuns = map[int64]*launchRunState{}
)
func lpState(id int64) *launchRunState {
lpRunMu.Lock()
defer lpRunMu.Unlock()
st, ok := lpRuns[id]
if !ok {
st = &launchRunState{Status: "stopped"}
lpRuns[id] = st
}
return st
}
func (st *launchRunState) snapshot() (status, errMsg, lastLog string) {
st.mu.Lock()
defer st.mu.Unlock()
return st.Status, st.Error, st.LastLog
}
func (st *launchRunState) set(status, errMsg, lastLog string) {
st.mu.Lock()
defer st.mu.Unlock()
if status != "" {
st.Status = status
}
if errMsg != "" || status == "running" || status == "starting" {
st.Error = errMsg
}
if lastLog != "" {
st.LastLog = lastLog
}
if status == "starting" {
st.StartedAt = time.Now()
st.Error = ""
}
}
func (s *Store) appendLaunchLog(appID int64, level, line string) {
line = strings.TrimRight(line, "\r\n")
if strings.TrimSpace(line) == "" {
return
}
if len(line) > 4000 {
line = line[:4000] + "…"
}
if level == "" {
level = "info"
}
_, _ = s.db.Exec(`INSERT INTO launch_logs(app_id,level,line,created_at) VALUES(?,?,?,?)`,
appID, level, line, nowRFC())
// 每个应用最多保留 800 行,避免无限膨胀。
_, _ = s.db.Exec(`DELETE FROM launch_logs WHERE app_id=? AND id NOT IN (
SELECT id FROM launch_logs WHERE app_id=? ORDER BY id DESC LIMIT 800)`, appID, appID)
}
func (s *Store) ListLaunchLogs(appID int64, limit int) ([]LaunchLogLine, error) {
if limit <= 0 || limit > 500 {
limit = 200
}
rows, e := s.db.Query(`SELECT id,app_id,level,line,created_at FROM launch_logs WHERE app_id=? ORDER BY id DESC LIMIT ?`, appID, limit)
if e != nil {
return nil, e
}
defer rows.Close()
out := []LaunchLogLine{}
for rows.Next() {
var x LaunchLogLine
if e = rows.Scan(&x.ID, &x.AppID, &x.Level, &x.Line, &x.CreatedAt); e != nil {
return nil, e
}
out = append(out, x)
}
// 按时间正序返回,便于控制台阅读。
for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
out[i], out[j] = out[j], out[i]
}
return out, rows.Err()
}
func (s *Store) ClearLaunchLogs(appID int64) error {
_, e := s.db.Exec(`DELETE FROM launch_logs WHERE app_id=?`, appID)
return e
}
func (a *App) ListLaunchLogs(appID int64, limit int) ([]LaunchLogLine, error) {
if e := a.ready(); e != nil {
return nil, e
}
return a.store.ListLaunchLogs(appID, limit)
}
func (a *App) ClearLaunchLogs(appID int64) error {
if e := a.ready(); e != nil {
return e
}
return a.store.ClearLaunchLogs(appID)
}
func (a *App) writeLaunchLog(appID int64, level, line string) {
a.store.appendLaunchLog(appID, level, line)
st := lpState(appID)
st.set("", "", line)
a.emit("launchpad:log", map[string]any{"appId": appID, "level": level, "line": line})
}
// pipeLaunchOutput 把子进程 stdout/stderr 写入本地日志。
func (a *App) pipeLaunchOutput(appID int64, r io.Reader, fallbackLevel string) {
sc := bufio.NewScanner(r)
buf := make([]byte, 0, 64*1024)
sc.Buffer(buf, 1024*1024)
for sc.Scan() {
line := sc.Text()
level := launchLogLevelFromLine(line)
if level == "info" && fallbackLevel == "error" {
level = "error"
}
a.writeLaunchLog(appID, level, line)
}
}
func launchLogLevelFromLine(line string) string {
l := strings.ToLower(line)
switch {
case strings.Contains(l, "error"), strings.Contains(l, "fatal"), strings.Contains(l, "panic"), strings.Contains(l, "failed"):
return "error"
case strings.Contains(l, "warn"):
return "warning"
default:
return "info"
}
}