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  }