| 1 | """executor.py."""
 | 
| 2 | from __future__ import print_function
 | 
| 3 | 
 | 
| 4 | from errno import EINTR
 | 
| 5 | 
 | 
| 6 | from _devbuild.gen.id_kind_asdl import Id
 | 
| 7 | from _devbuild.gen.option_asdl import builtin_i
 | 
| 8 | from _devbuild.gen.runtime_asdl import RedirValue, trace
 | 
| 9 | from _devbuild.gen.syntax_asdl import (
 | 
| 10 |     command,
 | 
| 11 |     command_e,
 | 
| 12 |     CommandSub,
 | 
| 13 |     CompoundWord,
 | 
| 14 |     loc,
 | 
| 15 |     loc_t,
 | 
| 16 | )
 | 
| 17 | from builtin import hay_ysh
 | 
| 18 | from core import dev
 | 
| 19 | from core import error
 | 
| 20 | from core import process
 | 
| 21 | from core.error import e_die, e_die_status
 | 
| 22 | from core import pyos
 | 
| 23 | from core import pyutil
 | 
| 24 | from core import state
 | 
| 25 | from core import ui
 | 
| 26 | from core import vm
 | 
| 27 | from frontend import consts
 | 
| 28 | from frontend import lexer
 | 
| 29 | from mycpp.mylib import log
 | 
| 30 | 
 | 
| 31 | import posix_ as posix
 | 
| 32 | 
 | 
| 33 | from typing import cast, Dict, List, Optional, TYPE_CHECKING
 | 
| 34 | if TYPE_CHECKING:
 | 
| 35 |     from _devbuild.gen.runtime_asdl import (cmd_value, CommandStatus,
 | 
| 36 |                                             StatusArray)
 | 
| 37 |     from _devbuild.gen.syntax_asdl import command_t
 | 
| 38 |     from builtin import trap_osh
 | 
| 39 |     from core import optview
 | 
| 40 |     from core import state
 | 
| 41 |     from core.vm import _Builtin
 | 
| 42 | 
 | 
| 43 | _ = log
 | 
| 44 | 
 | 
| 45 | 
 | 
| 46 | class _ProcessSubFrame(object):
 | 
| 47 |     """To keep track of diff <(cat 1) <(cat 2) > >(tac)"""
 | 
| 48 | 
 | 
| 49 |     def __init__(self):
 | 
| 50 |         # type: () -> None
 | 
| 51 | 
 | 
| 52 |         # These objects appear unconditionally in the main loop, and aren't
 | 
| 53 |         # commonly used, so we manually optimize [] into None.
 | 
| 54 | 
 | 
| 55 |         self._to_wait = []  # type: List[process.Process]
 | 
| 56 |         self._to_close = []  # type: List[int]  # file descriptors
 | 
| 57 |         self._locs = []  # type: List[loc_t]
 | 
| 58 |         self._modified = False
 | 
| 59 | 
 | 
| 60 |     def WasModified(self):
 | 
| 61 |         # type: () -> bool
 | 
| 62 |         return self._modified
 | 
| 63 | 
 | 
| 64 |     def Append(self, p, fd, status_loc):
 | 
| 65 |         # type: (process.Process, int, loc_t) -> None
 | 
| 66 |         self._modified = True
 | 
| 67 | 
 | 
| 68 |         self._to_wait.append(p)
 | 
| 69 |         self._to_close.append(fd)
 | 
| 70 |         self._locs.append(status_loc)
 | 
| 71 | 
 | 
| 72 |     def MaybeWaitOnProcessSubs(self, waiter, status_array):
 | 
| 73 |         # type: (process.Waiter, StatusArray) -> None
 | 
| 74 | 
 | 
| 75 |         # Wait in the same order that they were evaluated.  That seems fine.
 | 
| 76 |         for fd in self._to_close:
 | 
| 77 |             posix.close(fd)
 | 
| 78 | 
 | 
| 79 |         codes = []  # type: List[int]
 | 
| 80 |         locs = []  # type: List[loc_t]
 | 
| 81 |         for i, p in enumerate(self._to_wait):
 | 
| 82 |             #log('waiting for %s', p)
 | 
| 83 |             st = p.Wait(waiter)
 | 
| 84 |             codes.append(st)
 | 
| 85 |             locs.append(self._locs[i])
 | 
| 86 | 
 | 
| 87 |         status_array.codes = codes
 | 
| 88 |         status_array.locs = locs
 | 
| 89 | 
 | 
| 90 | 
 | 
| 91 | # Big flgas for RunSimpleCommand
 | 
| 92 | DO_FORK = 1 << 1
 | 
| 93 | NO_CALL_PROCS = 1 << 2  # command ls suppresses function lookup
 | 
| 94 | USE_DEFAULT_PATH = 1 << 3  # for command -p ls changes the path
 | 
| 95 | 
 | 
| 96 | # Copied from var.c in dash
 | 
| 97 | DEFAULT_PATH = [
 | 
| 98 |     '/usr/local/sbin', '/usr/local/bin', '/usr/sbin', '/usr/bin', '/sbin',
 | 
| 99 |     '/bin'
 | 
| 100 | ]
 | 
| 101 | 
 | 
| 102 | 
 | 
| 103 | class ShellExecutor(vm._Executor):
 | 
