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

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