Skip to content

Commit

Permalink
[refactor] Move signal handling from cpp/ -> mycpp/iolib
Browse files Browse the repository at this point in the history
Because mylib::Stdin()->readline() needs to raise KeyboardInterrupt!

Signal handling is inherently part of the mycpp runtime.
  • Loading branch information
Andy C committed Oct 29, 2024
1 parent 2736350 commit b546dff
Show file tree
Hide file tree
Showing 22 changed files with 559 additions and 488 deletions.
1 change: 1 addition & 0 deletions build/dynamic-deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ frontend/py.*\.py # py_readline.py ported by hand to C++
frontend/consts.py # frontend/consts_gen.py
frontend/match.py # frontend/lexer_gen.py
mycpp/iolib.py # Implemented in gc_iolib.{h,cC}
mycpp/mops.py # Implemented in gc_mops.{h,cC}
pgen2/grammar.py # These files are re-done in C++
Expand Down
10 changes: 5 additions & 5 deletions builtin/trap_osh.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
from core import dev
from core import error
from core import main_loop
from core import pyos
from core import vm
from frontend import flag_util
from frontend import match
from frontend import reader
from frontend import signal_def
from mycpp import iolib
from mycpp import mylib
from mycpp.mylib import iteritems, print_stderr, log
from mycpp import mops
Expand All @@ -42,7 +42,7 @@ class TrapState(object):
"""

def __init__(self, signal_safe):
# type: (pyos.SignalSafe) -> None
# type: (iolib.SignalSafe) -> None
self.signal_safe = signal_safe
self.hooks = {} # type: Dict[str, command_t]
self.traps = {} # type: Dict[int, command_t]
Expand Down Expand Up @@ -88,7 +88,7 @@ def AddUserTrap(self, sig_num, handler):
elif sig_num == SIGWINCH:
self.signal_safe.SetSigWinchCode(SIGWINCH)
else:
pyos.RegisterSignalInterest(sig_num)
iolib.RegisterSignalInterest(sig_num)

def RemoveUserTrap(self, sig_num):
# type: (int) -> None
Expand All @@ -99,7 +99,7 @@ def RemoveUserTrap(self, sig_num):
self.signal_safe.SetSigIntTrapped(False)
pass
elif sig_num == SIGWINCH:
self.signal_safe.SetSigWinchCode(pyos.UNTRAPPED_SIGWINCH)
self.signal_safe.SetSigWinchCode(iolib.UNTRAPPED_SIGWINCH)
else:
# TODO: In process.InitInteractiveShell(), 4 signals are set to
# SIG_IGN, not SIG_DFL:
Expand All @@ -109,7 +109,7 @@ def RemoveUserTrap(self, sig_num):
# Should we restore them? It's rare that you type 'trap' in
# interactive shells, but it might be more correct. See what other
# shells do.
pyos.sigaction(sig_num, SIG_DFL)
iolib.sigaction(sig_num, SIG_DFL)

def GetPendingTraps(self):
# type: () -> Optional[List[command_t]]
Expand Down
4 changes: 2 additions & 2 deletions core/comp_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
if TYPE_CHECKING:
from frontend.py_readline import Readline
from core.util import _DebugFile
from core import pyos
from mycpp import iolib

# ANSI escape codes affect the prompt!
# https://superuser.com/questions/301353/escape-non-printing-characters-in-a-function-for-a-bash-prompt
Expand Down Expand Up @@ -316,7 +316,7 @@ def __init__(
prompt_state, # type: PromptState
debug_f, # type: _DebugFile
readline, # type: Optional[Readline]
signal_safe, # type: pyos.SignalSafe
signal_safe, # type: iolib.SignalSafe
):
# type: (...) -> None
"""
Expand Down
25 changes: 13 additions & 12 deletions core/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from data_lang import j8_lite
from frontend import location
from frontend import match
from mycpp import iolib
from mycpp import mylib
from mycpp.mylib import log, print_stderr, probe, tagswitch, iteritems

Expand Down Expand Up @@ -109,27 +110,27 @@ def __exit__(self, type, value, traceback):


def InitInteractiveShell(signal_safe):
# type: (pyos.SignalSafe) -> None
# type: (iolib.SignalSafe) -> None
"""Called when initializing an interactive shell."""

# The shell itself should ignore Ctrl-\.
pyos.sigaction(SIGQUIT, SIG_IGN)
iolib.sigaction(SIGQUIT, SIG_IGN)

