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"
|
|||
|
|
}
|
|||
|
|
}
|