diff --git a/cmd/picoclaw/internal/gateway/command.go b/cmd/picoclaw/internal/gateway/command.go index 4812f1bee..7fa588c5c 100644 --- a/cmd/picoclaw/internal/gateway/command.go +++ b/cmd/picoclaw/internal/gateway/command.go @@ -34,7 +34,7 @@ func NewGatewayCommand() *cobra.Command { return nil }, RunE: func(_ *cobra.Command, _ []string) error { - return gateway.Run(debug, internal.GetConfigPath(), allowEmpty) + return gateway.Run(debug, internal.GetPicoclawHome(), internal.GetConfigPath(), allowEmpty) }, } diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go index 454ee2c48..fc2465747 100644 --- a/pkg/gateway/gateway.go +++ b/pkg/gateway/gateway.go @@ -47,6 +47,10 @@ const ( serviceShutdownTimeout = 30 * time.Second providerReloadTimeout = 30 * time.Second gracefulShutdownTimeout = 15 * time.Second + + logPath = "logs" + panicFile = "gateway_panic.log" + logFile = "gateway.log" ) type services struct { @@ -79,7 +83,19 @@ func (p *startupBlockedProvider) GetDefaultModel() string { } // Run starts the gateway runtime using the configuration loaded from configPath. -func Run(debug bool, configPath string, allowEmptyStartup bool) error { +func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) error { + panicPath := filepath.Join(homePath, logPath, panicFile) + panicFunc, err := logger.InitPanic(panicPath) + if err != nil { + return fmt.Errorf("error initializing panic log: %w", err) + } + defer panicFunc() + + if err = logger.EnableFileLogging(filepath.Join(homePath, logPath, logFile)); err != nil { + panic(fmt.Sprintf("error enabling file logging: %v", err)) + } + defer logger.DisableFileLogging() + cfg, err := config.LoadConfig(configPath) if err != nil { return fmt.Errorf("error loading config: %w", err) diff --git a/pkg/logger/panic.go b/pkg/logger/panic.go new file mode 100644 index 000000000..e53e4351a --- /dev/null +++ b/pkg/logger/panic.go @@ -0,0 +1,36 @@ +package logger + +import ( + "fmt" + "os" + "path/filepath" + "runtime/debug" + "time" +) + +func InitPanic(filePath string) (func(), error) { + if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil { + return nil, fmt.Errorf("failed to create log directory: %w", err) + } + writer := initPanicFile(filePath) + if writer == nil { + return nil, fmt.Errorf("failed to create log file: %s", filePath) + } + return func() { + defer writer.Close() + if err := recover(); err != nil { + now := time.Now().Format("2006-01-02 15:04:05") + stack := debug.Stack() + logMsg := "\n\n====================\n[" + now + "] PANIC OCCURRED: " + fmt.Sprintf( + "%v", + err, + ) + "\n" + string( + stack, + ) + + writer.Write([]byte(logMsg)) + + os.Exit(1) + } + }, nil +} diff --git a/pkg/logger/panic_unix.go b/pkg/logger/panic_unix.go new file mode 100644 index 000000000..1178f6a5a --- /dev/null +++ b/pkg/logger/panic_unix.go @@ -0,0 +1,21 @@ +//go:build !windows +// +build !windows + +package logger + +import ( + "fmt" + "io" + "os" +) + +func initPanicFile(panicFile string) io.WriteCloser { + file, err := os.OpenFile(panicFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND|os.O_SYNC, 0o600) + if err != nil { + panic(fmt.Sprintf("error in open panic: %v", err)) + } + if err = Dup2(int(file.Fd()), int(os.Stderr.Fd())); err != nil { + panic(fmt.Sprintf("error in syscall.Dup2: %v", err)) + } + return file +} diff --git a/pkg/logger/panic_win.go b/pkg/logger/panic_win.go new file mode 100644 index 000000000..29d3f21d8 --- /dev/null +++ b/pkg/logger/panic_win.go @@ -0,0 +1,25 @@ +//go:build windows +// +build windows + +package logger + +import ( + "fmt" + "io" + "os" + + "golang.org/x/sys/windows" +) + +func initPanicFile(panicFile string) io.WriteCloser { + file, err := os.OpenFile(panicFile, os.O_WRONLY|os.O_CREATE|os.O_SYNC|os.O_APPEND, 0600) + if err != nil { + panic(fmt.Sprintf("error in open panic: %v", err)) + } + err = windows.SetStdHandle(windows.STD_ERROR_HANDLE, windows.Handle(file.Fd())) + if err != nil { + panic(fmt.Sprintf("Failed to redirect stderr to file: %v", err)) + } + os.Stderr = file + return file +} diff --git a/pkg/logger/syscall_amd64.go b/pkg/logger/syscall_amd64.go new file mode 100644 index 000000000..e862b4578 --- /dev/null +++ b/pkg/logger/syscall_amd64.go @@ -0,0 +1,12 @@ +//go:build linux && amd64 +// +build linux,amd64 + +package logger + +import ( + "syscall" +) + +func Dup2(oldfd int, newfd int) error { + return syscall.Dup2(oldfd, newfd) +} diff --git a/pkg/logger/syscall_arm64.go b/pkg/logger/syscall_arm64.go new file mode 100644 index 000000000..9854bc85e --- /dev/null +++ b/pkg/logger/syscall_arm64.go @@ -0,0 +1,12 @@ +//go:build linux && arm64 +// +build linux,arm64 + +package logger + +import ( + "syscall" +) + +func Dup2(oldfd int, newfd int) error { + return syscall.Dup3(oldfd, newfd, 0) +} diff --git a/pkg/logger/syscall_darwin.go b/pkg/logger/syscall_darwin.go new file mode 100644 index 000000000..80306b521 --- /dev/null +++ b/pkg/logger/syscall_darwin.go @@ -0,0 +1,12 @@ +//go:build darwin +// +build darwin + +package logger + +import ( + "syscall" +) + +func Dup2(oldfd int, newfd int) error { + return syscall.Dup2(oldfd, newfd) +} diff --git a/pkg/logger/syscall_loong64.go b/pkg/logger/syscall_loong64.go new file mode 100644 index 000000000..6f36d26a4 --- /dev/null +++ b/pkg/logger/syscall_loong64.go @@ -0,0 +1,12 @@ +//go:build linux && loong64 +// +build linux,loong64 + +package logger + +import ( + "syscall" +) + +func Dup2(oldfd int, newfd int) error { + return syscall.Dup3(oldfd, newfd, 0) +} diff --git a/web/backend/main.go b/web/backend/main.go index b1db3c57a..8183731fe 100644 --- a/web/backend/main.go +++ b/web/backend/main.go @@ -33,6 +33,10 @@ import ( const ( appName = "PicoClaw" + + logPath = "logs" + panicFile = "launcher_panic.log" + logFile = "launcher.log" ) var ( @@ -72,6 +76,14 @@ func main() { // Initialize logger picoHome := utils.GetPicoclawHome() + + f := filepath.Join(picoHome, logPath, panicFile) + panicFunc, err := logger.InitPanic(f) + if err != nil { + panic(fmt.Sprintf("error initializing panic log: %v", err)) + } + defer panicFunc() + // By default, detect terminal to decide console log behavior // If -console-logs flag is explicitly set, it overrides the detection enableConsole := *console @@ -79,11 +91,9 @@ func main() { // Disable console logging by setting level to Fatal (no output) logger.SetConsoleLevel(logger.FATAL) - logPath := filepath.Join(picoHome, "logs", "web.log") - if err := logger.EnableFileLogging(logPath); err != nil { - // FIXME: https://github.com/sipeed/picoclaw/issues/1734 - fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err) - os.Exit(1) + f := filepath.Join(picoHome, logPath, logFile) + if err = logger.EnableFileLogging(f); err != nil { + panic(fmt.Sprintf("error enabling file logging: %v", err)) } defer logger.DisableFileLogging() }