| 104 |     """An executor combined with the OSH language evaluators in osh/ to create
 | 
| 105 |     a shell interpreter."""
 | 
| 106 | 
 | 
| 107 |     def __init__(
 | 
| 108 |             self,
 | 
| 109 |             mem,  # type: state.Mem
 | 
| 110 |             exec_opts,  # type: optview.Exec
 | 
| 111 |             mutable_opts,  # type: state.MutableOpts
 | 
| 112 |             procs,  # type: state.Procs
 | 
| 113 |             hay_state,  # type: hay_ysh.HayState
 | 
| 114 |             builtins,  # type: Dict[int, _Builtin]
 | 
| 115 |             search_path,  # type: state.SearchPath
 | 
| 116 |             ext_prog,  # type: process.ExternalProgram
 | 
| 117 |             waiter,  # type: process.Waiter
 | 
| 118 |             tracer,  # type: dev.Tracer
 | 
| 119 |             job_control,  # type: process.JobControl
 | 
| 120 |             job_list,  # type: process.JobList
 | 
| 121 |             fd_state,  # type: process.FdState
 | 
| 122 |             trap_state,  # type: trap_osh.TrapState
 | 
| 123 |             errfmt  # type: ui.ErrorFormatter
 | 
| 124 |     ):
 | 
| 125 |         # type: (...) -> None
 | 
| 126 |         vm._Executor.__init__(self)
 | 
| 127 |         self.mem = mem
 | 
| 128 |         self.exec_opts = exec_opts
 | 
| 129 |         self.mutable_opts = mutable_opts  # for IsDisabled(), not mutating
 | 
| 130 |         self.procs = procs
 | 
| 131 |         self.hay_state = hay_state
 | 
| 132 |         self.builtins = builtins
 | 
| 133 |         self.search_path = search_path
 | 
| 134 |         self.ext_prog = ext_prog
 | 
| 135 |         self.waiter = waiter
 | 
| 136 |         self.tracer = tracer
 | 
| 137 |         self.multi_trace = tracer.multi_trace
 | 
| 138 |         self.job_control = job_control
 | 
| 139 |         # sleep 5 & puts a (PID, job#) entry here.  And then "jobs" displays it.
 | 
| 140 |         self.job_list = job_list
 | 
| 141 |         self.fd_state = fd_state
 | 
| 142 |         self.trap_state = trap_state
 | 
| 143 |         self.errfmt = errfmt
 | 
| 144 |         self.process_sub_stack = []  # type: List[_ProcessSubFrame]
 | 
| 145 |         self.clean_frame_pool = []  # type: List[_ProcessSubFrame]
 | 
| 146 | 
 | 
| 147 |         # When starting a pipeline in the foreground, we need to pass a handle to it
 | 
| 148 |         # through the evaluation of the last node back to ourselves for execution.
 | 
| 149 |         # We use this handle to make sure any processes forked for the last part of
 | 
| 150 |         # the pipeline are placed into the same process group as the rest of the
 | 
| 151 |         # pipeline. Since there is, by design, only ever one foreground pipeline and
 | 
| 152 |         # any pipelines started within subshells run in their parent's process
 | 
| 153 |         # group, we only need one pointer here, not some collection.
 | 
| 154 |         self.fg_pipeline = None  # type: Optional[process.Pipeline]
 | 
| 155 | 
 | 
| 156 |     def CheckCircularDeps(self):
 | 
| 157 |         # type: () -> None
 | 
| 158 |         assert self.cmd_ev is not None
 | 
| 159 | 
 | 
| 160 |     def _MakeProcess(self, node, inherit_errexit=True):
 | 
| 161 |         # type: (command_t, bool) -> process.Process
 | 
| 162 |         """Assume we will run the node in another process.
 | 
| 163 | 
 | 
| 164 |         Return a process.
 | 
| 165 |         """
 | 
| 166 |         UP_node = node
 | 
| 167 |         if node.tag() == command_e.ControlFlow:
 | 
| 168 |             node = cast(command.ControlFlow, UP_node)
 | 
| 169 |             # Pipeline or subshells with control flow are invalid, e.g.:
 | 
| 170 |             # - break | less
 | 
| 171 |             # - continue | less
 | 
| 172 |             # - ( return )
 | 
| 173 |             # NOTE: This could be done at parse time too.
 | 
| 174 |             if node.keyword.id != Id.ControlFlow_Exit:
 | 
| 175 |                 e_die(
 | 
| 176 |                     'Invalid control flow %r in pipeline / subshell / background'
 | 
| 177 |                     % lexer.TokenVal(node.keyword), node.keyword)
 | 
| 178 | 
 | 
| 179 |         # NOTE: If ErrExit(), we could be verbose about subprogram errors?  This
 | 
| 180 |         # only really matters when executing 'exit 42', because the child shell
 | 
| 181 |         # inherits errexit and will be verbose.  Other notes:
 | 
| 182 |         #
 | 
| 183 |         # - We might want errors to fit on a single line so they don't get #
 | 
| 184 |         #   interleaved.
 | 
| 185 |         # - We could turn the `exit` builtin into a error.FatalRuntime exception
 | 
| 186 |         #   and get this check for "free".
 | 
