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

1954 lines, 945 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, probe, 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 probe('process', 'ExternalProgram_Exec', argv0_path)
682 self._Exec(argv0_path, cmd_val.argv, cmd_val.arg_locs[0], environ,
683 True)
684 assert False, "This line should never execute" # NO RETURN
685
686 def _Exec(self, argv0_path, argv, argv0_loc, environ, should_retry):
687 # type: (str, List[str], loc_t, Dict[str, str], bool) -> None
688 if len(self.hijack_shebang):
689 opened = True
690 try:
691 f = self.fd_state.Open(argv0_path)
692 except (IOError, OSError) as e:
693 opened = False
694
695 if opened:
696 with ctx_FileCloser(f):
697 # Test if the shebang looks like a shell. TODO: The file might be
698 # binary with no newlines, so read 80 bytes instead of readline().
699
700 #line = f.read(80) # type: ignore # TODO: fix this
701 line = f.readline()
702
703 if match.ShouldHijack(line):
704 h_argv = [self.hijack_shebang, argv0_path]
705 h_argv.extend(argv[1:])
706 argv = h_argv
707 argv0_path = self.hijack_shebang
708 self.debug_f.writeln('Hijacked: %s' % argv0_path)
709 else:
710 #self.debug_f.log('Not hijacking %s (%r)', argv, line)
711 pass
712
713 try:
714 posix.execve(argv0_path, argv, environ)
715 except (IOError, OSError) as e:
716 # Run with /bin/sh when ENOEXEC error (no shebang). All shells do this.
717 if e.errno == ENOEXEC and should_retry:
718 new_argv = ['/bin/sh', argv0_path]
719 new_argv.extend(argv[1:])
720 self._Exec('/bin/sh', new_argv, argv0_loc, environ, False)
721 # NO RETURN
722
723 # Would be nice: when the path is relative and ENOENT: print PWD and do
724 # spelling correction?
725
726 self.errfmt.Print_(
727 "Can't execute %r: %s" % (argv0_path, pyutil.strerror(e)),
728 argv0_loc)
729
730 # POSIX mentions 126 and 127 for two specific errors. The rest are
731 # unspecified.
732 #
733 # http://pubs.opengroup.org/onlinepubs/9699919799.2016edition/utilities/V3_chap02.html#tag_18_08_02
734 if e.errno == EACCES:
735 status = 126
736 elif e.errno == ENOENT:
737 # TODO: most shells print 'command not found', rather than strerror()
738 # == "No such file or directory". That's better because it's at the
739 # end of the path search, and we're never searching for a directory.
740 status = 127
741 else:
742 # dash uses 2, but we use that for parse errors. This seems to be
743 # consistent with mksh and zsh.
744 status = 127
745
746 posix._exit(status)
747 # NO RETURN
748
749
750class Thunk(object):
751 """Abstract base class for things runnable in another process."""
752
753 def __init__(self):
754 # type: () -> None
755 """Empty constructor for mycpp."""
756 pass
757
758 def Run(self):
759 # type: () -> None
760 """Returns a status code."""
761 raise NotImplementedError()
762
763 def UserString(self):
764 # type: () -> str
765 """Display for the 'jobs' list."""
766 raise NotImplementedError()
767
768 def __repr__(self):
769 # type: () -> str
770 return self.UserString()
771
772
773class ExternalThunk(Thunk):
774 """An external executable."""
775
776 def __init__(self, ext_prog, argv0_path, cmd_val, environ):
777 # type: (ExternalProgram, str, cmd_value.Argv, Dict[str, str]) -> None
778 self.ext_prog = ext_prog
779 self.argv0_path = argv0_path
780 self.cmd_val = cmd_val
781 self.environ = environ
782
783 def UserString(self):
784 # type: () -> str
785
786 # NOTE: This is the format the Tracer uses.
787 # bash displays sleep $n & (code)
788 # but OSH displays sleep 1 & (argv array)
789 # We could switch the former but I'm not sure it's necessary.
790 tmp = [j8_lite.MaybeShellEncode(a) for a in self.cmd_val.argv]
791 return '[process] %s' % ' '.join(tmp)
792
793 def Run(self):
794 # type: () -> None
795 """An ExternalThunk is run in parent for the exec builtin."""
796 self.ext_prog.Exec(self.argv0_path, self.cmd_val, self.environ)
797
798
799class SubProgramThunk(Thunk):
800 """A subprogram that can be executed in another process."""
801
802 def __init__(self,
803 cmd_ev,
804 node,
805 trap_state,
806 multi_trace,
807 inherit_errexit,
808 inherit_errtrace):
809 # type: (CommandEvaluator, command_t, trap_osh.TrapState, dev.MultiTracer, bool, bool) -> None
810 self.cmd_ev = cmd_ev
811 self.node = node
812 self.trap_state = trap_state
813 self.multi_trace = multi_trace
814 self.inherit_errexit = inherit_errexit # for bash errexit compatibility
815 self.inherit_errtrace = inherit_errtrace # for bash errtrace compatibility
816
817 def UserString(self):
818 # type: () -> str
819
820 # NOTE: These can be pieces of a pipeline, so they're arbitrary nodes.
821 # TODO: Extract SPIDS from node to display source? Note that
822 # CompoundStatus also has locations of each pipeline component; see
823 # Executor.RunPipeline()
824 thunk_str = ui.CommandType(self.node)
825 return '[subprog] %s' % thunk_str
826
827 def Run(self):
828 # type: () -> None
829 #self.errfmt.OneLineErrExit() # don't quote code in child processes
830 probe('process', 'SubProgramThunk_Run')
831
832 # TODO: break circular dep. Bit flags could go in ASDL or headers.
833 from osh import cmd_eval
834
835 # signal handlers aren't inherited
836 self.trap_state.ClearForSubProgram(self.inherit_errtrace)
837
838 # NOTE: may NOT return due to exec().
839 if not self.inherit_errexit:
840 self.cmd_ev.mutable_opts.DisableErrExit()
841 try:
842 # optimize to eliminate redundant subshells like ( echo hi ) | wc -l etc.
843 self.cmd_ev.ExecuteAndCatch(self.node, cmd_flags=cmd_eval.Optimize)
844 status = self.cmd_ev.LastStatus()
845 # NOTE: We ignore the is_fatal return value. The user should set -o
846 # errexit so failures in subprocesses cause failures in the parent.
847 except util.UserExit as e:
848 status = e.status
849
850 # Handle errors in a subshell. These two cases are repeated from main()
851 # and the core/completion.py hook.
852 except KeyboardInterrupt:
853 print('')
854 status = 130 # 128 + 2
855 except (IOError, OSError) as e:
856 print_stderr('oils I/O error (subprogram): %s' %
857 pyutil.strerror(e))
858 status = 2
859
860 # If ProcessInit() doesn't turn off buffering, this is needed before
861 # _exit()
862 pyos.FlushStdout()
863
864 self.multi_trace.WriteDumps()
865
866 # We do NOT want to raise SystemExit here. Otherwise dev.Tracer::Pop()
867 # gets called in BOTH processes.
868 # The crash dump seems to be unaffected.
869 posix._exit(status)
870
871
872class _HereDocWriterThunk(Thunk):
873 """Write a here doc to one end of a pipe.
874
875 May be be executed in either a child process or the main shell
876 process.
877 """
878
879 def __init__(self, w, body_str):
880 # type: (int, str) -> None
881 self.w = w
882 self.body_str = body_str
883
884 def UserString(self):
885 # type: () -> str
886
887 # You can hit Ctrl-Z and the here doc writer will be suspended! Other
888 # shells don't have this problem because they use temp files! That's a bit
889 # unfortunate.
890 return '[here doc writer]'
891
892 def Run(self):
893 # type: () -> None
894 """do_exit: For small pipelines."""
895 probe('process', 'HereDocWriterThunk_Run')
896 #log('Writing %r', self.body_str)
897 posix.write(self.w, self.body_str)
898 #log('Wrote %r', self.body_str)
899 posix.close(self.w)
900 #log('Closed %d', self.w)
901
902 posix._exit(0)
903
904
905class Job(object):
906 """Interface for both Process and Pipeline.
907
908 They both can be put in the background and waited on.
909
910 Confusing thing about pipelines in the background: They have TOO MANY NAMES.
911
912 sleep 1 | sleep 2 &
913
914 - The LAST PID is what's printed at the prompt. This is $!, a PROCESS ID and
915 not a JOB ID.
916 # https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html#Special-Parameters
917 - The process group leader (setpgid) is the FIRST PID.
918 - It's also %1 or %+. The last job started.
919 """
920
921 def __init__(self):
922 # type: () -> None
923 # Initial state with & or Ctrl-Z is Running.
924 self.state = job_state_e.Running
925 self.job_id = -1
926 self.in_background = False
927
928 def DisplayJob(self, job_id, f, style):
929 # type: (int, mylib.Writer, int) -> None
930 raise NotImplementedError()
931
932 def State(self):
933 # type: () -> job_state_t
934 return self.state
935
936 def ProcessGroupId(self):
937 # type: () -> int
938 """Return the process group ID associated with this job."""
939 raise NotImplementedError()
940
941 def JobWait(self, waiter):
942 # type: (Waiter) -> wait_status_t
943 """Wait for this process/pipeline to be stopped or finished."""
944 raise NotImplementedError()
945
946 def SetBackground(self):
947 # type: () -> None
948 """Record that this job is running in the background."""
949 self.in_background = True
950
951 def SetForeground(self):
952 # type: () -> None
953 """Record that this job is running in the foreground."""
954 self.in_background = False
955
956
957class Process(Job):
958 """A process to run.
959
960 TODO: Should we make it clear that this is a FOREGROUND process? A
961 background process is wrapped in a "job". It is unevaluated.
962
963 It provides an API to manipulate file descriptor state in parent and child.
964 """
965
966 def __init__(self, thunk, job_control, job_list, tracer):
967 # type: (Thunk, JobControl, JobList, dev.Tracer) -> None
968 """
969 Args:
970 thunk: Thunk instance
971 job_list: for process bookkeeping
972 """
973 Job.__init__(self)
974 assert isinstance(thunk, Thunk), thunk
975 self.thunk = thunk
976 self.job_control = job_control
977 self.job_list = job_list
978 self.tracer = tracer
979
980 # For pipelines
981 self.parent_pipeline = None # type: Pipeline
982 self.state_changes = [] # type: List[ChildStateChange]
983 self.close_r = -1
984 self.close_w = -1
985
986 self.pid = -1
987 self.status = -1
988
989 def Init_ParentPipeline(self, pi):
990 # type: (Pipeline) -> None
991 """For updating PIPESTATUS."""
992 self.parent_pipeline = pi
993
994 def __repr__(self):
995 # type: () -> str
996
997 # note: be wary of infinite mutual recursion
998 #s = ' %s' % self.parent_pipeline if self.parent_pipeline else ''
999 #return '<Process %s%s>' % (self.thunk, s)
1000 return '<Process %s %s>' % (_JobStateStr(self.state), self.thunk)
1001
1002 def ProcessGroupId(self):
1003 # type: () -> int
1004 """Returns the group ID of this process."""
1005 # This should only ever be called AFTER the process has started
1006 assert self.pid != -1
1007 if self.parent_pipeline:
1008 # XXX: Maybe we should die here instead? Unclear if this branch
1009 # should even be reachable with the current builtins.
1010 return self.parent_pipeline.ProcessGroupId()
1011
1012 return self.pid
1013
1014 def DisplayJob(self, job_id, f, style):
1015 # type: (int, mylib.Writer, int) -> None
1016 if job_id == -1:
1017 job_id_str = ' '
1018 else:
1019 job_id_str = '%%%d' % job_id
1020 if style == STYLE_PID_ONLY:
1021 f.write('%d\n' % self.pid)
1022 else:
1023 f.write('%s %d %7s ' %
1024 (job_id_str, self.pid, _JobStateStr(self.state)))
1025 f.write(self.thunk.UserString())
1026 f.write('\n')
1027
1028 def AddStateChange(self, s):
1029 # type: (ChildStateChange) -> None
1030 self.state_changes.append(s)
1031
1032 def AddPipeToClose(self, r, w):
1033 # type: (int, int) -> None
1034 self.close_r = r
1035 self.close_w = w
1036
1037 def MaybeClosePipe(self):
1038 # type: () -> None
1039 if self.close_r != -1:
1040 posix.close(self.close_r)
1041 posix.close(self.close_w)
1042
1043 def StartProcess(self, why):
1044 # type: (trace_t) -> int
1045 """Start this process with fork(), handling redirects."""
1046 pid = posix.fork()
1047 if pid < 0:
1048 # When does this happen?
1049 e_die('Fatal error in posix.fork()')
1050
1051 elif pid == 0: # child
1052 # Note: this happens in BOTH interactive and non-interactive shells.
1053 # We technically don't need to do most of it in non-interactive, since we
1054 # did not change state in InitInteractiveShell().
1055
1056 for st in self.state_changes:
1057 st.Apply()
1058
1059 # Python sets SIGPIPE handler to SIG_IGN by default. Child processes
1060 # shouldn't have this.
1061 # https://docs.python.org/2/library/signal.html
1062 # See Python/pythonrun.c.
1063 pyos.Sigaction(SIGPIPE, SIG_DFL)
1064
1065 # Respond to Ctrl-\ (core dump)
1066 pyos.Sigaction(SIGQUIT, SIG_DFL)
1067
1068 # Only standalone children should get Ctrl-Z. Pipelines remain in the
1069 # foreground because suspending them is difficult with our 'lastpipe'
1070 # semantics.
1071 pid = posix.getpid()
1072 if posix.getpgid(0) == pid and self.parent_pipeline is None:
1073 pyos.Sigaction(SIGTSTP, SIG_DFL)
1074
1075 # More signals from
1076 # https://www.gnu.org/software/libc/manual/html_node/Launching-Jobs.html
1077 # (but not SIGCHLD)
1078 pyos.Sigaction(SIGTTOU, SIG_DFL)
1079 pyos.Sigaction(SIGTTIN, SIG_DFL)
1080
1081 self.tracer.OnNewProcess(pid)
1082 # clear foreground pipeline for subshells
1083 self.thunk.Run()
1084 # Never returns
1085
1086 #log('STARTED process %s, pid = %d', self, pid)
1087 self.tracer.OnProcessStart(pid, why)
1088
1089 # Class invariant: after the process is started, it stores its PID.
1090 self.pid = pid
1091
1092 # SetPgid needs to be applied from the child and the parent to avoid
1093 # racing in calls to tcsetpgrp() in the parent. See APUE sec. 9.2.
1094 for st in self.state_changes:
1095 st.ApplyFromParent(self)
1096
1097 # Program invariant: We keep track of every child process!
1098 self.job_list.AddChildProcess(pid, self)
1099
1100 return pid
1101
1102 def Wait(self, waiter):
1103 # type: (Waiter) -> int
1104 """Wait for this process to finish."""
1105 while self.state == job_state_e.Running:
1106 # Only return if there's nothing to wait for. Keep waiting if we were
1107 # interrupted with a signal.
1108 if waiter.WaitForOne() == W1_ECHILD:
1109 break
1110
1111 assert self.status >= 0, self.status
1112 return self.status
1113
1114 def JobWait(self, waiter):
1115 # type: (Waiter) -> wait_status_t
1116 # wait builtin can be interrupted
1117 while self.state == job_state_e.Running:
1118 result = waiter.WaitForOne()
1119
1120 if result >= 0: # signal
1121 return wait_status.Cancelled(result)
1122
1123 if result == W1_ECHILD:
1124 break
1125
1126 return wait_status.Proc(self.status)
1127
1128 def WhenStopped(self, stop_sig):
1129 # type: (int) -> None
1130
1131 # 128 is a shell thing
1132 # https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html
1133 self.status = 128 + stop_sig
1134 self.state = job_state_e.Stopped
1135
1136 if self.job_id == -1:
1137 # This process was started in the foreground
1138 self.job_list.AddJob(self)
1139
1140 if not self.in_background:
1141 self.job_control.MaybeTakeTerminal()
1142 self.SetBackground()
1143
1144 def WhenDone(self, pid, status):
1145 # type: (int, int) -> None
1146 """Called by the Waiter when this Process finishes."""
1147
1148 #log('WhenDone %d %d', pid, status)
1149 assert pid == self.pid, 'Expected %d, got %d' % (self.pid, pid)
1150 self.status = status
1151 self.state = job_state_e.Done
1152 if self.parent_pipeline:
1153 self.parent_pipeline.WhenDone(pid, status)
1154 else:
1155 if self.job_id != -1:
1156 # Job might have been brought to the foreground after being
1157 # assigned a job ID.
1158 if self.in_background:
1159 print_stderr('[%d] Done PID %d' % (self.job_id, self.pid))
1160
1161 self.job_list.RemoveJob(self.job_id)
1162
1163 self.job_list.RemoveChildProcess(self.pid)
1164
1165 if not self.in_background:
1166 self.job_control.MaybeTakeTerminal()
1167
1168 def RunProcess(self, waiter, why):
1169 # type: (Waiter, trace_t) -> int
1170 """Run this process synchronously."""
1171 self.StartProcess(why)
1172 # ShellExecutor might be calling this for the last part of a pipeline.
1173 if self.parent_pipeline is None:
1174 # QUESTION: Can the PGID of a single process just be the PID? i.e. avoid
1175 # calling getpgid()?
1176 self.job_control.MaybeGiveTerminal(posix.getpgid(self.pid))
1177 return self.Wait(waiter)
1178
1179
1180class ctx_Pipe(object):
1181
1182 def __init__(self, fd_state, fd, err_out):
1183 # type: (FdState, int, List[error.IOError_OSError]) -> None
1184 fd_state.PushStdinFromPipe(fd)
1185 self.fd_state = fd_state
1186 self.err_out = err_out
1187
1188 def __enter__(self):
1189 # type: () -> None
1190 pass
1191
1192 def __exit__(self, type, value, traceback):
1193 # type: (Any, Any, Any) -> None
1194 self.fd_state.Pop(self.err_out)
1195
1196
1197class Pipeline(Job):
1198 """A pipeline of processes to run.
1199
1200 Cases we handle:
1201
1202 foo | bar
1203 $(foo | bar)
1204 foo | bar | read v
1205 """
1206
1207 def __init__(self, sigpipe_status_ok, job_control, job_list, tracer):
1208 # type: (bool, JobControl, JobList, dev.Tracer) -> None
1209 Job.__init__(self)
1210 self.job_control = job_control
1211 self.job_list = job_list
1212 self.tracer = tracer
1213
1214 self.procs = [] # type: List[Process]
1215 self.pids = [] # type: List[int] # pids in order
1216 self.pipe_status = [] # type: List[int] # status in order
1217 self.status = -1 # for 'wait' jobs
1218
1219 self.pgid = INVALID_PGID
1220
1221 # Optional for foreground
1222 self.last_thunk = None # type: Tuple[CommandEvaluator, command_t]
1223 self.last_pipe = None # type: Tuple[int, int]
1224
1225 self.sigpipe_status_ok = sigpipe_status_ok
1226
1227 def ProcessGroupId(self):
1228 # type: () -> int
1229 """Returns the group ID of this pipeline."""
1230 return self.pgid
1231
1232 def DisplayJob(self, job_id, f, style):
1233 # type: (int, mylib.Writer, int) -> None
1234 if style == STYLE_PID_ONLY:
1235 f.write('%d\n' % self.procs[0].pid)
1236 else:
1237 # Note: this is STYLE_LONG.
1238 for i, proc in enumerate(self.procs):
1239 if i == 0: # show job ID for first element in pipeline
1240 job_id_str = '%%%d' % job_id
1241 else:
1242 job_id_str = ' ' # 2 spaces
1243
1244 f.write('%s %d %7s ' %
1245 (job_id_str, proc.pid, _JobStateStr(proc.state)))
1246 f.write(proc.thunk.UserString())
1247 f.write('\n')
1248
1249 def DebugPrint(self):
1250 # type: () -> None
1251 print('Pipeline in state %s' % _JobStateStr(self.state))
1252 if mylib.PYTHON: # %s for Process not allowed in C++
1253 for proc in self.procs:
1254 print(' proc %s' % proc)
1255 _, last_node = self.last_thunk
1256 print(' last %s' % last_node)
1257 print(' pipe_status %s' % self.pipe_status)
1258
1259 def Add(self, p):
1260 # type: (Process) -> None
1261 """Append a process to the pipeline."""
1262 if len(self.procs) == 0:
1263 self.procs.append(p)
1264 return
1265
1266 r, w = posix.pipe()
1267 #log('pipe for %s: %d %d', p, r, w)
1268 prev = self.procs[-1]
1269
1270 prev.AddStateChange(StdoutToPipe(r, w)) # applied on StartPipeline()
1271 p.AddStateChange(StdinFromPipe(r, w)) # applied on StartPipeline()
1272
1273 p.AddPipeToClose(r, w) # MaybeClosePipe() on StartPipeline()
1274
1275 self.procs.append(p)
1276
1277 def AddLast(self, thunk):
1278 # type: (Tuple[CommandEvaluator, command_t]) -> None
1279 """Append the last noden to the pipeline.
1280
1281 This is run in the CURRENT process. It is OPTIONAL, because
1282 pipelines in the background are run uniformly.
1283 """
1284 self.last_thunk = thunk
1285
1286 assert len(self.procs) != 0
1287
1288 r, w = posix.pipe()
1289 prev = self.procs[-1]
1290 prev.AddStateChange(StdoutToPipe(r, w))
1291
1292 self.last_pipe = (r, w) # So we can connect it to last_thunk
1293
1294 def StartPipeline(self, waiter):
1295 # type: (Waiter) -> None
1296
1297 # If we are creating a pipeline in a subshell or we aren't running with job
1298 # control, our children should remain in our inherited process group.
1299 # the pipelines's group ID.
1300 if self.job_control.Enabled():
1301 self.pgid = OWN_LEADER # first process in pipeline is the leader
1302
1303 for i, proc in enumerate(self.procs):
1304 if self.pgid != INVALID_PGID:
1305 proc.AddStateChange(SetPgid(self.pgid, self.tracer))
1306
1307 # Figure out the pid
1308 pid = proc.StartProcess(trace.PipelinePart)
1309 if i == 0 and self.pgid != INVALID_PGID:
1310 # Mimic bash and use the PID of the FIRST process as the group for the
1311 # whole pipeline.
1312 self.pgid = pid
1313
1314 self.pids.append(pid)
1315 self.pipe_status.append(-1) # uninitialized
1316
1317 # NOTE: This is done in the SHELL PROCESS after every fork() call.
1318 # It can't be done at the end; otherwise processes will have descriptors
1319 # from non-adjacent pipes.
1320 proc.MaybeClosePipe()
1321
1322 if self.last_thunk:
1323 self.pipe_status.append(-1) # for self.last_thunk
1324
1325 def LastPid(self):
1326 # type: () -> int
1327 """For the odd $! variable.
1328
1329 It would be better if job IDs or PGIDs were used consistently.
1330 """
1331 return self.pids[-1]
1332
1333 def Wait(self, waiter):
1334 # type: (Waiter) -> List[int]
1335 """Wait for this pipeline to finish."""
1336
1337 assert self.procs, "no procs for Wait()"
1338 # waitpid(-1) zero or more times
1339 while self.state == job_state_e.Running:
1340 # Keep waiting until there's nothing to wait for.
1341 if waiter.WaitForOne() == W1_ECHILD:
1342 break
1343
1344 return self.pipe_status
1345
1346 def JobWait(self, waiter):
1347 # type: (Waiter) -> wait_status_t
1348 """Called by 'wait' builtin, e.g. 'wait %1'."""
1349 # wait builtin can be interrupted
1350 assert self.procs, "no procs for Wait()"
1351 while self.state == job_state_e.Running:
1352 result = waiter.WaitForOne()
1353
1354 if result >= 0: # signal
1355 return wait_status.Cancelled(result)
1356
1357 if result == W1_ECHILD:
1358 break
1359
1360 return wait_status.Pipeline(self.pipe_status)
1361
1362 def RunLastPart(self, waiter, fd_state):
1363 # type: (Waiter, FdState) -> List[int]
1364 """Run this pipeline synchronously (foreground pipeline).
1365
1366 Returns:
1367 pipe_status (list of integers).
1368 """
1369 assert len(self.pids) == len(self.procs)
1370
1371 # TODO: break circular dep. Bit flags could go in ASDL or headers.
1372 from osh import cmd_eval
1373
1374 # This is tcsetpgrp()
1375 # TODO: fix race condition -- I believe the first process could have
1376 # stopped already, and thus getpgid() will fail
1377 self.job_control.MaybeGiveTerminal(self.pgid)
1378
1379 # Run the last part of the pipeline IN PARALLEL with other processes. It
1380 # may or may not fork:
1381 # echo foo | read line # no fork, the builtin runs in THIS shell process
1382 # ls | wc -l # fork for 'wc'
1383
1384 cmd_ev, last_node = self.last_thunk
1385
1386 assert self.last_pipe is not None
1387 r, w = self.last_pipe # set in AddLast()
1388 posix.close(w) # we will not write here
1389
1390 # Fix lastpipe / job control / DEBUG trap interaction
1391 cmd_flags = cmd_eval.NoDebugTrap if self.job_control.Enabled() else 0
1392
1393 # The ERR trap only runs for the WHOLE pipeline, not the COMPONENTS in
1394 # a pipeline.
1395 cmd_flags |= cmd_eval.NoErrTrap
1396
1397 io_errors = [] # type: List[error.IOError_OSError]
1398 with ctx_Pipe(fd_state, r, io_errors):
1399 cmd_ev.ExecuteAndCatch(last_node, cmd_flags)
1400
1401 if len(io_errors):
1402 e_die('Error setting up last part of pipeline: %s' %
1403 pyutil.strerror(io_errors[0]))
1404
1405 # We won't read anymore. If we don't do this, then 'cat' in 'cat
1406 # /dev/urandom | sleep 1' will never get SIGPIPE.
1407 posix.close(r)
1408
1409 self.pipe_status[-1] = cmd_ev.LastStatus()
1410 if self.AllDone():
1411 self.state = job_state_e.Done
1412
1413 #log('pipestatus before all have finished = %s', self.pipe_status)
1414 return self.Wait(waiter)
1415
1416 def AllDone(self):
1417 # type: () -> bool
1418
1419 # mycpp rewrite: all(status != -1 for status in self.pipe_status)
1420 for status in self.pipe_status:
1421 if status == -1:
1422 return False
1423 return True
1424
1425 def WhenDone(self, pid, status):
1426 # type: (int, int) -> None
1427 """Called by Process.WhenDone."""
1428 #log('Pipeline WhenDone %d %d', pid, status)
1429 i = self.pids.index(pid)
1430 assert i != -1, 'Unexpected PID %d' % pid
1431
1432 if status == 141 and self.sigpipe_status_ok:
1433 status = 0
1434
1435 self.job_list.RemoveChildProcess(pid)
1436 self.pipe_status[i] = status
1437 if self.AllDone():
1438 if self.job_id != -1:
1439 # Job might have been brought to the foreground after being
1440 # assigned a job ID.
1441 if self.in_background:
1442 print_stderr('[%d] Done PGID %d' %
1443 (self.job_id, self.pids[0]))
1444
1445 self.job_list.RemoveJob(self.job_id)
1446
1447 # status of pipeline is status of last process
1448 self.status = self.pipe_status[-1]
1449 self.state = job_state_e.Done
1450 if not self.in_background:
1451 self.job_control.MaybeTakeTerminal()
1452
1453
1454def _JobStateStr(i):
1455 # type: (job_state_t) -> str
1456 return job_state_str(i)[10:] # remove 'job_state.'
1457
1458
1459def _GetTtyFd():
1460 # type: () -> int
1461 """Returns -1 if stdio is not a TTY."""
1462 try:
1463 return posix.open("/dev/tty", O_NONBLOCK | O_NOCTTY | O_RDWR, 0o666)
1464 except (IOError, OSError) as e:
1465 return -1
1466
1467
1468class ctx_TerminalControl(object):
1469
1470 def __init__(self, job_control, errfmt):
1471 # type: (JobControl, ui.ErrorFormatter) -> None
1472 job_control.InitJobControl()
1473 self.job_control = job_control
1474 self.errfmt = errfmt
1475
1476 def __enter__(self):
1477 # type: () -> None
1478 pass
1479
1480 def __exit__(self, type, value, traceback):
1481 # type: (Any, Any, Any) -> None
1482
1483 # Return the TTY to the original owner before exiting.
1484 try:
1485 self.job_control.MaybeReturnTerminal()
1486 except error.FatalRuntime as e:
1487 # Don't abort the shell on error, just print a message.
1488 self.errfmt.PrettyPrintError(e)
1489
1490
1491class JobControl(object):
1492 """Interface to setpgid(), tcsetpgrp(), etc."""
1493
1494 def __init__(self):
1495 # type: () -> None
1496
1497 # The main shell's PID and group ID.
1498 self.shell_pid = -1
1499 self.shell_pgid = -1
1500
1501 # The fd of the controlling tty. Set to -1 when job control is disabled.
1502 self.shell_tty_fd = -1
1503
1504 # For giving the terminal back to our parent before exiting (if not a login
1505 # shell).
1506 self.original_tty_pgid = -1
1507
1508 def InitJobControl(self):
1509 # type: () -> None
1510 self.shell_pid = posix.getpid()
1511 orig_shell_pgid = posix.getpgid(0)
1512 self.shell_pgid = orig_shell_pgid
1513 self.shell_tty_fd = _GetTtyFd()
1514
1515 # If we aren't the leader of our process group, create a group and mark
1516 # ourselves as the leader.
1517 if self.shell_pgid != self.shell_pid:
1518 try:
1519 posix.setpgid(self.shell_pid, self.shell_pid)
1520 self.shell_pgid = self.shell_pid
1521 except (IOError, OSError) as e:
1522 self.shell_tty_fd = -1
1523
1524 if self.shell_tty_fd != -1:
1525 self.original_tty_pgid = posix.tcgetpgrp(self.shell_tty_fd)
1526
1527 # If stdio is a TTY, put the shell's process group in the foreground.
1528 try:
1529 posix.tcsetpgrp(self.shell_tty_fd, self.shell_pgid)
1530 except (IOError, OSError) as e:
1531 # We probably aren't in the session leader's process group. Disable job
1532 # control.
1533 self.shell_tty_fd = -1
1534 self.shell_pgid = orig_shell_pgid
1535 posix.setpgid(self.shell_pid, self.shell_pgid)
1536
1537 def Enabled(self):
1538 # type: () -> bool
1539
1540 # TODO: get rid of this syscall? SubProgramThunk should set a flag I
1541 # think.
1542 curr_pid = posix.getpid()
1543 # Only the main shell should bother with job control functions.
1544 return curr_pid == self.shell_pid and self.shell_tty_fd != -1
1545
1546 # TODO: This isn't a PID. This is a process group ID?
1547 #
1548 # What should the table look like?
1549 #
1550 # Do we need the last PID? I don't know why bash prints that. Probably so
1551 # you can do wait $!
1552 # wait -n waits for any node to go from job_state_e.Running to job_state_e.Done?
1553 #
1554 # And it needs a flag for CURRENT, for the implicit arg to 'fg'.
1555 # job_id is just an integer. This is sort of lame.
1556 #
1557 # [job_id, flag, pgid, job_state, node]
1558
1559 def MaybeGiveTerminal(self, pgid):
1560 # type: (int) -> None
1561 """If stdio is a TTY, move the given process group to the
1562 foreground."""
1563 if not self.Enabled():
1564 # Only call tcsetpgrp when job control is enabled.
1565 return
1566
1567 try:
1568 posix.tcsetpgrp(self.shell_tty_fd, pgid)
1569 except (IOError, OSError) as e:
1570 e_die('osh: Failed to move process group %d to foreground: %s' %
1571 (pgid, pyutil.strerror(e)))
1572
1573 def MaybeTakeTerminal(self):
1574 # type: () -> None
1575 """If stdio is a TTY, return the main shell's process group to the
1576 foreground."""
1577 self.MaybeGiveTerminal(self.shell_pgid)
1578
1579 def MaybeReturnTerminal(self):
1580 # type: () -> None
1581 """Called before the shell exits."""
1582 self.MaybeGiveTerminal(self.original_tty_pgid)
1583
1584
1585class JobList(object):
1586 """Global list of jobs, used by a few builtins."""
1587
1588 def __init__(self):
1589 # type: () -> None
1590
1591 # job_id -> Job instance
1592 self.jobs = {} # type: Dict[int, Job]
1593
1594 # pid -> Process. This is for STOP notification.
1595 self.child_procs = {} # type: Dict[int, Process]
1596 self.debug_pipelines = [] # type: List[Pipeline]
1597
1598 # Counter used to assign IDs to jobs. It is incremented every time a job
1599 # is created. Once all active jobs are done it is reset to 1. I'm not
1600 # sure if this reset behavior is mandated by POSIX, but other shells do
1601 # it, so we mimic for the sake of compatibility.
1602 self.job_id = 1
1603
1604 def AddJob(self, job):
1605 # type: (Job) -> int
1606 """Add a background job to the list.
1607
1608 A job is either a Process or Pipeline. You can resume a job with 'fg',
1609 kill it with 'kill', etc.
1610
1611 Two cases:
1612
1613 1. async jobs: sleep 5 | sleep 4 &
1614 2. stopped jobs: sleep 5; then Ctrl-Z
1615 """
1616 job_id = self.job_id
1617 self.jobs[job_id] = job
1618 job.job_id = job_id
1619 self.job_id += 1
1620 return job_id
1621
1622 def RemoveJob(self, job_id):
1623 # type: (int) -> None
1624 """Process and Pipeline can call this."""
1625 mylib.dict_erase(self.jobs, job_id)
1626
1627 if len(self.jobs) == 0:
1628 self.job_id = 1
1629
1630 def AddChildProcess(self, pid, proc):
1631 # type: (int, Process) -> None
1632 """Every child process should be added here as soon as we know its PID.
1633
1634 When the Waiter gets an EXITED or STOPPED notification, we need
1635 to know about it so 'jobs' can work.
1636 """
1637 self.child_procs[pid] = proc
1638
1639 def RemoveChildProcess(self, pid):
1640 # type: (int) -> None
1641 """Remove the child process with the given PID."""
1642 mylib.dict_erase(self.child_procs, pid)
1643
1644 if mylib.PYTHON:
1645
1646 def AddPipeline(self, pi):
1647 # type: (Pipeline) -> None
1648 """For debugging only."""
1649 self.debug_pipelines.append(pi)
1650
1651 def ProcessFromPid(self, pid):
1652 # type: (int) -> Process
1653 """For wait $PID.
1654
1655 There's no way to wait for a pipeline with a PID. That uses job
1656 syntax, e.g. %1. Not a great interface.
1657 """
1658 return self.child_procs.get(pid)
1659
1660 def GetCurrentAndPreviousJobs(self):
1661 # type: () -> Tuple[Optional[Job], Optional[Job]]
1662 """Return the "current" and "previous" jobs (AKA `%+` and `%-`).
1663
1664 See the POSIX specification for the `jobs` builtin for details:
1665 https://pubs.opengroup.org/onlinepubs/007904875/utilities/jobs.html
1666
1667 IMPORTANT NOTE: This method assumes that the jobs list will not change
1668 during its execution! This assumption holds for now because we only ever
1669 update the jobs list from the main loop after WaitPid() informs us of a
1670 change. If we implement `set -b` and install a signal handler for
1671 SIGCHLD we should be careful to synchronize it with this function. The
1672 unsafety of mutating GC data structures from a signal handler should
1673 make this a non-issue, but if bugs related to this appear this note may
1674 be helpful...
1675 """
1676 # Split all active jobs by state and sort each group by decreasing job
1677 # ID to approximate newness.
1678 stopped_jobs = [] # type: List[Job]
1679 running_jobs = [] # type: List[Job]
1680 for i in xrange(0, self.job_id):
1681 job = self.jobs.get(i, None)
1682 if not job:
1683 continue
1684
1685 if job.state == job_state_e.Stopped:
1686 stopped_jobs.append(job)
1687
1688 elif job.state == job_state_e.Running:
1689 running_jobs.append(job)
1690
1691 current = None # type: Optional[Job]
1692 previous = None # type: Optional[Job]
1693 # POSIX says: If there is any suspended job, then the current job shall
1694 # be a suspended job. If there are at least two suspended jobs, then the
1695 # previous job also shall be a suspended job.
1696 #
1697 # So, we will only return running jobs from here if there are no recent
1698 # stopped jobs.
1699 if len(stopped_jobs) > 0:
1700 current = stopped_jobs.pop()
1701
1702 if len(stopped_jobs) > 0:
1703 previous = stopped_jobs.pop()
1704
1705 if len(running_jobs) > 0 and not current:
1706 current = running_jobs.pop()
1707
1708 if len(running_jobs) > 0 and not previous:
1709 previous = running_jobs.pop()
1710
1711 if not previous:
1712 previous = current
1713
1714 return current, previous
1715
1716 def GetJobWithSpec(self, job_spec):
1717 # type: (str) -> Optional[Job]
1718 """Parse the given job spec and return the matching job. If there is no
1719 matching job, this function returns None.
1720
1721 See the POSIX spec for the `jobs` builtin for details about job specs:
1722 https://pubs.opengroup.org/onlinepubs/007904875/utilities/jobs.html
1723 """
1724 if job_spec in CURRENT_JOB_SPECS:
1725 current, _ = self.GetCurrentAndPreviousJobs()
1726 return current
1727
1728 if job_spec == '%-':
1729 _, previous = self.GetCurrentAndPreviousJobs()
1730 return previous
1731
1732 # TODO: Add support for job specs based on prefixes of process argv.
1733 m = util.RegexSearch(r'^%([0-9]+)$', job_spec)
1734 if m is not None:
1735 assert len(m) == 2
1736 job_id = int(m[1])
1737 if job_id in self.jobs:
1738 return self.jobs[job_id]
1739
1740 return None
1741
1742 def DisplayJobs(self, style):
1743 # type: (int) -> None
1744 """Used by the 'jobs' builtin.
1745
1746 https://pubs.opengroup.org/onlinepubs/9699919799/utilities/jobs.html
1747
1748 "By default, the jobs utility shall display the status of all stopped jobs,
1749 running background jobs and all jobs whose status has changed and have not
1750 been reported by the shell."
1751 """
1752 # NOTE: A job is a background process or pipeline.
1753 #
1754 # echo hi | wc -l -- this starts two processes. Wait for TWO
1755 # echo hi | wc -l & -- this starts a process which starts two processes
1756 # Wait for ONE.
1757 #
1758 # 'jobs -l' GROUPS the PIDs by job. It has the job number, + - indicators
1759 # for %% and %-, PID, status, and "command".
1760 #
1761 # Every component of a pipeline is on the same line with 'jobs', but
1762 # they're separated into different lines with 'jobs -l'.
1763 #
1764 # See demo/jobs-builtin.sh
1765
1766 # $ jobs -l
1767 # [1]+ 24414 Stopped sleep 5
1768 # 24415 | sleep 5
1769 # [2] 24502 Running sleep 6
1770 # 24503 | sleep 6
1771 # 24504 | sleep 5 &
1772 # [3]- 24508 Running sleep 6
1773 # 24509 | sleep 6
1774 # 24510 | sleep 5 &
1775
1776 f = mylib.Stdout()
1777 for job_id, job in iteritems(self.jobs):
1778 # Use the %1 syntax
1779 job.DisplayJob(job_id, f, style)
1780
1781 def DebugPrint(self):
1782 # type: () -> None
1783
1784 f = mylib.Stdout()
1785 f.write('\n')
1786 f.write('[process debug info]\n')
1787
1788 for pid, proc in iteritems(self.child_procs):
1789 proc.DisplayJob(-1, f, STYLE_DEFAULT)
1790 #p = ' |' if proc.parent_pipeline else ''
1791 #print('%d %7s %s%s' % (pid, _JobStateStr(proc.state), proc.thunk.UserString(), p))
1792
1793 if len(self.debug_pipelines):
1794 f.write('\n')
1795 f.write('[pipeline debug info]\n')
1796 for pi in self.debug_pipelines:
1797 pi.DebugPrint()
1798
1799 def ListRecent(self):
1800 # type: () -> None
1801 """For jobs -n, which I think is also used in the interactive
1802 prompt."""
1803 pass
1804
1805 def NumRunning(self):
1806 # type: () -> int
1807 """Return the number of running jobs.
1808
1809 Used by 'wait' and 'wait -n'.
1810 """
1811 count = 0
1812 for _, job in iteritems(self.jobs): # mycpp rewrite: from itervalues()
1813 if job.State() == job_state_e.Running:
1814 count += 1
1815 return count
1816
1817
1818# Some WaitForOne() return values
1819W1_OK = -2 # waitpid(-1) returned
1820W1_ECHILD = -3 # no processes to wait for
1821W1_AGAIN = -4 # WNOHANG was passed and there were no state changes
1822
1823
1824class Waiter(object):
1825 """A capability to wait for processes.
1826
1827 This must be a singleton (and is because CommandEvaluator is a singleton).
1828
1829 Invariants:
1830 - Every child process is registered once
1831 - Every child process is waited for
1832
1833 Canonical example of why we need a GLOBAL waiter:
1834
1835 { sleep 3; echo 'done 3'; } &
1836 { sleep 4; echo 'done 4'; } &
1837
1838 # ... do arbitrary stuff ...
1839
1840 { sleep 1; exit 1; } | { sleep 2; exit 2; }
1841
1842 Now when you do wait() after starting the pipeline, you might get a pipeline
1843 process OR a background process! So you have to distinguish between them.
1844 """
1845
1846 def __init__(self, job_list, exec_opts, signal_safe, tracer):
1847 # type: (JobList, optview.Exec, pyos.SignalSafe, dev.Tracer) -> None
1848 self.job_list = job_list
1849 self.exec_opts = exec_opts
1850 self.signal_safe = signal_safe
1851 self.tracer = tracer
1852 self.last_status = 127 # wait -n error code
1853
1854 def WaitForOne(self, waitpid_options=0):
1855 # type: (int) -> int
1856 """Wait until the next process returns (or maybe Ctrl-C).
1857
1858 Returns:
1859 One of these negative numbers:
1860 W1_ECHILD Nothing to wait for
1861 W1_OK Caller should keep waiting
1862 UNTRAPPED_SIGWINCH
1863 Or
1864 result > 0 Signal that waitpid() was interrupted with
1865
1866 In the interactive shell, we return 0 if we get a Ctrl-C, so the caller
1867 will try again.
1868
1869 Callers:
1870 wait -n -- loop until there is one fewer process (TODO)
1871 wait -- loop until there are no processes
1872 wait $! -- loop until job state is Done (process or pipeline)
1873 Process::Wait() -- loop until Process state is done
1874 Pipeline::Wait() -- loop until Pipeline state is done
1875
1876 Comparisons:
1877 bash: jobs.c waitchld() Has a special case macro(!) CHECK_WAIT_INTR for
1878 the wait builtin
1879
1880 dash: jobs.c waitproc() uses sigfillset(), sigprocmask(), etc. Runs in a
1881 loop while (gotsigchld), but that might be a hack for System V!
1882
1883 Should we have a cleaner API like named posix::wait_for_one() ?
1884
1885 wait_result =
1886 ECHILD -- nothing to wait for
1887 | Done(int pid, int status) -- process done
1888 | EINTR(bool sigint) -- may or may not retry
1889 """
1890 pid, status = pyos.WaitPid(waitpid_options)
1891 if pid == 0: # WNOHANG passed, and no state changes
1892 return W1_AGAIN
1893 elif pid < 0: # error case
1894 err_num = status
1895 #log('waitpid() error => %d %s', e.errno, pyutil.strerror(e))
1896 if err_num == ECHILD:
1897 return W1_ECHILD # nothing to wait for caller should stop
1898 elif err_num == EINTR: # Bug #858 fix
1899 #log('WaitForOne() => %d', self.trap_state.GetLastSignal())
1900 return self.signal_safe.LastSignal() # e.g. 1 for SIGHUP
1901 else:
1902 # The signature of waitpid() means this shouldn't happen
1903 raise AssertionError()
1904
1905 # All child processes are supposed to be in this dict. But this may
1906 # legitimately happen if a grandchild outlives the child (its parent).
1907 # Then it is reparented under this process, so we might receive
1908 # notification of its exit, even though we didn't start it. We can't have
1909 # any knowledge of such processes, so print a warning.
1910 if pid not in self.job_list.child_procs:
1911 print_stderr("osh: PID %d stopped, but osh didn't start it" % pid)
1912 return W1_OK
1913
1914 proc = self.job_list.child_procs[pid]
1915 if 0:
1916 self.job_list.DebugPrint()
1917
1918 if WIFSIGNALED(status):
1919 term_sig = WTERMSIG(status)
1920 status = 128 + term_sig
1921
1922 # Print newline after Ctrl-C.
1923 if term_sig == SIGINT:
1924 print('')
1925
1926 proc.WhenDone(pid, status)
1927
1928 elif WIFEXITED(status):
1929 status = WEXITSTATUS(status)
1930 #log('exit status: %s', status)
1931 proc.WhenDone(pid, status)
1932
1933 elif WIFSTOPPED(status):
1934 #status = WEXITSTATUS(status)
1935 stop_sig = WSTOPSIG(status)
1936
1937 print_stderr('')
1938 print_stderr('[PID %d] Stopped with signal %d' % (pid, stop_sig))
1939 proc.WhenStopped(stop_sig)
1940
1941 else:
1942 raise AssertionError(status)
1943
1944 self.last_status = status # for wait -n
1945 self.tracer.OnProcessEnd(pid, status)
1946 return W1_OK
1947
1948 def PollNotifications(self):
1949 # type: () -> None
1950 """
1951 Process all pending state changes.
1952 """
1953 while self.WaitForOne(waitpid_options=WNOHANG) == W1_OK:
1954 continue