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

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