| 187 |         thunk = process.SubProgramThunk(self.cmd_ev,
 | 
| 188 |                                         node,
 | 
| 189 |                                         self.trap_state,
 | 
| 190 |                                         self.multi_trace,
 | 
| 191 |                                         inherit_errexit=inherit_errexit)
 | 
| 192 |         p = process.Process(thunk, self.job_control, self.job_list,
 | 
| 193 |                             self.tracer)
 | 
| 194 |         return p
 | 
| 195 | 
 | 
| 196 |     def RunBuiltin(self, builtin_id, cmd_val):
 | 
| 197 |         # type: (int, cmd_value.Argv) -> int
 | 
| 198 |         """Run a builtin.
 | 
| 199 | 
 | 
| 200 |         Also called by the 'builtin' builtin.
 | 
| 201 |         """
 | 
| 202 |         self.tracer.OnBuiltin(builtin_id, cmd_val.argv)
 | 
| 203 | 
 | 
| 204 |         builtin_func = self.builtins[builtin_id]
 | 
| 205 | 
 | 
| 206 |         io_errors = []  # type: List[error.IOError_OSError]
 | 
| 207 |         with vm.ctx_FlushStdout(io_errors):
 | 
| 208 |             # note: could be second word, like 'builtin read'
 | 
| 209 |             with ui.ctx_Location(self.errfmt, cmd_val.arg_locs[0]):
 | 
| 210 |                 try:
 | 
| 211 |                     status = builtin_func.Run(cmd_val)
 | 
| 212 |                     assert isinstance(status, int)
 | 
| 213 |                 except (IOError, OSError) as e:
 | 
| 214 |                     self.errfmt.PrintMessage(
 | 
| 215 |                         '%s builtin I/O error: %s' %
 | 
| 216 |                         (cmd_val.argv[0], pyutil.strerror(e)),
 | 
| 217 |                         cmd_val.arg_locs[0])
 | 
| 218 |                     return 1
 | 
| 219 |                 except error.Usage as e:
 | 
| 220 |                     arg0 = cmd_val.argv[0]
 | 
| 221 |                     # e.g. 'type' doesn't accept flag '-x'
 | 
| 222 |                     self.errfmt.PrefixPrint(e.msg, '%r ' % arg0, e.location)
 | 
| 223 |                     return 2  # consistent error code for usage error
 | 
| 224 | 
 | 
| 225 |         if len(io_errors):  # e.g. disk full, ulimit
 | 
| 226 |             self.errfmt.PrintMessage(
 | 
| 227 |                 '%s builtin I/O error: %s' %
 | 
| 228 |                 (cmd_val.argv[0], pyutil.strerror(io_errors[0])),
 | 
| 229 |                 cmd_val.arg_locs[0])
 | 
| 230 |             return 1
 | 
| 231 | 
 | 
| 232 |         return status
 | 
| 233 | 
 | 
| 234 |     def RunSimpleCommand(self, cmd_val, cmd_st, run_flags):
 | 
| 235 |         # type: (cmd_value.Argv, CommandStatus, int) -> int
 | 
| 236 |         """Run builtins, functions, external commands.
 | 
| 237 | 
 | 
| 238 |         Possible variations:
 | 
| 239 |         - YSH might have different, simpler rules.  No special builtins, etc.
 | 
| 240 |         - YSH might have OILS_PATH = :| /bin /usr/bin | or something.
 | 
| 241 |         - Interpreters might want to define all their own builtins.
 | 
| 242 |         """
 | 
| 243 |         argv = cmd_val.argv
 | 
| 244 |         if len(cmd_val.arg_locs):
 | 
| 245 |             arg0_loc = cmd_val.arg_locs[0]  # type: loc_t
 | 
| 246 |         else:
 | 
| 247 |             arg0_loc = loc.Missing
 | 
| 248 | 
 | 
| 249 |         # This happens when you write "$@" but have no arguments.
 | 
| 250 |         if len(argv) == 0:
 | 
| 251 |             if self.exec_opts.strict_argv():
 | 
| 252 |                 e_die("Command evaluated to an empty argv array", arg0_loc)
 | 
| 253 |             else:
 | 
| 254 |                 return 0  # status 0, or skip it?
 | 
| 255 | 
 | 
| 256 |         arg0 = argv[0]
 | 
| 257 | 
 | 
| 258 |         builtin_id = consts.LookupAssignBuiltin(arg0)
 | 
| 259 |         if builtin_id != consts.NO_INDEX:
 | 
| 260 |             # command readonly is disallowed, for technical reasons.  Could relax it
 | 
| 261 |             # later.
 | 
| 262 |             self.errfmt.Print_("Can't run assignment builtin recursively",
 | 
| 263 |                                arg0_loc)
 | 
| 264 |             return 1
 | 
| 265 | 
 | 
| 266 |         builtin_id = consts.LookupSpecialBuiltin(arg0)
 | 
| 267 |         if builtin_id != consts.NO_INDEX:
 | 
| 268 |             cmd_st.show_code = True  # this is a "leaf" for errors
 | 
| 269 |             status = self.RunBuiltin(builtin_id, cmd_val)
 | 
| 270 |             # TODO: Enable this and fix spec test failures.
 | 
| 271 |             # Also update _SPECIAL_BUILTINS in osh/builtin.py.
 | 
