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

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