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

713 lines, 386 significant
1"""executor.py."""
2from __future__ import print_function
3
4from errno import EINTR
5
6from _devbuild.gen.id_kind_asdl import Id
7from _devbuild.gen.option_asdl import builtin_i
8from _devbuild.gen.runtime_asdl import RedirValue, trace
9from _devbuild.gen.syntax_asdl import (
10 command,
11 command_e,
12 CommandSub,
13 CompoundWord,
14 loc,
15 loc_t,
16)
17from builtin import hay_ysh
18from core import dev
19from core import error
20from core import process
21from core.error import e_die, e_die_status
22from core import pyos
23from core import pyutil
24from core import state
25from core import ui
26from core import vm
27from frontend import consts
28from frontend import lexer
29from mycpp.mylib import log
30
31import posix_ as posix
32
33from typing import cast, Dict, List, Optional, TYPE_CHECKING
34if 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
46class _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
92DO_FORK = 1 << 1
93NO_CALL_PROCS = 1 << 2 # command ls suppresses function lookup
94USE_DEFAULT_PATH = 1 << 3 # for command -p ls changes the path
95
96# Copied from var.c in dash
97DEFAULT_PATH = [
98 '/usr/local/sbin', '/usr/local/bin', '/usr/sbin', '/usr/bin', '/sbin',
99 '/bin'
100]
101
102
103class 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.GetProc(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