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

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