From d43e50ee2d98da882dc36ee15fecb119d01d1840 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 29 Jun 2023 22:44:58 +0700 Subject: [PATCH] cmd/ctrld: produce better message when "ctrd start" failed The current error message is not much helpful, not all users are able to investigate system log file to find the reason. Instead, gathering the log output of "ctrld run" command, and if error happens or self-check failed, print the log to users. --- cmd/ctrld/cli.go | 50 +++++++++++++++++++++++++++++++++++++++++------ cmd/ctrld/prog.go | 40 +++++++++++++++++++++++++++++++++---- 2 files changed, 80 insertions(+), 10 deletions(-) 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 +}