| 272 |             #if status != 0:
 | 
| 273 |             #  e_die_status(status, 'special builtin failed')
 | 
| 274 |             return status
 | 
| 275 | 
 | 
| 276 |         call_procs = not (run_flags & NO_CALL_PROCS)
 | 
| 277 |         # Builtins like 'true' can be redefined as functions.
 | 
| 278 |         if call_procs:
 | 
| 279 |             # TODO: Look shell functions in self.sh_funcs, but procs are
 | 
| 280 |             # value.Proc in the var namespace.
 | 
| 281 |             # Pitfall: What happens if there are two of the same name?  I guess
 | 
| 282 |             # that's why you have = and 'type' inspect them
 | 
| 283 | 
 | 
| 284 |             proc_node = self.procs.Get(arg0)
 | 
| 285 |             if proc_node is not None:
 | 
| 286 |                 if self.exec_opts.strict_errexit():
 | 
| 287 |                     disabled_tok = self.mutable_opts.ErrExitDisabledToken()
 | 
| 288 |                     if disabled_tok:
 | 
| 289 |                         self.errfmt.Print_(
 | 
| 290 |                             'errexit was disabled for this construct',
 | 
| 291 |                             disabled_tok)
 | 
| 292 |                         self.errfmt.StderrLine('')
 | 
| 293 |                         e_die(
 | 
| 294 |                             "Can't run a proc while errexit is disabled. "
 | 
| 295 |                             "Use 'try' or wrap it in a process with $0 myproc",
 | 
| 296 |                             arg0_loc)
 | 
| 297 | 
 | 
| 298 |                 with dev.ctx_Tracer(self.tracer, 'proc', argv):
 | 
| 299 |                     # NOTE: Functions could call 'exit 42' directly, etc.
 | 
| 300 |                     status = self.cmd_ev.RunProc(proc_node, cmd_val)
 | 
| 301 |                 return status
 | 
| 302 | 
 | 
| 303 |         # Notes:
 | 
| 304 |         # - procs shadow hay names
 | 
| 305 |         # - hay names shadow normal builtins?  Should we limit to CAPS or no?
 | 
| 306 |         if self.hay_state.Resolve(arg0):
 | 
| 307 |             return self.RunBuiltin(builtin_i.haynode, cmd_val)
 | 
| 308 | 
 | 
| 309 |         builtin_id = consts.LookupNormalBuiltin(arg0)
 | 
| 310 | 
 | 
| 311 |         if self.exec_opts._running_hay():
 | 
| 312 |             # Hay: limit the builtins that can be run
 | 
| 313 |             # - declare 'use dialect'
 | 
| 314 |             # - echo and write for debugging
 | 
| 315 |             # - no JSON?
 | 
| 316 |             if builtin_id in (builtin_i.haynode, builtin_i.use, builtin_i.echo,
 | 
| 317 |                               builtin_i.write):
 | 
| 318 |                 cmd_st.show_code = True  # this is a "leaf" for errors
 | 
| 319 |                 return self.RunBuiltin(builtin_id, cmd_val)
 | 
| 320 | 
 | 
| 321 |             self.errfmt.Print_('Unknown command %r while running hay' % arg0,
 | 
| 322 |                                arg0_loc)
 | 
| 323 |             return 127
 | 
| 324 | 
 | 
| 325 |         if builtin_id != consts.NO_INDEX:
 | 
| 326 |             cmd_st.show_code = True  # this is a "leaf" for errors
 | 
| 327 |             return self.RunBuiltin(builtin_id, cmd_val)
 | 
| 328 | 
 | 
| 329 |         environ = self.mem.GetExported()  # Include temporary variables
 | 
| 330 | 
 | 
| 331 |         if cmd_val.typed_args:
 | 
| 332 |             e_die(
 | 
| 333 |                 '%r appears to be external. External commands don\'t accept typed args (OILS-ERR-200)'
 | 
| 334 |                 % arg0, cmd_val.typed_args.left)
 | 
| 335 | 
 | 
| 336 |         # Resolve argv[0] BEFORE forking.
 | 
| 337 |         if run_flags & USE_DEFAULT_PATH:
 | 
| 338 |             argv0_path = state.LookupExecutable(arg0, DEFAULT_PATH)
 | 
| 339 |         else:
 | 
| 340 |             argv0_path = self.search_path.CachedLookup(arg0)
 | 
| 341 |         if argv0_path is None:
 | 
| 342 |             self.errfmt.Print_('%r not found (OILS-ERR-100)' % arg0, arg0_loc)
 | 
| 343 |             return 127
 | 
| 344 | 
 | 
| 345 |         # Normal case: ls /
 | 
| 346 |         if run_flags & DO_FORK:
 | 
| 347 |             thunk = process.ExternalThunk(self.ext_prog, argv0_path, cmd_val,
 | 
| 348 |                                           environ)
 | 
| 349 |             p = process.Process(thunk, self.job_control, self.job_list,
 | 
| 350 |                                 self.tracer)
 | 
| 351 | 
 | 
| 352 |             if self.job_control.Enabled():
 | 
| 353 |                 if self.fg_pipeline is not None:
 | 
| 354 |                     pgid = self.fg_pipeline.ProcessGroupId()
 | 
| 355 |                     # If job control is enabled, this should be true
 | 
