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

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