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

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