| 356 |                     assert pgid != process.INVALID_PGID
 | 
| 357 | 
 | 
| 358 |                     change = process.SetPgid(pgid, self.tracer)
 | 
| 359 |                     self.fg_pipeline = None  # clear to avoid confusion in subshells
 | 
| 360 |                 else:
 | 
| 361 |                     change = process.SetPgid(process.OWN_LEADER, self.tracer)
 | 
| 362 |                 p.AddStateChange(change)
 | 
| 363 | 
 | 
| 364 |             status = p.RunProcess(self.waiter, trace.External(cmd_val.argv))
 | 
| 365 | 
 | 
| 366 |             # this is close to a "leaf" for errors
 | 
| 367 |             # problem: permission denied EACCESS prints duplicate messages
 | 
| 368 |             # TODO: add message command 'ls' failed
 | 
| 369 |             cmd_st.show_code = True
 | 
| 370 | 
 | 
| 371 |             return status
 | 
| 372 | 
 | 
| 373 |         self.tracer.OnExec(cmd_val.argv)
 | 
| 374 | 
 | 
| 375 |         # Already forked for pipeline: ls / | wc -l
 | 
| 376 |         self.ext_prog.Exec(argv0_path, cmd_val, environ)  # NEVER RETURNS
 | 
| 377 | 
 | 
| 378 |         raise AssertionError('for -Wreturn-type in C++')
 | 
| 379 | 
 | 
| 380 |     def RunBackgroundJob(self, node):
 | 
| 381 |         # type: (command_t) -> int
 | 
| 382 |         """For & etc."""
 | 
| 383 |         # Special case for pipeline.  There is some evidence here:
 | 
| 384 |         # https://www.gnu.org/software/libc/manual/html_node/Launching-Jobs.html#Launching-Jobs
 | 
| 385 |         #
 | 
| 386 |         #  "You can either make all the processes in the process group be children
 | 
| 387 |         #  of the shell process, or you can make one process in group be the
 | 
| 388 |         #  ancestor of all the other processes in that group. The sample shell
 | 
| 389 |         #  program presented in this chapter uses the first approach because it
 | 
| 390 |         #  makes bookkeeping somewhat simpler."
 | 
| 391 |         UP_node = node
 | 
| 392 | 
 | 
| 393 |         if UP_node.tag() == command_e.Pipeline:
 | 
| 394 |             node = cast(command.Pipeline, UP_node)
 | 
| 395 |             pi = process.Pipeline(self.exec_opts.sigpipe_status_ok(),
 | 
| 396 |                                   self.job_control, self.job_list, self.tracer)
 | 
| 397 |             for child in node.children:
 | 
| 398 |                 p = self._MakeProcess(child)
 | 
| 399 |                 p.Init_ParentPipeline(pi)
 | 
| 400 |                 pi.Add(p)
 | 
| 401 | 
 | 
| 402 |             pi.StartPipeline(self.waiter)
 | 
| 403 |             pi.SetBackground()
 | 
| 404 |             last_pid = pi.LastPid()
 | 
| 405 |             self.mem.last_bg_pid = last_pid  # for $!
 | 
| 406 | 
 | 
| 407 |             self.job_list.AddJob(pi)  # show in 'jobs' list
 | 
| 408 | 
 | 
| 409 |         else:
 | 
| 410 |             # Problem: to get the 'set -b' behavior of immediate notifications, we
 | 
| 411 |             # have to register SIGCHLD.  But then that introduces race conditions.
 | 
| 412 |             # If we haven't called Register yet, then we won't know who to notify.
 | 
| 413 | 
 | 
| 414 |             p = self._MakeProcess(node)
 | 
| 415 |             if self.job_control.Enabled():
 | 
| 416 |                 p.AddStateChange(
 | 
| 417 |                     process.SetPgid(process.OWN_LEADER, self.tracer))
 | 
| 418 | 
 | 
| 419 |             p.SetBackground()
 | 
| 420 |             pid = p.StartProcess(trace.Fork)
 | 
| 421 |             self.mem.last_bg_pid = pid  # for $!
 | 
| 422 |             self.job_list.AddJob(p)  # show in 'jobs' list
 | 
| 423 |         return 0
 | 
| 424 | 
 | 
| 425 |     def RunPipeline(self, node, status_out):
 | 
| 426 |         # type: (command.Pipeline, CommandStatus) -> None
 | 
| 427 | 
 | 
| 428 |         pi = process.Pipeline(self.exec_opts.sigpipe_status_ok(),
 | 
| 429 |                               self.job_control, self.job_list, self.tracer)
 | 
| 430 | 
 | 
| 431 |         # initialized with CommandStatus.CreateNull()
 | 
| 432 |         pipe_locs = []  # type: List[loc_t]
 | 
| 433 | 
 | 
| 434 |         # First n-1 processes (which is empty when n == 1)
 | 
| 435 |         n = len(node.children)
 | 
| 436 |         for i in xrange(n - 1):
 | 
| 437 |             child = node.children[i]
 | 
| 438 | 
 | 
| 439 |             # TODO: determine these locations at parse time?
 | 
| 440 |             pipe_locs.append(loc.Command(child))
 | 
| 441 | 
 | 
| 442 |             p = self._MakeProcess(child)
 | 
