更新若干功能
This commit is contained in:
155
launch_run.go
Normal file
155
launch_run.go
Normal file
@@ -0,0 +1,155 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user