github.com/kolbycrouch/elvish@v0.14.1-0.20210614162631-215b9ac1c423/pkg/shell/interact.go (about) 1 package shell 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "path/filepath" 8 "strings" 9 "syscall" 10 "time" 11 12 "src.elv.sh/pkg/cli" 13 "src.elv.sh/pkg/cli/term" 14 "src.elv.sh/pkg/diag" 15 "src.elv.sh/pkg/edit" 16 "src.elv.sh/pkg/eval" 17 "src.elv.sh/pkg/parse" 18 "src.elv.sh/pkg/sys" 19 ) 20 21 // InteractiveRescueShell determines whether a panic results in a rescue shell 22 // being launched. It should be set to false by interactive mode unit tests. 23 var interactiveRescueShell bool = true 24 25 // InteractConfig keeps configuration for the interactive mode. 26 type InteractConfig struct { 27 SpawnDaemon bool 28 Paths Paths 29 } 30 31 // Interactive mode panic handler. 32 func handlePanic() { 33 r := recover() 34 if r != nil { 35 println() 36 print(sys.DumpStack()) 37 println() 38 fmt.Println(r) 39 println("\nExecing recovery shell /bin/sh") 40 syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()) 41 } 42 } 43 44 // Interact runs an interactive shell session. 45 func Interact(fds [3]*os.File, cfg *InteractConfig) { 46 if interactiveRescueShell { 47 defer handlePanic() 48 } 49 ev, cleanup := setupShell(fds, cfg.Paths, cfg.SpawnDaemon) 50 defer cleanup() 51 52 // Build Editor. 53 var ed editor 54 if sys.IsATTY(fds[0]) { 55 newed := edit.NewEditor(cli.NewTTY(fds[0], fds[2]), ev, ev.DaemonClient()) 56 ev.AddBuiltin(eval.NsBuilder{}.AddNs("edit", newed.Ns()).Ns()) 57 ed = newed 58 } else { 59 ed = newMinEditor(fds[0], fds[2]) 60 } 61 62 // Source rc.elv. 63 if cfg.Paths.Rc != "" { 64 err := sourceRC(fds, ev, ed, cfg.Paths.Rc) 65 if err != nil { 66 diag.ShowError(fds[2], err) 67 } 68 } 69 70 term.Sanitize(fds[0], fds[2]) 71 72 cooldown := time.Second 73 cmdNum := 0 74 75 for { 76 cmdNum++ 77 78 line, err := ed.ReadCode() 79 if err == io.EOF { 80 break 81 } else if err != nil { 82 fmt.Fprintln(fds[2], "Editor error:", err) 83 if _, isMinEditor := ed.(*minEditor); !isMinEditor { 84 fmt.Fprintln(fds[2], "Falling back to basic line editor") 85 ed = newMinEditor(fds[0], fds[2]) 86 } else { 87 fmt.Fprintln(fds[2], "Don't know what to do, pid is", os.Getpid()) 88 fmt.Fprintln(fds[2], "Restarting editor in", cooldown) 89 time.Sleep(cooldown) 90 if cooldown < time.Minute { 91 cooldown *= 2 92 } 93 } 94 continue 95 } 96 97 // No error; reset cooldown. 98 cooldown = time.Second 99 100 // Execute the command line only if it is not entirely whitespace. This keeps side-effects, 101 // such as executing `$edit:after-command` hooks, from occurring when we didn't actually 102 // evaluate any code entered by the user. 103 if strings.TrimSpace(line) == "" { 104 continue 105 } 106 src := parse.Source{Name: fmt.Sprintf("[tty %v]", cmdNum), Code: line} 107 duration, err := evalInTTY(ev, fds, src) 108 ed.RunAfterCommandHooks(src, duration, err) 109 term.Sanitize(fds[0], fds[2]) 110 if err != nil { 111 diag.ShowError(fds[2], err) 112 } 113 } 114 } 115 116 func sourceRC(fds [3]*os.File, ev *eval.Evaler, ed eval.Editor, rcPath string) error { 117 absPath, err := filepath.Abs(rcPath) 118 if err != nil { 119 return fmt.Errorf("cannot get full path of rc.elv: %v", err) 120 } 121 code, err := readFileUTF8(absPath) 122 if err != nil { 123 if os.IsNotExist(err) { 124 return nil 125 } 126 return err 127 } 128 src := parse.Source{Name: absPath, Code: code, IsFile: true} 129 duration, err := evalInTTY(ev, fds, src) 130 ed.RunAfterCommandHooks(src, duration, err) 131 return err 132 }