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