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

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