github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/shell/interact.go (about) 1 package shell 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "strings" 10 "syscall" 11 "time" 12 13 "github.com/markusbkk/elvish/pkg/cli" 14 "github.com/markusbkk/elvish/pkg/cli/term" 15 "github.com/markusbkk/elvish/pkg/daemon/daemondefs" 16 "github.com/markusbkk/elvish/pkg/diag" 17 "github.com/markusbkk/elvish/pkg/edit" 18 "github.com/markusbkk/elvish/pkg/eval" 19 "github.com/markusbkk/elvish/pkg/mods/daemon" 20 "github.com/markusbkk/elvish/pkg/mods/store" 21 "github.com/markusbkk/elvish/pkg/parse" 22 "github.com/markusbkk/elvish/pkg/strutil" 23 "github.com/markusbkk/elvish/pkg/sys" 24 "github.com/markusbkk/elvish/pkg/ui" 25 ) 26 27 // InteractiveRescueShell determines whether a panic results in a rescue shell 28 // being launched. It should be set to false by interactive mode unit tests. 29 var interactiveRescueShell bool = true 30 31 // Configuration for the interactive mode. 32 type interactCfg struct { 33 RC string 34 35 ActivateDaemon daemondefs.ActivateFunc 36 SpawnConfig *daemondefs.SpawnConfig 37 } 38 39 // Interface satisfied by the line editor. Used for swapping out the editor with 40 // minEditor when necessary. 41 type editor interface { 42 ReadCode() (string, error) 43 RunAfterCommandHooks(src parse.Source, duration float64, err error) 44 } 45 46 // Runs an interactive shell session. 47 func interact(ev *eval.Evaler, fds [3]*os.File, cfg *interactCfg) { 48 if interactiveRescueShell { 49 defer handlePanic() 50 } 51 52 var daemonClient daemondefs.Client 53 if cfg.ActivateDaemon != nil && cfg.SpawnConfig != nil { 54 // TODO(xiaq): Connect to daemon and install daemon module 55 // asynchronously. 56 cl, err := cfg.ActivateDaemon(fds[2], cfg.SpawnConfig) 57 if err != nil { 58 fmt.Fprintln(fds[2], "Cannot connect to daemon:", err) 59 fmt.Fprintln(fds[2], "Daemon-related functions will likely not work.") 60 } 61 defer func() { 62 err := cl.Close() 63 if err != nil { 64 fmt.Fprintln(fds[2], 65 "warning: failed to close connection to daemon:", err) 66 } 67 }() 68 daemonClient = cl 69 // Even if error is not nil, we install daemon-related functionalities 70 // anyway. Daemon may eventually come online and become functional. 71 ev.AddBeforeExit(func() { cl.Close() }) 72 ev.AddModule("store", store.Ns(cl)) 73 ev.AddModule("daemon", daemon.Ns(cl)) 74 } 75 76 // Build Editor. 77 var ed editor 78 if sys.IsATTY(fds[0]) { 79 newed := edit.NewEditor(cli.NewTTY(fds[0], fds[2]), ev, daemonClient) 80 ev.ExtendBuiltin(eval.BuildNs().AddNs("edit", newed)) 81 ev.BgJobNotify = func(s string) { newed.Notify(ui.T(s)) } 82 ed = newed 83 } else { 84 ed = newMinEditor(fds[0], fds[2]) 85 } 86 87 // Source rc.elv. 88 if cfg.RC != "" { 89 err := sourceRC(fds, ev, ed, cfg.RC) 90 if err != nil { 91 diag.ShowError(fds[2], err) 92 } 93 } 94 95 term.Sanitize(fds[0], fds[2]) 96 97 cooldown := time.Second 98 cmdNum := 0 99 100 for { 101 cmdNum++ 102 103 line, err := ed.ReadCode() 104 if err == io.EOF { 105 break 106 } else if err != nil { 107 fmt.Fprintln(fds[2], "Editor error:", err) 108 if _, isMinEditor := ed.(*minEditor); !isMinEditor { 109 fmt.Fprintln(fds[2], "Falling back to basic line editor") 110 ed = newMinEditor(fds[0], fds[2]) 111 } else { 112 fmt.Fprintln(fds[2], "Don't know what to do, pid is", os.Getpid()) 113 fmt.Fprintln(fds[2], "Restarting editor in", cooldown) 114 time.Sleep(cooldown) 115 if cooldown < time.Minute { 116 cooldown *= 2 117 } 118 } 119 continue 120 } 121 122 // No error; reset cooldown. 123 cooldown = time.Second 124 125 // Execute the command line only if it is not entirely whitespace. This keeps side-effects, 126 // such as executing `$edit:after-command` hooks, from occurring when we didn't actually 127 // evaluate any code entered by the user. 128 if strings.TrimSpace(line) == "" { 129 continue 130 } 131 src := parse.Source{Name: fmt.Sprintf("[tty %v]", cmdNum), Code: line} 132 duration, err := evalInTTY(ev, fds, src) 133 ed.RunAfterCommandHooks(src, duration, err) 134 term.Sanitize(fds[0], fds[2]) 135 if err != nil { 136 diag.ShowError(fds[2], err) 137 } 138 } 139 } 140 141 // Interactive mode panic handler. 142 func handlePanic() { 143 r := recover() 144 if r != nil { 145 println() 146 print(sys.DumpStack()) 147 println() 148 fmt.Println(r) 149 println("\nExecing recovery shell /bin/sh") 150 syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()) 151 } 152 } 153 154 func sourceRC(fds [3]*os.File, ev *eval.Evaler, ed editor, rcPath string) error { 155 absPath, err := filepath.Abs(rcPath) 156 if err != nil { 157 return fmt.Errorf("cannot get full path of rc.elv: %v", err) 158 } 159 code, err := readFileUTF8(absPath) 160 if err != nil { 161 if os.IsNotExist(err) { 162 return nil 163 } 164 return err 165 } 166 src := parse.Source{Name: absPath, Code: code, IsFile: true} 167 duration, err := evalInTTY(ev, fds, src) 168 ed.RunAfterCommandHooks(src, duration, err) 169 return err 170 } 171 172 type minEditor struct { 173 in *bufio.Reader 174 out io.Writer 175 } 176 177 func newMinEditor(in, out *os.File) *minEditor { 178 return &minEditor{bufio.NewReader(in), out} 179 } 180 181 func (ed *minEditor) RunAfterCommandHooks(src parse.Source, duration float64, err error) { 182 // no-op; minEditor doesn't support this hook. 183 } 184 185 func (ed *minEditor) ReadCode() (string, error) { 186 wd, err := os.Getwd() 187 if err != nil { 188 wd = "?" 189 } 190 fmt.Fprintf(ed.out, "%s> ", wd) 191 line, err := ed.in.ReadString('\n') 192 return strutil.ChopLineEnding(line), err 193 }