diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index f3cfe01..5a8999e 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -120,16 +120,24 @@ func initCLI() { initConsoleLogging() }, Run: func(cmd *cobra.Command, args []string) { - if daemon && runtime.GOOS == "windows" { - mainLog.Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.") - } - waitCh := make(chan struct{}) stopCh := make(chan struct{}) p := &prog{ waitCh: waitCh, stopCh: stopCh, } + sockPath := filepath.Join(homedir, ctrldLogUnixSock) + if addr, err := net.ResolveUnixAddr("unix", sockPath); err == nil { + if conn, err := net.Dial(addr.Network(), addr.String()); err == nil { + consoleWriter.Out = io.MultiWriter(os.Stdout, conn) + p.logConn = conn + } + } + + if daemon && runtime.GOOS == "windows" { + mainLog.Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.") + } + if !daemon { // We need to call s.Run() as soon as possible to response to the OS manager, so it // can see ctrld is running and don't mark ctrld as failed service. @@ -309,12 +317,36 @@ func initCLI() { if configPath != "" { v.SetConfigFile(configPath) } + + // A buffer channel to gather log output from runCmd and report + // to user in case self-check process failed. + runCmdLogCh := make(chan string, 256) if dir, err := userHomeDir(); err == nil { setWorkingDirectory(sc, dir) if configPath == "" && writeDefaultConfig { defaultConfigFile = filepath.Join(dir, defaultConfigFile) } sc.Arguments = append(sc.Arguments, "--homedir="+dir) + sockPath := filepath.Join(dir, ctrldLogUnixSock) + _ = os.Remove(sockPath) + go func() { + defer func() { + close(runCmdLogCh) + _ = os.Remove(sockPath) + }() + if conn := runLogServer(sockPath); conn != nil { + // Enough buffer for log message, we don't produce + // such long log message, but just in case. + buf := make([]byte, 1024) + for { + n, err := conn.Read(buf) + if err != nil { + return + } + runCmdLogCh <- string(buf[:n]) + } + } + }() } tryReadingConfig(writeDefaultConfig) @@ -370,13 +402,19 @@ func initCLI() { case service.StatusRunning: mainLog.Notice().Msg("Service started") default: - mainLog.Error().Msg("Service did not start, please check system/service log for details error") + marker := bytes.Repeat([]byte("="), 32) + mainLog.Error().Msg("ctrld service may not have started due to an error or misconfiguration, service log:") + _, _ = mainLog.Write(marker) + for msg := range runCmdLogCh { + _, _ = mainLog.Write([]byte(msg)) + } + _, _ = mainLog.Write(marker) uninstall(p, s) os.Exit(1) } // On Linux, Darwin, Freebsd, ctrld set DNS on startup, because the DNS setting could be // reset after rebooting. On windows, we only need to set once here. See prog.preRun in - // prog_*.go file for dedicated code on each platforms. + // prog_*.go file for dedicated code on each platform. if runtime.GOOS == "windows" { p.setDNS() } diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index cf062b5..e53436f 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -22,7 +22,10 @@ import ( "github.com/Control-D-Inc/ctrld/internal/router/ubios" ) -const defaultSemaphoreCap = 256 +const ( + defaultSemaphoreCap = 256 + ctrldLogUnixSock = "ctrld_start.sock" +) var logf = func(format string, args ...any) { mainLog.Debug().Msgf(format, args...) @@ -39,9 +42,10 @@ var svcConfig = &service.Config{ var useSystemdResolved = false type prog struct { - mu sync.Mutex - waitCh chan struct{} - stopCh chan struct{} + mu sync.Mutex + waitCh chan struct{} + stopCh chan struct{} + logConn net.Conn cfg *ctrld.Config cache dnscache.Cacher @@ -174,6 +178,12 @@ func (p *prog) run() { for _, f := range p.onStarted { f() } + // Stop writing log to unix socket. + consoleWriter.Out = os.Stdout + initLoggingWithBackup(false) + if p.logConn != nil { + _ = p.logConn.Close() + } wg.Wait() } @@ -301,3 +311,25 @@ func randomLocalIP() string { n := rand.Intn(254-2) + 2 return fmt.Sprintf("127.0.0.%d", n) } + +// runLogServer starts a unix listener, use by startCmd to gather log from runCmd. +func runLogServer(sockPath string) net.Conn { + addr, err := net.ResolveUnixAddr("unix", sockPath) + if err != nil { + mainLog.Warn().Err(err).Msg("invalid log sock path") + return nil + } + ln, err := net.ListenUnix("unix", addr) + if err != nil { + mainLog.Warn().Err(err).Msg("could not listen log socket") + return nil + } + defer ln.Close() + + server, err := ln.Accept() + if err != nil { + mainLog.Warn().Err(err).Msg("could not accept connection") + return nil + } + return server +}