OILS / builtin / trap_osh.py View on Github | oilshell.org

317 lines, 167 significant
1#!/usr/bin/env python2
2"""Builtin_trap.py."""
3from __future__ import print_function
4
5from signal import SIG_DFL, SIGINT, SIGKILL, SIGSTOP, SIGWINCH
6
7from _devbuild.gen import arg_types
8from _devbuild.gen.runtime_asdl import cmd_value
9from _devbuild.gen.syntax_asdl import loc, source
10from core import alloc
11from core import dev
12from core import error
13from core import main_loop
14from mycpp.mylib import log
15from core import pyos
16from core import vm
17from frontend import flag_util
18from frontend import signal_def
19from frontend import reader
20from mycpp import mylib
21from mycpp.mylib import iteritems, print_stderr
22from mycpp import mops
23
24from typing import Dict, List, Optional, TYPE_CHECKING
25if TYPE_CHECKING:
26 from _devbuild.gen.syntax_asdl import command_t
27 from display import ui
28 from frontend.parse_lib import ParseContext
29
30_ = log
31
32
33class TrapState(object):
34 """Traps are shell callbacks that the user wants to run on certain events.
35
36 There are 2 catogires:
37 1. Signals like SIGUSR1
38 2. Hooks like EXIT
39
40 Signal handlers execute in the main loop, and within blocking syscalls.
41
42 EXIT, DEBUG, ERR, RETURN execute in specific places in the interpreter.
43 """
44
45 def __init__(self, signal_safe):
46 # type: (pyos.SignalSafe) -> None
47 self.signal_safe = signal_safe
48 self.hooks = {} # type: Dict[str, command_t]
49 self.traps = {} # type: Dict[int, command_t]
50
51 def ClearForSubProgram(self, inherit_errtrace):
52 # type: (bool) -> None
53 """SubProgramThunk uses this because traps aren't inherited."""
54
55 # bash clears hooks like DEBUG in subshells.
56 # The ERR can be preserved if set -o errtrace
57 hook_err = self.hooks.get('ERR')
58 self.hooks.clear()
59 if hook_err is not None and inherit_errtrace:
60 self.hooks['ERR'] = hook_err
61
62 self.traps.clear()
63
64 def GetHook(self, hook_name):
65 # type: (str) -> command_t
66 """ e.g. EXIT hook. """
67 return self.hooks.get(hook_name, None)
68
69 def AddUserHook(self, hook_name, handler):
70 # type: (str, command_t) -> None
71 self.hooks[hook_name] = handler
72
73 def RemoveUserHook(self, hook_name):
74 # type: (str) -> None
75 mylib.dict_erase(self.hooks, hook_name)
76
77 def AddUserTrap(self, sig_num, handler):
78 # type: (int, command_t) -> None
79 """ e.g. SIGUSR1 """
80 self.traps[sig_num] = handler
81
82 if sig_num == SIGINT:
83 # Don't disturb the underlying runtime's SIGINT handllers
84 # 1. CPython has one for KeyboardInterrupt
85 # 2. mycpp runtime simulates KeyboardInterrupt:
86 # pyos::InitSignalSafe() calls RegisterSignalInterest(SIGINT),
87 # then we PollSigInt() in the osh/cmd_eval.py main loop
88 self.signal_safe.SetSigIntTrapped(True)
89 elif sig_num == SIGWINCH:
90 self.signal_safe.SetSigWinchCode(SIGWINCH)
91 else:
92 pyos.RegisterSignalInterest(sig_num)
93
94 def RemoveUserTrap(self, sig_num):
95 # type: (int) -> None
96
97 mylib.dict_erase(self.traps, sig_num)
98
99 if sig_num == SIGINT:
100 self.signal_safe.SetSigIntTrapped(False)
101 pass
102 elif sig_num == SIGWINCH:
103 self.signal_safe.SetSigWinchCode(pyos.UNTRAPPED_SIGWINCH)
104 else:
105 # TODO: In process.InitInteractiveShell(), 4 signals are set to
106 # SIG_IGN, not SIG_DFL:
107 #
108 # SIGQUIT SIGTSTP SIGTTOU SIGTTIN
109 #
110 # Should we restore them? It's rare that you type 'trap' in
111 # interactive shells, but it might be more correct. See what other
112 # shells do.
113 pyos.sigaction(sig_num, SIG_DFL)
114
115 def GetPendingTraps(self):
116 # type: () -> Optional[List[command_t]]
117 """Transfer ownership of queue of pending trap handlers to caller."""
118 signals = self.signal_safe.TakePendingSignals()
119 if 0:
120 log('*** GetPendingTraps')
121 for si in signals:
122 log('SIGNAL %d', si)
123 #import traceback
124 #traceback.print_stack()
125
126 # Optimization for the common case: do not allocate a list. This function
127 # is called in the interpreter loop.
128 if len(signals) == 0:
129 self.signal_safe.ReuseEmptyList(signals)
130 return None
131
132 run_list = [] # type: List[command_t]
133 for sig_num in signals:
134 node = self.traps.get(sig_num, None)
135 if node is not None:
136 run_list.append(node)
137
138 # Optimization to avoid allocation in the main loop.
139 del signals[:]
140 self.signal_safe.ReuseEmptyList(signals)
141
142 return run_list
143
144 def ThisProcessHasTraps(self):
145 # type: () -> bool
146 """
147 noforklast optimizations are not enabled when the process has code to
148 run after fork!
149 """
150 if 0:
151 log('traps %d', len(self.traps))
152 log('hooks %d', len(self.hooks))
153 return len(self.traps) != 0 or len(self.hooks) != 0
154
155
156def _IsUnsignedInteger(s):
157 # type: (str) -> bool
158
159 try:
160 intval = mops.FromStr(s)
161 except ValueError:
162 return False
163 return not mops.Greater(mops.ZERO, intval)
164
165def _GetSignalNumber(sig_spec):
166 # type: (str) -> int
167
168 # POSIX lists the numbers that are required.
169 # http://pubs.opengroup.org/onlinepubs/9699919799/
170 #
171 # Added 13 for SIGPIPE because autoconf's 'configure' uses it!
172 if sig_spec.strip() in ('1', '2', '3', '6', '9', '13', '14', '15'):
173 return int(sig_spec)
174
175 # INT is an alias for SIGINT
176 if sig_spec.startswith('SIG'):
177 sig_spec = sig_spec[3:]
178 return signal_def.GetNumber(sig_spec)
179
180
181_HOOK_NAMES = ['EXIT', 'ERR', 'RETURN', 'DEBUG']
182
183# bash's default -p looks like this:
184# trap -- '' SIGTSTP
185# trap -- '' SIGTTIN
186# trap -- '' SIGTTOU
187#
188# CPython registers different default handlers. The C++ rewrite should make
189# OVM match sh/bash more closely.
190
191# Example of trap:
192# trap -- 'echo "hi there" | wc ' SIGINT
193#
194# Then hit Ctrl-C.
195
196
197class Trap(vm._Builtin):
198
199 def __init__(self, trap_state, parse_ctx, tracer, errfmt):
200 # type: (TrapState, ParseContext, dev.Tracer, ui.ErrorFormatter) -> None
201 self.trap_state = trap_state
202 self.parse_ctx = parse_ctx
203 self.arena = parse_ctx.arena
204 self.tracer = tracer
205 self.errfmt = errfmt
206
207 def _ParseTrapCode(self, code_str):
208 # type: (str) -> command_t
209 """
210 Returns:
211 A node, or None if the code is invalid.
212 """
213 line_reader = reader.StringLineReader(code_str, self.arena)
214 c_parser = self.parse_ctx.MakeOshParser(line_reader)
215
216 # TODO: the SPID should be passed through argv.
217 src = source.ArgvWord('trap', loc.Missing)
218 with alloc.ctx_SourceCode(self.arena, src):
219 try:
220 node = main_loop.ParseWholeFile(c_parser)
221 except error.Parse as e:
222 self.errfmt.PrettyPrintError(e)
223 return None
224
225 return node
226
227 def Run(self, cmd_val):
228 # type: (cmd_value.Argv) -> int
229 attrs, arg_r = flag_util.ParseCmdVal('trap', cmd_val)
230 arg = arg_types.trap(attrs.attrs)
231
232 if arg.p: # Print registered handlers
233 # The unit tests rely on this being one line.
234 # bash prints a line that can be re-parsed.
235 for name, _ in iteritems(self.trap_state.hooks):
236 print('%s TrapState' % (name, ))
237
238 for sig_num, _ in iteritems(self.trap_state.traps):
239 print('%d TrapState' % (sig_num, ))
240
241 return 0
242
243 if arg.l: # List valid signals and hooks
244 for hook_name in _HOOK_NAMES:
245 print(' %s' % hook_name)
246
247 signal_def.PrintSignals()
248
249 return 0
250
251 code_str = arg_r.ReadRequired('requires a code string')
252 sig_spec, sig_loc = arg_r.ReadRequired2(
253 'requires a signal or hook name')
254
255 # sig_key is NORMALIZED sig_spec: a signal number string or string hook
256 # name.
257 sig_key = None # type: Optional[str]
258 sig_num = signal_def.NO_SIGNAL
259
260 if sig_spec in _HOOK_NAMES:
261 sig_key = sig_spec
262 elif sig_spec == '0': # Special case
263 sig_key = 'EXIT'
264 else:
265 sig_num = _GetSignalNumber(sig_spec)
266 if sig_num != signal_def.NO_SIGNAL:
267 sig_key = str(sig_num)
268
269 if sig_key is None:
270 self.errfmt.Print_("Invalid signal or hook %r" % sig_spec,
271 blame_loc=cmd_val.arg_locs[2])
272 return 1
273
274 # NOTE: sig_spec isn't validated when removing handlers.
275 # Per POSIX, if the first argument to trap is an unsigned integer
276 # then reset every condition
277 # https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html#tag_18_28
278 if code_str == '-' or _IsUnsignedInteger(code_str):
279 if sig_key in _HOOK_NAMES:
280 self.trap_state.RemoveUserHook(sig_key)
281 return 0
282
283 if sig_num != signal_def.NO_SIGNAL:
284 self.trap_state.RemoveUserTrap(sig_num)
285 return 0
286
287 raise AssertionError('Signal or trap')
288
289 # Try parsing the code first.
290
291 # TODO: If simple_trap is on (for oil:upgrade), then it must be a function
292 # name? And then you wrap it in 'try'?
293
294 node = self._ParseTrapCode(code_str)
295 if node is None:
296 return 1 # ParseTrapCode() prints an error for us.
297
298 # Register a hook.
299 if sig_key in _HOOK_NAMES:
300 if sig_key == 'RETURN':
301 print_stderr("osh warning: The %r hook isn't implemented" %
302 sig_spec)
303 self.trap_state.AddUserHook(sig_key, node)
304 return 0
305
306 # Register a signal.
307 if sig_num != signal_def.NO_SIGNAL:
308 # For signal handlers, the traps dictionary is used only for debugging.
309 if sig_num in (SIGKILL, SIGSTOP):
310 self.errfmt.Print_("Signal %r can't be handled" % sig_spec,
311 blame_loc=sig_loc)
312 # Other shells return 0, but this seems like an obvious error
313 return 1
314 self.trap_state.AddUserTrap(sig_num, node)
315 return 0
316
317 raise AssertionError('Signal or trap')