github.com/ladydascalie/elvish@v0.0.0-20170703214355-2964dd3ece7f/main.go (about)

     1  // Elvish is an experimental Unix shell. It tries to incorporate a powerful
     2  // programming language with an extensible, friendly user interface.
     3  package main
     4  
     5  // This package sets up the basic environment and calls the appropriate
     6  // "subprogram", one of the daemon, the terminal interface, or the web
     7  // interface.
     8  
     9  import (
    10  	"errors"
    11  	"flag"
    12  	"fmt"
    13  	"log"
    14  	"os"
    15  	"path"
    16  	"runtime/pprof"
    17  	"strconv"
    18  	"syscall"
    19  	"time"
    20  
    21  	"github.com/elves/elvish/daemon"
    22  	"github.com/elves/elvish/daemon/api"
    23  	"github.com/elves/elvish/daemon/service"
    24  	"github.com/elves/elvish/eval"
    25  	"github.com/elves/elvish/eval/re"
    26  	"github.com/elves/elvish/shell"
    27  	"github.com/elves/elvish/store/storedefs"
    28  	"github.com/elves/elvish/util"
    29  	"github.com/elves/elvish/web"
    30  )
    31  
    32  // defaultPort is the default port on which the web interface runs. The number
    33  // is chosen because it resembles "elvi".
    34  const defaultWebPort = 3171
    35  
    36  var logger = util.GetLogger("[main] ")
    37  
    38  var (
    39  	// Flags handled in this package, or common to shell and daemon.
    40  	help = flag.Bool("help", false, "show usage help and quit")
    41  
    42  	logpath    = flag.String("log", "", "a file to write debug log to")
    43  	cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
    44  	dbpath     = flag.String("db", "", "path to the database")
    45  	sockpath   = flag.String("sock", "", "path to the daemon socket")
    46  
    47  	isdaemon = flag.Bool("daemon", false, "run daemon instead of shell")
    48  	isweb    = flag.Bool("web", false, "run backend of web interface")
    49  	webport  = flag.Int("port", defaultWebPort, "the port of the web backend")
    50  
    51  	// Flags for shell and web.
    52  	cmd = flag.Bool("c", false, "take first argument as a command to execute")
    53  
    54  	// Flags for daemon.
    55  	forked        = flag.Int("forked", 0, "how many times the daemon has forked")
    56  	binpath       = flag.String("bin", "", "path to the elvish binary")
    57  	logpathprefix = flag.String("logprefix", "", "the prefix for the daemon log file")
    58  )
    59  
    60  func usage() {
    61  	fmt.Println("usage: elvish [flags] [script]")
    62  	fmt.Println("flags:")
    63  	flag.PrintDefaults()
    64  }
    65  
    66  func main() {
    67  	// This is needed for defers to be honored.
    68  	ret := 0
    69  	defer os.Exit(ret)
    70  
    71  	// Parse and check flags.
    72  	flag.Usage = usage
    73  	flag.Parse()
    74  	args := flag.Args()
    75  	if *help {
    76  		usage()
    77  		return
    78  	}
    79  	if *isdaemon && len(args) > 0 {
    80  		// The daemon takes no argument.
    81  		usage()
    82  		ret = 2
    83  		return
    84  	}
    85  
    86  	// Flags common to all sub-programs: log and CPU profile.
    87  	if *isdaemon {
    88  		if *forked == 2 && *logpathprefix != "" {
    89  			// Honor logpathprefix.
    90  			pid := syscall.Getpid()
    91  			err := util.SetOutputFile(*logpathprefix + strconv.Itoa(pid))
    92  			if err != nil {
    93  				fmt.Fprintln(os.Stderr, err)
    94  			}
    95  		} else {
    96  			util.SetOutputFile("/dev/stderr")
    97  		}
    98  	} else if *logpath != "" {
    99  		err := util.SetOutputFile(*logpath)
   100  		if err != nil {
   101  			fmt.Fprintln(os.Stderr, err)
   102  		}
   103  	}
   104  	if *cpuprofile != "" {
   105  		f, err := os.Create(*cpuprofile)
   106  		if err != nil {
   107  			log.Fatal(err)
   108  		}
   109  		pprof.StartCPUProfile(f)
   110  		defer pprof.StopCPUProfile()
   111  	}
   112  
   113  	// Pick a sub-program to run.
   114  	if *isdaemon {
   115  		d := daemon.Daemon{
   116  			Forked:        *forked,
   117  			BinPath:       *binpath,
   118  			DbPath:        *dbpath,
   119  			SockPath:      *sockpath,
   120  			LogPathPrefix: *logpathprefix,
   121  		}
   122  		ret = d.Main(service.Serve)
   123  	} else {
   124  		// Shell or web. Set up common runtime components.
   125  		ev, cl := initRuntime()
   126  		defer func() {
   127  			err := cl.Close()
   128  			if err != nil {
   129  				fmt.Fprintln(os.Stderr, "warning: failed to close connection to daemon:", err)
   130  			}
   131  		}()
   132  
   133  		if *isweb {
   134  			if *cmd {
   135  				fmt.Fprintln(os.Stderr, "-c -web not yet supported")
   136  				ret = 2
   137  				return
   138  			}
   139  			w := web.NewWeb(ev, *webport)
   140  			ret = w.Run(args)
   141  		} else {
   142  			sh := shell.NewShell(ev, cl, *cmd)
   143  			ret = sh.Run(args)
   144  		}
   145  	}
   146  }
   147  
   148  const (
   149  	daemonWaitOneLoop = 10 * time.Millisecond
   150  	daemonWaitLoops   = 100
   151  	daemonWaitTotal   = daemonWaitOneLoop * daemonWaitLoops
   152  )
   153  
   154  func initRuntime() (*eval.Evaler, *api.Client) {
   155  	var dataDir string
   156  	var err error
   157  
   158  	// Determine data directory.
   159  	dataDir, err = storedefs.EnsureDataDir()
   160  	if err != nil {
   161  		fmt.Fprintln(os.Stderr, "warning: cannot create data directory ~/.elvish")
   162  	} else {
   163  		if *dbpath == "" {
   164  			*dbpath = dataDir + "/db"
   165  		}
   166  	}
   167  
   168  	// Determine runtime directory.
   169  	runDir, err := getSecureRunDir()
   170  	if err != nil {
   171  		fmt.Fprintln(os.Stderr, "cannot get runtime dir /tmp/elvish-$uid, falling back to data dir ~/.elvish:", err)
   172  		runDir = dataDir
   173  	}
   174  	if *sockpath == "" {
   175  		*sockpath = runDir + "/sock"
   176  	}
   177  
   178  	toSpawn := &daemon.Daemon{
   179  		Forked:        *forked,
   180  		BinPath:       *binpath,
   181  		DbPath:        *dbpath,
   182  		SockPath:      *sockpath,
   183  		LogPathPrefix: runDir + "/daemon.log.",
   184  	}
   185  	var cl *api.Client
   186  	if *sockpath != "" && *dbpath != "" {
   187  		cl = api.NewClient(*sockpath)
   188  		_, statErr := os.Stat(*sockpath)
   189  		killed := false
   190  		if statErr == nil {
   191  			// Kill the daemon if it is outdated.
   192  			version, err := cl.Version()
   193  			if err != nil {
   194  				fmt.Fprintln(os.Stderr, "warning: socket exists but not responding version RPC:", err)
   195  				goto spawnDaemonEnd
   196  			}
   197  			logger.Printf("daemon serving version %d, want version %d", version, api.Version)
   198  			if version < api.Version {
   199  				pid, err := cl.Pid()
   200  				if err != nil {
   201  					fmt.Fprintln(os.Stderr, "warning: socket exists but not responding pid RPC:", err)
   202  					goto spawnDaemonEnd
   203  				}
   204  				logger.Printf("killing outdated daemon with pid %d", pid)
   205  				err = syscall.Kill(pid, syscall.SIGTERM)
   206  				if err != nil {
   207  					fmt.Fprintln(os.Stderr, "warning: failed to kill outdated daemon process:", err)
   208  					goto spawnDaemonEnd
   209  				}
   210  				logger.Println("killed outdated daemon")
   211  				killed = true
   212  			}
   213  		}
   214  		if os.IsNotExist(statErr) || killed {
   215  			logger.Println("socket does not exists, starting daemon")
   216  			err := toSpawn.Spawn()
   217  			if err != nil {
   218  				fmt.Fprintln(os.Stderr, "warning: cannot start daemon:", err)
   219  			} else {
   220  				logger.Println("started daemon")
   221  			}
   222  			for i := 0; i < daemonWaitLoops; i++ {
   223  				_, err := cl.Version()
   224  				if err == nil {
   225  					logger.Println("daemon online")
   226  					goto spawnDaemonEnd
   227  				} else if i == daemonWaitLoops-1 {
   228  					fmt.Fprintf(os.Stderr, "cannot connect to daemon after %v: %v\n", daemonWaitTotal, err)
   229  					cl = nil
   230  					goto spawnDaemonEnd
   231  				}
   232  				time.Sleep(daemonWaitOneLoop)
   233  			}
   234  		}
   235  	}
   236  spawnDaemonEnd:
   237  
   238  	// TODO(xiaq): This information might belong somewhere else.
   239  	extraModules := map[string]eval.Namespace{
   240  		"re": re.Namespace(),
   241  	}
   242  	return eval.NewEvaler(cl, toSpawn, dataDir, extraModules), cl
   243  }
   244  
   245  var (
   246  	ErrBadOwner      = errors.New("bad owner")
   247  	ErrBadPermission = errors.New("bad permission")
   248  )
   249  
   250  // getSecureRunDir stats /tmp/elvish-$uid, creating it if it doesn't yet exist,
   251  // and return the directory name if it has the correct owner and permission.
   252  func getSecureRunDir() (string, error) {
   253  	uid := syscall.Getuid()
   254  
   255  	runDir := path.Join(os.TempDir(), fmt.Sprintf("elvish-%d", uid))
   256  	err := os.MkdirAll(runDir, 0700)
   257  	if err != nil {
   258  		return "", fmt.Errorf("mkdir: %v", err)
   259  	}
   260  
   261  	var stat syscall.Stat_t
   262  	err = syscall.Stat(runDir, &stat)
   263  	if err != nil {
   264  		return "", fmt.Errorf("stat: %v", err)
   265  	}
   266  
   267  	if int(stat.Uid) != uid {
   268  		return "", ErrBadOwner
   269  	}
   270  	if stat.Mode&077 != 0 {
   271  		return "", ErrBadPermission
   272  	}
   273  	return runDir, err
   274  }