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

295 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.
79
80 SIGUSR1.
81 """
82 self.traps[sig_num] = handler
83
84 if sig_num == SIGWINCH:
85 self.signal_safe.SetSigWinchCode(SIGWINCH)
86 else:
87 pyos.RegisterSignalInterest(sig_num)
88
89 def RemoveUserTrap(self, sig_num):
90 # type: (int) -> None
91
92 mylib.dict_erase(self.traps, sig_num)
93
94 if sig_num == SIGINT:
95 # Don't disturb the runtime signal handlers:
96 # 1. from CPython
97 # 2. pyos::InitSignalSafe() calls RegisterSignalInterest(SIGINT)
98 pass
99 elif sig_num == SIGWINCH:
100 self.signal_safe.SetSigWinchCode(pyos.UNTRAPPED_SIGWINCH)
101 else:
102 pyos.Sigaction(sig_num, SIG_DFL)
103
104 def GetPendingTraps(self):
105 # type: () -> Optional[List[command_t]]
106 """Transfer ownership of the current queue of pending trap handlers to
107 the caller."""
108 signals = self.signal_safe.TakePendingSignals()
109 if 0:
110 log('*** GetPendingTraps')
111 for si in signals:
112 log('SIGNAL %d', si)
113 #import traceback
114 #traceback.print_stack()
115
116 # Optimization for the common case: do not allocate a list. This function
117 # is called in the interpreter loop.
118 if len(signals) == 0:
119 self.signal_safe.ReuseEmptyList(signals)
120 return None
121
122 run_list = [] # type: List[command_t]
123 for sig_num in signals:
124 node = self.traps.get(sig_num, None)
125 if node is not None:
126 run_list.append(node)
127
128 # Optimization to avoid allocation in the main loop.
129 del signals[:]
130 self.signal_safe.ReuseEmptyList(signals)
131
132 return run_list
133
134 def ThisProcessHasTraps(self):
135 # type: () -> bool
136 """
137 noforklast optimizations are not enabled when the process has code to
138 run after fork!
139 """
140 if 0:
141 log('traps %d', len(self.traps))
142 log('hooks %d', len(self.hooks))
143 return len(self.traps) != 0 or len(self.hooks) != 0
144
145
146def _GetSignalNumber(sig_spec):
147 # type: (str) -> int
148
149 # POSIX lists the numbers that are required.
150 # http://pubs.opengroup.org/onlinepubs/9699919799/
151 #
152 # Added 13 for SIGPIPE because autoconf's 'configure' uses it!
153 if sig_spec.strip() in ('1', '2', '3', '6', '9', '13', '14', '15'):
154 return int(sig_spec)
155
156 # INT is an alias for SIGINT
157 if sig_spec.startswith('SIG'):
158 sig_spec = sig_spec[3:]
159 return signal_def.GetNumber(sig_spec)
160
161
162_HOOK_NAMES = ['EXIT', 'ERR', 'RETURN', 'DEBUG']
163
164# bash's default -p looks like this:
165# trap -- '' SIGTSTP
166# trap -- '' SIGTTIN
167# trap -- '' SIGTTOU
168#
169# CPython registers different default handlers. The C++ rewrite should make
170# OVM match sh/bash more closely.
171
172# Example of trap:
173# trap -- 'echo "hi there" | wc ' SIGINT
174#
175# Then hit Ctrl-C.
176
177
178class Trap(vm._Builtin):
179
180 def __init__(self, trap_state, parse_ctx, tracer, errfmt):
181 # type: (TrapState, ParseContext, dev.Tracer, ui.ErrorFormatter) -> None
182 self.trap_state = trap_state
183 self.parse_ctx = parse_ctx
184 self.arena = parse_ctx.arena
185 self.tracer = tracer
186 self.errfmt = errfmt
187
188 def _ParseTrapCode(self, code_str):
189 # type: (str) -> command_t
190 """
191 Returns:
192 A node, or None if the code is invalid.
193 """
194 line_reader = reader.StringLineReader(code_str, self.arena)
195 c_parser = self.parse_ctx.MakeOshParser(line_reader)
196
197 # TODO: the SPID should be passed through argv.
198 src = source.ArgvWord('trap', loc.Missing)
199 with alloc.ctx_SourceCode(self.arena, src):
200 try:
201 node = main_loop.ParseWholeFile(c_parser)
202 except error.Parse as e:
203 self.errfmt.PrettyPrintError(e)
204 return None
205
206 return node
207
208 def Run(self, cmd_val):
209 # type: (cmd_value.Argv) -> int
210 attrs, arg_r = flag_util.ParseCmdVal('trap', cmd_val)
211 arg = arg_types.trap(attrs.attrs)
212
213 if arg.p: # Print registered handlers
214 # The unit tests rely on this being one line.
215 # bash prints a line that can be re-parsed.
216 for name, _ in iteritems(self.trap_state.hooks):
217 print('%s TrapState' % (name, ))
218
219 for sig_num, _ in iteritems(self.trap_state.traps):
220 print('%d TrapState' % (sig_num, ))
221
222 return 0
223
224 if arg.l: # List valid signals and hooks
225 for hook_name in _HOOK_NAMES:
226 print(' %s' % hook_name)
227
228 signal_def.PrintSignals()
229
230 return 0
231
232 code_str = arg_r.ReadRequired('requires a code string')
233 sig_spec, sig_loc = arg_r.ReadRequired2(
234 'requires a signal or hook name')
235
236 # sig_key is NORMALIZED sig_spec: a signal number string or string hook
237 # name.
238 sig_key = None # type: Optional[str]
239 sig_num = signal_def.NO_SIGNAL
240
241 if sig_spec in _HOOK_NAMES:
242 sig_key = sig_spec
243 elif sig_spec == '0': # Special case
244 sig_key = 'EXIT'
245 else:
246 sig_num = _GetSignalNumber(sig_spec)
247 if sig_num != signal_def.NO_SIGNAL:
248 sig_key = str(sig_num)
249
250 if sig_key is None:
251 self.errfmt.Print_("Invalid signal or hook %r" % sig_spec,
252 blame_loc=cmd_val.arg_locs[2])
253 return 1
254
255 # NOTE: sig_spec isn't validated when removing handlers.
256 if code_str == '-':
257 if sig_key in _HOOK_NAMES:
258 self.trap_state.RemoveUserHook(sig_key)
259 return 0
260
261 if sig_num != signal_def.NO_SIGNAL:
262 self.trap_state.RemoveUserTrap(sig_num)
263 return 0
264
265 raise AssertionError('Signal or trap')
266
267 # Try parsing the code first.
268
269 # TODO: If simple_trap is on (for oil:upgrade), then it must be a function
270 # name? And then you wrap it in 'try'?
271
272 node = self._ParseTrapCode(code_str)
273 if node is None:
274 return 1 # ParseTrapCode() prints an error for us.
275
276 # Register a hook.
277 if sig_key in _HOOK_NAMES:
278 if sig_key == 'RETURN':
279 print_stderr("osh warning: The %r hook isn't implemented" %
280 sig_spec)
281 self.trap_state.AddUserHook(sig_key, node)
282 return 0
283
284 # Register a signal.
285 if sig_num != signal_def.NO_SIGNAL:
286 # For signal handlers, the traps dictionary is used only for debugging.
287 if sig_num in (SIGKILL, SIGSTOP):
288 self.errfmt.Print_("Signal %r can't be handled" % sig_spec,
289 blame_loc=sig_loc)
290 # Other shells return 0, but this seems like an obvious error
291 return 1
292 self.trap_state.AddUserTrap(sig_num, node)
293 return 0
294
295 raise AssertionError('Signal or trap')