github.com/neugram/ng@v0.0.0-20180309130942-d472ff93d872/eval/shell/job.go (about)

     1  // Copyright 2015 The Neugram Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package shell
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"os"
    12  	"os/signal"
    13  	"path/filepath"
    14  	"strconv"
    15  	"strings"
    16  	"sync"
    17  	"syscall"
    18  
    19  	"neugram.io/ng/eval/environ"
    20  	"neugram.io/ng/format"
    21  	"neugram.io/ng/syntax/expr"
    22  	"neugram.io/ng/syntax/shell"
    23  	"neugram.io/ng/syntax/token"
    24  )
    25  
    26  type State struct {
    27  	Env   *environ.Environ
    28  	Alias *environ.Environ
    29  
    30  	bgMu sync.Mutex
    31  	bg   []*Job
    32  }
    33  
    34  type Params interface {
    35  	Get(name string) string
    36  	Set(name, value string)
    37  }
    38  
    39  type paramset interface {
    40  	Get(name string) string
    41  }
    42  
    43  type Job struct {
    44  	State  *State
    45  	Cmd    *expr.ShellList
    46  	Stdin  *os.File
    47  	Stdout *os.File
    48  	Stderr *os.File
    49  	Params Params
    50  
    51  	mu      sync.Mutex
    52  	err     error
    53  	pgid    int
    54  	termios syscall.Termios
    55  	cond    sync.Cond
    56  	done    bool
    57  	running bool
    58  }
    59  
    60  func (j *Job) Start() (err error) {
    61  	if interactive {
    62  		shellState, err = tcgetattr(os.Stdin.Fd())
    63  		if err != nil {
    64  			return err
    65  		}
    66  		if err := tcsetattr(os.Stdin.Fd(), &basicState); err != nil {
    67  			return err
    68  		}
    69  	}
    70  
    71  	j.mu.Lock()
    72  	j.cond.L = &j.mu
    73  	j.running = true
    74  	j.mu.Unlock()
    75  
    76  	go j.exec()
    77  	return nil
    78  }
    79  
    80  func (j *Job) Result() (done bool, err error) {
    81  	j.mu.Lock()
    82  	defer j.mu.Unlock()
    83  	if !j.done {
    84  		return false, nil
    85  	}
    86  	return true, j.err
    87  }
    88  
    89  func (j *Job) Continue() error {
    90  	j.mu.Lock()
    91  	if j.done {
    92  		j.mu.Unlock()
    93  		return nil
    94  	}
    95  
    96  	if interactive {
    97  		if err := tcsetpgrp(os.Stdin.Fd(), j.pgid); err != nil {
    98  			j.mu.Unlock()
    99  			return err
   100  		}
   101  		if err := tcsetattr(os.Stdin.Fd(), &j.termios); err != nil {
   102  			j.mu.Unlock()
   103  			return err
   104  		}
   105  	}
   106  
   107  	j.running = true
   108  	syscall.Kill(-j.pgid, syscall.SIGCONT)
   109  
   110  	j.mu.Unlock()
   111  
   112  	_, err := j.Wait()
   113  	return err
   114  }
   115  
   116  func shellListString(cmd *expr.ShellList) string {
   117  	return format.Expr(cmd)
   118  }
   119  
   120  // Wait waits until the job is stopped or complete.
   121  func (j *Job) Wait() (done bool, err error) {
   122  	j.mu.Lock()
   123  	defer j.mu.Unlock()
   124  
   125  	for j.running {
   126  		j.cond.Wait()
   127  	}
   128  	if !j.done {
   129  		// Stopped, add Job to bg and save terminal.
   130  		if interactive {
   131  			j.termios, err = tcgetattr(os.Stdin.Fd())
   132  			if err != nil {
   133  				fmt.Fprintf(j.Stderr, "on stop: %v", err)
   134  			}
   135  		}
   136  		j.State.bgAdd(j)
   137  	}
   138  
   139  	if interactive {
   140  		// move the shell process to the foreground
   141  		if err := tcsetpgrp(os.Stdin.Fd(), shellPgid); err != nil {
   142  			fmt.Fprintf(j.Stderr, "on stop: %v", err)
   143  		}
   144  		if err := tcsetattr(os.Stdin.Fd(), &shellState); err != nil {
   145  			fmt.Fprintf(j.Stderr, "on stop: %v", err)
   146  		}
   147  	}
   148  
   149  	return j.done, j.err
   150  }
   151  
   152  // exec traverses the Cmd tree, starting procs.
   153  //
   154  // Some of the traversal blocks until certain procs are running or
   155  // complete, meaning exec lives until the job is complete.
   156  func (j *Job) exec() {
   157  	err := j.execShellList(j.Cmd, stdio{j.Stdin, j.Stdout, j.Stderr})
   158  
   159  	j.mu.Lock()
   160  	j.err = err
   161  	j.running = false
   162  	j.done = true
   163  	j.cond.Broadcast()
   164  	j.mu.Unlock()
   165  }
   166  
   167  type stdio struct {
   168  	in  *os.File
   169  	out *os.File
   170  	err *os.File
   171  }
   172  
   173  func (j *Job) execShellList(cmd *expr.ShellList, sio stdio) error {
   174  	for _, andor := range cmd.AndOr {
   175  		if err := j.execShellAndOr(andor, sio); err != nil {
   176  			return err
   177  		}
   178  	}
   179  	return nil
   180  }
   181  
   182  func (j *Job) execShellAndOr(andor *expr.ShellAndOr, sio stdio) error {
   183  	for i, p := range andor.Pipeline {
   184  		err := j.execPipeline(p, sio)
   185  		if i < len(andor.Pipeline)-1 {
   186  			switch andor.Sep[i] {
   187  			case token.LogicalAnd:
   188  				if err != nil {
   189  					return err
   190  				}
   191  			case token.LogicalOr:
   192  				if err == nil {
   193  					return nil
   194  				}
   195  			default:
   196  				panic("unknown AndOr separator: " + andor.Sep[i].String())
   197  			}
   198  		} else if err != nil {
   199  			return err
   200  		}
   201  	}
   202  	return nil
   203  }
   204  
   205  func (j *Job) execPipeline(plcmd *expr.ShellPipeline, sio stdio) (err error) {
   206  	if interactive && j.pgid == 0 && len(plcmd.Cmd) > 1 {
   207  		// All the processes of a pipeline run with the same
   208  		// process group ID. To do this, a shell will typically
   209  		// use the pid of the first process as the pgid for the
   210  		// entire pipeline.
   211  		//
   212  		// This technique has an edge case. If the first process
   213  		// exits before the second gets a chance to start, then
   214  		// the pgid is invalid and the pipeline will fail to start.
   215  		// An easy way to run into this is if the first process is
   216  		// a short echo, that can fit its entire output into the
   217  		// kernel pipe buffer. For example:
   218  		//
   219  		//	echo hello | cat
   220  		//
   221  		// The solution adopted by typical shells is pipe
   222  		// communication between the fork and exec calls of the
   223  		// first process. The first process waits for the pipe to
   224  		// close, and the shell closes it after the whole pipeline
   225  		// is started, effectively pausing the first process
   226  		// before it starts.
   227  		//
   228  		// That's not an option for us if we use the syscall
   229  		// package's implementation of ForkExec. Rather than
   230  		// reinvent the wheel, we try a different trick: we start
   231  		// a well-behaved placeholder process, the pgidLeader, to
   232  		// pin a pgid for the duration of pipeline creation.
   233  		//
   234  		// What an unusual contraption.
   235  		pgidLeader, err := startPgidLeader()
   236  		if err != nil {
   237  			return err
   238  		}
   239  		j.pgid = pgidLeader.Pid
   240  		defer func() {
   241  			pgidLeader.Kill()
   242  			j.pgid = 0
   243  		}()
   244  	}
   245  	defer func() {
   246  		j.pgid = 0
   247  	}()
   248  
   249  	sios := make([]stdio, len(plcmd.Cmd))
   250  	sios[0].in = sio.in
   251  	sios[len(sios)-1].out = sio.out
   252  	for i := range sios {
   253  		sios[i].err = sio.err
   254  	}
   255  	defer func() {
   256  		if err != nil {
   257  			for i := 0; i < len(sios)-1; i++ {
   258  				if sios[i].out != nil {
   259  					sios[i].out.Close()
   260  				}
   261  				if sios[i+1].in != nil {
   262  					sios[i+1].in.Close()
   263  				}
   264  			}
   265  		}
   266  	}()
   267  	for i := 0; i < len(sios)-1; i++ {
   268  		r, w, err := os.Pipe()
   269  		if err != nil {
   270  			return err
   271  		}
   272  		sios[i].out = w
   273  		sios[i+1].in = r
   274  	}
   275  	pl := &pipeline{
   276  		job: j,
   277  	}
   278  	for i, cmd := range plcmd.Cmd {
   279  		if cmd.Subshell != nil {
   280  			return fmt.Errorf("missing subshell support") // TODO
   281  		}
   282  		p, err := j.setupSimpleCmd(cmd.SimpleCmd, sios[i])
   283  		if err != nil {
   284  			return err
   285  		}
   286  		if p != nil {
   287  			pl.proc = append(pl.proc, p)
   288  		}
   289  	}
   290  	if len(pl.proc) > 0 {
   291  		if err := pl.start(); err != nil {
   292  			return err
   293  		}
   294  		if err := pl.waitUntilDone(); err != nil {
   295  			return err
   296  		}
   297  	}
   298  	return nil
   299  }
   300  
   301  func (j *Job) setupSimpleCmd(cmd *expr.ShellSimpleCmd, sio stdio) (*proc, error) {
   302  	if len(cmd.Args) == 0 {
   303  		for _, v := range cmd.Assign {
   304  			j.Params.Set(v.Key, v.Value)
   305  		}
   306  		return nil, nil
   307  	}
   308  	argv, err := shell.Expansion(cmd.Args, j.Params)
   309  	if err != nil {
   310  		return nil, err
   311  	}
   312  	if a := j.State.Alias.Get(argv[0]); a != "" {
   313  		// TODO: This is entirely wrong. The alias string needs to be
   314  		// parsed like a typical shell command. That is:
   315  		//	alias["gsm"] = `go build "-ldflags=-w -s"`
   316  		// should be three args, not four.
   317  		aliasArgs := strings.Split(a, " ")
   318  		argv = append(aliasArgs, argv[1:]...)
   319  	}
   320  	switch argv[0] {
   321  	case "cd":
   322  		dir := ""
   323  		if len(argv) == 1 {
   324  			dir = j.State.Env.Get("HOME")
   325  		} else {
   326  			dir = argv[1]
   327  		}
   328  		wd := ""
   329  		if filepath.IsAbs(dir) {
   330  			wd = filepath.Clean(dir)
   331  		} else {
   332  			wd = filepath.Join(j.State.Env.Get("PWD"), dir)
   333  		}
   334  		if err := os.Chdir(wd); err != nil {
   335  			return nil, err
   336  		}
   337  		j.State.Env.Set("PWD", wd)
   338  		fmt.Fprintf(os.Stdout, "%s\n", wd)
   339  		return nil, nil
   340  	case "fg":
   341  		return nil, j.State.bgFg(strings.Join(argv[1:], " "))
   342  	case "jobs":
   343  		j.State.bgList(j.Stderr)
   344  		return nil, nil
   345  	case "export":
   346  		return nil, j.export(argv[1:])
   347  	case "exit", "logout":
   348  		return nil, fmt.Errorf("ng does not know %q, try $$", argv[0])
   349  	}
   350  	env := j.State.Env.List()
   351  	if len(cmd.Assign) != 0 {
   352  		baseEnv := env
   353  		env = make([]string, 0, len(cmd.Assign)+len(baseEnv))
   354  		for _, kv := range cmd.Assign {
   355  			env = append(env, kv.Key+"="+kv.Value)
   356  		}
   357  		env = append(env, baseEnv...)
   358  	}
   359  	p := &proc{
   360  		job:  j,
   361  		argv: argv,
   362  		sio:  sio,
   363  		env:  env,
   364  	}
   365  	for _, r := range cmd.Redirect {
   366  		switch r.Token {
   367  		case token.Greater, token.TwoGreater, token.AndGreater:
   368  			flag := os.O_RDWR | os.O_CREATE
   369  			if r.Token == token.Greater || r.Token == token.AndGreater {
   370  				flag |= os.O_TRUNC
   371  			} else {
   372  				flag |= os.O_APPEND
   373  			}
   374  			f, err := os.OpenFile(r.Filename, flag, 0666)
   375  			if err != nil {
   376  				return nil, err
   377  			}
   378  			if r.Token == token.AndGreater {
   379  				p.sio.out = f
   380  				p.sio.err = f
   381  			} else if r.Number == nil || *r.Number == 1 {
   382  				p.sio.out = f
   383  			} else if *r.Number == 2 {
   384  				p.sio.err = f
   385  			}
   386  		case token.GreaterAnd:
   387  			dstnum, err := strconv.Atoi(r.Filename)
   388  			if err != nil {
   389  				return nil, fmt.Errorf("bad redirect target: %q", r.Filename)
   390  			}
   391  			var dst *os.File
   392  			switch dstnum {
   393  			case 1:
   394  				dst = p.sio.out
   395  			case 2:
   396  				dst = p.sio.err
   397  			}
   398  			switch *r.Number {
   399  			case 1:
   400  				p.sio.out = dst
   401  			case 2:
   402  				p.sio.err = dst
   403  			}
   404  		case token.Less:
   405  			return nil, fmt.Errorf("TODO: %s", r.Token)
   406  		default:
   407  			return nil, fmt.Errorf("unknown shell redirect %s", r.Token)
   408  		}
   409  	}
   410  	return p, nil
   411  }
   412  
   413  func startPgidLeader() (*os.Process, error) {
   414  	path, err := executable()
   415  	if err != nil {
   416  		return nil, fmt.Errorf("pgidleader init: %v", err)
   417  	}
   418  	argv := []string{os.Args[0], "-pgidleader"}
   419  
   420  	p, err := os.StartProcess(path, argv, &os.ProcAttr{
   421  		Files: []*os.File{}, //r, os.Stdout, os.Stderr},
   422  		Sys: &syscall.SysProcAttr{
   423  			Setpgid: true, // job gets new pgid
   424  		},
   425  	})
   426  	if err != nil {
   427  		return nil, fmt.Errorf("pgidleader init start: %v", err)
   428  	}
   429  	return p, nil
   430  }
   431  
   432  type pipeline struct {
   433  	job  *Job
   434  	proc []*proc
   435  	//pgid int
   436  }
   437  
   438  func (pl *pipeline) start() (err error) {
   439  	pl.job.mu.Lock()
   440  	defer pl.job.mu.Unlock()
   441  
   442  	for _, p := range pl.proc {
   443  		p.path, err = findExecInPath(p.argv[0], pl.job.State.Env)
   444  		if err != nil {
   445  			return err
   446  		}
   447  	}
   448  
   449  	defer func() {
   450  		if err != nil {
   451  			for _, p := range pl.proc {
   452  				if p.process != nil {
   453  					p.process.Kill()
   454  					p.process = nil
   455  				}
   456  			}
   457  		}
   458  	}()
   459  	for i, p := range pl.proc {
   460  		attr := &os.ProcAttr{
   461  			Env:   p.env,
   462  			Files: []*os.File{p.sio.in, p.sio.out, p.sio.err},
   463  		}
   464  		attr.Sys = &syscall.SysProcAttr{
   465  			Setpgid:    true, // job gets new pgid
   466  			Foreground: interactive,
   467  			Pgid:       pl.job.pgid,
   468  		}
   469  		p.process, err = os.StartProcess(p.path, p.argv, attr)
   470  		if i == 0 && p.sio.in != p.job.Stdin {
   471  			p.sio.in.Close()
   472  		}
   473  		if i == len(pl.proc)-1 && p.sio.out != p.job.Stdout {
   474  			p.sio.out.Close()
   475  		}
   476  		if err != nil {
   477  			return err
   478  		}
   479  
   480  		if pl.job.pgid == 0 {
   481  			pl.job.pgid, err = syscall.Getpgid(p.process.Pid)
   482  			if err != nil {
   483  				return fmt.Errorf("cannot get pgid of new process: %v", err)
   484  			}
   485  			if interactive {
   486  				if err := tcsetpgrp(os.Stdin.Fd(), pl.job.pgid); err != nil {
   487  					return err
   488  				}
   489  			}
   490  		}
   491  	}
   492  	return nil
   493  }
   494  
   495  func (pl *pipeline) waitUntilDone() error {
   496  	var err error
   497  	for _, p := range pl.proc {
   498  		err = p.waitUntilDone()
   499  	}
   500  	return err
   501  }
   502  
   503  type exitError struct {
   504  	code int
   505  }
   506  
   507  func (err exitError) Error() string { return fmt.Sprintf("exit code: %d", err.code) }
   508  
   509  func (p *proc) waitUntilDone() error {
   510  	pid := p.process.Pid
   511  	//pid := pl.job.pgid
   512  	for {
   513  		wstatus := new(syscall.WaitStatus)
   514  		_, err := syscall.Wait4(pid, wstatus, syscall.WUNTRACED|syscall.WCONTINUED, nil)
   515  		switch {
   516  		case err != nil || wstatus.Exited():
   517  			// TODO: should we close these right after the process forks?
   518  			if p.sio.in != p.job.Stdin {
   519  				p.sio.in.Close()
   520  			}
   521  			if p.sio.out != p.job.Stdout {
   522  				p.sio.out.Close()
   523  			}
   524  			//fmt.Fprintf(os.Stderr, "process exited with %v\n", err)
   525  			if c := wstatus.ExitStatus(); c != 0 {
   526  				return exitError{code: c}
   527  			}
   528  			return nil
   529  		case wstatus.Stopped():
   530  			p.job.cond.L.Lock()
   531  			p.job.running = false
   532  			p.job.cond.Broadcast()
   533  			p.job.cond.L.Unlock()
   534  		case wstatus.Continued():
   535  			// BUG: on darwin at least, this isn't firing.
   536  		case wstatus.Signaled():
   537  			// ignore
   538  		default:
   539  			panic(fmt.Sprintf("unexpected wstatus: %#+v", wstatus))
   540  		}
   541  	}
   542  }
   543  
   544  type proc struct {
   545  	job     *Job
   546  	argv    []string
   547  	env     []string
   548  	path    string
   549  	process *os.Process
   550  	sio     stdio
   551  }
   552  
   553  // TODO: make interactive a property of a *shell.State.
   554  // Ensure that only one State at a time in a process can be interactive.
   555  var (
   556  	interactive bool
   557  	basicState  syscall.Termios
   558  	shellState  syscall.Termios
   559  	shellPgid   int
   560  )
   561  
   562  var jobSignals = []os.Signal{
   563  	syscall.SIGQUIT,
   564  	syscall.SIGTTOU,
   565  	syscall.SIGTTIN,
   566  }
   567  
   568  // TODO: make this a method on shell.State
   569  func Init() {
   570  	if len(os.Args) == 2 && os.Args[1] == "-pgidleader" {
   571  		select {}
   572  	}
   573  
   574  	var err error
   575  	basicState, err = tcgetattr(os.Stdin.Fd())
   576  	if err == nil {
   577  		interactive = true
   578  	}
   579  
   580  	if interactive {
   581  		// Become foreground process group.
   582  		for {
   583  			shellPgid, err = syscall.Getpgid(syscall.Getpid())
   584  			if err != nil {
   585  				panic(err)
   586  			}
   587  			foreground, err := tcgetpgrp(os.Stdin.Fd())
   588  			if err != nil {
   589  				panic(err)
   590  			}
   591  			if foreground == shellPgid {
   592  				break
   593  			}
   594  			syscall.Kill(-shellPgid, syscall.SIGTTIN)
   595  		}
   596  
   597  		// We ignore SIGTSTP and SIGINT with signal.Notify to avoid setting
   598  		// the signal to SIG_IGN, a state that exec(3) will pass on to
   599  		// child processes, making it impossible to Ctrl+Z any process that
   600  		// does not install its own SIGTSTP handler.
   601  		ignoreCh := make(chan os.Signal, 1)
   602  		go func() {
   603  			for range ignoreCh {
   604  			}
   605  		}()
   606  		signal.Notify(ignoreCh, syscall.SIGTSTP, syscall.SIGINT)
   607  		signal.Ignore(jobSignals...)
   608  
   609  		shellPgid = os.Getpid()
   610  		if err := syscall.Setpgid(shellPgid, shellPgid); err != nil {
   611  			panic(err)
   612  		}
   613  		if err := tcsetpgrp(os.Stdin.Fd(), shellPgid); err != nil {
   614  			panic(err)
   615  		}
   616  	}
   617  }
   618  
   619  func (s *State) bgAdd(j *Job) {
   620  	s.bgMu.Lock()
   621  	defer s.bgMu.Unlock()
   622  	s.bg = append(s.bg, j)
   623  	fmt.Fprintf(j.Stderr, "\n[%d]+  Stopped  %s\n", len(s.bg), shellListString(j.Cmd))
   624  }
   625  
   626  func (s *State) bgList(w io.Writer) {
   627  	s.bgMu.Lock()
   628  	defer s.bgMu.Unlock()
   629  	for _, j := range s.bg {
   630  		state := "Stopped"
   631  		if j.running { // TODO: need to hold lock, but need to not deadlock
   632  			state = "Running"
   633  		}
   634  		fmt.Fprintf(j.Stderr, "\n[%d]+  %s  %s\n", len(s.bg), state, shellListString(j.Cmd))
   635  	}
   636  }
   637  
   638  func (s *State) bgFg(spec string) error {
   639  	jobspec := 1
   640  	var err error
   641  	if spec != "" {
   642  		jobspec, err = strconv.Atoi(spec)
   643  	}
   644  	if err != nil {
   645  		return fmt.Errorf("fg: %v", err)
   646  	}
   647  
   648  	s.bgMu.Lock()
   649  	if len(s.bg) == 0 {
   650  		s.bgMu.Unlock()
   651  		return fmt.Errorf("fg: no jobs\n")
   652  	}
   653  	if jobspec > len(s.bg) {
   654  		s.bgMu.Unlock()
   655  		return fmt.Errorf("fg: %d: no such job\n", jobspec)
   656  	}
   657  	j := s.bg[jobspec-1]
   658  	s.bg = append(s.bg[:jobspec-1], s.bg[jobspec:]...)
   659  	fmt.Fprintf(j.Stderr, "%s\n", shellListString(j.Cmd))
   660  	s.bgMu.Unlock()
   661  	return j.Continue()
   662  }
   663  
   664  func (j *Job) export(pairs []string) error {
   665  	for _, p := range pairs {
   666  		parts := strings.SplitN(p, "=", 2)
   667  		val := ""
   668  		if len(parts) > 1 {
   669  			val = parts[1]
   670  		}
   671  		j.State.Env.Set(parts[0], val)
   672  	}
   673  	return nil
   674  }
   675  
   676  func Run(shellState *State, p Params, e *expr.Shell) (string, error) {
   677  	res := make(chan string)
   678  	out := os.Stdout
   679  	if e.DropOut {
   680  		out = devNull
   681  		close(res)
   682  	} else if e.TrapOut {
   683  		r, w, err := os.Pipe()
   684  		if err != nil {
   685  			panic(err)
   686  		}
   687  		out = w
   688  		go func() {
   689  			b, err := ioutil.ReadAll(r)
   690  			if err != nil {
   691  				panic(err)
   692  			}
   693  			res <- string(b)
   694  		}()
   695  	} else {
   696  		close(res)
   697  	}
   698  
   699  	var err error
   700  	for _, cmd := range e.Cmds {
   701  		j := &Job{
   702  			State:  shellState,
   703  			Cmd:    cmd,
   704  			Params: p,
   705  			Stdin:  os.Stdin,
   706  			Stdout: out,
   707  			Stderr: os.Stderr,
   708  		}
   709  		if err = j.Start(); err != nil {
   710  			break
   711  		}
   712  		var done bool
   713  		done, err = j.Wait()
   714  		if err != nil {
   715  			break
   716  		}
   717  		if !done {
   718  			break // TODO not right, instead we should just have one cmd, not Cmds here.
   719  		}
   720  	}
   721  	if e.TrapOut {
   722  		out.Close()
   723  	}
   724  	str := <-res
   725  	return str, err
   726  }
   727  
   728  var devNull *os.File
   729  
   730  func init() {
   731  	var err error
   732  	devNull, err = os.Open("/dev/null")
   733  	if err != nil {
   734  		panic(err)
   735  	}
   736  }