OILS / builtin / trap_osh.py View on Github | oils.pub

436 lines, 248 significant
1#!/usr/bin/env python2
2from __future__ import print_function
3
4from signal import SIG_DFL, SIG_IGN, SIGINT, SIGWINCH
5
6from _devbuild.gen import arg_types
7from _devbuild.gen.runtime_asdl import cmd_value
8from _devbuild.gen.syntax_asdl import loc, loc_t, source, command_e, command
9from core import alloc
10from core import dev
11from core import error
12from core import main_loop
13from core import vm
14from frontend import flag_util
15from frontend import reader
16from frontend import signal_def
17from frontend import typed_args
18from builtin import process_osh # for PrintSignals()
19from data_lang import j8_lite
20from mycpp import iolib
21from mycpp import mylib
22from mycpp.mylib import iteritems, print_stderr, log
23
24from typing import Dict, List, Optional, TYPE_CHECKING, cast
25if TYPE_CHECKING:
26 from _devbuild.gen.syntax_asdl import command_t
27 from core import optview
28 from display import ui
29 from frontend import args
30 from frontend.parse_lib import ParseContext
31
32_ = log
33
34
35class TrapState(object):
36 """Traps are shell callbacks that the user wants to run on certain events.
37
38 There are 2 catogires:
39 1. Signals like SIGUSR1
40 2. Hooks like EXIT
41
42 Signal handlers execute in the main loop, and within blocking syscalls.
43
44 EXIT, DEBUG, ERR, RETURN execute in specific places in the interpreter.
45 """
46
47 def __init__(self, signal_safe):
48 # type: (iolib.SignalSafe) -> None
49 self.signal_safe = signal_safe
50 self.hooks = {} # type: Dict[str, command_t]
51 self.traps = {} # type: Dict[int, command_t]
52
53 def ClearForSubProgram(self, inherit_errtrace):
54 # type: (bool) -> None
55 """SubProgramThunk uses this because traps aren't inherited."""
56
57 # bash clears hooks like DEBUG in subshells.
58 # The ERR can be preserved if set -o errtrace
59 hook_err = self.hooks.get('ERR')
60 self.hooks.clear()
61 if hook_err is not None and inherit_errtrace:
62 self.hooks['ERR'] = hook_err
63
64 self.traps.clear()
65
66 def GetHook(self, hook_name):
67 # type: (str) -> command_t
68 """ e.g. EXIT hook. """
69 return self.hooks.get(hook_name, None)
70
71 def GetTrap(self, sig_num):
72 # type: (int) -> command_t
73 return self.traps.get(sig_num, None)
74
75 def _AddUserTrap(self, sig_num, handler):
76 # type: (int, command_t) -> None
77 """ e.g. SIGUSR1 """
78 self.traps[sig_num] = handler
79
80 if handler.tag() == command_e.NoOp:
81 # This is the case:
82 # trap '' SIGINT SIGWINCH
83 # It's handled the same as removing a trap:
84 # trap - SIGINT SIGWINCH
85 #
86 # That is, the signal_safe calls are the same. This seems right
87 # because the shell interpreter itself cares about SIGINT and
88 # SIGWINCH too -- not just user traps.
89 if sig_num == SIGINT:
90 self.signal_safe.SetSigIntTrapped(False)
91 elif sig_num == SIGWINCH:
92 self.signal_safe.SetSigWinchCode(iolib.UNTRAPPED_SIGWINCH)
93 else:
94 iolib.sigaction(sig_num, SIG_IGN)
95 else:
96 if sig_num == SIGINT:
97 # Don't disturb the underlying runtime's SIGINT handllers
98 # 1. CPython has one for KeyboardInterrupt
99 # 2. mycpp runtime simulates KeyboardInterrupt:
100 # pyos::InitSignalSafe() calls RegisterSignalInterest(SIGINT),
101 # then we PollSigInt() in the osh/cmd_eval.py main loop
102 self.signal_safe.SetSigIntTrapped(True)
103 elif sig_num == SIGWINCH:
104 self.signal_safe.SetSigWinchCode(SIGWINCH)
105 else:
106 iolib.RegisterSignalInterest(sig_num)
107
108 def _RemoveUserTrap(self, sig_num):
109 # type: (int) -> None
110
111 mylib.dict_erase(self.traps, sig_num)
112
113 if sig_num == SIGINT:
114 self.signal_safe.SetSigIntTrapped(False)
115 elif sig_num == SIGWINCH:
116 self.signal_safe.SetSigWinchCode(iolib.UNTRAPPED_SIGWINCH)
117 else:
118 # TODO: In process.InitInteractiveShell(), 4 signals are set to
119 # SIG_IGN, not SIG_DFL:
120 #
121 # SIGQUIT SIGTSTP SIGTTOU SIGTTIN
122 #
123 # Should we restore them? It's rare that you type 'trap' in
124 # interactive shells, but it might be more correct. See what other
125 # shells do.
126 iolib.sigaction(sig_num, SIG_DFL)
127
128 def AddItem(self, parsed_id, handler):
129 # type: (str, command_t) -> None
130 """Add trap or hook, parsed to EXIT or INT (not 0 or SIGINT)"""
131 if parsed_id in _HOOK_NAMES:
132 self.hooks[parsed_id] = handler
133 else:
134 sig_num = signal_def.GetNumber(parsed_id)
135 # Should have already been validated
136 assert sig_num is not signal_def.NO_SIGNAL
137
138 self._AddUserTrap(sig_num, handler)
139
140 def RemoveItem(self, parsed_id):
141 # type: (str) -> None
142 """Remove trap or hook, parsed to EXIT or INT (not 0 or SIGINT)"""
143 if parsed_id in _HOOK_NAMES:
144 mylib.dict_erase(self.hooks, parsed_id)
145 else:
146 sig_num = signal_def.GetNumber(parsed_id)
147 # Should have already been validated
148 assert sig_num is not signal_def.NO_SIGNAL
149
150 self._RemoveUserTrap(sig_num)
151
152 def GetPendingTraps(self):
153 # type: () -> Optional[List[command_t]]
154 """Transfer ownership of queue of pending trap handlers to caller."""
155 signals = self.signal_safe.TakePendingSignals()
156 if 0:
157 log('*** GetPendingTraps')
158 for si in signals:
159 log('SIGNAL %d', si)
160 #import traceback
161 #traceback.print_stack()
162
163 # Optimization for the common case: do not allocate a list. This function
164 # is called in the interpreter loop.
165 if len(signals) == 0:
166 self.signal_safe.ReuseEmptyList(signals)
167 return None
168
169 run_list = [] # type: List[command_t]
170 for sig_num in signals:
171 node = self.traps.get(sig_num, None)
172 if node is not None:
173 run_list.append(node)
174
175 # Optimization to avoid allocation in the main loop.
176 del signals[:]
177 self.signal_safe.ReuseEmptyList(signals)
178
179 return run_list
180
181 def ThisProcessHasTraps(self):
182 # type: () -> bool
183 """
184 noforklast optimizations are not enabled when the process has code to
185 run after fork!
186 """
187 if 0:
188 log('traps %d', len(self.traps))
189 log('hooks %d', len(self.hooks))
190 return len(self.traps) != 0 or len(self.hooks) != 0
191
192
193_HOOK_NAMES = ['EXIT', 'ERR', 'RETURN', 'DEBUG']
194
195
196def _ParseSignalOrHook(user_str, blame_loc, allow_legacy=True):
197 # type: (str, loc_t, bool) -> str
198 """Convert user string to a parsed/normalized string.
199
200 These can be passed to AddItem() and RemoveItem()
201
202 See unit tests in builtin/trap_osh_test.py
203 '0' -> 'EXIT'
204 'EXIT' -> 'EXIT'
205 'eXIT' -> 'EXIT'
206
207 '2' -> 'INT'
208 'iNT' -> 'INT'
209 'sIGINT' -> 'INT'
210
211 'zz' -> error
212 '-150' -> error
213 '10000' -> error
214 """
215 if allow_legacy and user_str.isdigit():
216 try:
217 sig_num = int(user_str)
218 except ValueError:
219 raise error.Usage("got overflowing integer: %s" % user_str,
220 blame_loc)
221
222 if sig_num == 0: # Special case
223 return 'EXIT'
224
225 name = signal_def.GetName(sig_num)
226 if name is None:
227 return None
228 return name[3:] # Remove SIG
229
230 user_str = user_str.upper() # Ignore case
231
232 if user_str in _HOOK_NAMES:
233 return user_str
234
235 if user_str.startswith('SIG'):
236 user_str = user_str[3:]
237
238 n = signal_def.GetNumber(user_str)
239 if n == signal_def.NO_SIGNAL:
240 return None
241
242 return user_str
243
244
245def ParseSignalOrHook(user_str, blame_loc, allow_legacy=True):
246 # type: (str, loc_t, bool) -> str
247 """Convenience wrapper"""
248 parsed_id = _ParseSignalOrHook(user_str,
249 blame_loc,
250 allow_legacy=allow_legacy)
251 if parsed_id is None:
252 raise error.Usage('expected signal or hook, got %r' % user_str,
253 blame_loc)
254 return parsed_id
255
256
257class Trap(vm._Builtin):
258
259 def __init__(self, trap_state, parse_ctx, exec_opts, tracer, errfmt):
260 # type: (TrapState, ParseContext, optview.Exec, dev.Tracer, ui.ErrorFormatter) -> None
261 self.trap_state = trap_state
262 self.parse_ctx = parse_ctx
263 self.arena = parse_ctx.arena
264 self.exec_opts = exec_opts
265 self.tracer = tracer
266 self.errfmt = errfmt
267
268 def _ParseTrapCode(self, code_str):
269 # type: (str) -> command_t
270 """
271 Returns:
272 A node, or None if the code is invalid.
273 """
274 line_reader = reader.StringLineReader(code_str, self.arena)
275 c_parser = self.parse_ctx.MakeOshParser(line_reader)
276
277 # TODO: the SPID should be passed through argv.
278 src = source.Dynamic('trap arg', loc.Missing)
279 with alloc.ctx_SourceCode(self.arena, src):
280 try:
281 node = main_loop.ParseWholeFile(c_parser)
282 except error.Parse as e:
283 self.errfmt.PrettyPrintError(e)
284 return None
285
286 return node
287
288 def _GetCommandSourceCode(self, body):
289 # type: (command_t) -> str
290
291 # TODO: Print ANY command_t variant
292 handler_string = '<unknown>' # type: str
293
294 if body.tag() == command_e.Simple:
295 simple_cmd = cast(command.Simple, body)
296 if simple_cmd.blame_tok:
297 handler_string = simple_cmd.blame_tok.line.content
298 return handler_string
299
300 def _PrintTrapEntry(self, handler, name):
301 # type: (command_t, str) -> None
302 if handler.tag() == command_e.NoOp:
303 print("trap -- '' %s" % name)
304 else:
305 code = self._GetCommandSourceCode(handler)
306 print("trap -- %s %s" % (j8_lite.ShellEncode(code), name))
307
308 def _PrintState(self):
309 # type: () -> None
310 for name, handler in iteritems(self.trap_state.hooks):
311 self._PrintTrapEntry(handler, name)
312
313 # Print in order of signal number
314 n = signal_def.MaxSigNumber() + 1
315 for sig_num in xrange(n):
316 handler = self.trap_state.GetTrap(sig_num)
317 if handler is None:
318 continue
319
320 sig_name = signal_def.GetName(sig_num)
321 assert sig_name is not None
322
323 self._PrintTrapEntry(handler, sig_name)
324
325 def _PrintNames(self):
326 # type: () -> None
327 for hook_name in _HOOK_NAMES:
328 # EXIT is 0, but we hide that
329 print(' %s' % hook_name)
330
331 process_osh.PrintSignals()
332
333 def _AddTheRest(self, arg_r, node, allow_legacy=True):
334 # type: (args.Reader, command_t, bool) -> int
335 """Add a handler for all args"""
336 while not arg_r.AtEnd():
337 arg_str, arg_loc = arg_r.Peek2()
338 parsed_id = ParseSignalOrHook(arg_str,
339 arg_loc,
340 allow_legacy=allow_legacy)
341
342 if parsed_id == 'RETURN':
343 print_stderr("osh warning: The %r hook isn't implemented" %
344 arg_str)
345 if parsed_id == 'STOP' or parsed_id == 'KILL':
346 self.errfmt.Print_("Signal %r can't be handled" % arg_str,
347 blame_loc=arg_loc)
348 # Other shells return 0, but this seems like an obvious error
349 return 2
350
351 self.trap_state.AddItem(parsed_id, node)
352
353 arg_r.Next()
354 return 0
355
356 def _RemoveTheRest(self, arg_r, allow_legacy=True):
357 # type: (args.Reader, bool) -> None
358 """Remove handlers for all args"""
359 while not arg_r.AtEnd():
360 arg_str, arg_loc = arg_r.Peek2()
361 parsed_id = ParseSignalOrHook(arg_str,
362 arg_loc,
363 allow_legacy=allow_legacy)
364 self.trap_state.RemoveItem(parsed_id)
365 arg_r.Next()
366
367 def Run(self, cmd_val):
368 # type: (cmd_value.Argv) -> int
369 attrs, arg_r = flag_util.ParseCmdVal('trap',
370 cmd_val,
371 accept_typed_args=True)
372 arg = arg_types.trap(attrs.attrs)
373
374 if arg.add: # trap --add
375 cmd_frag = typed_args.RequiredBlockAsFrag(cmd_val)
376 return self._AddTheRest(arg_r, cmd_frag, allow_legacy=False)
377
378 if arg.ignore: # trap --ignore
379 return self._AddTheRest(arg_r, command.NoOp, allow_legacy=False)
380
381 if arg.remove: # trap --remove
382 self._RemoveTheRest(arg_r, allow_legacy=False)
383 return 0
384
385 if arg.p: # trap -p prints handlers
386 self._PrintState()
387 return 0
388
389 if arg.l: # List valid signals and hooks
390 self._PrintNames()
391 return 0
392
393 # Anything other than the above is not supported in YSH pass
394 if self.exec_opts.simple_trap_builtin():
395 raise error.Usage(
396 'expected --add, --remove, -l, or -p (simple_trap_builtin)',
397 cmd_val.arg_locs[0])
398
399 # 'trap' with no arguments is equivalent to 'trap -p'
400 if arg_r.AtEnd():
401 self._PrintState()
402 return 0
403
404 first_arg, first_loc = arg_r.Peek2()
405
406 # If the first arg is '-' or an unsigned integer, then remove the
407 # handlers. For example, 'trap 0 2' or 'trap 0 SIGINT'
408 #
409 # https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html#tag_18_28
410 first_is_dash = (first_arg == '-')
411 if first_is_dash or first_arg.isdigit():
412 if first_is_dash:
413 arg_r.Next()
414
415 self._RemoveTheRest(arg_r)
416 return 0
417
418 arg_r.Next()
419
420 # If first arg is empty string '', ignore the specified signals
421 if len(first_arg) == 0:
422 return self._AddTheRest(arg_r, command.NoOp)
423
424 # Legacy behavior for only one arg: 'trap SIGNAL' removes the handler
425 if arg_r.AtEnd():
426 parsed_id = ParseSignalOrHook(first_arg, first_loc)
427 self.trap_state.RemoveItem(parsed_id)
428 return 0
429
430 # Unlike other shells, we parse the code upon registration
431 node = self._ParseTrapCode(first_arg)
432 if node is None:
433 return 1 # _ParseTrapCode() prints an error for us.
434
435 # trap COMMAND SIGNAL+
436 return self._AddTheRest(arg_r, node)