mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-13 09:57:51 +08:00
6a1e7b3996
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
182 lines
4.5 KiB
Go
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
|
|
}
|