mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-20 12:30:04 +00:00
feat(alpha): add process supervision
This commit is contained in:
@@ -3,11 +3,15 @@ module browseros-alpha
|
||||
go 1.25.7
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
46
packages/browseros-agent/tools/alpha/proc/log.go
Normal file
46
packages/browseros-agent/tools/alpha/proc/log.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package proc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
Name string
|
||||
Color *color.Color
|
||||
}
|
||||
|
||||
var (
|
||||
TagBuild = Tag{"build", color.New(color.FgYellow)}
|
||||
TagAgent = Tag{"agent", color.New(color.FgMagenta)}
|
||||
TagServer = Tag{"server", color.New(color.FgCyan)}
|
||||
TagBrowser = Tag{"browser", color.New(color.FgBlue)}
|
||||
TagInfo = Tag{"info", color.New(color.FgGreen)}
|
||||
TagTest = Tag{"test", color.New(color.FgWhite)}
|
||||
|
||||
ErrorColor = color.New(color.FgRed)
|
||||
WarnColor = color.New(color.FgYellow)
|
||||
BoldColor = color.New(color.Bold)
|
||||
DimColor = color.New(color.Faint)
|
||||
)
|
||||
|
||||
func LogMsg(t Tag, msg string) {
|
||||
fmt.Printf("%s %s\n", t.Color.Sprintf("[%s]", t.Name), msg)
|
||||
}
|
||||
|
||||
func LogMsgf(t Tag, format string, args ...any) {
|
||||
LogMsg(t, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func StreamLines(r interface{ Read([]byte) (int, error) }, t Tag) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line != "" {
|
||||
fmt.Printf("%s %s\n", t.Color.Sprintf("[%s]", t.Name), line)
|
||||
}
|
||||
}
|
||||
}
|
||||
157
packages/browseros-agent/tools/alpha/proc/managed.go
Normal file
157
packages/browseros-agent/tools/alpha/proc/managed.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProcConfig struct {
|
||||
Tag Tag
|
||||
Dir string
|
||||
Env []string
|
||||
Restart bool
|
||||
Cmd []string
|
||||
BeforeStart func() error
|
||||
}
|
||||
|
||||
type ManagedProc struct {
|
||||
Cfg ProcConfig
|
||||
cancel context.CancelFunc
|
||||
mu sync.Mutex
|
||||
proc *os.Process
|
||||
exited chan struct{}
|
||||
}
|
||||
|
||||
func StartManaged(ctx context.Context, wg *sync.WaitGroup, cfg ProcConfig) *ManagedProc {
|
||||
procCtx, procCancel := context.WithCancel(ctx)
|
||||
mp := &ManagedProc{
|
||||
Cfg: cfg,
|
||||
cancel: procCancel,
|
||||
exited: make(chan struct{}),
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
mp.run(procCtx)
|
||||
}()
|
||||
|
||||
return mp
|
||||
}
|
||||
|
||||
func (mp *ManagedProc) run(ctx context.Context) {
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if mp.Cfg.BeforeStart != nil {
|
||||
if err := mp.Cfg.BeforeStart(); err != nil {
|
||||
LogMsg(mp.Cfg.Tag, ErrorColor.Sprintf("Pre-start failed: %v", err))
|
||||
if !mp.Cfg.Restart || ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
LogMsgf(mp.Cfg.Tag, "Starting: %s", DimColor.Sprint(strings.Join(mp.Cfg.Cmd, " ")))
|
||||
|
||||
cmd := exec.Command(mp.Cfg.Cmd[0], mp.Cfg.Cmd[1:]...)
|
||||
cmd.Dir = mp.Cfg.Dir
|
||||
if mp.Cfg.Env != nil {
|
||||
cmd.Env = mp.Cfg.Env
|
||||
}
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
stderr, _ := cmd.StderrPipe()
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
LogMsg(mp.Cfg.Tag, ErrorColor.Sprintf("Error starting: %v", err))
|
||||
if !mp.Cfg.Restart || ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
mp.mu.Lock()
|
||||
mp.proc = cmd.Process
|
||||
mp.exited = make(chan struct{})
|
||||
mp.mu.Unlock()
|
||||
|
||||
var streamWg sync.WaitGroup
|
||||
streamWg.Add(2)
|
||||
go func() { defer streamWg.Done(); StreamLines(stdout, mp.Cfg.Tag) }()
|
||||
go func() { defer streamWg.Done(); StreamLines(stderr, mp.Cfg.Tag) }()
|
||||
|
||||
streamWg.Wait()
|
||||
_ = cmd.Wait()
|
||||
|
||||
mp.mu.Lock()
|
||||
mp.proc = nil
|
||||
close(mp.exited)
|
||||
mp.mu.Unlock()
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
exitCode := cmd.ProcessState.ExitCode()
|
||||
if exitCode != 0 {
|
||||
LogMsg(mp.Cfg.Tag, ErrorColor.Sprintf("Process exited with code %d", exitCode))
|
||||
} else {
|
||||
LogMsg(mp.Cfg.Tag, "Process exited cleanly")
|
||||
}
|
||||
|
||||
if !mp.Cfg.Restart {
|
||||
return
|
||||
}
|
||||
|
||||
LogMsg(mp.Cfg.Tag, WarnColor.Sprint("Restarting in 1s..."))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mp *ManagedProc) Stop() {
|
||||
mp.cancel()
|
||||
mp.mu.Lock()
|
||||
proc := mp.proc
|
||||
exited := mp.exited
|
||||
mp.mu.Unlock()
|
||||
|
||||
if proc != nil {
|
||||
_ = syscall.Kill(-proc.Pid, syscall.SIGTERM)
|
||||
select {
|
||||
case <-exited:
|
||||
case <-time.After(5 * time.Second):
|
||||
_ = syscall.Kill(-proc.Pid, syscall.SIGKILL)
|
||||
select {
|
||||
case <-exited:
|
||||
case <-time.After(3 * time.Second):
|
||||
LogMsg(mp.Cfg.Tag, WarnColor.Sprint("Process did not exit after SIGKILL, giving up"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mp *ManagedProc) ForceKill() {
|
||||
mp.mu.Lock()
|
||||
proc := mp.proc
|
||||
mp.mu.Unlock()
|
||||
|
||||
if proc != nil {
|
||||
_ = syscall.Kill(-proc.Pid, syscall.SIGKILL)
|
||||
}
|
||||
}
|
||||
48
packages/browseros-agent/tools/alpha/proc/run.go
Normal file
48
packages/browseros-agent/tools/alpha/proc/run.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func RunBlocking(ctx context.Context, dir string, t Tag, args ...string) error {
|
||||
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
|
||||
cmd.Dir = dir
|
||||
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
stderr, _ := cmd.StderrPipe()
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() { defer wg.Done(); StreamLines(stdout, t) }()
|
||||
go func() { defer wg.Done(); StreamLines(stderr, t) }()
|
||||
wg.Wait()
|
||||
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
func RunBlockingWithEnv(ctx context.Context, dir string, env []string, t Tag, args ...string) error {
|
||||
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = env
|
||||
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
stderr, _ := cmd.StderrPipe()
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() { defer wg.Done(); StreamLines(stdout, t) }()
|
||||
go func() { defer wg.Done(); StreamLines(stderr, t) }()
|
||||
wg.Wait()
|
||||
|
||||
return cmd.Wait()
|
||||
}
|
||||
Reference in New Issue
Block a user