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

319 lines, 165 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 core import pyos
15from core import vm
16from frontend import flag_util
17from frontend import match
18from frontend import reader
19from frontend import signal_def
20from mycpp import mylib
21from mycpp.mylib import iteritems, print_stderr, log
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 if not match.LooksLikeInteger(s):
159 return False
160
161 # Note: could simplify this by making match.LooksLikeUnsigned()
162
163 # not (0 > s) is (s >= 0)
164 return not mops.Greater(mops.ZERO, mops.FromStr(s))
165
166
167def _GetSignalNumber(sig_spec):
168 # type: (str) -> int
169
170 # POSIX lists the numbers that are required.
171 # http://pubs.opengroup.org/onlinepubs/9699919799/
172 #
173 # Added 13 for SIGPIPE because autoconf's 'configure' uses it!
174 if sig_spec.strip() in ('1', '2', '3', '6', '9', '13', '14', '15'):
175 return int(sig_spec)
176
177 # INT is an alias for SIGINT
178 if sig_spec.startswith('SIG'):
179 sig_spec = sig_spec[3:]
180 return signal_def.GetNumber(sig_spec)
181
182
183_HOOK_NAMES = ['EXIT', 'ERR', 'RETURN', 'DEBUG']
184
185# bash's default -p looks like this:
186# trap -- '' SIGTSTP
187# trap -- '' SIGTTIN
188# trap -- '' SIGTTOU
189#
190# CPython registers different default handlers. The C++ rewrite should make
191# OVM match sh/bash more closely.
192
193# Example of trap:
194# trap -- 'echo "hi there" | wc ' SIGINT
195#
196# Then hit Ctrl-C.
197
198
199class Trap(vm._Builtin):
200
201 def __init__(self, trap_state, parse_ctx, tracer, errfmt):
202 # type: (TrapState, ParseContext, dev.Tracer, ui.ErrorFormatter) -> None
203 self.trap_state = trap_state
204 self.parse_ctx = parse_ctx
205 self.arena = parse_ctx.arena
206 self.tracer = tracer
207 self.errfmt = errfmt
208
209 def _ParseTrapCode(self, code_str):
210 # type: (str) -> command_t
211 """
212 Returns:
213 A node, or None if the code is invalid.
214 """
215 line_reader = reader.StringLineReader(code_str, self.arena)
216 c_parser = self.parse_ctx.MakeOshParser(line_reader)
217
218 # TODO: the SPID should be passed through argv.
219 src = source.Dynamic('trap arg', loc.Missing)
220 with alloc.ctx_SourceCode(self.arena, src):
221 try:
222 node = main_loop.ParseWholeFile(c_parser)
223 except error.Parse as e:
224 self.errfmt.PrettyPrintError(e)
225 return None
226
227 return node
228
229 def Run(self, cmd_val):
230 # type: (cmd_value.Argv) -> int
231 attrs, arg_r = flag_util.ParseCmdVal('trap', cmd_val)
232 arg = arg_types.trap(attrs.attrs)
233
234 if arg.p: # Print registered handlers
235 # The unit tests rely on this being one line.
236 # bash prints a line that can be re-parsed.
237 for name, _ in iteritems(self.trap_state.hooks):
238 print('%s TrapState' % (name, ))
239
240 for sig_num, _ in iteritems(self.trap_state.traps):
241 print('%d TrapState' % (sig_num, ))
242
243 return 0
244
245 if arg.l: # List valid signals and hooks
246 for hook_name in _HOOK_NAMES:
247 print(' %s' % hook_name)
248
249 signal_def.PrintSignals()
250
251 return 0
252
253 code_str = arg_r.ReadRequired('requires a code string')
254 sig_spec, sig_loc = arg_r.ReadRequired2(
255 'requires a signal or hook name')
256
257 # sig_key is NORMALIZED sig_spec: a signal number string or string hook
258 # name.
259 sig_key = None # type: Optional[str]
260 sig_num = signal_def.NO_SIGNAL
261
262 if sig_spec in _HOOK_NAMES:
263 sig_key = sig_spec
264 elif sig_spec == '0': # Special case
265 sig_key = 'EXIT'
266 else:
267 sig_num = _GetSignalNumber(sig_spec)
268 if sig_num != signal_def.NO_SIGNAL:
269 sig_key = str(sig_num)
270
271 if sig_key is None:
272 self.errfmt.Print_("Invalid signal or hook %r" % sig_spec,
273 blame_loc=cmd_val.arg_locs[2])
274 return 1
275
276 # NOTE: sig_spec isn't validated when removing handlers.
277 # Per POSIX, if the first argument to trap is an unsigned integer
278 # then reset every condition
279 # https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html#tag_18_28
280 if code_str == '-' or _IsUnsignedInteger(code_str):
281 if sig_key in _HOOK_NAMES:
282 self.trap_state.RemoveUserHook(sig_key)
283 return 0
284
285 if sig_num != signal_def.NO_SIGNAL:
286 self.trap_state.RemoveUserTrap(sig_num)
287 return 0
288
289 raise AssertionError('Signal or trap')
290
291 # Try parsing the code first.
292
293 # TODO: If simple_trap is on (for ysh:upgrade), then it must be a function
294 # name? And then you wrap it in 'try'?
295
296 node = self._ParseTrapCode(code_str)
297 if node is None:
298 return 1 # ParseTrapCode() prints an error for us.
299
300 # Register a hook.
301 if sig_key in _HOOK_NAMES:
302 if sig_key == 'RETURN':
303 print_stderr("osh warning: The %r hook isn't implemented" %
304 sig_spec)
305 self.trap_state.AddUserHook(sig_key, node)
306 return 0
307
308 # Register a signal.
309 if sig_num != signal_def.NO_SIGNAL:
310 # For signal handlers, the traps dictionary is used only for debugging.
311 if sig_num in (SIGKILL, SIGSTOP):
312 self.errfmt.Print_("Signal %r can't be handled" % sig_spec,
313 blame_loc=sig_loc)
314 # Other shells return 0, but this seems like an obvious error
315 return 1
316 self.trap_state.AddUserTrap(sig_num, node)
317 return 0
318
319 raise AssertionError('Signal or trap')