156 lines
3.8 KiB
Go
156 lines
3.8 KiB
Go
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"
|
||
}
|
||
}
|