src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/daemon/activate.go (about) 1 package daemon 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "time" 10 11 "src.elv.sh/pkg/daemon/daemondefs" 12 "src.elv.sh/pkg/daemon/internal/api" 13 "src.elv.sh/pkg/fsutil" 14 ) 15 16 var ( 17 daemonSpawnTimeout = time.Second 18 daemonSpawnWaitPerLoop = 10 * time.Millisecond 19 20 daemonKillTimeout = time.Second 21 daemonKillWaitPerLoop = 10 * time.Millisecond 22 ) 23 24 type daemonStatus int 25 26 const ( 27 daemonOK daemonStatus = iota 28 sockfileMissing 29 sockfileOtherError 30 connectionRefused 31 connectionOtherError 32 daemonOutdated 33 ) 34 35 const connectionRefusedFmt = "Socket file %s exists but refuses requests. This is likely because the daemon was terminated abnormally. Going to remove socket file and re-spawn the daemon.\n" 36 37 // Activate returns a daemon client, either by connecting to an existing daemon, 38 // or spawning a new one. It always returns a non-nil client, even if there was an error. 39 func Activate(stderr io.Writer, spawnCfg *daemondefs.SpawnConfig) (daemondefs.Client, error) { 40 sockpath := spawnCfg.SockPath 41 cl := NewClient(sockpath) 42 status, err := detectDaemon(sockpath, cl) 43 shouldSpawn := false 44 45 switch status { 46 case daemonOK: 47 case sockfileMissing: 48 shouldSpawn = true 49 case sockfileOtherError: 50 return cl, fmt.Errorf("socket file %s inaccessible: %w", sockpath, err) 51 case connectionRefused: 52 fmt.Fprintf(stderr, connectionRefusedFmt, sockpath) 53 err := os.Remove(sockpath) 54 if err != nil { 55 return cl, fmt.Errorf("failed to remove socket file: %w", err) 56 } 57 shouldSpawn = true 58 case connectionOtherError: 59 return cl, fmt.Errorf("unexpected RPC error on socket %s: %w", sockpath, err) 60 case daemonOutdated: 61 fmt.Fprintln(stderr, "Daemon is outdated; going to kill old daemon and re-spawn") 62 err := killDaemon(sockpath, cl) 63 if err != nil { 64 return cl, fmt.Errorf("failed to kill old daemon: %w", err) 65 } 66 shouldSpawn = true 67 default: 68 return cl, fmt.Errorf("code bug: unknown daemon status %d", status) 69 } 70 71 if !shouldSpawn { 72 return cl, nil 73 } 74 75 err = spawn(spawnCfg) 76 if err != nil { 77 return cl, fmt.Errorf("failed to spawn daemon: %w", err) 78 } 79 80 // Wait for daemon to come online 81 start := time.Now() 82 for time.Since(start) < daemonSpawnTimeout { 83 cl.ResetConn() 84 status, err := detectDaemon(sockpath, cl) 85 86 switch status { 87 case daemonOK: 88 return cl, nil 89 case sockfileMissing: 90 // Continue waiting 91 case sockfileOtherError: 92 return cl, fmt.Errorf("socket file %s inaccessible: %w", sockpath, err) 93 case connectionRefused: 94 // Continue waiting 95 case connectionOtherError: 96 return cl, fmt.Errorf("unexpected RPC error on socket %s: %w", sockpath, err) 97 case daemonOutdated: 98 return cl, fmt.Errorf("code bug: newly spawned daemon is outdated") 99 default: 100 return cl, fmt.Errorf("code bug: unknown daemon status %d", status) 101 } 102 time.Sleep(daemonSpawnWaitPerLoop) 103 } 104 return cl, fmt.Errorf("daemon did not come up within %v", daemonSpawnTimeout) 105 } 106 107 func detectDaemon(sockpath string, cl daemondefs.Client) (daemonStatus, error) { 108 _, err := os.Lstat(sockpath) 109 if err != nil { 110 if os.IsNotExist(err) { 111 return sockfileMissing, err 112 } 113 return sockfileOtherError, err 114 } 115 116 version, err := cl.Version() 117 if err != nil { 118 if errors.Is(err, errConnRefused) { 119 return connectionRefused, err 120 } 121 return connectionOtherError, err 122 } 123 if version < api.Version { 124 return daemonOutdated, nil 125 } 126 return daemonOK, nil 127 } 128 129 func killDaemon(sockpath string, cl daemondefs.Client) error { 130 pid, err := cl.Pid() 131 if err != nil { 132 return fmt.Errorf("kill daemon: %w", err) 133 } 134 process, err := os.FindProcess(pid) 135 if err != nil { 136 return fmt.Errorf("kill daemon: %w", err) 137 } 138 err = process.Signal(os.Interrupt) 139 if err != nil { 140 return fmt.Errorf("kill daemon: %w", err) 141 } 142 // Wait until the old daemon has removed the socket file, so that it doesn't 143 // inadvertently remove the socket file of the new daemon we will start. 144 start := time.Now() 145 for time.Since(start) < daemonKillTimeout { 146 _, err := os.Lstat(sockpath) 147 if err == nil { 148 time.Sleep(daemonKillWaitPerLoop) 149 } else if os.IsNotExist(err) { 150 return nil 151 } else { 152 return fmt.Errorf("kill daemon: %w", err) 153 } 154 } 155 return fmt.Errorf("kill daemon: daemon did not remove socket within %v", daemonKillTimeout) 156 } 157 158 // Can be overridden in tests to avoid actual forking. 159 var startProcess = func(name string, argv []string, attr *os.ProcAttr) error { 160 _, err := os.StartProcess(name, argv, attr) 161 return err 162 } 163 164 // Spawns a daemon process in the background by invoking BinPath, passing 165 // BinPath, DbPath and SockPath as command-line arguments after resolving them 166 // to absolute paths. The daemon log file is created in RunDir, and the stdout 167 // and stderr of the daemon is redirected to the log file. 168 // 169 // A suitable ProcAttr is chosen depending on the OS and makes sure that the 170 // daemon is detached from the current terminal, so that it is not affected by 171 // I/O or signals in the current terminal and keeps running after the current 172 // process quits. 173 func spawn(cfg *daemondefs.SpawnConfig) error { 174 binPath, err := os.Executable() 175 if err != nil { 176 return errors.New("cannot find elvish: " + err.Error()) 177 } 178 dbPath, err := abs("DbPath", cfg.DbPath) 179 if err != nil { 180 return err 181 } 182 sockPath, err := abs("SockPath", cfg.SockPath) 183 if err != nil { 184 return err 185 } 186 187 args := []string{ 188 binPath, 189 "-daemon", 190 "-db", dbPath, 191 "-sock", sockPath, 192 } 193 194 // The daemon does not read any input; open DevNull and use it for stdin. We 195 // could also just close the stdin, but on Unix that would make the first 196 // file opened by the daemon take FD 0. 197 in, err := os.OpenFile(os.DevNull, os.O_RDONLY, 0) 198 if err != nil { 199 return err 200 } 201 defer in.Close() 202 203 out, err := fsutil.ClaimFile(cfg.RunDir, "daemon-*.log") 204 if err != nil { 205 return err 206 } 207 defer out.Close() 208 209 procattrs := procAttrForSpawn([]*os.File{in, out, out}) 210 211 err = startProcess(binPath, args, procattrs) 212 return err 213 } 214 215 func abs(name, path string) (string, error) { 216 if path == "" { 217 return "", fmt.Errorf("%s is required for spawning daemon", name) 218 } 219 absPath, err := filepath.Abs(path) 220 if err != nil { 221 return "", fmt.Errorf("cannot resolve %s to absolute path: %s", name, err) 222 } 223 return absPath, nil 224 }