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

156 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
}
}