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

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