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

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