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

304 lines, 160 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 mycpp.mylib import log
15from core import pyos
16from core import vm
17from frontend import flag_util
18from frontend import signal_def
19from frontend import reader
20from mycpp import mylib
21from mycpp.mylib import iteritems, print_stderr
22
23from typing import Dict, List, Optional, TYPE_CHECKING
24if TYPE_CHECKING:
25 from _devbuild.gen.syntax_asdl import command_t
26 from display import ui
27 from frontend.parse_lib import ParseContext
28
29_ = log
30
31
32class TrapState(object):
33 """Traps are shell callbacks that the user wants to run on certain events.
34
35 There are 2 catogires:
36 1. Signals like SIGUSR1
37 2. Hooks like EXIT
38
39 Signal handlers execute in the main loop, and within blocking syscalls.
40
41 EXIT, DEBUG, ERR, RETURN execute in specific places in the interpreter.
42 """
43
44 def __init__(self, signal_safe):
45 # type: (pyos.SignalSafe) -> None
46 self.signal_safe = signal_safe
47 self.hooks = {} # type: Dict[str, command_t]
48 self.traps = {} # type: Dict[int, command_t]
49
50 def ClearForSubProgram(self, inherit_errtrace):
51 # type: (bool) -> None
52 """SubProgramThunk uses this because traps aren't inherited."""
53
54 # bash clears hooks like DEBUG in subshells.
55 # The ERR can be preserved if set -o errtrace
56 hook_err = self.hooks.get('ERR')
57 self.hooks.clear()
58 if hook_err is not None and inherit_errtrace:
59 self.hooks['ERR'] = hook_err
60
61 self.traps.clear()
62
63 def GetHook(self, hook_name):
64 # type: (str) -> command_t
65 """ e.g. EXIT hook. """
66 return self.hooks.get(hook_name, None)
67
68 def AddUserHook(self, hook_name, handler):
69 # type: (str, command_t) -> None
70 self.hooks[hook_name] = handler
71
72 def RemoveUserHook(self, hook_name):
73 # type: (str) -> None
74 mylib.dict_erase(self.hooks, hook_name)
75
76 def AddUserTrap(self, sig_num, handler):
77 # type: (int, command_t) -> None
78 """ e.g. SIGUSR1 """
79 self.traps[sig_num] = handler
80
81 if sig_num == SIGINT:
82 # Don't disturb the underlying runtime's SIGINT handllers
83 # 1. CPython has one for KeyboardInterrupt
84 # 2. mycpp runtime simulates KeyboardInterrupt:
85 # pyos::InitSignalSafe() calls RegisterSignalInterest(SIGINT),
86 # then we PollSigInt() in the osh/cmd_eval.py main loop
87 self.signal_safe.SetSigIntTrapped(True)
88 elif sig_num == SIGWINCH:
89 self.signal_safe.SetSigWinchCode(SIGWINCH)
90 else:
91 pyos.RegisterSignalInterest(sig_num)
92
93 def RemoveUserTrap(self, sig_num):
94 # type: (int) -> None
95
96 mylib.dict_erase(self.traps, sig_num)
97
98 if sig_num == SIGINT:
99 self.signal_safe.SetSigIntTrapped(False)
100 pass
101 elif sig_num == SIGWINCH:
102 self.signal_safe.SetSigWinchCode(pyos.UNTRAPPED_SIGWINCH)
103 else:
104 # TODO: In process.InitInteractiveShell(), 4 signals are set to
105 # SIG_IGN, not SIG_DFL:
106 #
107 # SIGQUIT SIGTSTP SIGTTOU SIGTTIN
108 #
109 # Should we restore them? It's rare that you type 'trap' in
110 # interactive shells, but it might be more correct. See what other
111 # shells do.
112 pyos.sigaction(sig_num, SIG_DFL)
113
114 def GetPendingTraps(self):
115 # type: () -> Optional[List[command_t]]
116 """Transfer ownership of queue of pending trap handlers to caller."""
117 signals = self.signal_safe.TakePendingSignals()
118 if 0:
119 log('*** GetPendingTraps')
120 for si in signals:
121 log('SIGNAL %d', si)
122 #import traceback
123 #traceback.print_stack()
124
125 # Optimization for the common case: do not allocate a list. This function
126 # is called in the interpreter loop.
127 if len(signals) == 0:
128 self.signal_safe.ReuseEmptyList(signals)
129 return None
130
131 run_list = [] # type: List[command_t]
132 for sig_num in signals:
133 node = self.traps.get(sig_num, None)
134 if node is not None:
135 run_list.append(node)
136
137 # Optimization to avoid allocation in the main loop.
138 del signals[:]
139 self.signal_safe.ReuseEmptyList(signals)
140
141 return run_list
142
143 def ThisProcessHasTraps(self):
144 # type: () -> bool
145 """
146 noforklast optimizations are not enabled when the process has code to
147 run after fork!
148 """
149 if 0:
150 log('traps %d', len(self.traps))
151 log('hooks %d', len(self.hooks))
152 return len(self.traps) != 0 or len(self.hooks) != 0
153
154
155def _GetSignalNumber(sig_spec):
156 # type: (str) -> int
157
158 # POSIX lists the numbers that are required.
159 # http://pubs.opengroup.org/onlinepubs/9699919799/
160 #
161 # Added 13 for SIGPIPE because autoconf's 'configure' uses it!
162 if sig_spec.strip() in ('1', '2', '3', '6', '9', '13', '14', '15'):
163 return int(sig_spec)
164
165 # INT is an alias for SIGINT
166 if sig_spec.startswith('SIG'):
167 sig_spec = sig_spec[3:]
168 return signal_def.GetNumber(sig_spec)
169
170
171_HOOK_NAMES = ['EXIT', 'ERR', 'RETURN', 'DEBUG']
172
173# bash's default -p looks like this:
174# trap -- '' SIGTSTP
175# trap -- '' SIGTTIN
176# trap -- '' SIGTTOU
177#
178# CPython registers different default handlers. The C++ rewrite should make
179# OVM match sh/bash more closely.
180
181# Example of trap:
182# trap -- 'echo "hi there" | wc ' SIGINT
183#
184# Then hit Ctrl-C.
185
186
187class Trap(vm._Builtin):
188
189 def __init__(self, trap_state, parse_ctx, tracer, errfmt):
190 # type: (TrapState, ParseContext, dev.Tracer, ui.ErrorFormatter) -> None
191 self.trap_state = trap_state
192 self.parse_ctx = parse_ctx
193 self.arena = parse_ctx.arena
194 self.tracer = tracer
195 self.errfmt = errfmt
196
197 def _ParseTrapCode(self, code_str):
198 # type: (str) -> command_t
199 """
200 Returns:
201 A node, or None if the code is invalid.
202 """
203 line_reader = reader.StringLineReader(code_str, self.arena)
204 c_parser = self.parse_ctx.MakeOshParser(line_reader)
205
206 # TODO: the SPID should be passed through argv.
207 src = source.ArgvWord('trap', loc.Missing)
208 with alloc.ctx_SourceCode(self.arena, src):
209 try:
210 node = main_loop.ParseWholeFile(c_parser)
211 except error.Parse as e:
212 self.errfmt.PrettyPrintError(e)
213 return None
214
215 return node
216
217 def Run(self, cmd_val):
218 # type: (cmd_value.Argv) -> int
219 attrs, arg_r = flag_util.ParseCmdVal('trap', cmd_val)
220 arg = arg_types.trap(attrs.attrs)
221
222 if arg.p: # Print registered handlers
223 # The unit tests rely on this being one line.
224 # bash prints a line that can be re-parsed.
225 for name, _ in iteritems(self.trap_state.hooks):
226 print('%s TrapState' % (name, ))
227
228 for sig_num, _ in iteritems(self.trap_state.traps):
229 print('%d TrapState' % (sig_num, ))
230
231 return 0
232
233 if arg.l: # List valid signals and hooks
234 for hook_name in _HOOK_NAMES:
235 print(' %s' % hook_name)
236
237 signal_def.PrintSignals()
238
239 return 0
240
241 code_str = arg_r.ReadRequired('requires a code string')
242 sig_spec, sig_loc = arg_r.ReadRequired2(
243 'requires a signal or hook name')
244
245 # sig_key is NORMALIZED sig_spec: a signal number string or string hook
246 # name.
247 sig_key = None # type: Optional[str]
248 sig_num = signal_def.NO_SIGNAL
249
250 if sig_spec in _HOOK_NAMES:
251 sig_key = sig_spec
252 elif sig_spec == '0': # Special case
253 sig_key = 'EXIT'
254 else:
255 sig_num = _GetSignalNumber(sig_spec)
256 if sig_num != signal_def.NO_SIGNAL:
257 sig_key = str(sig_num)
258
259 if sig_key is None:
260 self.errfmt.Print_("Invalid signal or hook %r" % sig_spec,
261 blame_loc=cmd_val.arg_locs[2])
262 return 1
263
264 # NOTE: sig_spec isn't validated when removing handlers.
265 if code_str == '-':
266 if sig_key in _HOOK_NAMES:
267 self.trap_state.RemoveUserHook(sig_key)
268 return 0
269
270 if sig_num != signal_def.NO_SIGNAL:
271 self.trap_state.RemoveUserTrap(sig_num)
272 return 0
273
274 raise AssertionError('Signal or trap')
275
276 # Try parsing the code first.
277
278 # TODO: If simple_trap is on (for oil:upgrade), then it must be a function
279 # name? And then you wrap it in 'try'?
280
281 node = self._ParseTrapCode(code_str)
282 if node is None:
283 return 1 # ParseTrapCode() prints an error for us.
284
285 # Register a hook.
286 if sig_key in _HOOK_NAMES:
287 if sig_key == 'RETURN':
288 print_stderr("osh warning: The %r hook isn't implemented" %
289 sig_spec)
290 self.trap_state.AddUserHook(sig_key, node)
291 return 0
292
293 # Register a signal.
294 if sig_num != signal_def.NO_SIGNAL:
295 # For signal handlers, the traps dictionary is used only for debugging.
296 if sig_num in (SIGKILL, SIGSTOP):
297 self.errfmt.Print_("Signal %r can't be handled" % sig_spec,
298 blame_loc=sig_loc)
299 # Other shells return 0, but this seems like an obvious error
300 return 1
301 self.trap_state.AddUserTrap(sig_num, node)
302 return 0
303
304 raise AssertionError('Signal or trap')