Permalink
Browse files

trap: Implement signal handlers and the EXIT hook.

- Fix removal of hooks/signal handlers.
- Warn if the ERR, DEBUG, or RETURN hook is used.

Addresses issue #60.
  • Loading branch information...
Andy Chu
Andy Chu committed Jan 21, 2018
1 parent 2cc3933 commit 889f4d1a7f08e6da1cb30f18d3a13e0d424fc3ea
Showing with 104 additions and 21 deletions.
  1. +1 −1 bin/oil.py
  2. +64 −13 core/builtin.py
  3. +32 −5 core/cmd_exec.py
  4. +6 −1 spec/builtin-trap.test.sh
  5. +1 −1 test/spec.sh
View
@@ -406,7 +406,7 @@ def OshMain(argv, login_shell):
if do_exec:
_tlog('Execute(node)')
status = ex.Execute(node)
status = ex.Execute(node, run_exit_trap=True)
# We only do this in the "happy" case for now. ex.Execute() can raise
# exceptions.
View
@@ -983,6 +983,30 @@ def DeclareTypeset(argv, mem, funcs):
import signal
class _TrapThunk(object):
"""A function that is called by Python's signal module.
Similar to process.SubProgramThunk."""
def __init__(self, ex, node):
self.ex = ex
self.node = node
def __call__(self, unused_signalnum, unused_frame):
"""For Python's signal module."""
self.Run()
def Run(self):
"""For hooks."""
unused_status = self.ex.Execute(self.node)
def __str__(self):
# Used by trap -p
# TODO: Abbreviate with fmt.PrettyPrint?
return str(self.node)
def _MakeSignals():
"""Piggy-back on CPython to get a list of portable signals.
@@ -1006,10 +1030,17 @@ def _MakeSignals():
TRAP_SPEC.ShortFlag('-p')
TRAP_SPEC.ShortFlag('-l')
# TODO:
#
# bash's default -p looks like this:
# trap -- '' SIGTSTP
# trap -- '' SIGTTIN
# trap -- '' SIGTTOU
#
# CPython registers different default handlers. Wait until C++ rewrite to
def Trap(argv, traps, ex):
arg, i = TRAP_SPEC.Parse(argv)
status = 0
if arg.p: # Print registered handlers
for name, value in traps.iteritems():
@@ -1041,30 +1072,50 @@ def Trap(argv, traps, ex):
# NOTE: sig_spec isn't validated when removing handlers.
if code_str == '-':
try:
del traps[sig_spec]
except KeyError:
pass
return 0
if sig_spec in _HOOK_NAMES:
try:
del traps[sig_spec]
except KeyError:
pass
return 0
sig_val = _SIGNAL_NAMES.get(sig_spec)
if sig_val is not None:
try:
del traps[sig_spec]
except KeyError:
pass
# Restore default
signal.signal(sig_val, signal.SIG_DFL)
return 0
# Try parsing code first.
util.error("Can't remove invalid trap %r" % sig_spec)
return 1
# Try parsing the code first.
node = ex.ParseTrapCode(code_str)
if node is None:
return 1 # ParseTrapCode() prints an error for us.
# Register a hook
# Register a hook.
if sig_spec in _HOOK_NAMES:
traps[sig_spec] = node
if sig_spec in ('ERR', 'RETURN', 'DEBUG'):
util.warn("*** The %r isn't yet implemented in OSH ***", sig_spec)
traps[sig_spec] = _TrapThunk(ex, node)
return 0
# Register a signal
# Register a signal.
sig_val = _SIGNAL_NAMES.get(sig_spec)
if sig_val is not None:
traps[sig_spec] = node
# TODO: call signal.signal()
handler = _TrapThunk(ex, node)
# For signal handlers, the traps dictionary is used only for debugging.
traps[sig_spec] = handler
signal.signal(sig_val, handler)
return 0
util.error('Invalid signal %r' % sig_spec)
util.error('Invalid trap %r' % sig_spec)
return 1
# Example:
View
@@ -133,7 +133,7 @@ def __init__(self, mem, fd_state, status_lines, funcs, completion,
self.arith_ev = expr_eval.ArithEvaluator(mem, exec_opts, self.word_ev)
self.bool_ev = expr_eval.BoolEvaluator(mem, exec_opts, self.word_ev)
self.traps = {} # signal/hook -> LST node
self.traps = {} # signal/hook name -> callable
self.dir_stack = state.DirStack()
# TODO: Pass these in from main()
@@ -1100,7 +1100,8 @@ def _Dispatch(self, node, fork_external):
return status, check_errexit
def _Execute(self, node, fork_external=True):
"""
"""Does redirects, calls _Dispatch(), and does errexit check.
Args:
node: of type AstNode
fork_external: if we get a SimpleCommand that is an external command,
@@ -1160,9 +1161,28 @@ def _ExecuteList(self, children):
status = self._Execute(child) # last status wins
return status
def Execute(self, node, fork_external=True):
"""Execute a top level LST node."""
# Use exceptions internally, but exit codes externally.
def Execute(self, node, fork_external=True, run_exit_trap=False):
"""Execute a subprogram, handling _ControlFlow and fatal exceptions.
Callers:
- SubProgramThunk for pipelines, subshell, command sub, process sub
- .oilrc
- _TrapThunk
- Interactive loop
- main program
Most other clients call _Execute():
- _Source() for source builtin
- _Eval() for eval builtin
- RunFunc() for function call
Args:
node: LST subtree
fork_external: whether external commands require forking
Returns:
status: numeric exit code
"""
try:
status = self._Execute(node, fork_external=fork_external)
except _ControlFlow as e:
@@ -1176,7 +1196,14 @@ def Execute(self, node, fork_external=True):
print('osh failed: %s' % e.UserErrorString(), file=sys.stderr)
status = e.exit_status if e.exit_status is not None else 1
# TODO: dump self.mem if requested. Maybe speify with OIL_DUMP_PREFIX.
finally:
if run_exit_trap:
# NOTE: The trap itself can call exit!
thunk = self.traps.get('EXIT')
if thunk:
thunk.Run()
# Other exceptions: SystemExit for sys.exit()
return status
def RunCommandSub(self, node):
@@ -6,14 +6,19 @@ trap -l | grep SIGINT >/dev/null
## N-I dash/mksh status: 1
### trap -p
trap -p | grep trap >/dev/null
trap 'echo exit' EXIT
trap -p | grep EXIT >/dev/null
## status: 0
## N-I dash/mksh status: 1
### Register invalid trap
trap 'foo' SIGINVALID
## status: 1
### Remove invalid trap
trap - SIGINVALID
## status: 1
### Invalid trap invocation
trap 'foo'
echo status=$?
View
@@ -290,7 +290,7 @@ builtin-test() {
}
builtin-trap() {
sh-spec spec/builtin-trap.test.sh --osh-failures-allowed 5 \
sh-spec spec/builtin-trap.test.sh --osh-failures-allowed 3 \
${REF_SHELLS[@]} $OSH "$@"
}

0 comments on commit 889f4d1

Please sign in to comment.