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 }