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

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