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 }