# This prevents Ctrl-Z from suspending OSH in interactive mode.
pyos.sigaction(SIGTSTP, SIG_IGN)
iolib.sigaction(SIGTSTP, SIG_IGN)

# More signals from
# https://www.gnu.org/software/libc/manual/html_node/Initializing-the-Shell.html
# (but not SIGCHLD)
pyos.sigaction(SIGTTOU, SIG_IGN)
pyos.sigaction(SIGTTIN, SIG_IGN)
iolib.sigaction(SIGTTOU, SIG_IGN)
iolib.sigaction(SIGTTIN, SIG_IGN)

# Register a callback to receive terminal width changes.
# NOTE: In line_input.c, we turned off rl_catch_sigwinch.

# This is ALWAYS on, which means that it can cause EINTR, and wait() and
# read() have to handle it
pyos.RegisterSignalInterest(SIGWINCH)
iolib.RegisterSignalInterest(SIGWINCH)


def SaveFd(fd):
Expand Down Expand Up @@ -1073,23 +1074,23 @@ def StartProcess(self, why):
# shouldn't have this.
# https://docs.python.org/2/library/signal.html
# See Python/pythonrun.c.
pyos.sigaction(SIGPIPE, SIG_DFL)
iolib.sigaction(SIGPIPE, SIG_DFL)

# Respond to Ctrl-\ (core dump)
pyos.sigaction(SIGQUIT, SIG_DFL)
iolib.sigaction(SIGQUIT, SIG_DFL)

# Only standalone children should get Ctrl-Z. Pipelines remain in the
# foreground because suspending them is difficult with our 'lastpipe'
# semantics.
pid = posix.getpid()
if posix.getpgid(0) == pid and self.parent_pipeline is None:
pyos.sigaction(SIGTSTP, SIG_DFL)
iolib.sigaction(SIGTSTP, SIG_DFL)

# More signals from
# https://www.gnu.org/software/libc/manual/html_node/Launching-Jobs.html
# (but not SIGCHLD)
pyos.sigaction(SIGTTOU, SIG_DFL)
pyos.sigaction(SIGTTIN, SIG_DFL)
iolib.sigaction(SIGTTOU, SIG_DFL)
iolib.sigaction(SIGTTIN, SIG_DFL)

