mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 23:53:25 +00:00
Compare commits
2 Commits
dev
...
fix/dev-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
162e78ce78 | ||
|
|
2feac502af |
@@ -92,6 +92,7 @@ Each start:
|
||||
- Runs `tools/dev/setup.sh`.
|
||||
- Builds the WXT extension.
|
||||
- Starts BrowserOS and the local Bun server.
|
||||
- Tees BrowserOS and server output to log files under the copied profile.
|
||||
|
||||
Use this when you want to refresh the copied profile before launching:
|
||||
|
||||
@@ -107,6 +108,28 @@ balpha start --headless
|
||||
|
||||
Stop the environment with `Ctrl+C`.
|
||||
|
||||
## Logs
|
||||
|
||||
`balpha start` writes process logs to:
|
||||
|
||||
```text
|
||||
~/.config/balpha/profile/logs
|
||||
```
|
||||
|
||||
The current files are:
|
||||
|
||||
- `chromium.log`: BrowserOS/Chromium stdout and stderr.
|
||||
- `server.log`: local Bun server stdout and stderr.
|
||||
|
||||
When either file is older than one day at startup, `balpha` rotates it to
|
||||
`<name>.old` before writing a fresh log.
|
||||
|
||||
To print the log directory and file paths:
|
||||
|
||||
```bash
|
||||
balpha logs
|
||||
```
|
||||
|
||||
## Update The Checkout
|
||||
|
||||
`balpha start` intentionally does not pull. To update the configured repo:
|
||||
|
||||
@@ -24,6 +24,7 @@ func BuildArgs(cfg ArgsConfig) []string {
|
||||
"--show-component-extension-options",
|
||||
"--disable-browseros-server",
|
||||
"--disable-browseros-extensions",
|
||||
"--enable-logging=stderr",
|
||||
fmt.Sprintf("--remote-debugging-port=%d", cfg.Ports.CDP),
|
||||
// Keep all server aliases until installed BrowserOS apps converge on one switch.
|
||||
fmt.Sprintf("--browseros-mcp-port=%d", cfg.Ports.Server),
|
||||
|
||||
@@ -26,6 +26,7 @@ func TestBuildArgs(t *testing.T) {
|
||||
"--profile-directory=Default",
|
||||
"--disable-browseros-server",
|
||||
"--disable-browseros-extensions",
|
||||
"--enable-logging=stderr",
|
||||
"--load-extension=/repo/packages/browseros-agent/apps/agent/dist/chrome-mv3-dev",
|
||||
"chrome://newtab",
|
||||
} {
|
||||
|
||||
44
packages/browseros-agent/tools/alpha/cmd/logs.go
Normal file
44
packages/browseros-agent/tools/alpha/cmd/logs.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"browseros-alpha/config"
|
||||
"browseros-alpha/proc"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(logsCmd)
|
||||
}
|
||||
|
||||
var logsCmd = &cobra.Command{
|
||||
Use: "logs",
|
||||
Short: "Print balpha log files",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := loadConfigWithoutValidation()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printLogs(cmd.OutOrStdout(), cfg)
|
||||
},
|
||||
}
|
||||
|
||||
func printLogs(out io.Writer, cfg config.Config) error {
|
||||
logDir := cfg.LogDir()
|
||||
fmt.Fprintf(out, "Log directory: %s\n", logDir)
|
||||
files, err := proc.ListLogFiles(logDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(files) == 0 {
|
||||
fmt.Fprintln(out, "No log files found.")
|
||||
return nil
|
||||
}
|
||||
for _, file := range files {
|
||||
fmt.Fprintf(out, "%s (%d bytes, modified %s)\n", file.Path, file.Size, file.ModTime.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
55
packages/browseros-agent/tools/alpha/cmd/logs_test.go
Normal file
55
packages/browseros-agent/tools/alpha/cmd/logs_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"browseros-alpha/config"
|
||||
)
|
||||
|
||||
func TestPrintLogsShowsDirectoryAndFiles(t *testing.T) {
|
||||
devDir := t.TempDir()
|
||||
cfg := config.Config{DevUserDataDir: devDir}
|
||||
logDir := cfg.LogDir()
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range []string{"server.log", "chromium.log"} {
|
||||
if err := os.WriteFile(filepath.Join(logDir, name), []byte("log"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := printLogs(&out, cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := out.String()
|
||||
for _, want := range []string{
|
||||
"Log directory: " + logDir,
|
||||
filepath.Join(logDir, "chromium.log"),
|
||||
filepath.Join(logDir, "server.log"),
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("missing %q in\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintLogsHandlesMissingDirectory(t *testing.T) {
|
||||
cfg := config.Config{DevUserDataDir: t.TempDir()}
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := printLogs(&out, cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := out.String()
|
||||
if !strings.Contains(got, "No log files found.") {
|
||||
t.Fatalf("unexpected output:\n%s", got)
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,17 @@ var refreshProfileCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func loadConfig() (config.Config, error) {
|
||||
cfg, err := loadConfigWithoutValidation()
|
||||
if err != nil {
|
||||
return config.Config{}, err
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return config.Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func loadConfigWithoutValidation() (config.Config, error) {
|
||||
path, err := config.Path()
|
||||
if err != nil {
|
||||
return config.Config{}, err
|
||||
@@ -43,8 +54,5 @@ func loadConfig() (config.Config, error) {
|
||||
if err != nil {
|
||||
return config.Config{}, fmt.Errorf("missing config at %s; run balpha init: %w", path, err)
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return config.Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ import (
|
||||
var startRefreshProfile bool
|
||||
var startHeadless bool
|
||||
|
||||
const (
|
||||
serverLogName = "server.log"
|
||||
chromiumLogName = "chromium.log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
startCmd.Flags().BoolVar(&startRefreshProfile, "refresh-profile", false, "Refresh copied BrowserOS profile before launch")
|
||||
startCmd.Flags().BoolVar(&startHeadless, "headless", false, "Run BrowserOS headless")
|
||||
@@ -84,12 +89,17 @@ func runEnvironment(cfg config.Config, agentRoot string) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if err := os.MkdirAll(cfg.LogDir(), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var managed []*proc.ManagedProc
|
||||
managed = append(managed, proc.StartManaged(ctx, &wg, proc.ProcConfig{
|
||||
Tag: proc.TagBrowser,
|
||||
Dir: agentRoot,
|
||||
Restart: false,
|
||||
LogPath: cfg.LogPath(chromiumLogName),
|
||||
Cmd: browser.BuildArgs(browser.ArgsConfig{
|
||||
Binary: cfg.BrowserOSAppPath,
|
||||
AgentRoot: agentRoot,
|
||||
@@ -119,6 +129,7 @@ func runEnvironment(cfg config.Config, agentRoot string) error {
|
||||
Dir: serverDir,
|
||||
Env: env,
|
||||
Restart: true,
|
||||
LogPath: cfg.LogPath(serverLogName),
|
||||
Cmd: serverCommand(),
|
||||
}))
|
||||
printSummary(cfg, agentRoot)
|
||||
@@ -172,6 +183,7 @@ func printSummary(cfg config.Config, agentRoot string) {
|
||||
proc.LogMsgf(proc.TagInfo, "Repo: %s", cfg.RepoPath)
|
||||
proc.LogMsgf(proc.TagInfo, "Agent root: %s", agentRoot)
|
||||
proc.LogMsgf(proc.TagInfo, "Profile: %s", cfg.DevUserDataDir)
|
||||
proc.LogMsgf(proc.TagInfo, "Logs: %s", cfg.LogDir())
|
||||
proc.LogMsgf(proc.TagInfo, "Ports: CDP=%d Server=%d Extension=%d", cfg.Ports.CDP, cfg.Ports.Server, cfg.Ports.Extension)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ type packageJSON struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
const LogDirName = "logs"
|
||||
|
||||
func Path() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
@@ -127,6 +129,14 @@ func (c Config) DevProfilePath() string {
|
||||
return filepath.Join(c.DevUserDataDir, c.DevProfileDir)
|
||||
}
|
||||
|
||||
func (c Config) LogDir() string {
|
||||
return filepath.Join(c.DevUserDataDir, LogDirName)
|
||||
}
|
||||
|
||||
func (c Config) LogPath(name string) string {
|
||||
return filepath.Join(c.LogDir(), name)
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if c.RepoPath == "" {
|
||||
return fmt.Errorf("repo_path is required")
|
||||
|
||||
@@ -20,6 +20,9 @@ func TestDefaults(t *testing.T) {
|
||||
if cfg.DevUserDataDir != filepath.Join(home, ".config/balpha/profile") {
|
||||
t.Fatalf("unexpected dev dir: %s", cfg.DevUserDataDir)
|
||||
}
|
||||
if cfg.LogDir() != filepath.Join(home, ".config/balpha/profile/logs") {
|
||||
t.Fatalf("unexpected log dir: %s", cfg.LogDir())
|
||||
}
|
||||
if cfg.DevProfileDir != "Default" {
|
||||
t.Fatalf("unexpected dev profile: %s", cfg.DevProfileDir)
|
||||
}
|
||||
@@ -40,6 +43,15 @@ func TestDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogPathUsesProfileLogDir(t *testing.T) {
|
||||
cfg := Config{DevUserDataDir: "/tmp/balpha-profile"}
|
||||
got := cfg.LogPath("server.log")
|
||||
want := filepath.Join("/tmp/balpha-profile", "logs", "server.log")
|
||||
if got != want {
|
||||
t.Fatalf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveLoadRoundTrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
|
||||
@@ -3,15 +3,32 @@ package proc
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
const LogMaxAge = 24 * time.Hour
|
||||
|
||||
type Tag struct {
|
||||
Name string
|
||||
Color *color.Color
|
||||
}
|
||||
|
||||
type LogFile struct {
|
||||
Name string
|
||||
Path string
|
||||
Size int64
|
||||
ModTime time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
TagBuild = Tag{"build", color.New(color.FgYellow)}
|
||||
TagAgent = Tag{"agent", color.New(color.FgMagenta)}
|
||||
@@ -24,23 +41,115 @@ var (
|
||||
WarnColor = color.New(color.FgYellow)
|
||||
BoldColor = color.New(color.Bold)
|
||||
DimColor = color.New(color.Faint)
|
||||
|
||||
ansiPattern = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||
)
|
||||
|
||||
func LogMsg(t Tag, msg string) {
|
||||
fmt.Printf("%s %s\n", t.Color.Sprintf("[%s]", t.Name), msg)
|
||||
logMsg(t, msg, os.Stdout, nil, nil)
|
||||
}
|
||||
|
||||
func LogMsgf(t Tag, format string, args ...any) {
|
||||
LogMsg(t, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func StreamLines(r interface{ Read([]byte) (int, error) }, t Tag) {
|
||||
func LogMsgTee(t Tag, msg string, file io.Writer, fileMu *sync.Mutex) {
|
||||
logMsg(t, msg, os.Stdout, file, fileMu)
|
||||
}
|
||||
|
||||
func StreamLines(r io.Reader, t Tag) {
|
||||
streamLines(r, t, os.Stdout, nil, nil)
|
||||
}
|
||||
|
||||
func OpenLogFile(logDir string, name string, now time.Time) (*os.File, string, error) {
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
path := filepath.Join(logDir, name)
|
||||
if err := rotateLogIfNeeded(path, now); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return file, path, nil
|
||||
}
|
||||
|
||||
func ListLogFiles(logDir string) ([]LogFile, error) {
|
||||
entries, err := os.ReadDir(logDir)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files := []LogFile{}
|
||||
for _, entry := range entries {
|
||||
if !strings.HasSuffix(entry.Name(), ".log") && !strings.HasSuffix(entry.Name(), ".log.old") {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
continue
|
||||
}
|
||||
files = append(files, LogFile{
|
||||
Name: entry.Name(),
|
||||
Path: filepath.Join(logDir, entry.Name()),
|
||||
Size: info.Size(),
|
||||
ModTime: info.ModTime(),
|
||||
})
|
||||
}
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].Name < files[j].Name
|
||||
})
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func rotateLogIfNeeded(logPath string, now time.Time) error {
|
||||
info, err := os.Stat(logPath)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if now.Sub(info.ModTime()) <= LogMaxAge {
|
||||
return nil
|
||||
}
|
||||
backupPath := logPath + ".old"
|
||||
if err := os.Remove(backupPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return os.Rename(logPath, backupPath)
|
||||
}
|
||||
|
||||
func streamLines(r io.Reader, t Tag, terminal io.Writer, file io.Writer, fileMu *sync.Mutex) {
|
||||
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)
|
||||
logMsg(t, line, terminal, file, fileMu)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
logMsg(t, fmt.Sprintf("log stream error: %v", err), terminal, file, fileMu)
|
||||
}
|
||||
}
|
||||
|
||||
func logMsg(t Tag, msg string, terminal io.Writer, file io.Writer, fileMu *sync.Mutex) {
|
||||
if fileMu != nil {
|
||||
fileMu.Lock()
|
||||
defer fileMu.Unlock()
|
||||
}
|
||||
fmt.Fprintf(terminal, "%s %s\n", t.Color.Sprintf("[%s]", t.Name), msg)
|
||||
if file == nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(file, "[%s] %s\n", t.Name, ansiPattern.ReplaceAllString(msg, ""))
|
||||
}
|
||||
|
||||
147
packages/browseros-agent/tools/alpha/proc/log_test.go
Normal file
147
packages/browseros-agent/tools/alpha/proc/log_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package proc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestOpenLogFileRotatesFileOlderThanOneDay(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "server.log")
|
||||
if err := os.WriteFile(path, []byte("old\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)
|
||||
oldTime := now.Add(-25 * time.Hour)
|
||||
if err := os.Chtimes(path, oldTime, oldTime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
file, gotPath, err := OpenLogFile(dir, "server.log", now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotPath != path {
|
||||
t.Fatalf("got %q want %q", gotPath, path)
|
||||
}
|
||||
if _, err := file.WriteString("new\n"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rotated, err := os.ReadFile(path + ".old")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(rotated) != "old\n" {
|
||||
t.Fatalf("rotated content = %q", rotated)
|
||||
}
|
||||
current, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(current) != "new\n" {
|
||||
t.Fatalf("current content = %q", current)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenLogFileAppendsFreshFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "chromium.log")
|
||||
if err := os.WriteFile(path, []byte("old\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)
|
||||
fresh := now.Add(-time.Hour)
|
||||
if err := os.Chtimes(path, fresh, fresh); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
file, _, err := OpenLogFile(dir, "chromium.log", now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := file.WriteString("new\n"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(path + ".old"); !os.IsNotExist(err) {
|
||||
t.Fatalf("unexpected rotated file, stat err=%v", err)
|
||||
}
|
||||
current, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(current) != "old\nnew\n" {
|
||||
t.Fatalf("current content = %q", current)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListLogFilesReturnsRegularFilesSortedByName(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
for _, name := range []string{"server.log", "chromium.log.old", "chromium.log", "server.log.backup"} {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(name), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(dir, "nested.log"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
files, err := ListLogFiles(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := []string{}
|
||||
for _, file := range files {
|
||||
got = append(got, file.Name)
|
||||
}
|
||||
want := []string{"chromium.log", "chromium.log.old", "server.log"}
|
||||
if strings.Join(got, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("got %#v want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamLinesWritesTerminalAndFile(t *testing.T) {
|
||||
var terminal bytes.Buffer
|
||||
var file bytes.Buffer
|
||||
var mu sync.Mutex
|
||||
|
||||
streamLines(strings.NewReader("first\nsecond\n"), TagServer, &terminal, &file, &mu)
|
||||
|
||||
terminalOutput := terminal.String()
|
||||
if !strings.Contains(terminalOutput, "[server] first") || !strings.Contains(terminalOutput, "[server] second") {
|
||||
t.Fatalf("unexpected terminal output: %q", terminalOutput)
|
||||
}
|
||||
fileOutput := file.String()
|
||||
if fileOutput != "[server] first\n[server] second\n" {
|
||||
t.Fatalf("unexpected file output: %q", fileOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamLinesLogsScannerErrors(t *testing.T) {
|
||||
var terminal bytes.Buffer
|
||||
var file bytes.Buffer
|
||||
var mu sync.Mutex
|
||||
longLine := strings.Repeat("x", 1024*1024+1)
|
||||
|
||||
streamLines(strings.NewReader(longLine), TagBrowser, &terminal, &file, &mu)
|
||||
|
||||
for name, got := range map[string]string{
|
||||
"terminal": terminal.String(),
|
||||
"file": file.String(),
|
||||
} {
|
||||
if !strings.Contains(got, "log stream error: bufio.Scanner: token too long") {
|
||||
t.Fatalf("%s output missing scanner error: %q", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
@@ -17,6 +19,7 @@ type ProcConfig struct {
|
||||
Restart bool
|
||||
Cmd []string
|
||||
BeforeStart func() error
|
||||
LogPath string
|
||||
}
|
||||
|
||||
type ManagedProc struct {
|
||||
@@ -45,6 +48,22 @@ func StartManaged(ctx context.Context, wg *sync.WaitGroup, cfg ProcConfig) *Mana
|
||||
}
|
||||
|
||||
func (mp *ManagedProc) run(ctx context.Context) {
|
||||
var logFile *os.File
|
||||
var logMu sync.Mutex
|
||||
if mp.Cfg.LogPath != "" {
|
||||
file, path, err := OpenLogFile(filepath.Dir(mp.Cfg.LogPath), filepath.Base(mp.Cfg.LogPath), time.Now())
|
||||
if err != nil {
|
||||
LogMsg(mp.Cfg.Tag, WarnColor.Sprintf("File logging disabled: %v", err))
|
||||
} else {
|
||||
logFile = file
|
||||
defer logFile.Close()
|
||||
LogMsgTee(mp.Cfg.Tag, "Writing log file: "+path, logFile, &logMu)
|
||||
}
|
||||
}
|
||||
log := func(msg string) {
|
||||
LogMsgTee(mp.Cfg.Tag, msg, logFile, &logMu)
|
||||
}
|
||||
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
@@ -52,7 +71,7 @@ func (mp *ManagedProc) run(ctx context.Context) {
|
||||
|
||||
if mp.Cfg.BeforeStart != nil {
|
||||
if err := mp.Cfg.BeforeStart(); err != nil {
|
||||
LogMsg(mp.Cfg.Tag, ErrorColor.Sprintf("Pre-start failed: %v", err))
|
||||
log(ErrorColor.Sprintf("Pre-start failed: %v", err))
|
||||
if !mp.Cfg.Restart || ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
@@ -61,7 +80,7 @@ func (mp *ManagedProc) run(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
LogMsgf(mp.Cfg.Tag, "Starting: %s", DimColor.Sprint(strings.Join(mp.Cfg.Cmd, " ")))
|
||||
log(fmt.Sprintf("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
|
||||
@@ -74,7 +93,7 @@ func (mp *ManagedProc) run(ctx context.Context) {
|
||||
stderr, _ := cmd.StderrPipe()
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
LogMsg(mp.Cfg.Tag, ErrorColor.Sprintf("Error starting: %v", err))
|
||||
log(ErrorColor.Sprintf("Error starting: %v", err))
|
||||
if !mp.Cfg.Restart || ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
@@ -94,8 +113,8 @@ func (mp *ManagedProc) run(ctx context.Context) {
|
||||
|
||||
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) }()
|
||||
go func() { defer streamWg.Done(); streamLines(stdout, mp.Cfg.Tag, os.Stdout, logFile, &logMu) }()
|
||||
go func() { defer streamWg.Done(); streamLines(stderr, mp.Cfg.Tag, os.Stdout, logFile, &logMu) }()
|
||||
|
||||
streamWg.Wait()
|
||||
_ = cmd.Wait()
|
||||
@@ -111,16 +130,16 @@ func (mp *ManagedProc) run(ctx context.Context) {
|
||||
|
||||
exitCode := cmd.ProcessState.ExitCode()
|
||||
if exitCode != 0 {
|
||||
LogMsg(mp.Cfg.Tag, ErrorColor.Sprintf("Process exited with code %d", exitCode))
|
||||
log(ErrorColor.Sprintf("Process exited with code %d", exitCode))
|
||||
} else {
|
||||
LogMsg(mp.Cfg.Tag, "Process exited cleanly")
|
||||
log("Process exited cleanly")
|
||||
}
|
||||
|
||||
if !mp.Cfg.Restart {
|
||||
return
|
||||
}
|
||||
|
||||
LogMsg(mp.Cfg.Tag, WarnColor.Sprint("Restarting in 1s..."))
|
||||
log(WarnColor.Sprint("Restarting in 1s..."))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user