| 443 |             p.Init_ParentPipeline(pi)
 | 
| 444 |             pi.Add(p)
 | 
| 445 | 
 | 
| 446 |         last_child = node.children[n - 1]
 | 
| 447 |         # Last piece of code is in THIS PROCESS.  'echo foo | read line; echo $line'
 | 
| 448 |         pi.AddLast((self.cmd_ev, last_child))
 | 
| 449 |         pipe_locs.append(loc.Command(last_child))
 | 
| 450 | 
 | 
| 451 |         with dev.ctx_Tracer(self.tracer, 'pipeline', None):
 | 
| 452 |             pi.StartPipeline(self.waiter)
 | 
| 453 |             self.fg_pipeline = pi
 | 
| 454 |             status_out.pipe_status = pi.RunLastPart(self.waiter, self.fd_state)
 | 
| 455 |             self.fg_pipeline = None  # clear in case we didn't end up forking
 | 
| 456 | 
 | 
| 457 |         status_out.pipe_locs = pipe_locs
 | 
| 458 | 
 | 
| 459 |     def RunSubshell(self, node):
 | 
| 460 |         # type: (command_t) -> int
 | 
| 461 |         p = self._MakeProcess(node)
 | 
| 462 |         if self.job_control.Enabled():
 | 
| 463 |             p.AddStateChange(process.SetPgid(process.OWN_LEADER, self.tracer))
 | 
| 464 | 
 | 
| 465 |         return p.RunProcess(self.waiter, trace.ForkWait)
 | 
| 466 | 
 | 
| 467 |     def RunCommandSub(self, cs_part):
 | 
| 468 |         # type: (CommandSub) -> str
 | 
| 469 | 
 | 
| 470 |         if not self.exec_opts._allow_command_sub():
 | 
| 471 |             # _allow_command_sub is used in two places.  Only one of them turns off _allow_process_sub
 | 
| 472 |             if not self.exec_opts._allow_process_sub():
 | 
| 473 |                 why = "status wouldn't be checked (strict_errexit)"
 | 
| 474 |             else:
 | 
| 475 |                 why = 'eval_unsafe_arith is off'
 | 
| 476 | 
 | 
| 477 |             e_die("Command subs not allowed here because %s" % why,
 | 
| 478 |                   loc.WordPart(cs_part))
 | 
| 479 | 
 | 
| 480 |         node = cs_part.child
 | 
| 481 | 
 | 
| 482 |         # Hack for weird $(<file) construct
 | 
| 483 |         if node.tag() == command_e.Redirect:
 | 
| 484 |             redir_node = cast(command.Redirect, node)
 | 
| 485 |             # Detect '< file'
 | 
| 486 |             if (len(redir_node.redirects) == 1 and
 | 
| 487 |                     redir_node.redirects[0].op.id == Id.Redir_Less and
 | 
| 488 |                     redir_node.child.tag() == command_e.NoOp):
 | 
| 489 | 
 | 
| 490 |                 # Change it to __cat < file.
 | 
| 491 |                 # TODO: could be 'internal cat' (issue #1013)
 | 
| 492 |                 tok = lexer.DummyToken(Id.Lit_Chars, '__cat')
 | 
| 493 |                 cat_word = CompoundWord([tok])
 | 
| 494 | 
 | 
| 495 |                 # Blame < because __cat has no location
 | 
| 496 |                 blame_tok = redir_node.redirects[0].op
 | 
| 497 |                 simple = command.Simple(blame_tok, [], [cat_word], None, None,
 | 
| 498 |                                         True)
 | 
| 499 | 
 | 
| 500 |                 # MUTATE redir node so it's like $(<file _cat)
 | 
| 501 |                 redir_node.child = simple
 | 
| 502 | 
 | 
| 503 |         p = self._MakeProcess(node,
 | 
| 504 |                               inherit_errexit=self.exec_opts.inherit_errexit())
 | 
| 505 |         # Shell quirk: Command subs remain part of the shell's process group, so we
 | 
| 506 |         # don't use p.AddStateChange(process.SetPgid(...))
 | 
| 507 | 
 | 
| 508 |         r, w = posix.pipe()
 | 
| 509 |         p.AddStateChange(process.StdoutToPipe(r, w))
 | 
| 510 | 
 | 
| 511 |         p.StartProcess(trace.CommandSub)
 | 
| 512 |         #log('Command sub started %d', pid)
 | 
| 513 | 
 | 
| 514 |         chunks = []  # type: List[str]
 | 
| 515 |         posix.close(w)  # not going to write
 | 
| 516 |         while True:
 | 
| 517 |             n, err_num = pyos.Read(r, 4096, chunks)
 | 
| 518 | 
 | 
| 519 |             if n < 0:
 | 
| 520 |                 if err_num == EINTR:
 | 
| 521 |                     pass  # retry
 | 
| 522 |                 else:
 | 
| 523 |                     # Like the top level IOError handler
 | 
| 524 |                     e_die_status(
 | 
| 525 |                         2,
 | 
| 526 |                         'osh I/O error (read): %s' % posix.strerror(err_num))
 | 
| 527 | 
 | 
| 528 |             elif n == 0:  # EOF
 | 
| 529 |                 break
 | 
| 530 |         posix.close(r)
 | 
| 531 | 
 | 
