elvish/pkg/shell/interact.go
Qi Xiao 6a1e7b3996 pkg/shell: Add more daemon-related tests.
Also:

- Remove duplicate code for closing daemon client.

- Avoid using (*testing.T).TempDir - it uses test name in the path, which can
  exceed the limit of the address when calling bind(2):
  https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/sys_un.h.html#tag_13_6
7_04
2022-06-19 21:45:36 +01:00

182 lines
4.5 KiB
Go

package shell
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"time"
"src.elv.sh/pkg/cli"
"src.elv.sh/pkg/daemon/daemondefs"
"src.elv.sh/pkg/diag"
"src.elv.sh/pkg/edit"
"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/mods/daemon"
"src.elv.sh/pkg/mods/store"
"src.elv.sh/pkg/parse"
"src.elv.sh/pkg/strutil"
"src.elv.sh/pkg/sys"
"src.elv.sh/pkg/ui"
)
// InteractiveRescueShell determines whether a panic results in a rescue shell
// being launched. It should be set to false by interactive mode unit tests.
var interactiveRescueShell bool = true
// Configuration for the interactive mode.
type interactCfg struct {
RC string
ActivateDaemon daemondefs.ActivateFunc
SpawnConfig *daemondefs.SpawnConfig
}
// Interface satisfied by the line editor. Used for swapping out the editor with
// minEditor when necessary.
type editor interface {
ReadCode() (string, error)
RunAfterCommandHooks(src parse.Source, duration float64, err error)
}
// Runs an interactive shell session.
func interact(ev *eval.Evaler, fds [3]*os.File, cfg *interactCfg) {
if interactiveRescueShell {
defer handlePanic()
}
var daemonClient daemondefs.Client
if cfg.ActivateDaemon != nil && cfg.SpawnConfig != nil {
// TODO(xiaq): Connect to daemon and install daemon module
// asynchronously.
cl, err := cfg.ActivateDaemon(fds[2], cfg.SpawnConfig)
if err != nil {
fmt.Fprintln(fds[2], "Cannot connect to daemon:", err)
fmt.Fprintln(fds[2], "Daemon-related functions will likely not work.")
}
if cl != nil {
// Even if error is not nil, we install daemon-related
// functionalities anyway. Daemon may eventually come online and
// become functional.
daemonClient = cl
ev.PreExitHooks = append(ev.PreExitHooks, func() { cl.Close() })
ev.AddModule("store", store.Ns(cl))
ev.AddModule("daemon", daemon.Ns(cl))
}
}
// Build Editor.
var ed editor
if sys.IsATTY(fds[0]) {
newed := edit.NewEditor(cli.NewTTY(fds[0], fds[2]), ev, daemonClient)
ev.ExtendBuiltin(eval.BuildNs().AddNs("edit", newed))
ev.BgJobNotify = func(s string) { newed.Notify(ui.T(s)) }
ed = newed
} else {
ed = newMinEditor(fds[0], fds[2])
}
// Source rc.elv.
if cfg.RC != "" {
err := sourceRC(fds, ev, ed, cfg.RC)
if err != nil {
diag.ShowError(fds[2], err)
}
}
cooldown := time.Second
cmdNum := 0
for {
cmdNum++
line, err := ed.ReadCode()
if err == io.EOF {
break
} else if err != nil {
fmt.Fprintln(fds[2], "Editor error:", err)
if _, isMinEditor := ed.(*minEditor); !isMinEditor {
fmt.Fprintln(fds[2], "Falling back to basic line editor")
ed = newMinEditor(fds[0], fds[2])
} else {
fmt.Fprintln(fds[2], "Don't know what to do, pid is", os.Getpid())
fmt.Fprintln(fds[2], "Restarting editor in", cooldown)
time.Sleep(cooldown)
if cooldown < time.Minute {
cooldown *= 2
}
}
continue
}
// No error; reset cooldown.
cooldown = time.Second
// Execute the command line only if it is not entirely whitespace. This keeps side-effects,
// such as executing `$edit:after-command` hooks, from occurring when we didn't actually
// evaluate any code entered by the user.
if strings.TrimSpace(line) == "" {
continue
}
err = evalInTTY(fds, ev, ed,
parse.Source{Name: fmt.Sprintf("[tty %v]", cmdNum), Code: line})
if err != nil {
diag.ShowError(fds[2], err)
}
}
}
// Interactive mode panic handler.
func handlePanic() {
r := recover()
if r != nil {
println()
print(sys.DumpStack())
println()
fmt.Println(r)
println("\nExecing recovery shell /bin/sh")
syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ())
}
}
func sourceRC(fds [3]*os.File, ev *eval.Evaler, ed editor, rcPath string) error {
absPath, err := filepath.Abs(rcPath)
if err != nil {
return fmt.Errorf("cannot get full path of rc.elv: %v", err)
}
code, err := readFileUTF8(absPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
return evalInTTY(fds, ev, ed, parse.Source{Name: absPath, Code: code, IsFile: true})
}
type minEditor struct {
in *bufio.Reader
out io.Writer
}
func newMinEditor(in, out *os.File) *minEditor {
return &minEditor{bufio.NewReader(in), out}
}
func (ed *minEditor) RunAfterCommandHooks(src parse.Source, duration float64, err error) {
// no-op; minEditor doesn't support this hook.
}
func (ed *minEditor) ReadCode() (string, error) {
wd, err := os.Getwd()
if err != nil {
wd = "?"
}
fmt.Fprintf(ed.out, "%s> ", wd)
line, err := ed.in.ReadString('\n')
return strutil.ChopLineEnding(line), err
}