self.tracer.OnNewProcess(pid)
# clear foreground pipeline for subshells
Expand Down Expand Up @@ -1861,7 +1862,7 @@ class Waiter(object):
"""

def __init__(self, job_list, exec_opts, signal_safe, tracer):
# type: (JobList, optview.Exec, pyos.SignalSafe, dev.Tracer) -> None
# type: (JobList, optview.Exec, iolib.SignalSafe, dev.Tracer) -> None
self.job_list = job_list
self.exec_opts = exec_opts
self.signal_safe = signal_safe
Expand Down
160 changes: 1 addition & 159 deletions core/pyos.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
from errno import EINTR
import pwd
import resource
import signal
import select
import sys
import termios # for read -n
import time

from mycpp.iolib import gSignalSafe
from mycpp import mops
from mycpp.mylib import log

Expand Down Expand Up @@ -282,164 +282,6 @@ def InputAvailable(fd):
return len(r) != 0


UNTRAPPED_SIGWINCH = -1


class SignalSafe(object):
"""State that is shared between the main thread and signal handlers.
See C++ implementation in cpp/core.h
"""

def __init__(self):
# type: () -> None
self.pending_signals = [] # type: List[int]
self.last_sig_num = 0 # type: int
self.sigint_trapped = False
self.received_sigint = False
self.received_sigwinch = False
self.sigwinch_code = UNTRAPPED_SIGWINCH

def UpdateFromSignalHandler(self, sig_num, unused_frame):
# type: (int, Any) -> None
"""Receive the given signal, and update shared state.
This method is registered as a Python signal handler.
"""
self.pending_signals.append(sig_num)

if sig_num == signal.SIGINT:
self.received_sigint = True

if sig_num == signal.SIGWINCH:
self.received_sigwinch = True
sig_num = self.sigwinch_code # mutate param

self.last_sig_num = sig_num

def LastSignal(self):
# type: () -> int
"""Return the number of the last signal received."""
return self.last_sig_num

def PollSigInt(self):
# type: () -> bool
"""Has SIGINT received since the last time PollSigInt() was called?"""
result = self.received_sigint
self.received_sigint = False
return result

def PollUntrappedSigInt(self):
# type: () -> bool
"""Has SIGINT received since the last time PollSigInt() was called?"""
received = self.PollSigInt()
return received and not self.sigint_trapped

if 0:

def SigIntTrapped(self):
# type: () -> bool
return self.sigint_trapped

def SetSigIntTrapped(self, b):
# type: (bool) -> None
"""Set a flag to tell us whether sigint is trapped by the user."""
self.sigint_trapped = b

def SetSigWinchCode(self, code):
# type: (int) -> None
"""Depending on whether or not SIGWINCH is trapped by a user, it is
expected to report a different code to `wait`.
SetSigWinchCode() lets us set which code is reported.
"""
self.sigwinch_code = code

def PollSigWinch(self):
# type: () -> bool
"""Has SIGWINCH been received since the last time PollSigWinch() was
called?"""
result = self.received_sigwinch
self.received_sigwinch = False
return result

def TakePendingSignals(self):
# type: () -> List[int]
"""Transfer ownership of queue of pending signals to caller."""

# A note on signal-safety here. The main loop might be calling this function
# at the same time a signal is firing and appending to
# `self.pending_signals`. We can forgoe using a lock here
# (which would be problematic for the signal handler) because mutual
# exclusivity should be maintained by the atomic nature of pointer
# assignment (i.e. word-sized writes) on most modern platforms.
# The replacement run list is allocated before the swap, so it can be
# interrupted at any point without consequence.
# This means the signal handler always has exclusive access to
# `self.pending_signals`. In the worst case the signal handler might write to
# `new_queue` and the corresponding trap handler won't get executed
# until the main loop calls this function again.
# NOTE: It's important to distinguish between signal-safety an
# thread-safety here. Signals run in the same process context as the main
# loop, while concurrent threads do not and would have to worry about
# cache-coherence and instruction reordering.
new_queue = [] # type: List[int]
ret = self.pending_signals
self.pending_signals = new_queue
return ret

def ReuseEmptyList(self, empty_list):
# type: (List[int]) -> None
"""This optimization only happens in C++."""
pass


gSignalSafe = None # type: SignalSafe

gOrigSigIntHandler = None # type: Any


def InitSignalSafe():
# type: () -> SignalSafe
"""Set global instance so the signal handler can access it."""
global gSignalSafe
gSignalSafe = SignalSafe()

# See
# - demo/cpython/keyboard_interrupt.py
# - pyos::InitSignalSafe()

# In C++, we do
# RegisterSignalInterest(signal.SIGINT)

global gOrigSigIntHandler
gOrigSigIntHandler = signal.signal(signal.SIGINT,
gSignalSafe.UpdateFromSignalHandler)

return gSignalSafe


def sigaction(sig_num, handler):
# type: (int, Any) -> None
"""
Handle a signal with SIG_DFL or SIG_IGN, not our own signal handler.
"""

# SIGINT and SIGWINCH must be registered through SignalSafe
assert sig_num != signal.SIGINT
assert sig_num != signal.SIGWINCH
signal.signal(sig_num, handler)


def RegisterSignalInterest(sig_num):
# type: (int) -> None
"""Have the kernel notify the main loop about the given signal."""
#log('RegisterSignalInterest %d', sig_num)

assert gSignalSafe is not None
signal.signal(sig_num, gSignalSafe.UpdateFromSignalHandler)


def MakeDirCacheKey(path):
# type: (str) -> Tuple[str, int]
"""Returns a pair (path with last modified time) that can be used to cache
Expand Down
7 changes: 2 additions & 5 deletions core/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from core import completion
from core import main_loop
from core import optview
from core import pyos
from core import process
from core import pyutil
from core import state
Expand Down Expand Up @@ -78,6 +77,7 @@
from osh import split
from osh import word_eval

from mycpp import iolib
from mycpp import mops
from mycpp import mylib
from mycpp.mylib import NewDict, iteritems, print_stderr, log
Expand Down Expand Up @@ -477,10 +477,7 @@ def Main(
multi_trace)
fd_state.tracer = tracer # circular dep

# RegisterSignalInterest should return old sigint handler
# then InteractiveLineReader can use it
# InteractiveLineReader
signal_safe = pyos.InitSignalSafe()
signal_safe = iolib.InitSignalSafe()
trap_state = trap_osh.TrapState(signal_safe)

waiter = process.Waiter(job_list, exec_opts, signal_safe, tracer)
Expand Down
Loading

0 comments on commit b546dff

Please sign in to comment.