github.com/ladydascalie/elvish@v0.0.0-20170703214355-2964dd3ece7f/shell/shell.go (about)

     1  // Package shell is the entry point for the terminal interface of Elvish.
     2  package shell
     3  
     4  import (
     5  	"bufio"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"os/signal"
    11  	"syscall"
    12  	"time"
    13  	"unicode/utf8"
    14  
    15  	"github.com/elves/elvish/daemon/api"
    16  	"github.com/elves/elvish/edit"
    17  	"github.com/elves/elvish/eval"
    18  	"github.com/elves/elvish/sys"
    19  	"github.com/elves/elvish/util"
    20  )
    21  
    22  var logger = util.GetLogger("[shell] ")
    23  
    24  // Shell keeps flags to the shell.
    25  type Shell struct {
    26  	ev     *eval.Evaler
    27  	daemon *api.Client
    28  	cmd    bool
    29  }
    30  
    31  func NewShell(ev *eval.Evaler, daemon *api.Client, cmd bool) *Shell {
    32  	return &Shell{ev, daemon, cmd}
    33  }
    34  
    35  // Run runs Elvish using the default terminal interface. It blocks until Elvish
    36  // quites, and returns the exit code.
    37  func (sh *Shell) Run(args []string) int {
    38  	defer rescue()
    39  
    40  	handleUsr1AndQuit()
    41  	logSignals()
    42  
    43  	if len(args) > 0 {
    44  		if len(args) > 1 {
    45  			fmt.Fprintln(os.Stderr, "passing argument is not yet supported.")
    46  			return 2
    47  		}
    48  		arg := args[0]
    49  		if sh.cmd {
    50  			sourceTextAndPrintError(sh.ev, "code from -c", arg)
    51  		} else {
    52  			script(sh.ev, arg)
    53  		}
    54  	} else if !sys.IsATTY(0) {
    55  		script(sh.ev, "/dev/stdin")
    56  	} else {
    57  		interact(sh.ev, sh.daemon)
    58  	}
    59  
    60  	return 0
    61  }
    62  
    63  func rescue() {
    64  	r := recover()
    65  	if r != nil {
    66  		println()
    67  		fmt.Println(r)
    68  		print(sys.DumpStack())
    69  		println("\nexecing recovery shell /bin/sh")
    70  		syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ())
    71  	}
    72  }
    73  
    74  func script(ev *eval.Evaler, fname string) {
    75  	if !source(ev, fname, false) {
    76  		os.Exit(1)
    77  	}
    78  }
    79  
    80  func source(ev *eval.Evaler, fname string, notexistok bool) bool {
    81  	src, err := readFileUTF8(fname)
    82  	if err != nil {
    83  		if notexistok && os.IsNotExist(err) {
    84  			return true
    85  		}
    86  		fmt.Fprintln(os.Stderr, err)
    87  		return false
    88  	}
    89  
    90  	return sourceTextAndPrintError(ev, fname, src)
    91  }
    92  
    93  // sourceTextAndPrintError sources text, prints error if there is any, and
    94  // returns whether there was no error.
    95  func sourceTextAndPrintError(ev *eval.Evaler, name, src string) bool {
    96  	err := ev.SourceText(name, src)
    97  	if err != nil {
    98  		switch err := err.(type) {
    99  		case util.Pprinter:
   100  			fmt.Fprintln(os.Stderr, err.Pprint(""))
   101  		default:
   102  			fmt.Fprintf(os.Stderr, "\033[31;1m%s\033[m", err.Error())
   103  		}
   104  		return false
   105  	}
   106  	return true
   107  }
   108  
   109  func readFileUTF8(fname string) (string, error) {
   110  	bytes, err := ioutil.ReadFile(fname)
   111  	if err != nil {
   112  		return "", err
   113  	}
   114  	if !utf8.Valid(bytes) {
   115  		return "", fmt.Errorf("%s: source is not valid UTF-8", fname)
   116  	}
   117  	return string(bytes), nil
   118  }
   119  
   120  func interact(ev *eval.Evaler, daemon *api.Client) {
   121  	// Build Editor.
   122  	sigch := make(chan os.Signal)
   123  	signal.Notify(sigch)
   124  	ed := edit.NewEditor(os.Stdin, os.Stderr, sigch, ev, daemon)
   125  
   126  	// Source rc.elv.
   127  	if ev.DataDir != "" {
   128  		source(ev, ev.DataDir+"/rc.elv", true)
   129  	}
   130  
   131  	// Build readLine function.
   132  	readLine := func() (string, error) {
   133  		return ed.ReadLine()
   134  	}
   135  
   136  	cooldown := time.Second
   137  	usingBasic := false
   138  	cmdNum := 0
   139  
   140  	for {
   141  		cmdNum++
   142  		// name := fmt.Sprintf("<tty %d>", cmdNum)
   143  
   144  		line, err := readLine()
   145  
   146  		if err == io.EOF {
   147  			break
   148  		} else if err != nil {
   149  			fmt.Println("Editor error:", err)
   150  			if !usingBasic {
   151  				fmt.Println("Falling back to basic line editor")
   152  				readLine = basicReadLine
   153  				usingBasic = true
   154  			} else {
   155  				fmt.Println("Don't know what to do, pid is", os.Getpid())
   156  				fmt.Println("Restarting editor in", cooldown)
   157  				time.Sleep(cooldown)
   158  				if cooldown < time.Minute {
   159  					cooldown *= 2
   160  				}
   161  			}
   162  			continue
   163  		}
   164  
   165  		// No error; reset cooldown.
   166  		cooldown = time.Second
   167  
   168  		sourceTextAndPrintError(ev, "[interactive]", line)
   169  	}
   170  }
   171  
   172  func basicReadLine() (string, error) {
   173  	stdin := bufio.NewReaderSize(os.Stdin, 0)
   174  	return stdin.ReadString('\n')
   175  }
   176  
   177  func logSignals() {
   178  	sigs := make(chan os.Signal)
   179  	signal.Notify(sigs)
   180  	go func() {
   181  		for sig := range sigs {
   182  			logger.Println("signal", sig)
   183  		}
   184  	}()
   185  }
   186  
   187  func handleUsr1AndQuit() {
   188  	sigs := make(chan os.Signal)
   189  	signal.Notify(sigs, syscall.SIGUSR1, syscall.SIGQUIT)
   190  	go func() {
   191  		for sig := range sigs {
   192  			fmt.Print(sys.DumpStack())
   193  			if sig == syscall.SIGQUIT {
   194  				os.Exit(3)
   195  			}
   196  		}
   197  	}()
   198  }