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  }