| 532 |         status = p.Wait(self.waiter)
 | 
| 533 | 
 | 
| 534 |         # OSH has the concept of aborting in the middle of a WORD.  We're not
 | 
| 535 |         # waiting until the command is over!
 | 
| 536 |         if self.exec_opts.command_sub_errexit():
 | 
| 537 |             if status != 0:
 | 
| 538 |                 msg = 'Command Sub exited with status %d' % status
 | 
| 539 |                 raise error.ErrExit(status, msg, loc.WordPart(cs_part))
 | 
| 540 | 
 | 
| 541 |         else:
 | 
| 542 |             # Set a flag so we check errexit at the same time as bash.  Example:
 | 
| 543 |             #
 | 
| 544 |             # a=$(false)
 | 
| 545 |             # echo foo  # no matter what comes here, the flag is reset
 | 
| 546 |             #
 | 
| 547 |             # Set ONLY until this command node has finished executing.
 | 
| 548 | 
 | 
| 549 |             # HACK: move this
 | 
| 550 |             self.cmd_ev.check_command_sub_status = True
 | 
| 551 |             self.mem.SetLastStatus(status)
 | 
| 552 | 
 | 
| 553 |         # Runtime errors test case: # $("echo foo > $@")
 | 
| 554 |         # Why rstrip()?
 | 
| 555 |         # https://unix.stackexchange.com/questions/17747/why-does-shell-command-substitution-gobble-up-a-trailing-newline-char
 | 
| 556 |         return ''.join(chunks).rstrip('\n')
 | 
| 557 | 
 | 
| 558 |     def RunProcessSub(self, cs_part):
 | 
| 559 |         # type: (CommandSub) -> str
 | 
| 560 |         """Process sub creates a forks a process connected to a pipe.
 | 
| 561 | 
 | 
| 562 |         The pipe is typically passed to another process via a /dev/fd/$FD path.
 | 
| 563 | 
 | 
| 564 |         Life cycle of a process substitution:
 | 
| 565 | 
 | 
| 566 |         1. Start with this code
 | 
| 567 | 
 | 
| 568 |           diff <(seq 3) <(seq 4)
 | 
| 569 | 
 | 
| 570 |         2. To evaluate the command line, we evaluate every word.  The
 | 
| 571 |         NormalWordEvaluator this method, RunProcessSub(), which does 3 things:
 | 
| 572 | 
 | 
| 573 |           a. Create a pipe(), getting r and w
 | 
| 574 |           b. Starts the seq process, which inherits r and w
 | 
| 575 |              It has a StdoutToPipe() redirect, which means that it dup2(w, 1)
 | 
| 576 |              and close(r)
 | 
| 577 |           c. Close the w FD, because neither the shell or 'diff' will write to it.
 | 
| 578 |              However we must retain 'r', because 'diff' hasn't opened /dev/fd yet!
 | 
| 579 |           d. We evaluate <(seq 3) to /dev/fd/$r, so "diff" can read from it
 | 
| 580 | 
 | 
| 581 |         3. Now we're done evaluating every word, so we know the command line of
 | 
| 582 |            diff, which looks like
 | 
| 583 | 
 | 
| 584 |           diff /dev/fd/64 /dev/fd/65
 | 
| 585 | 
 | 
| 586 |         Those are the FDs for the read ends of the pipes we created.
 | 
| 587 | 
 | 
| 588 |         4. diff inherits a copy of the read end of bot pipes.  But it actually
 | 
| 589 |         calls open() both files passed as argv.  (I think this is fine.)
 | 
| 590 | 
 | 
| 591 |         5. wait() for the diff process.
 | 
| 592 | 
 | 
| 593 |         6. The shell closes both the read ends of both pipes.  Neither us or
 | 
| 594 |         'diffd' will read again.
 | 
| 595 | 
 | 
| 596 |         7. The shell waits for both 'seq' processes.
 | 
| 597 | 
 | 
| 598 |         Related:
 | 
| 599 |           shopt -s process_sub_fail
 | 
| 600 |           _process_sub_status
 | 
| 601 |         """
 | 
| 602 |         cs_loc = loc.WordPart(cs_part)
 | 
| 603 | 
 | 
| 604 |         if not self.exec_opts._allow_process_sub():
 | 
| 605 |             e_die(
 | 
| 606 |                 "Process subs not allowed here because status wouldn't be checked (strict_errexit)",
 | 
| 607 |                 cs_loc)
 | 
| 608 | 
 | 
| 609 |         p = self._MakeProcess(cs_part.child)
 | 
| 610 | 
 | 
| 611 |         r, w = posix.pipe()
 | 
| 612 |         #log('pipe = %d, %d', r, w)
 | 
| 613 | 
 | 
| 614 |         op_id = cs_part.left_token.id
 | 
| 615 |         if op_id == Id.Left_ProcSubIn:
 | 
| 616 |             # Example: cat < <(head foo.txt)
 | 
| 617 |             #
 | 
| 618 |             # The head process should write its stdout to a pipe.
 | 
| 619 |             redir = process.StdoutToPipe(r,
 | 
| 620 |                                          w)  # type: process.ChildStateChange
 | 
| 621 | 
 | 
| 622 |         elif op_id == Id.Left_ProcSubOut:
 | 
