OILS / core / process.py View on Github | oilshell.org

1943 lines, 939 significant
1# Copyright 2016 Andy Chu. All rights reserved.
2# Licensed under the Apache License, Version 2.0 (the "License");
3# you may not use this file except in compliance with the License.
4# You may obtain a copy of the License at
5#
6# http://www.apache.org/licenses/LICENSE-2.0
7"""
8process.py - Launch processes and manipulate file descriptors.
9"""
10from __future__ import print_function
11
12from errno import EACCES, EBADF, ECHILD, EINTR, ENOENT, ENOEXEC
13import fcntl as fcntl_
14from fcntl import F_DUPFD, F_GETFD, F_SETFD, FD_CLOEXEC
15from signal import (SIG_DFL, SIG_IGN, SIGINT, SIGPIPE, SIGQUIT, SIGTSTP,
16 SIGTTOU, SIGTTIN, SIGWINCH)
17
18from _devbuild.gen.id_kind_asdl import Id
19from _devbuild.gen.runtime_asdl import (job_state_e, job_state_t,
20 job_state_str, wait_status,
21 wait_status_t, RedirValue,
22 redirect_arg, redirect_arg_e, trace,
23 trace_t)
24from _devbuild.gen.syntax_asdl import (
25 loc_t,
26 redir_loc,
27 redir_loc_e,
28 redir_loc_t,
29)
30from _devbuild.gen.value_asdl import (value, value_e)
31from core import dev
32from core import error
33from core.error import e_die
34from core import pyutil
35from core import pyos
36from core import state
37from core import ui
38from core import util
39from data_lang import j8_lite
40from frontend import location
41from frontend import match
42from mycpp import mylib
43from mycpp.mylib import log, print_stderr, tagswitch, iteritems
44
45import posix_ as posix
46from posix_ import (
47 # translated by mycpp and directly called! No wrapper!
48 WIFSIGNALED,
49 WIFEXITED,
50 WIFSTOPPED,
51 WEXITSTATUS,
52 WSTOPSIG,
53 WTERMSIG,
54 WNOHANG,
55 O_APPEND,
56 O_CREAT,
57 O_NONBLOCK,
58 O_NOCTTY,
59 O_RDONLY,
60 O_RDWR,
61 O_WRONLY,
62 O_TRUNC,
63)
64
65from typing import IO, List, Tuple, Dict, Optional, Any, cast, TYPE_CHECKING
66
67if TYPE_CHECKING:
68 from _devbuild.gen.runtime_asdl import cmd_value
69 from _devbuild.gen.syntax_asdl import command_t
70 from builtin import trap_osh
71 from core import optview
72 from core.ui import ErrorFormatter
73 from core.util import _DebugFile
74 from osh.cmd_eval import CommandEvaluator
75
76NO_FD = -1
77
78# Minimum file descriptor that the shell can use. Other descriptors can be
79# directly used by user programs, e.g. exec 9>&1
80#
81# Oil uses 100 because users are allowed TWO digits in frontend/lexer_def.py.
82# This is a compromise between bash (unlimited, but requires crazy
83# bookkeeping), and dash/zsh (10) and mksh (24)
84_SHELL_MIN_FD = 100
85
86# Style for 'jobs' builtin
87STYLE_DEFAULT = 0
88STYLE_LONG = 1
89STYLE_PID_ONLY = 2
90
91# To save on allocations in JobList::GetJobWithSpec()
92CURRENT_JOB_SPECS = ['', '%', '%%', '%+']
93
94
95class ctx_FileCloser(object):
96
97 def __init__(self, f):
98 # type: (mylib.LineReader) -> None
99 self.f = f
100
101 def __enter__(self):
102 # type: () -> None
103 pass
104
105 def __exit__(self, type, value, traceback):
106 # type: (Any, Any, Any) -> None
107 self.f.close()
108
109
110def InitInteractiveShell():
111 # type: () -> None
112 """Called when initializing an interactive shell."""
113
114 # The shell itself should ignore Ctrl-\.
115 pyos.Sigaction(SIGQUIT, SIG_IGN)
116
117 # This prevents Ctrl-Z from suspending OSH in interactive mode.
118 pyos.Sigaction(SIGTSTP, SIG_IGN)
119
120 # More signals from
121 # https://www.gnu.org/software/libc/manual/html_node/Initializing-the-Shell.html
122 # (but not SIGCHLD)
123 pyos.Sigaction(SIGTTOU, SIG_IGN)
124 pyos.Sigaction(SIGTTIN, SIG_IGN)
125
126 # Register a callback to receive terminal width changes.
127 # NOTE: In line_input.c, we turned off rl_catch_sigwinch.
128
129 # This is ALWAYS on, which means that it can cause EINTR, and wait() and
130 # read() have to handle it
131 pyos.RegisterSignalInterest(SIGWINCH)
132
133
134def SaveFd(fd):
135 # type: (int) -> int
136 saved = fcntl_.fcntl(fd, F_DUPFD, _SHELL_MIN_FD) # type: int
137 return saved
138
139
140class _RedirFrame(object):
141
142 def __init__(self, saved_fd, orig_fd, forget):
143 # type: (int, int, bool) -> None
144 self.saved_fd = saved_fd
145 self.orig_fd = orig_fd
146 self.forget = forget
147
148
149class _FdFrame(object):
150
151 def __init__(self):
152 # type: () -> None
153 self.saved = [] # type: List[_RedirFrame]
154 self.need_wait = [] # type: List[Process]
155
156 def Forget(self):
157 # type: () -> None
158 """For exec 1>&2."""
159 for rf in reversed(self.saved):
160 if rf.saved_fd != NO_FD and rf.forget:
161 posix.close(rf.saved_fd)
162
163 del self.saved[:] # like list.clear() in Python 3.3
164 del self.need_wait[:]
165
166 def __repr__(self):
167 # type: () -> str
168 return '<_FdFrame %s>' % self.saved
169
170
171class FdState(object):
172 """File descriptor state for the current process.
173
174 For example, you can do 'myfunc > out.txt' without forking. Child
175 processes inherit our state.
176 """
177
178 def __init__(
179 self,
180 errfmt, # type: ui.ErrorFormatter
181 job_control, # type: JobControl
182 job_list, # type: JobList
183 mem, #type: state.Mem
184 tracer, # type: Optional[dev.Tracer]
185 waiter, # type: Optional[Waiter]
186 ):
187 # type: (...) -> None
188 """
189 Args:
190 errfmt: for errors
191 job_list: For keeping track of _HereDocWriterThunk
192 """
193 self.errfmt = errfmt
194 self.job_control = job_control
195 self.job_list = job_list
196 self.cur_frame = _FdFrame() # for the top level
197 self.stack = [self.cur_frame]
198 self.mem = mem
199 self.tracer = tracer
200 self.waiter = waiter
201
202 def Open(self, path):
203 # type: (str) -> mylib.LineReader
204 """Opens a path for read, but moves it out of the reserved 3-9 fd
205 range.
206
207 Returns:
208 A Python file object. The caller is responsible for Close().
209
210 Raises:
211 IOError or OSError if the path can't be found. (This is Python-induced wart)
212 """
213 fd_mode = O_RDONLY
214 f = self._Open(path, 'r', fd_mode)
215
216 # Hacky downcast
217 return cast('mylib.LineReader', f)
218
219 # used for util.DebugFile
220 def OpenForWrite(self, path):
221 # type: (str) -> mylib.Writer
222 fd_mode = O_CREAT | O_RDWR
223 f = self._Open(path, 'w', fd_mode)
224
225 # Hacky downcast
226 return cast('mylib.Writer', f)
227
228 def _Open(self, path, c_mode, fd_mode):
229 # type: (str, str, int) -> IO[str]
230 fd = posix.open(path, fd_mode, 0o666) # may raise OSError
231
232 # Immediately move it to a new location
233 new_fd = SaveFd(fd)
234 posix.close(fd)
235
236 # Return a Python file handle
237 f = posix.fdopen(new_fd, c_mode) # may raise IOError
238 return f
239
240 def _WriteFdToMem(self, fd_name, fd):
241 # type: (str, int) -> None
242 if self.mem:
243 # setvar, not setref
244 state.OshLanguageSetValue(self.mem, location.LName(fd_name),
245 value.Str(str(fd)))
246
247 def _ReadFdFromMem(self, fd_name):
248 # type: (str) -> int
249 val = self.mem.GetValue(fd_name)
250 if val.tag() == value_e.Str:
251 try:
252 return int(cast(value.Str, val).s)
253 except ValueError:
254 return NO_FD
255 return NO_FD
256
257 def _PushSave(self, fd):
258 # type: (int) -> bool
259 """Save fd to a new location and remember to restore it later."""
260 #log('---- _PushSave %s', fd)
261 ok = True
262 try:
263 new_fd = SaveFd(fd)
264 except (IOError, OSError) as e:
265 ok = False
266 # Example program that causes this error: exec 4>&1. Descriptor 4 isn't
267 # open.
268 # This seems to be ignored in dash too in savefd()?
269 if e.errno != EBADF:
270 raise
271 if ok:
272 posix.close(fd)
273 fcntl_.fcntl(new_fd, F_SETFD, FD_CLOEXEC)
274 self.cur_frame.saved.append(_RedirFrame(new_fd, fd, True))
275 else:
276 # if we got EBADF, we still need to close the original on Pop()
277 self._PushClose(fd)
278
279 return ok
280
281 def _PushDup(self, fd1, blame_loc):
282 # type: (int, redir_loc_t) -> int
283 """Save fd2 in a higher range, and dup fd1 onto fd2.
284
285 Returns whether F_DUPFD/dup2 succeeded, and the new descriptor.
286 """
287 UP_loc = blame_loc
288 if blame_loc.tag() == redir_loc_e.VarName:
289 fd2_name = cast(redir_loc.VarName, UP_loc).name
290 try:
291 # F_DUPFD: GREATER than range
292 new_fd = fcntl_.fcntl(fd1, F_DUPFD, _SHELL_MIN_FD) # type: int
293 except (IOError, OSError) as e:
294 if e.errno == EBADF:
295 print_stderr('F_DUPFD fd %d: %s' %
296 (fd1, pyutil.strerror(e)))
297 return NO_FD
298 else:
299 raise # this redirect failed
300
301 self._WriteFdToMem(fd2_name, new_fd)
302
303 elif blame_loc.tag() == redir_loc_e.Fd:
304 fd2 = cast(redir_loc.Fd, UP_loc).fd
305
306 if fd1 == fd2:
307 # The user could have asked for it to be open on descriptor 3, but open()
308 # already returned 3, e.g. echo 3>out.txt
309 return NO_FD
310
311 # Check the validity of fd1 before _PushSave(fd2)
312 try:
313 fcntl_.fcntl(fd1, F_GETFD)
314 except (IOError, OSError) as e:
315 print_stderr('F_GETFD fd %d: %s' % (fd1, pyutil.strerror(e)))
316 raise
317
318 need_restore = self._PushSave(fd2)
319
320 #log('==== dup2 %s %s\n' % (fd1, fd2))
321 try:
322 posix.dup2(fd1, fd2)
323 except (IOError, OSError) as e:
324 # bash/dash give this error too, e.g. for 'echo hi 1>&3'
325 print_stderr('dup2(%d, %d): %s' %
326 (fd1, fd2, pyutil.strerror(e)))
327
328 # Restore and return error
329 if need_restore:
330 rf = self.cur_frame.saved.pop()
331 posix.dup2(rf.saved_fd, rf.orig_fd)
332 posix.close(rf.saved_fd)
333
334 raise # this redirect failed
335
336 new_fd = fd2
337
338 else:
339 raise AssertionError()
340
341 return new_fd
342
343 def _PushCloseFd(self, blame_loc):
344 # type: (redir_loc_t) -> bool
345 """For 2>&-"""
346 # exec {fd}>&- means close the named descriptor
347
348 UP_loc = blame_loc
349 if blame_loc.tag() == redir_loc_e.VarName:
350 fd_name = cast(redir_loc.VarName, UP_loc).name
351 fd = self._ReadFdFromMem(fd_name)
352 if fd == NO_FD:
353 return False
354
355 elif blame_loc.tag() == redir_loc_e.Fd:
356 fd = cast(redir_loc.Fd, UP_loc).fd
357
358 else:
359 raise AssertionError()
360
361 self._PushSave(fd)
362
363 return True
364
365 def _PushClose(self, fd):
366 # type: (int) -> None
367 self.cur_frame.saved.append(_RedirFrame(NO_FD, fd, False))
368
369 def _PushWait(self, proc):
370 # type: (Process) -> None
371 self.cur_frame.need_wait.append(proc)
372
373 def _ApplyRedirect(self, r):
374 # type: (RedirValue) -> None
375 arg = r.arg
376 UP_arg = arg
377 with tagswitch(arg) as case:
378
379 if case(redirect_arg_e.Path):
380 arg = cast(redirect_arg.Path, UP_arg)
381
382 if r.op_id in (Id.Redir_Great, Id.Redir_AndGreat): # > &>
383 # NOTE: This is different than >| because it respects noclobber, but
384 # that option is almost never used. See test/wild.sh.
385 mode = O_CREAT | O_WRONLY | O_TRUNC
386 elif r.op_id == Id.Redir_Clobber: # >|
387 mode = O_CREAT | O_WRONLY | O_TRUNC
388 elif r.op_id in (Id.Redir_DGreat,
389 Id.Redir_AndDGreat): # >> &>>
390 mode = O_CREAT | O_WRONLY | O_APPEND
391 elif r.op_id == Id.Redir_Less: # <
392 mode = O_RDONLY
393 elif r.op_id == Id.Redir_LessGreat: # <>
394 mode = O_CREAT | O_RDWR
395 else:
396 raise NotImplementedError(r.op_id)
397
398 # NOTE: 0666 is affected by umask, all shells use it.
399 try:
400 open_fd = posix.open(arg.filename, mode, 0o666)
401 except (IOError, OSError) as e:
402 self.errfmt.Print_("Can't open %r: %s" %
403 (arg.filename, pyutil.strerror(e)),
404 blame_loc=r.op_loc)
405 raise # redirect failed
406
407 new_fd = self._PushDup(open_fd, r.loc)
408 if new_fd != NO_FD:
409 posix.close(open_fd)
410
411 # Now handle &> and &>> and their variants. These pairs are the same:
412 #
413 # stdout_stderr.py &> out-err.txt
414 # stdout_stderr.py > out-err.txt 2>&1
415 #
416 # stdout_stderr.py 3&> out-err.txt
417 # stdout_stderr.py 3> out-err.txt 2>&3
418 #
419 # Ditto for {fd}> and {fd}&>
420
421 if r.op_id in (Id.Redir_AndGreat, Id.Redir_AndDGreat):
422 self._PushDup(new_fd, redir_loc.Fd(2))
423
424 elif case(redirect_arg_e.CopyFd): # e.g. echo hi 1>&2
425 arg = cast(redirect_arg.CopyFd, UP_arg)
426
427 if r.op_id == Id.Redir_GreatAnd: # 1>&2
428 self._PushDup(arg.target_fd, r.loc)
429
430 elif r.op_id == Id.Redir_LessAnd: # 0<&5
431 # The only difference between >& and <& is the default file
432 # descriptor argument.
433 self._PushDup(arg.target_fd, r.loc)
434
435 else:
436 raise NotImplementedError()
437
438 elif case(redirect_arg_e.MoveFd): # e.g. echo hi 5>&6-
439 arg = cast(redirect_arg.MoveFd, UP_arg)
440 new_fd = self._PushDup(arg.target_fd, r.loc)
441 if new_fd != NO_FD:
442 posix.close(arg.target_fd)
443
444 UP_loc = r.loc
445 if r.loc.tag() == redir_loc_e.Fd:
446 fd = cast(redir_loc.Fd, UP_loc).fd
447 else:
448 fd = NO_FD
449
450 self.cur_frame.saved.append(_RedirFrame(new_fd, fd, False))
451
452 elif case(redirect_arg_e.CloseFd): # e.g. echo hi 5>&-
453 self._PushCloseFd(r.loc)
454
455 elif case(redirect_arg_e.HereDoc):
456 arg = cast(redirect_arg.HereDoc, UP_arg)
457
458 # NOTE: Do these descriptors have to be moved out of the range 0-9?
459 read_fd, write_fd = posix.pipe()
460
461 self._PushDup(read_fd, r.loc) # stdin is now the pipe
462
463 # We can't close like we do in the filename case above? The writer can
464 # get a "broken pipe".
465 self._PushClose(read_fd)
466
467 thunk = _HereDocWriterThunk(write_fd, arg.body)
468
469 # Use PIPE_SIZE to save a process in the case of small here
470 # docs, which are the common case. (dash does this.)
471
472 # Note: could instrument this to see how often it happens.
473 # Though strace -ff can also work.
474 start_process = len(arg.body) > 4096
475 #start_process = True
476
477 if start_process:
478 here_proc = Process(thunk, self.job_control, self.job_list,
479 self.tracer)
480
481 # NOTE: we could close the read pipe here, but it doesn't really
482 # matter because we control the code.
483 here_proc.StartProcess(trace.HereDoc)
484 #log('Started %s as %d', here_proc, pid)
485 self._PushWait(here_proc)
486
487 # Now that we've started the child, close it in the parent.
488 posix.close(write_fd)
489
490 else:
491 posix.write(write_fd, arg.body)
492 posix.close(write_fd)
493
494 def Push(self, redirects, err_out):
495 # type: (List[RedirValue], List[error.IOError_OSError]) -> None
496 """Apply a group of redirects and remember to undo them."""
497
498 #log('> fd_state.Push %s', redirects)
499 new_frame = _FdFrame()
500 self.stack.append(new_frame)
501 self.cur_frame = new_frame
502
503 for r in redirects:
504 #log('apply %s', r)
505 with ui.ctx_Location(self.errfmt, r.op_loc):
506 try:
507 self._ApplyRedirect(r)
508 except (IOError, OSError) as e:
509 err_out.append(e)
510 # This can fail too
511 self.Pop(err_out)
512 return # for bad descriptor, etc.
513
514 def PushStdinFromPipe(self, r):
515 # type: (int) -> bool
516 """Save the current stdin and make it come from descriptor 'r'.
517
518 'r' is typically the read-end of a pipe. For 'lastpipe'/ZSH
519 semantics of
520
521 echo foo | read line; echo $line
522 """
523 new_frame = _FdFrame()
524 self.stack.append(new_frame)
525 self.cur_frame = new_frame
526
527 self._PushDup(r, redir_loc.Fd(0))
528 return True
529
530 def Pop(self, err_out):
531 # type: (List[error.IOError_OSError]) -> None
532 frame = self.stack.pop()
533 #log('< Pop %s', frame)
534 for rf in reversed(frame.saved):
535 if rf.saved_fd == NO_FD:
536 #log('Close %d', orig)
537 try:
538 posix.close(rf.orig_fd)
539 except (IOError, OSError) as e:
540 err_out.append(e)
541 log('Error closing descriptor %d: %s', rf.orig_fd,
542 pyutil.strerror(e))
543 return
544 else:
545 try:
546 posix.dup2(rf.saved_fd, rf.orig_fd)
547 except (IOError, OSError) as e:
548 err_out.append(e)
549 log('dup2(%d, %d) error: %s', rf.saved_fd, rf.orig_fd,
550 pyutil.strerror(e))
551 #log('fd state:')
552 #posix.system('ls -l /proc/%s/fd' % posix.getpid())
553 return
554 posix.close(rf.saved_fd)
555 #log('dup2 %s %s', saved, orig)
556
557 # Wait for here doc processes to finish.
558 for proc in frame.need_wait:
559 unused_status = proc.Wait(self.waiter)
560
561 def MakePermanent(self):
562 # type: () -> None
563 self.cur_frame.Forget()
564
565
566class ChildStateChange(object):
567
568 def __init__(self):
569 # type: () -> None
570 """Empty constructor for mycpp."""
571 pass
572
573 def Apply(self):
574 # type: () -> None
575 raise NotImplementedError()
576
577 def ApplyFromParent(self, proc):
578 # type: (Process) -> None
579 """Noop for all state changes other than SetPgid for mycpp."""
580 pass
581
582
583class StdinFromPipe(ChildStateChange):
584
585 def __init__(self, pipe_read_fd, w):
586 # type: (int, int) -> None
587 self.r = pipe_read_fd
588 self.w = w
589
590 def __repr__(self):
591 # type: () -> str
592 return '<StdinFromPipe %d %d>' % (self.r, self.w)
593
594 def Apply(self):
595 # type: () -> None
596 posix.dup2(self.r, 0)
597 posix.close(self.r) # close after dup
598
599 posix.close(self.w) # we're reading from the pipe, not writing
600 #log('child CLOSE w %d pid=%d', self.w, posix.getpid())
601
602
603class StdoutToPipe(ChildStateChange):
604
605 def __init__(self, r, pipe_write_fd):
606 # type: (int, int) -> None
607 self.r = r
608 self.w = pipe_write_fd
609
610 def __repr__(self):
611 # type: () -> str
612 return '<StdoutToPipe %d %d>' % (self.r, self.w)
613
614 def Apply(self):
615 # type: () -> None
616 posix.dup2(self.w, 1)
617 posix.close(self.w) # close after dup
618
619 posix.close(self.r) # we're writing to the pipe, not reading
620 #log('child CLOSE r %d pid=%d', self.r, posix.getpid())
621
622
623INVALID_PGID = -1
624# argument to setpgid() that means the process is its own leader
625OWN_LEADER = 0
626
627
628class SetPgid(ChildStateChange):
629
630 def __init__(self, pgid, tracer):
631 # type: (int, dev.Tracer) -> None
632 self.pgid = pgid
633 self.tracer = tracer
634
635 def Apply(self):
636 # type: () -> None
637 try:
638 posix.setpgid(0, self.pgid)
639 except (IOError, OSError) as e:
640 self.tracer.OtherMessage(
641 'osh: child %d failed to set its process group to %d: %s' %
642 (posix.getpid(), self.pgid, pyutil.strerror(e)))
643
644 def ApplyFromParent(self, proc):
645 # type: (Process) -> None
646 try:
647 posix.setpgid(proc.pid, self.pgid)
648 except (IOError, OSError) as e:
649 self.tracer.OtherMessage(
650 'osh: parent failed to set process group for PID %d to %d: %s'
651 % (proc.pid, self.pgid, pyutil.strerror(e)))
652
653
654class ExternalProgram(object):
655 """The capability to execute an external program like 'ls'."""
656
657 def __init__(
658 self,
659 hijack_shebang, # type: str
660 fd_state, # type: FdState
661 errfmt, # type: ErrorFormatter
662 debug_f, # type: _DebugFile
663 ):
664 # type: (...) -> None
665 """
666 Args:
667 hijack_shebang: The path of an interpreter to run instead of the one
668 specified in the shebang line. May be empty.
669 """
670 self.hijack_shebang = hijack_shebang
671 self.fd_state = fd_state
672 self.errfmt = errfmt
673 self.debug_f = debug_f
674
675 def Exec(self, argv0_path, cmd_val, environ):
676 # type: (str, cmd_value.Argv, Dict[str, str]) -> None
677 """Execute a program and exit this process.
678
679 Called by: ls / exec ls / ( ls / )
680 """
681 self._Exec(argv0_path, cmd_val.argv, cmd_val.arg_locs[0], environ,
682 True)
683 assert False, "This line should never execute" # NO RETURN
684
685 def _Exec(self, argv0_path, argv, argv0_loc, environ, should_retry):
686 # type: (str, List[str], loc_t, Dict[str, str], bool) -> None
687 if len(self.hijack_shebang):
688 opened = True
689 try:
690 f = self.fd_state.Open(argv0_path)
691 except (IOError, OSError) as e:
692 opened = False
693
694 if opened:
695 with ctx_FileCloser(f):
696 # Test if the shebang looks like a shell. TODO: The file might be
697 # binary with no newlines, so read 80 bytes instead of readline().
698
699 #line = f.read(80) # type: ignore # TODO: fix this
700 line = f.readline()
701
702 if match.ShouldHijack(line):
703 h_argv = [self.hijack_shebang, argv0_path]
704 h_argv.extend(argv[1:])
705 argv = h_argv
706 argv0_path = self.hijack_shebang
707 self.debug_f.writeln('Hijacked: %s' % argv0_path)
708 else:
709 #self.debug_f.log('Not hijacking %s (%r)', argv, line)
710 pass
711
712 try:
713 posix.execve(argv0_path, argv, environ)
714 except (IOError, OSError) as e:
715 # Run with /bin/sh when ENOEXEC error (no shebang). All shells do this.
716 if e.errno == ENOEXEC and should_retry:
717 new_argv = ['/bin/sh', argv0_path]
718 new_argv.extend(argv[1:])
719 self._Exec('/bin/sh', new_argv, argv0_loc, environ, False)
720 # NO RETURN
721
722 # Would be nice: when the path is relative and ENOENT: print PWD and do
723 # spelling correction?
724
725 self.errfmt.Print_(
726 "Can't execute %r: %s" % (argv0_path, pyutil.strerror(e)),
727 argv0_loc)
728
729 # POSIX mentions 126 and 127 for two specific errors. The rest are
730 # unspecified.
731 #
732 # http://pubs.opengroup.org/onlinepubs/9699919799.2016edition/utilities/V3_chap02.html#tag_18_08_02
733 if e.errno == EACCES:
734 status = 126
735 elif e.errno == ENOENT:
736 # TODO: most shells print 'command not found', rather than strerror()
737 # == "No such file or directory". That's better because it's at the
738 # end of the path search, and we're never searching for a directory.
739 status = 127
740 else:
741 # dash uses 2, but we use that for parse errors. This seems to be
742 # consistent with mksh and zsh.
743 status = 127
744
745 posix._exit(status)
746 # NO RETURN
747
748
749class Thunk(object):
750 """Abstract base class for things runnable in another process."""
751
752 def __init__(self):
753 # type: () -> None
754 """Empty constructor for mycpp."""
755 pass
756
757 def Run(self):
758 # type: () -> None
759 """Returns a status code."""
760 raise NotImplementedError()
761
762 def UserString(self):
763 # type: () -> str
764 """Display for the 'jobs' list."""
765 raise NotImplementedError()
766
767 def __repr__(self):
768 # type: () -> str
769 return self.UserString()
770
771
772class ExternalThunk(Thunk):
773 """An external executable."""
774
775 def __init__(self, ext_prog, argv0_path, cmd_val, environ):
776 # type: (ExternalProgram, str, cmd_value.Argv, Dict[str, str]) -> None
777 self.ext_prog = ext_prog
778 self.argv0_path = argv0_path
779 self.cmd_val = cmd_val
780 self.environ = environ
781
782 def UserString(self):
783 # type: () -> str
784
785 # NOTE: This is the format the Tracer uses.
786 # bash displays sleep $n & (code)
787 # but OSH displays sleep 1 & (argv array)
788 # We could switch the former but I'm not sure it's necessary.
789 tmp = [j8_lite.MaybeShellEncode(a) for a in self.cmd_val.argv]
790 return '[process] %s' % ' '.join(tmp)
791
792 def Run(self):
793 # type: () -> None
794 """An ExternalThunk is run in parent for the exec builtin."""
795 self.ext_prog.Exec(self.argv0_path, self.cmd_val, self.environ)
796
797
798class SubProgramThunk(Thunk):
799 """A subprogram that can be executed in another process."""
800
801 def __init__(self,
802 cmd_ev,
803 node,
804 trap_state,
805 multi_trace,
806 inherit_errexit=True):
807 # type: (CommandEvaluator, command_t, trap_osh.TrapState, dev.MultiTracer, bool) -> None
808 self.cmd_ev = cmd_ev
809 self.node = node
810 self.trap_state = trap_state
811 self.multi_trace = multi_trace
812 self.inherit_errexit = inherit_errexit # for bash errexit compatibility
813
814 def UserString(self):
815 # type: () -> str
816
817 # NOTE: These can be pieces of a pipeline, so they're arbitrary nodes.
818 # TODO: Extract SPIDS from node to display source? Note that
819 # CompoundStatus also has locations of each pipeline component; see
820 # Executor.RunPipeline()
821 thunk_str = ui.CommandType(self.node)
822 return '[subprog] %s' % thunk_str
823
824 def Run(self):
825 # type: () -> None
826 #self.errfmt.OneLineErrExit() # don't quote code in child processes
827
828 # TODO: break circular dep. Bit flags could go in ASDL or headers.
829 from osh import cmd_eval
830
831 # signal handlers aren't inherited
832 self.trap_state.ClearForSubProgram()
833
834 # NOTE: may NOT return due to exec().
835 if not self.inherit_errexit:
836 self.cmd_ev.mutable_opts.DisableErrExit()
837 try:
838 # optimize to eliminate redundant subshells like ( echo hi ) | wc -l etc.
839 self.cmd_ev.ExecuteAndCatch(self.node, cmd_flags=cmd_eval.Optimize)
840 status = self.cmd_ev.LastStatus()
841 # NOTE: We ignore the is_fatal return value. The user should set -o
842 # errexit so failures in subprocesses cause failures in the parent.
843 except util.UserExit as e:
844 status = e.status
845
846 # Handle errors in a subshell. These two cases are repeated from main()
847 # and the core/completion.py hook.
848 except KeyboardInterrupt:
849 print('')
850 status = 130 # 128 + 2
851 except (IOError, OSError) as e:
852 print_stderr('oils I/O error (subprogram): %s' %
853 pyutil.strerror(e))
854 status = 2
855
856 # If ProcessInit() doesn't turn off buffering, this is needed before
857 # _exit()
858 pyos.FlushStdout()
859
860 self.multi_trace.WriteDumps()
861
862 # We do NOT want to raise SystemExit here. Otherwise dev.Tracer::Pop()
863 # gets called in BOTH processes.
864 # The crash dump seems to be unaffected.
865 posix._exit(status)
866
867
868class _HereDocWriterThunk(Thunk):
869 """Write a here doc to one end of a pipe.
870
871 May be be executed in either a child process or the main shell
872 process.
873 """
874
875 def __init__(self, w, body_str):
876 # type: (int, str) -> None
877 self.w = w
878 self.body_str = body_str
879
880 def UserString(self):
881 # type: () -> str
882
883 # You can hit Ctrl-Z and the here doc writer will be suspended! Other
884 # shells don't have this problem because they use temp files! That's a bit
885 # unfortunate.
886 return '[here doc writer]'
887
888 def Run(self):
889 # type: () -> None
890 """do_exit: For small pipelines."""
891 #log('Writing %r', self.body_str)
892 posix.write(self.w, self.body_str)
893 #log('Wrote %r', self.body_str)
894 posix.close(self.w)
895 #log('Closed %d', self.w)
896
897 posix._exit(0)
898
899
900class Job(object):
901 """Interface for both Process and Pipeline.
902
903 They both can be put in the background and waited on.
904
905 Confusing thing about pipelines in the background: They have TOO MANY NAMES.
906
907 sleep 1 | sleep 2 &
908
909 - The LAST PID is what's printed at the prompt. This is $!, a PROCESS ID and
910 not a JOB ID.
911 # https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html#Special-Parameters
912 - The process group leader (setpgid) is the FIRST PID.
913 - It's also %1 or %+. The last job started.
914 """
915
916 def __init__(self):
917 # type: () -> None
918 # Initial state with & or Ctrl-Z is Running.
919 self.state = job_state_e.Running
920 self.job_id = -1
921 self.in_background = False
922
923 def DisplayJob(self, job_id, f, style):
924 # type: (int, mylib.Writer, int) -> None
925 raise NotImplementedError()
926
927 def State(self):
928 # type: () -> job_state_t
929 return self.state
930
931 def ProcessGroupId(self):
932 # type: () -> int
933 """Return the process group ID associated with this job."""
934 raise NotImplementedError()
935
936 def JobWait(self, waiter):
937 # type: (Waiter) -> wait_status_t
938 """Wait for this process/pipeline to be stopped or finished."""
939 raise NotImplementedError()
940
941 def SetBackground(self):
942 # type: () -> None
943 """Record that this job is running in the background."""
944 self.in_background = True
945
946 def SetForeground(self):
947 # type: () -> None
948 """Record that this job is running in the foreground."""
949 self.in_background = False
950
951
952class Process(Job):
953 """A process to run.
954
955 TODO: Should we make it clear that this is a FOREGROUND process? A
956 background process is wrapped in a "job". It is unevaluated.
957
958 It provides an API to manipulate file descriptor state in parent and child.
959 """
960
961 def __init__(self, thunk, job_control, job_list, tracer):
962 # type: (Thunk, JobControl, JobList, dev.Tracer) -> None
963 """
964 Args:
965 thunk: Thunk instance
966 job_list: for process bookkeeping
967 """
968 Job.__init__(self)
969 assert isinstance(thunk, Thunk), thunk
970 self.thunk = thunk
971 self.job_control = job_control
972 self.job_list = job_list
973 self.tracer = tracer
974
975 # For pipelines
976 self.parent_pipeline = None # type: Pipeline
977 self.state_changes = [] # type: List[ChildStateChange]
978 self.close_r = -1
979 self.close_w = -1
980
981 self.pid = -1
982 self.status = -1
983
984 def Init_ParentPipeline(self, pi):
985 # type: (Pipeline) -> None
986 """For updating PIPESTATUS."""
987 self.parent_pipeline = pi
988
989 def __repr__(self):
990 # type: () -> str
991
992 # note: be wary of infinite mutual recursion
993 #s = ' %s' % self.parent_pipeline if self.parent_pipeline else ''
994 #return '<Process %s%s>' % (self.thunk, s)
995 return '<Process %s %s>' % (_JobStateStr(self.state), self.thunk)
996
997 def ProcessGroupId(self):
998 # type: () -> int
999 """Returns the group ID of this process."""
1000 # This should only ever be called AFTER the process has started
1001 assert self.pid != -1
1002 if self.parent_pipeline:
1003 # XXX: Maybe we should die here instead? Unclear if this branch
1004 # should even be reachable with the current builtins.
1005 return self.parent_pipeline.ProcessGroupId()
1006
1007 return self.pid
1008
1009 def DisplayJob(self, job_id, f, style):
1010 # type: (int, mylib.Writer, int) -> None
1011 if job_id == -1:
1012 job_id_str = ' '
1013 else:
1014 job_id_str = '%%%d' % job_id
1015 if style == STYLE_PID_ONLY:
1016 f.write('%d\n' % self.pid)
1017 else:
1018 f.write('%s %d %7s ' %
1019 (job_id_str, self.pid, _JobStateStr(self.state)))
1020 f.write(self.thunk.UserString())
1021 f.write('\n')
1022
1023 def AddStateChange(self, s):
1024 # type: (ChildStateChange) -> None
1025 self.state_changes.append(s)
1026
1027 def AddPipeToClose(self, r, w):
1028 # type: (int, int) -> None
1029 self.close_r = r
1030 self.close_w = w
1031
1032 def MaybeClosePipe(self):
1033 # type: () -> None
1034 if self.close_r != -1:
1035 posix.close(self.close_r)
1036 posix.close(self.close_w)
1037
1038 def StartProcess(self, why):
1039 # type: (trace_t) -> int
1040 """Start this process with fork(), handling redirects."""
1041 pid = posix.fork()
1042 if pid < 0:
1043 # When does this happen?
1044 e_die('Fatal error in posix.fork()')
1045
1046 elif pid == 0: # child
1047 # Note: this happens in BOTH interactive and non-interactive shells.
1048 # We technically don't need to do most of it in non-interactive, since we
1049 # did not change state in InitInteractiveShell().
1050
1051 for st in self.state_changes:
1052 st.Apply()
1053
1054 # Python sets SIGPIPE handler to SIG_IGN by default. Child processes
1055 # shouldn't have this.
1056 # https://docs.python.org/2/library/signal.html
1057 # See Python/pythonrun.c.
1058 pyos.Sigaction(SIGPIPE, SIG_DFL)
1059
1060 # Respond to Ctrl-\ (core dump)
1061 pyos.Sigaction(SIGQUIT, SIG_DFL)
1062
1063 # Only standalone children should get Ctrl-Z. Pipelines remain in the
1064 # foreground because suspending them is difficult with our 'lastpipe'
1065 # semantics.
1066 pid = posix.getpid()
1067 if posix.getpgid(0) == pid and self.parent_pipeline is None:
1068 pyos.Sigaction(SIGTSTP, SIG_DFL)
1069
1070 # More signals from
1071 # https://www.gnu.org/software/libc/manual/html_node/Launching-Jobs.html
1072 # (but not SIGCHLD)
1073 pyos.Sigaction(SIGTTOU, SIG_DFL)
1074 pyos.Sigaction(SIGTTIN, SIG_DFL)
1075
1076 self.tracer.OnNewProcess(pid)
1077 # clear foreground pipeline for subshells
1078 self.thunk.Run()
1079 # Never returns
1080
1081 #log('STARTED process %s, pid = %d', self, pid)
1082 self.tracer.OnProcessStart(pid, why)
1083
1084 # Class invariant: after the process is started, it stores its PID.
1085 self.pid = pid
1086
1087 # SetPgid needs to be applied from the child and the parent to avoid
1088 # racing in calls to tcsetpgrp() in the parent. See APUE sec. 9.2.
1089 for st in self.state_changes:
1090 st.ApplyFromParent(self)
1091
1092 # Program invariant: We keep track of every child process!
1093 self.job_list.AddChildProcess(pid, self)
1094
1095 return pid
1096
1097 def Wait(self, waiter):
1098 # type: (Waiter) -> int
1099 """Wait for this process to finish."""
1100 while self.state == job_state_e.Running:
1101 # Only return if there's nothing to wait for. Keep waiting if we were
1102 # interrupted with a signal.
1103 if waiter.WaitForOne() == W1_ECHILD:
1104 break
1105
1106 assert self.status >= 0, self.status
1107 return self.status
1108
1109 def JobWait(self, waiter):
1110 # type: (Waiter) -> wait_status_t
1111 # wait builtin can be interrupted
1112 while self.state == job_state_e.Running:
1113 result = waiter.WaitForOne()
1114
1115 if result >= 0: # signal
1116 return wait_status.Cancelled(result)
1117
1118 if result == W1_ECHILD:
1119 break
1120
1121 return wait_status.Proc(self.status)
1122
1123 def WhenStopped(self, stop_sig):
1124 # type: (int) -> None
1125
1126 # 128 is a shell thing
1127 # https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html
1128 self.status = 128 + stop_sig
1129 self.state = job_state_e.Stopped
1130
1131 if self.job_id == -1:
1132 # This process was started in the foreground
1133 self.job_list.AddJob(self)
1134
1135 if not self.in_background:
1136 self.job_control.MaybeTakeTerminal()
1137 self.SetBackground()
1138
1139 def WhenDone(self, pid, status):
1140 # type: (int, int) -> None
1141 """Called by the Waiter when this Process finishes."""
1142
1143 #log('WhenDone %d %d', pid, status)
1144 assert pid == self.pid, 'Expected %d, got %d' % (self.pid, pid)
1145 self.status = status
1146 self.state = job_state_e.Done
1147 if self.parent_pipeline:
1148 self.parent_pipeline.WhenDone(pid, status)
1149 else:
1150 if self.job_id != -1:
1151 # Job might have been brought to the foreground after being
1152 # assigned a job ID.
1153 if self.in_background:
1154 print_stderr('[%d] Done PID %d' % (self.job_id, self.pid))
1155
1156 self.job_list.RemoveJob(self.job_id)
1157 self.job_list.RemoveChildProcess(self.pid)
1158
1159 if not self.in_background:
1160 self.job_control.MaybeTakeTerminal()
1161
1162 def RunProcess(self, waiter, why):
1163 # type: (Waiter, trace_t) -> int
1164 """Run this process synchronously."""
1165 self.StartProcess(why)
1166 # ShellExecutor might be calling this for the last part of a pipeline.
1167 if self.parent_pipeline is None:
1168 # QUESTION: Can the PGID of a single process just be the PID? i.e. avoid
1169 # calling getpgid()?
1170 self.job_control.MaybeGiveTerminal(posix.getpgid(self.pid))
1171 return self.Wait(waiter)
1172
1173
1174class ctx_Pipe(object):
1175
1176 def __init__(self, fd_state, fd, err_out):
1177 # type: (FdState, int, List[error.IOError_OSError]) -> None
1178 fd_state.PushStdinFromPipe(fd)
1179 self.fd_state = fd_state
1180 self.err_out = err_out
1181
1182 def __enter__(self):
1183 # type: () -> None
1184 pass
1185
1186 def __exit__(self, type, value, traceback):
1187 # type: (Any, Any, Any) -> None
1188 self.fd_state.Pop(self.err_out)
1189
1190
1191class Pipeline(Job):
1192 """A pipeline of processes to run.
1193
1194 Cases we handle:
1195
1196 foo | bar
1197 $(foo | bar)
1198 foo | bar | read v
1199 """
1200
1201 def __init__(self, sigpipe_status_ok, job_control, job_list, tracer):
1202 # type: (bool, JobControl, JobList, dev.Tracer) -> None
1203 Job.__init__(self)
1204 self.job_control = job_control
1205 self.job_list = job_list
1206 self.tracer = tracer
1207
1208 self.procs = [] # type: List[Process]
1209 self.pids = [] # type: List[int] # pids in order
1210 self.pipe_status = [] # type: List[int] # status in order
1211 self.status = -1 # for 'wait' jobs
1212
1213 self.pgid = INVALID_PGID
1214
1215 # Optional for foreground
1216 self.last_thunk = None # type: Tuple[CommandEvaluator, command_t]
1217 self.last_pipe = None # type: Tuple[int, int]
1218
1219 self.sigpipe_status_ok = sigpipe_status_ok
1220
1221 def ProcessGroupId(self):
1222 # type: () -> int
1223 """Returns the group ID of this pipeline."""
1224 return self.pgid
1225
1226 def DisplayJob(self, job_id, f, style):
1227 # type: (int, mylib.Writer, int) -> None
1228 if style == STYLE_PID_ONLY:
1229 f.write('%d\n' % self.procs[0].pid)
1230 else:
1231 # Note: this is STYLE_LONG.
1232 for i, proc in enumerate(self.procs):
1233 if i == 0: # show job ID for first element in pipeline
1234 job_id_str = '%%%d' % job_id
1235 else:
1236 job_id_str = ' ' # 2 spaces
1237
1238 f.write('%s %d %7s ' %
1239 (job_id_str, proc.pid, _JobStateStr(proc.state)))
1240 f.write(proc.thunk.UserString())
1241 f.write('\n')
1242
1243 def DebugPrint(self):
1244 # type: () -> None
1245 print('Pipeline in state %s' % _JobStateStr(self.state))
1246 if mylib.PYTHON: # %s for Process not allowed in C++
1247 for proc in self.procs:
1248 print(' proc %s' % proc)
1249 _, last_node = self.last_thunk
1250 print(' last %s' % last_node)
1251 print(' pipe_status %s' % self.pipe_status)
1252
1253 def Add(self, p):
1254 # type: (Process) -> None
1255 """Append a process to the pipeline."""
1256 if len(self.procs) == 0:
1257 self.procs.append(p)
1258 return
1259
1260 r, w = posix.pipe()
1261 #log('pipe for %s: %d %d', p, r, w)
1262 prev = self.procs[-1]
1263
1264 prev.AddStateChange(StdoutToPipe(r, w)) # applied on StartPipeline()
1265 p.AddStateChange(StdinFromPipe(r, w)) # applied on StartPipeline()
1266
1267 p.AddPipeToClose(r, w) # MaybeClosePipe() on StartPipeline()
1268
1269 self.procs.append(p)
1270
1271 def AddLast(self, thunk):
1272 # type: (Tuple[CommandEvaluator, command_t]) -> None
1273 """Append the last noden to the pipeline.
1274
1275 This is run in the CURRENT process. It is OPTIONAL, because
1276 pipelines in the background are run uniformly.
1277 """
1278 self.last_thunk = thunk
1279
1280 assert len(self.procs) != 0
1281
1282 r, w = posix.pipe()
1283 prev = self.procs[-1]
1284 prev.AddStateChange(StdoutToPipe(r, w))
1285
1286 self.last_pipe = (r, w) # So we can connect it to last_thunk
1287
1288 def StartPipeline(self, waiter):
1289 # type: (Waiter) -> None
1290
1291 # If we are creating a pipeline in a subshell or we aren't running with job
1292 # control, our children should remain in our inherited process group.
1293 # the pipelines's group ID.
1294 if self.job_control.Enabled():
1295 self.pgid = OWN_LEADER # first process in pipeline is the leader
1296
1297 for i, proc in enumerate(self.procs):
1298 if self.pgid != INVALID_PGID:
1299 proc.AddStateChange(SetPgid(self.pgid, self.tracer))
1300
1301 # Figure out the pid
1302 pid = proc.StartProcess(trace.PipelinePart)
1303 if i == 0 and self.pgid != INVALID_PGID:
1304 # Mimic bash and use the PID of the FIRST process as the group for the
1305 # whole pipeline.
1306 self.pgid = pid
1307
1308 self.pids.append(pid)
1309 self.pipe_status.append(-1) # uninitialized
1310
1311 # NOTE: This is done in the SHELL PROCESS after every fork() call.
1312 # It can't be done at the end; otherwise processes will have descriptors
1313 # from non-adjacent pipes.
1314 proc.MaybeClosePipe()
1315
1316 if self.last_thunk:
1317 self.pipe_status.append(-1) # for self.last_thunk
1318
1319 def LastPid(self):
1320 # type: () -> int
1321 """For the odd $! variable.
1322
1323 It would be better if job IDs or PGIDs were used consistently.
1324 """
1325 return self.pids[-1]
1326
1327 def Wait(self, waiter):
1328 # type: (Waiter) -> List[int]
1329 """Wait for this pipeline to finish."""
1330
1331 assert self.procs, "no procs for Wait()"
1332 # waitpid(-1) zero or more times
1333 while self.state == job_state_e.Running:
1334 # Keep waiting until there's nothing to wait for.
1335 if waiter.WaitForOne() == W1_ECHILD:
1336 break
1337
1338 return self.pipe_status
1339
1340 def JobWait(self, waiter):
1341 # type: (Waiter) -> wait_status_t
1342 """Called by 'wait' builtin, e.g. 'wait %1'."""
1343 # wait builtin can be interrupted
1344 assert self.procs, "no procs for Wait()"
1345 while self.state == job_state_e.Running:
1346 result = waiter.WaitForOne()
1347
1348 if result >= 0: # signal
1349 return wait_status.Cancelled(result)
1350
1351 if result == W1_ECHILD:
1352 break
1353
1354 return wait_status.Pipeline(self.pipe_status)
1355
1356 def RunLastPart(self, waiter, fd_state):
1357 # type: (Waiter, FdState) -> List[int]
1358 """Run this pipeline synchronously (foreground pipeline).
1359
1360 Returns:
1361 pipe_status (list of integers).
1362 """
1363 assert len(self.pids) == len(self.procs)
1364
1365 # TODO: break circular dep. Bit flags could go in ASDL or headers.
1366 from osh import cmd_eval
1367
1368 # This is tcsetpgrp()
1369 # TODO: fix race condition -- I believe the first process could have
1370 # stopped already, and thus getpgid() will fail
1371 self.job_control.MaybeGiveTerminal(self.pgid)
1372
1373 # Run the last part of the pipeline IN PARALLEL with other processes. It
1374 # may or may not fork:
1375 # echo foo | read line # no fork, the builtin runs in THIS shell process
1376 # ls | wc -l # fork for 'wc'
1377
1378 cmd_ev, last_node = self.last_thunk
1379
1380 assert self.last_pipe is not None
1381 r, w = self.last_pipe # set in AddLast()
1382 posix.close(w) # we will not write here
1383
1384 # Fix lastpipe / job control / DEBUG trap interaction
1385 cmd_flags = cmd_eval.NoDebugTrap if self.job_control.Enabled() else 0
1386 io_errors = [] # type: List[error.IOError_OSError]
1387 with ctx_Pipe(fd_state, r, io_errors):
1388 cmd_ev.ExecuteAndCatch(last_node, cmd_flags)
1389
1390 if len(io_errors):
1391 e_die('Error setting up last part of pipeline: %s' %
1392 pyutil.strerror(io_errors[0]))
1393
1394 # We won't read anymore. If we don't do this, then 'cat' in 'cat
1395 # /dev/urandom | sleep 1' will never get SIGPIPE.
1396 posix.close(r)
1397
1398 self.pipe_status[-1] = cmd_ev.LastStatus()
1399 if self.AllDone():
1400 self.state = job_state_e.Done
1401
1402 #log('pipestatus before all have finished = %s', self.pipe_status)
1403 return self.Wait(waiter)
1404
1405 def AllDone(self):
1406 # type: () -> bool
1407
1408 # mycpp rewrite: all(status != -1 for status in self.pipe_status)
1409 for status in self.pipe_status:
1410 if status == -1:
1411 return False
1412 return True
1413
1414 def WhenDone(self, pid, status):
1415 # type: (int, int) -> None
1416 """Called by Process.WhenDone."""
1417 #log('Pipeline WhenDone %d %d', pid, status)
1418 i = self.pids.index(pid)
1419 assert i != -1, 'Unexpected PID %d' % pid
1420
1421 if status == 141 and self.sigpipe_status_ok:
1422 status = 0
1423
1424 self.job_list.RemoveChildProcess(pid)
1425 self.pipe_status[i] = status
1426 if self.AllDone():
1427 if self.job_id != -1:
1428 # Job might have been brought to the foreground after being
1429 # assigned a job ID.
1430 if self.in_background:
1431 print_stderr('[%d] Done PGID %d' %
1432 (self.job_id, self.pids[0]))
1433
1434 self.job_list.RemoveJob(self.job_id)
1435
1436 # status of pipeline is status of last process
1437 self.status = self.pipe_status[-1]
1438 self.state = job_state_e.Done
1439 if not self.in_background:
1440 self.job_control.MaybeTakeTerminal()
1441
1442
1443def _JobStateStr(i):
1444 # type: (job_state_t) -> str
1445 return job_state_str(i)[10:] # remove 'job_state.'
1446
1447
1448def _GetTtyFd():
1449 # type: () -> int
1450 """Returns -1 if stdio is not a TTY."""
1451 try:
1452 return posix.open("/dev/tty", O_NONBLOCK | O_NOCTTY | O_RDWR, 0o666)
1453 except (IOError, OSError) as e:
1454 return -1
1455
1456
1457class ctx_TerminalControl(object):
1458
1459 def __init__(self, job_control, errfmt):
1460 # type: (JobControl, ui.ErrorFormatter) -> None
1461 job_control.InitJobControl()
1462 self.job_control = job_control
1463 self.errfmt = errfmt
1464
1465 def __enter__(self):
1466 # type: () -> None
1467 pass
1468
1469 def __exit__(self, type, value, traceback):
1470 # type: (Any, Any, Any) -> None
1471
1472 # Return the TTY to the original owner before exiting.
1473 try:
1474 self.job_control.MaybeReturnTerminal()
1475 except error.FatalRuntime as e:
1476 # Don't abort the shell on error, just print a message.
1477 self.errfmt.PrettyPrintError(e)
1478
1479
1480class JobControl(object):
1481 """Interface to setpgid(), tcsetpgrp(), etc."""
1482
1483 def __init__(self):
1484 # type: () -> None
1485
1486 # The main shell's PID and group ID.
1487 self.shell_pid = -1
1488 self.shell_pgid = -1
1489
1490 # The fd of the controlling tty. Set to -1 when job control is disabled.
1491 self.shell_tty_fd = -1
1492
1493 # For giving the terminal back to our parent before exiting (if not a login
1494 # shell).
1495 self.original_tty_pgid = -1
1496
1497 def InitJobControl(self):
1498 # type: () -> None
1499 self.shell_pid = posix.getpid()
1500 orig_shell_pgid = posix.getpgid(0)
1501 self.shell_pgid = orig_shell_pgid
1502 self.shell_tty_fd = _GetTtyFd()
1503
1504 # If we aren't the leader of our process group, create a group and mark
1505 # ourselves as the leader.
1506 if self.shell_pgid != self.shell_pid:
1507 try:
1508 posix.setpgid(self.shell_pid, self.shell_pid)
1509 self.shell_pgid = self.shell_pid
1510 except (IOError, OSError) as e:
1511 self.shell_tty_fd = -1
1512
1513 if self.shell_tty_fd != -1:
1514 self.original_tty_pgid = posix.tcgetpgrp(self.shell_tty_fd)
1515
1516 # If stdio is a TTY, put the shell's process group in the foreground.
1517 try:
1518 posix.tcsetpgrp(self.shell_tty_fd, self.shell_pgid)
1519 except (IOError, OSError) as e:
1520 # We probably aren't in the session leader's process group. Disable job
1521 # control.
1522 self.shell_tty_fd = -1
1523 self.shell_pgid = orig_shell_pgid
1524 posix.setpgid(self.shell_pid, self.shell_pgid)
1525
1526 def Enabled(self):
1527 # type: () -> bool
1528
1529 # TODO: get rid of this syscall? SubProgramThunk should set a flag I
1530 # think.
1531 curr_pid = posix.getpid()
1532 # Only the main shell should bother with job control functions.
1533 return curr_pid == self.shell_pid and self.shell_tty_fd != -1
1534
1535 # TODO: This isn't a PID. This is a process group ID?
1536 #
1537 # What should the table look like?
1538 #
1539 # Do we need the last PID? I don't know why bash prints that. Probably so
1540 # you can do wait $!
1541 # wait -n waits for any node to go from job_state_e.Running to job_state_e.Done?
1542 #
1543 # And it needs a flag for CURRENT, for the implicit arg to 'fg'.
1544 # job_id is just an integer. This is sort of lame.
1545 #
1546 # [job_id, flag, pgid, job_state, node]
1547
1548 def MaybeGiveTerminal(self, pgid):
1549 # type: (int) -> None
1550 """If stdio is a TTY, move the given process group to the
1551 foreground."""
1552 if not self.Enabled():
1553 # Only call tcsetpgrp when job control is enabled.
1554 return
1555
1556 try:
1557 posix.tcsetpgrp(self.shell_tty_fd, pgid)
1558 except (IOError, OSError) as e:
1559 e_die('osh: Failed to move process group %d to foreground: %s' %
1560 (pgid, pyutil.strerror(e)))
1561
1562 def MaybeTakeTerminal(self):
1563 # type: () -> None
1564 """If stdio is a TTY, return the main shell's process group to the
1565 foreground."""
1566 self.MaybeGiveTerminal(self.shell_pgid)
1567
1568 def MaybeReturnTerminal(self):
1569 # type: () -> None
1570 """Called before the shell exits."""
1571 self.MaybeGiveTerminal(self.original_tty_pgid)
1572
1573
1574class JobList(object):
1575 """Global list of jobs, used by a few builtins."""
1576
1577 def __init__(self):
1578 # type: () -> None
1579
1580 # job_id -> Job instance
1581 self.jobs = {} # type: Dict[int, Job]
1582
1583 # pid -> Process. This is for STOP notification.
1584 self.child_procs = {} # type: Dict[int, Process]
1585 self.debug_pipelines = [] # type: List[Pipeline]
1586
1587 # Counter used to assign IDs to jobs. It is incremented every time a job
1588 # is created. Once all active jobs are done it is reset to 1. I'm not
1589 # sure if this reset behavior is mandated by POSIX, but other shells do
1590 # it, so we mimic for the sake of compatibility.
1591 self.job_id = 1
1592
1593 def AddJob(self, job):
1594 # type: (Job) -> int
1595 """Add a background job to the list.
1596
1597 A job is either a Process or Pipeline. You can resume a job with 'fg',
1598 kill it with 'kill', etc.
1599
1600 Two cases:
1601
1602 1. async jobs: sleep 5 | sleep 4 &
1603 2. stopped jobs: sleep 5; then Ctrl-Z
1604 """
1605 job_id = self.job_id
1606 self.jobs[job_id] = job
1607 job.job_id = job_id
1608 self.job_id += 1
1609 return job_id
1610
1611 def RemoveJob(self, job_id):
1612 # type: (int) -> None
1613 """Process and Pipeline can call this."""
1614 mylib.dict_erase(self.jobs, job_id)
1615
1616 if len(self.jobs) == 0:
1617 self.job_id = 1
1618
1619 def AddChildProcess(self, pid, proc):
1620 # type: (int, Process) -> None
1621 """Every child process should be added here as soon as we know its PID.
1622
1623 When the Waiter gets an EXITED or STOPPED notification, we need
1624 to know about it so 'jobs' can work.
1625 """
1626 self.child_procs[pid] = proc
1627
1628 def RemoveChildProcess(self, pid):
1629 # type: (int) -> None
1630 """Remove the child process with the given PID."""
1631 mylib.dict_erase(self.child_procs, pid)
1632
1633 if mylib.PYTHON:
1634
1635 def AddPipeline(self, pi):
1636 # type: (Pipeline) -> None
1637 """For debugging only."""
1638 self.debug_pipelines.append(pi)
1639
1640 def ProcessFromPid(self, pid):
1641 # type: (int) -> Process
1642 """For wait $PID.
1643
1644 There's no way to wait for a pipeline with a PID. That uses job
1645 syntax, e.g. %1. Not a great interface.
1646 """
1647 return self.child_procs.get(pid)
1648
1649 def GetCurrentAndPreviousJobs(self):
1650 # type: () -> Tuple[Optional[Job], Optional[Job]]
1651 """Return the "current" and "previous" jobs (AKA `%+` and `%-`).
1652
1653 See the POSIX specification for the `jobs` builtin for details:
1654 https://pubs.opengroup.org/onlinepubs/007904875/utilities/jobs.html
1655
1656 IMPORTANT NOTE: This method assumes that the jobs list will not change
1657 during its execution! This assumption holds for now because we only ever
1658 update the jobs list from the main loop after WaitPid() informs us of a
1659 change. If we implement `set -b` and install a signal handler for
1660 SIGCHLD we should be careful to synchronize it with this function. The
1661 unsafety of mutating GC data structures from a signal handler should
1662 make this a non-issue, but if bugs related to this appear this note may
1663 be helpful...
1664 """
1665 # Split all active jobs by state and sort each group by decreasing job
1666 # ID to approximate newness.
1667 stopped_jobs = [] # type: List[Job]
1668 running_jobs = [] # type: List[Job]
1669 for i in xrange(0, self.job_id):
1670 job = self.jobs.get(i, None)
1671 if not job:
1672 continue
1673
1674 if job.state == job_state_e.Stopped:
1675 stopped_jobs.append(job)
1676
1677 elif job.state == job_state_e.Running:
1678 running_jobs.append(job)
1679
1680 current = None # type: Optional[Job]
1681 previous = None # type: Optional[Job]
1682 # POSIX says: If there is any suspended job, then the current job shall
1683 # be a suspended job. If there are at least two suspended jobs, then the
1684 # previous job also shall be a suspended job.
1685 #
1686 # So, we will only return running jobs from here if there are no recent
1687 # stopped jobs.
1688 if len(stopped_jobs) > 0:
1689 current = stopped_jobs.pop()
1690
1691 if len(stopped_jobs) > 0:
1692 previous = stopped_jobs.pop()
1693
1694 if len(running_jobs) > 0 and not current:
1695 current = running_jobs.pop()
1696
1697 if len(running_jobs) > 0 and not previous:
1698 previous = running_jobs.pop()
1699
1700 if not previous:
1701 previous = current
1702
1703 return current, previous
1704
1705 def GetJobWithSpec(self, job_spec):
1706 # type: (str) -> Optional[Job]
1707 """Parse the given job spec and return the matching job. If there is no
1708 matching job, this function returns None.
1709
1710 See the POSIX spec for the `jobs` builtin for details about job specs:
1711 https://pubs.opengroup.org/onlinepubs/007904875/utilities/jobs.html
1712 """
1713 if job_spec in CURRENT_JOB_SPECS:
1714 current, _ = self.GetCurrentAndPreviousJobs()
1715 return current
1716
1717 if job_spec == '%-':
1718 _, previous = self.GetCurrentAndPreviousJobs()
1719 return previous
1720
1721 # TODO: Add support for job specs based on prefixes of process argv.
1722 m = util.RegexSearch(r'^%([0-9]+)$', job_spec)
1723 if m is not None:
1724 assert len(m) == 2
1725 job_id = int(m[1])
1726 if job_id in self.jobs:
1727 return self.jobs[job_id]
1728
1729 return None
1730
1731 def DisplayJobs(self, style):
1732 # type: (int) -> None
1733 """Used by the 'jobs' builtin.
1734
1735 https://pubs.opengroup.org/onlinepubs/9699919799/utilities/jobs.html
1736
1737 "By default, the jobs utility shall display the status of all stopped jobs,
1738 running background jobs and all jobs whose status has changed and have not
1739 been reported by the shell."
1740 """
1741 # NOTE: A job is a background process or pipeline.
1742 #
1743 # echo hi | wc -l -- this starts two processes. Wait for TWO
1744 # echo hi | wc -l & -- this starts a process which starts two processes
1745 # Wait for ONE.
1746 #
1747 # 'jobs -l' GROUPS the PIDs by job. It has the job number, + - indicators
1748 # for %% and %-, PID, status, and "command".
1749 #
1750 # Every component of a pipeline is on the same line with 'jobs', but
1751 # they're separated into different lines with 'jobs -l'.
1752 #
1753 # See demo/jobs-builtin.sh
1754
1755 # $ jobs -l
1756 # [1]+ 24414 Stopped sleep 5
1757 # 24415 | sleep 5
1758 # [2] 24502 Running sleep 6
1759 # 24503 | sleep 6
1760 # 24504 | sleep 5 &
1761 # [3]- 24508 Running sleep 6
1762 # 24509 | sleep 6
1763 # 24510 | sleep 5 &
1764
1765 f = mylib.Stdout()
1766 for job_id, job in iteritems(self.jobs):
1767 # Use the %1 syntax
1768 job.DisplayJob(job_id, f, style)
1769
1770 def DebugPrint(self):
1771 # type: () -> None
1772
1773 f = mylib.Stdout()
1774 f.write('\n')
1775 f.write('[process debug info]\n')
1776
1777 for pid, proc in iteritems(self.child_procs):
1778 proc.DisplayJob(-1, f, STYLE_DEFAULT)
1779 #p = ' |' if proc.parent_pipeline else ''
1780 #print('%d %7s %s%s' % (pid, _JobStateStr(proc.state), proc.thunk.UserString(), p))
1781
1782 if len(self.debug_pipelines):
1783 f.write('\n')
1784 f.write('[pipeline debug info]\n')
1785 for pi in self.debug_pipelines:
1786 pi.DebugPrint()
1787
1788 def ListRecent(self):
1789 # type: () -> None
1790 """For jobs -n, which I think is also used in the interactive
1791 prompt."""
1792 pass
1793
1794 def NumRunning(self):
1795 # type: () -> int
1796 """Return the number of running jobs.
1797
1798 Used by 'wait' and 'wait -n'.
1799 """
1800 count = 0
1801 for _, job in iteritems(self.jobs): # mycpp rewrite: from itervalues()
1802 if job.State() == job_state_e.Running:
1803 count += 1
1804 return count
1805
1806
1807# Some WaitForOne() return values
1808W1_OK = -2 # waitpid(-1) returned
1809W1_ECHILD = -3 # no processes to wait for
1810W1_AGAIN = -4 # WNOHANG was passed and there were no state changes
1811
1812
1813class Waiter(object):
1814 """A capability to wait for processes.
1815
1816 This must be a singleton (and is because CommandEvaluator is a singleton).
1817
1818 Invariants:
1819 - Every child process is registered once
1820 - Every child process is waited for
1821
1822 Canonical example of why we need a GLOBAL waiter:
1823
1824 { sleep 3; echo 'done 3'; } &
1825 { sleep 4; echo 'done 4'; } &
1826
1827 # ... do arbitrary stuff ...
1828
1829 { sleep 1; exit 1; } | { sleep 2; exit 2; }
1830
1831 Now when you do wait() after starting the pipeline, you might get a pipeline
1832 process OR a background process! So you have to distinguish between them.
1833 """
1834
1835 def __init__(self, job_list, exec_opts, signal_safe, tracer):
1836 # type: (JobList, optview.Exec, pyos.SignalSafe, dev.Tracer) -> None
1837 self.job_list = job_list
1838 self.exec_opts = exec_opts
1839 self.signal_safe = signal_safe
1840 self.tracer = tracer
1841 self.last_status = 127 # wait -n error code
1842
1843 def WaitForOne(self, waitpid_options=0):
1844 # type: (int) -> int
1845 """Wait until the next process returns (or maybe Ctrl-C).
1846
1847 Returns:
1848 One of these negative numbers:
1849 W1_ECHILD Nothing to wait for
1850 W1_OK Caller should keep waiting
1851 UNTRAPPED_SIGWINCH
1852 Or
1853 result > 0 Signal that waitpid() was interrupted with
1854
1855 In the interactive shell, we return 0 if we get a Ctrl-C, so the caller
1856 will try again.
1857
1858 Callers:
1859 wait -n -- loop until there is one fewer process (TODO)
1860 wait -- loop until there are no processes
1861 wait $! -- loop until job state is Done (process or pipeline)
1862 Process::Wait() -- loop until Process state is done
1863 Pipeline::Wait() -- loop until Pipeline state is done
1864
1865 Comparisons:
1866 bash: jobs.c waitchld() Has a special case macro(!) CHECK_WAIT_INTR for
1867 the wait builtin
1868
1869 dash: jobs.c waitproc() uses sigfillset(), sigprocmask(), etc. Runs in a
1870 loop while (gotsigchld), but that might be a hack for System V!
1871
1872 Should we have a cleaner API like named posix::wait_for_one() ?
1873
1874 wait_result =
1875 ECHILD -- nothing to wait for
1876 | Done(int pid, int status) -- process done
1877 | EINTR(bool sigint) -- may or may not retry
1878 """
1879 pid, status = pyos.WaitPid(waitpid_options)
1880 if pid == 0: # WNOHANG passed, and no state changes
1881 return W1_AGAIN
1882 elif pid < 0: # error case
1883 err_num = status
1884 #log('waitpid() error => %d %s', e.errno, pyutil.strerror(e))
1885 if err_num == ECHILD:
1886 return W1_ECHILD # nothing to wait for caller should stop
1887 elif err_num == EINTR: # Bug #858 fix
1888 #log('WaitForOne() => %d', self.trap_state.GetLastSignal())
1889 return self.signal_safe.LastSignal() # e.g. 1 for SIGHUP
1890 else:
1891 # The signature of waitpid() means this shouldn't happen
1892 raise AssertionError()
1893
1894 # All child processes are supposed to be in this dict. But this may
1895 # legitimately happen if a grandchild outlives the child (its parent).
1896 # Then it is reparented under this process, so we might receive
1897 # notification of its exit, even though we didn't start it. We can't have
1898 # any knowledge of such processes, so print a warning.
1899 if pid not in self.job_list.child_procs:
1900 print_stderr("osh: PID %d stopped, but osh didn't start it" % pid)
1901 return W1_OK
1902
1903 proc = self.job_list.child_procs[pid]
1904 if 0:
1905 self.job_list.DebugPrint()
1906
1907 if WIFSIGNALED(status):
1908 term_sig = WTERMSIG(status)
1909 status = 128 + term_sig
1910
1911 # Print newline after Ctrl-C.
1912 if term_sig == SIGINT:
1913 print('')
1914
1915 proc.WhenDone(pid, status)
1916
1917 elif WIFEXITED(status):
1918 status = WEXITSTATUS(status)
1919 #log('exit status: %s', status)
1920 proc.WhenDone(pid, status)
1921
1922 elif WIFSTOPPED(status):
1923 #status = WEXITSTATUS(status)
1924 stop_sig = WSTOPSIG(status)
1925
1926 print_stderr('')
1927 print_stderr('[PID %d] Stopped with signal %d' % (pid, stop_sig))
1928 proc.WhenStopped(stop_sig)
1929
1930 else:
1931 raise AssertionError(status)
1932
1933 self.last_status = status # for wait -n
1934 self.tracer.OnProcessEnd(pid, status)
1935 return W1_OK
1936
1937 def PollNotifications(self):
1938 # type: () -> None
1939 """
1940 Process all pending state changes.
1941 """
1942 while self.WaitForOne(waitpid_options=WNOHANG) == W1_OK:
1943 continue