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

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