github.com/kolbycrouch/elvish@v0.14.1-0.20210614162631-215b9ac1c423/pkg/shell/runtime.go (about) 1 package shell 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "os" 8 "time" 9 10 bolt "go.etcd.io/bbolt" 11 "src.elv.sh/pkg/daemon" 12 "src.elv.sh/pkg/eval" 13 daemonmod "src.elv.sh/pkg/eval/mods/daemon" 14 "src.elv.sh/pkg/eval/mods/file" 15 mathmod "src.elv.sh/pkg/eval/mods/math" 16 pathmod "src.elv.sh/pkg/eval/mods/path" 17 "src.elv.sh/pkg/eval/mods/platform" 18 "src.elv.sh/pkg/eval/mods/re" 19 "src.elv.sh/pkg/eval/mods/store" 20 "src.elv.sh/pkg/eval/mods/str" 21 "src.elv.sh/pkg/eval/mods/unix" 22 "src.elv.sh/pkg/rpc" 23 ) 24 25 const ( 26 daemonWaitLoops = 100 27 daemonWaitPerLoop = 10 * time.Millisecond 28 ) 29 30 type daemonStatus int 31 32 const ( 33 daemonOK daemonStatus = iota 34 sockfileMissing 35 sockfileOtherError 36 connectionShutdown 37 connectionOtherError 38 daemonInvalidDB 39 daemonOutdated 40 ) 41 42 const ( 43 daemonWontWorkMsg = "Daemon-related functions will likely not work." 44 connectionShutdownFmt = "Socket file %s exists but is not responding to request. This is likely due to abnormal shutdown of the daemon. Going to remove socket file and re-spawn a daemon.\n" 45 ) 46 47 var errInvalidDB = errors.New("daemon reported that database is invalid. If you upgraded Elvish from a pre-0.10 version, you need to upgrade your database by following instructions in https://github.com/elves/upgrade-db-for-0.10/") 48 49 // InitRuntime initializes the runtime. The caller should call CleanupRuntime 50 // when the Evaler is no longer needed. 51 func InitRuntime(stderr io.Writer, p Paths, spawn bool) *eval.Evaler { 52 ev := eval.NewEvaler() 53 ev.SetLibDir(p.LibDir) 54 ev.AddModule("math", mathmod.Ns) 55 ev.AddModule("path", pathmod.Ns) 56 ev.AddModule("platform", platform.Ns) 57 ev.AddModule("re", re.Ns) 58 ev.AddModule("str", str.Ns) 59 ev.AddModule("file", file.Ns) 60 if unix.ExposeUnixNs { 61 ev.AddModule("unix", unix.Ns) 62 } 63 64 if spawn && p.Sock != "" && p.Db != "" { 65 spawnCfg := &daemon.SpawnConfig{ 66 RunDir: p.RunDir, 67 BinPath: p.Bin, 68 DbPath: p.Db, 69 SockPath: p.Sock, 70 } 71 // TODO(xiaq): Connect to daemon and install daemon module 72 // asynchronously. 73 client, err := connectToDaemon(stderr, spawnCfg) 74 if err != nil { 75 fmt.Fprintln(stderr, "Cannot connect to daemon:", err) 76 fmt.Fprintln(stderr, daemonWontWorkMsg) 77 } 78 // Even if error is not nil, we install daemon-related functionalities 79 // anyway. Daemon may eventually come online and become functional. 80 ev.SetDaemonClient(client) 81 ev.AddModule("store", store.Ns(client)) 82 ev.AddModule("daemon", daemonmod.Ns(client, spawnCfg)) 83 } 84 return ev 85 } 86 87 // CleanupRuntime cleans up the runtime. 88 func CleanupRuntime(stderr io.Writer, ev *eval.Evaler) { 89 daemon := ev.DaemonClient() 90 if daemon != nil { 91 err := daemon.Close() 92 if err != nil { 93 fmt.Fprintln(stderr, 94 "warning: failed to close connection to daemon:", err) 95 } 96 } 97 } 98 99 func connectToDaemon(stderr io.Writer, spawnCfg *daemon.SpawnConfig) (daemon.Client, error) { 100 sockpath := spawnCfg.SockPath 101 cl := daemon.NewClient(sockpath) 102 status, err := detectDaemon(sockpath, cl) 103 shouldSpawn := false 104 105 switch status { 106 case daemonOK: 107 case sockfileMissing: 108 shouldSpawn = true 109 case sockfileOtherError: 110 return cl, fmt.Errorf("socket file %s inaccessible: %v", sockpath, err) 111 case connectionShutdown: 112 fmt.Fprintf(stderr, connectionShutdownFmt, sockpath) 113 err := os.Remove(sockpath) 114 if err != nil { 115 return cl, fmt.Errorf("failed to remove socket file: %v", err) 116 } 117 shouldSpawn = true 118 case connectionOtherError: 119 return cl, fmt.Errorf("unexpected RPC error on socket %s: %v", sockpath, err) 120 case daemonInvalidDB: 121 return cl, errInvalidDB 122 case daemonOutdated: 123 fmt.Fprintln(stderr, "Daemon is outdated; going to kill old daemon and re-spawn") 124 err := killDaemon(cl) 125 if err != nil { 126 return cl, fmt.Errorf("failed to kill old daemon: %v", err) 127 } 128 shouldSpawn = true 129 default: 130 return cl, fmt.Errorf("code bug: unknown daemon status %d", status) 131 } 132 133 if !shouldSpawn { 134 return cl, nil 135 } 136 137 err = daemon.Spawn(spawnCfg) 138 if err != nil { 139 return cl, fmt.Errorf("failed to spawn daemon: %v", err) 140 } 141 logger.Println("Spawned daemon") 142 143 // Wait for daemon to come online 144 for i := 0; i <= daemonWaitLoops; i++ { 145 cl.ResetConn() 146 status, err := detectDaemon(sockpath, cl) 147 148 switch status { 149 case daemonOK: 150 return cl, nil 151 case sockfileMissing: 152 // Continue waiting 153 case sockfileOtherError: 154 return cl, fmt.Errorf("socket file %s inaccessible: %v", sockpath, err) 155 case connectionShutdown: 156 // Continue waiting 157 case connectionOtherError: 158 return cl, fmt.Errorf("unexpected RPC error on socket %s: %v", sockpath, err) 159 case daemonInvalidDB: 160 return cl, errInvalidDB 161 case daemonOutdated: 162 return cl, fmt.Errorf("code bug: newly spawned daemon is outdated") 163 default: 164 return cl, fmt.Errorf("code bug: unknown daemon status %d", status) 165 } 166 time.Sleep(daemonWaitPerLoop) 167 } 168 return cl, fmt.Errorf("daemon unreachable after waiting for %s", daemonWaitLoops*daemonWaitPerLoop) 169 } 170 171 func detectDaemon(sockpath string, cl daemon.Client) (daemonStatus, error) { 172 _, err := os.Stat(sockpath) 173 if err != nil { 174 if os.IsNotExist(err) { 175 return sockfileMissing, err 176 } 177 return sockfileOtherError, err 178 } 179 180 version, err := cl.Version() 181 if err != nil { 182 switch { 183 case err == rpc.ErrShutdown: 184 return connectionShutdown, err 185 case err.Error() == bolt.ErrInvalid.Error(): 186 return daemonInvalidDB, err 187 default: 188 return connectionOtherError, err 189 } 190 } 191 if version < daemon.Version { 192 return daemonOutdated, nil 193 } 194 return daemonOK, nil 195 } 196 197 func killDaemon(cl daemon.Client) error { 198 pid, err := cl.Pid() 199 if err != nil { 200 return fmt.Errorf("cannot get pid of daemon: %v", err) 201 } 202 process, err := os.FindProcess(pid) 203 if err != nil { 204 return fmt.Errorf("cannot find daemon process (pid=%d): %v", pid, err) 205 } 206 return process.Signal(os.Interrupt) 207 }