| 623 |             # Example: head foo.txt > >(tac)
 | 
| 624 |             #
 | 
| 625 |             # The tac process should read its stdin from a pipe.
 | 
| 626 | 
 | 
| 627 |             # Note: this example sometimes requires you to hit "enter" in bash and
 | 
| 628 |             # zsh.  WHy?
 | 
| 629 |             redir = process.StdinFromPipe(r, w)
 | 
| 630 | 
 | 
| 631 |         else:
 | 
| 632 |             raise AssertionError()
 | 
| 633 | 
 | 
| 634 |         p.AddStateChange(redir)
 | 
| 635 | 
 | 
| 636 |         if self.job_control.Enabled():
 | 
| 637 |             p.AddStateChange(process.SetPgid(process.OWN_LEADER, self.tracer))
 | 
| 638 | 
 | 
| 639 |         # Fork, letting the child inherit the pipe file descriptors.
 | 
| 640 |         p.StartProcess(trace.ProcessSub)
 | 
| 641 | 
 | 
| 642 |         ps_frame = self.process_sub_stack[-1]
 | 
| 643 | 
 | 
| 644 |         # Note: bash never waits() on the process, but zsh does.  The calling
 | 
| 645 |         # program needs to read() before we can wait, e.g.
 | 
| 646 |         #   diff <(sort left.txt) <(sort right.txt)
 | 
| 647 | 
 | 
| 648 |         # After forking, close the end of the pipe we're not using.
 | 
| 649 |         if op_id == Id.Left_ProcSubIn:
 | 
| 650 |             posix.close(w)  # cat < <(head foo.txt)
 | 
| 651 |             ps_frame.Append(p, r, cs_loc)  # close later
 | 
| 652 |         elif op_id == Id.Left_ProcSubOut:
 | 
| 653 |             posix.close(r)
 | 
| 654 |             #log('Left_ProcSubOut closed %d', r)
 | 
| 655 |             ps_frame.Append(p, w, cs_loc)  # close later
 | 
| 656 |         else:
 | 
| 657 |             raise AssertionError()
 | 
| 658 | 
 | 
| 659 |         # Is /dev Linux-specific?
 | 
| 660 |         if op_id == Id.Left_ProcSubIn:
 | 
| 661 |             return '/dev/fd/%d' % r
 | 
| 662 | 
 | 
| 663 |         elif op_id == Id.Left_ProcSubOut:
 | 
| 664 |             return '/dev/fd/%d' % w
 | 
| 665 | 
 | 
| 666 |         else:
 | 
| 667 |             raise AssertionError()
 | 
| 668 | 
 | 
| 669 |     def PushRedirects(self, redirects, err_out):
 | 
| 670 |         # type: (List[RedirValue], List[error.IOError_OSError]) -> None
 | 
| 671 |         if len(redirects) == 0:  # Optimized to avoid allocs
 | 
| 672 |             return
 | 
| 673 |         self.fd_state.Push(redirects, err_out)
 | 
| 674 | 
 | 
| 675 |     def PopRedirects(self, num_redirects, err_out):
 | 
| 676 |         # type: (int, List[error.IOError_OSError]) -> None
 | 
| 677 |         if num_redirects == 0:  # Optimized to avoid allocs
 | 
| 678 |             return
 | 
| 679 |         self.fd_state.Pop(err_out)
 | 
| 680 | 
 | 
| 681 |     def PushProcessSub(self):
 | 
| 682 |         # type: () -> None
 | 
| 683 |         if len(self.clean_frame_pool):
 | 
| 684 |             # Optimized to avoid allocs
 | 
| 685 |             new_frame = self.clean_frame_pool.pop()
 | 
| 686 |         else:
 | 
| 687 |             new_frame = _ProcessSubFrame()
 | 
| 688 |         self.process_sub_stack.append(new_frame)
 | 
| 689 | 
 | 
| 690 |     def PopProcessSub(self, compound_st):
 | 
| 691 |         # type: (StatusArray) -> None
 | 
| 692 |         """This method is called by a context manager, which means we always
 | 
| 693 |         wait() on the way out, which I think is the right thing.
 | 
| 694 | 
 | 
| 695 |         We don't always set _process_sub_status, e.g. if some fatal
 | 
| 696 |         error occurs first, but we always wait.
 | 
| 697 |         """
 | 
| 698 |         frame = self.process_sub_stack.pop()
 | 
| 699 |         if frame.WasModified():
 | 
| 700 |             frame.MaybeWaitOnProcessSubs(self.waiter, compound_st)
 | 
| 701 |         else:
 | 
| 702 |             # Optimized to avoid allocs
 | 
| 703 |             self.clean_frame_pool.append(frame)
 | 
| 704 | 
 | 
| 705 |         # Note: the 3 lists in _ProcessSubFrame are hot in our profiles.  It would
 | 
| 706 |         # be nice to somehow "destroy" them here, rather than letting them become
 | 
| 707 |         # garbage that needs to be traced.
 | 
| 708 | 
 | 
| 709 |         # The CommandEvaluator could have a ProcessSubStack, which supports Push(),
 | 
| 710 |         # Pop(), and Top() of VALUES rather than GC objects?
 | 
| 711 | 
 | 
| 712 | 
 | 
| 713 | # vim: sw=4
 |