Commit e18adbab authored by Kirill Smelkov's avatar Kirill Smelkov

Nogil signals

Provide os/signal package that can be used to setup signal delivery to nogil
channels. This way for user code signal handling becomes regular handling of a
signalling channel instead of being something special or limited to only-main
python thread. The rationale for why we need it is explained below:

There are several problems with regular python's stdlib signal module:

1. Python2 does not call signal handler from under blocked lock.acquire.
   This means that if the main thread is blocked waiting on a semaphore,
   signal delivery will be delayed indefinitely, similarly to e.g. problem
   described in nxdtest!14 (comment 147527)
   where raising KeyboardInterrupt is delayed after SIGINT for many,
   potentially unbounded, seconds until ~semaphore wait finishes.

   Note that Python3 does not have this problem wrt stdlib locks and
   semaphores, but read below for the next point.

2. all pygolang communication operations (channels send/recv, sync.Mutex,
   sync.RWMutex, sync.Sema, sync.WaitGroup, sync.WorkGroup, ...) run with
   GIL released, but if blocked do not handle EINTR and do not schedule
   python signal handler to run (on main thread).

   Even if we could theoretically adjust this behaviour of pygolang at python
   level to match Python3, there are also C++ and pyx/nogil worlds. And we want gil
   and nogil worlds to interoperate (see https://pypi.org/project/pygolang/#cython-nogil-api),
   so that e.g. if completely nogil code happens to run on the main thread,
   signal handling is still possible, even if that signal handling was setup at
   python level.

With signals delivered to nogil channels both nogil world and python
world can setup signal handlers and to be notified of them irregardles
of whether main python thread is currently blocked in nogil wait or not.

/reviewed-on !17
parent ce507f4e
Pipeline #19469 passed with stage
in 0 seconds
...@@ -23,6 +23,8 @@ include golang/io.h ...@@ -23,6 +23,8 @@ include golang/io.h
include golang/io.cpp include golang/io.cpp
include golang/os.h include golang/os.h
include golang/os.cpp include golang/os.cpp
include golang/os/signal.h
include golang/os/signal.cpp
include golang/strings.h include golang/strings.h
include golang/strings.cpp include golang/strings.cpp
include golang/strings_test.cpp include golang/strings_test.cpp
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
/_golang.cpp /_golang.cpp
/_golang_test.cpp /_golang_test.cpp
/_io.cpp /_io.cpp
/_os.cpp
/_os_test.cpp /_os_test.cpp
/_strings_test.cpp /_strings_test.cpp
/_sync.cpp /_sync.cpp
......
...@@ -191,6 +191,7 @@ cpdef pypanic(arg) ...@@ -191,6 +191,7 @@ cpdef pypanic(arg)
# pychan is python wrapper over chan<object> or chan<structZ|bool|int|double|...> # pychan is python wrapper over chan<object> or chan<structZ|bool|int|double|...>
from cython cimport final from cython cimport final
from golang cimport os
# DType describes type of channel elements. # DType describes type of channel elements.
# TODO consider supporting NumPy dtypes too. # TODO consider supporting NumPy dtypes too.
...@@ -200,7 +201,8 @@ cdef enum DType: ...@@ -200,7 +201,8 @@ cdef enum DType:
DTYPE_BOOL = 2 # chan[bool] DTYPE_BOOL = 2 # chan[bool]
DTYPE_INT = 3 # chan[int] DTYPE_INT = 3 # chan[int]
DTYPE_DOUBLE = 4 # chan[double] DTYPE_DOUBLE = 4 # chan[double]
DTYPE_NTYPES = 5 DTYPE_OS_SIGNAL = 5 # chan[os::Signal] TODO register dynamically
DTYPE_NTYPES = 6
# pychan wraps a channel into python object. # pychan wraps a channel into python object.
# #
...@@ -226,6 +228,8 @@ cdef class pychan: ...@@ -226,6 +228,8 @@ cdef class pychan:
chan[cbool] chan_bool (pychan pych) chan[cbool] chan_bool (pychan pych)
chan[int] chan_int (pychan pych) chan[int] chan_int (pychan pych)
chan[double] chan_double (pychan pych) chan[double] chan_double (pychan pych)
# TODO move vvv out of pychan after dtypes are registered dynamically
chan[os.Signal] _chan_osSignal (pychan pych)
# pychan.from_chan_X returns pychan wrapping pyx/nogil-level chan[X]. # pychan.from_chan_X returns pychan wrapping pyx/nogil-level chan[X].
# X can be any C-level type, but not PyObject. # X can be any C-level type, but not PyObject.
...@@ -237,6 +241,9 @@ cdef class pychan: ...@@ -237,6 +241,9 @@ cdef class pychan:
cdef pychan from_chan_int (chan[int] ch) cdef pychan from_chan_int (chan[int] ch)
@staticmethod @staticmethod
cdef pychan from_chan_double (chan[double] ch) cdef pychan from_chan_double (chan[double] ch)
# TODO move vvv out of pychan after dtypes are registered dynamically
@staticmethod
cdef pychan _from_chan_osSignal (chan[os.Signal] ch)
# pyerror wraps an error into python object. # pyerror wraps an error into python object.
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
# cython: binding=False # cython: binding=False
# cython: c_string_type=str, c_string_encoding=utf8 # cython: c_string_type=str, c_string_encoding=utf8
# distutils: language = c++ # distutils: language = c++
# distutils: depends = libgolang.h # distutils: depends = libgolang.h os/signal.h
# #
# Copyright (C) 2018-2022 Nexedi SA and Contributors. # Copyright (C) 2018-2022 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com> # Kirill Smelkov <kirr@nexedi.com>
...@@ -44,6 +44,8 @@ cdef extern from "Python.h": ...@@ -44,6 +44,8 @@ cdef extern from "Python.h":
from libcpp.vector cimport vector from libcpp.vector cimport vector
from cython cimport final from cython cimport final
from golang cimport os # TODO remove after dtypes are reworked to register dynamically
import sys import sys
# ---- panic ---- # ---- panic ----
...@@ -338,6 +340,10 @@ cdef class pychan: ...@@ -338,6 +340,10 @@ cdef class pychan:
pychan_asserttype(pych, DTYPE_DOUBLE) pychan_asserttype(pych, DTYPE_DOUBLE)
return _wrapchan[double] (pych._ch) return _wrapchan[double] (pych._ch)
chan[os.Signal] _chan_osSignal (pychan pych):
pychan_asserttype(pych, DTYPE_OS_SIGNAL)
return _wrapchan[os.Signal] (pych._ch)
# pychan <- chan[X] # pychan <- chan[X]
@staticmethod @staticmethod
cdef pychan from_chan_structZ (chan[structZ] ch): cdef pychan from_chan_structZ (chan[structZ] ch):
...@@ -355,6 +361,10 @@ cdef class pychan: ...@@ -355,6 +361,10 @@ cdef class pychan:
cdef pychan from_chan_double (chan[double] ch): cdef pychan from_chan_double (chan[double] ch):
return pychan_from_raw(ch._rawchan(), DTYPE_DOUBLE) return pychan_from_raw(ch._rawchan(), DTYPE_DOUBLE)
@staticmethod
cdef pychan _from_chan_osSignal (chan[os.Signal] ch):
return pychan_from_raw(ch._rawchan(), DTYPE_OS_SIGNAL)
cdef void pychan_asserttype(pychan pych, DType dtype) nogil: cdef void pychan_asserttype(pychan pych, DType dtype) nogil:
if pych.dtype != dtype: if pych.dtype != dtype:
panic("pychan: channel type mismatch") panic("pychan: channel type mismatch")
...@@ -741,6 +751,26 @@ cdef object double_c_to_py(const chanElemBuf *cfrom): ...@@ -741,6 +751,26 @@ cdef object double_c_to_py(const chanElemBuf *cfrom):
return (<double *>cfrom)[0] return (<double *>cfrom)[0]
# DTYPE_OS_SIGNAL
# TODO make dtype registration dynamic and move to _os.pyx
dtypeRegistry[<int>DTYPE_OS_SIGNAL] = DTypeInfo(
name = "C.os::Signal",
size = sizeof(os.Signal),
py_to_c = ossig_py_to_c,
c_to_py = ossig_c_to_py,
pynil = mkpynil(DTYPE_OS_SIGNAL),
)
cdef bint ossig_py_to_c(object obj, chanElemBuf *cto) except False:
if not isinstance(obj, os.PySignal):
raise TypeError("type mismatch: expect os.PySignal; got %r" % (obj,))
(<os.Signal*>cto)[0] = (<os.PySignal>obj).sig
cdef object ossig_c_to_py(const chanElemBuf *cfrom):
sig = (<os.Signal*>cfrom)[0]
return os.PySignal.from_sig(sig)
# verify at init time that sizeof(chanElemBuf) = max(_.size) # verify at init time that sizeof(chanElemBuf) = max(_.size)
cdef verify_chanElemBuf(): cdef verify_chanElemBuf():
cdef int size_max = 0 cdef int size_max = 0
...@@ -764,10 +794,17 @@ cdef DType parse_dtype(dtype) except <DType>-1: ...@@ -764,10 +794,17 @@ cdef DType parse_dtype(dtype) except <DType>-1:
return DTYPE_PYOBJECT return DTYPE_PYOBJECT
_ = name2dtype.get(dtype) _ = name2dtype.get(dtype)
if _ is None: if _ is not None:
raise TypeError("pychan: invalid dtype: %r" % (dtype,))
return _ return _
# accept classes with .dtype attribute
if hasattr(dtype, "dtype"):
_ = name2dtype.get(dtype.dtype)
if _ is not None:
return _
raise TypeError("pychan: invalid dtype: %r" % (dtype,))
# ---- strings ---- # ---- strings ----
......
# cython: language_level=2
# Copyright (C) 2021-2022 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""Package os mirrors Go package os.
- `Signal` represents OS-level signal.
See also https://golang.org/pkg/os for Go os package documentation.
"""
#from golang cimport string # TODO restore after golang.pyx stops to import os.pyx
from libcpp.string cimport string # golang::string = std::string TODO remove after ^^^
cdef extern from "golang/os.h" namespace "golang::os" nogil:
struct Signal:
int signo
string String()
Signal _Signal_from_int(int signo)
# ---- python bits ----
from cython cimport final
@final
cdef class PySignal:
cdef Signal sig
# PySignal.from_sig returns PySignal wrapping py/nogil-level Signal sig
@staticmethod
cdef PySignal from_sig(Signal sig)
# -*- coding: utf-8 -*-
# cython: language_level=2
# Copyright (C) 2021-2022 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""_os.pyx implements os.pyx - see _os.pxd for package overview."""
from __future__ import print_function, absolute_import
from golang cimport __pystr
from cython cimport final
# Signal represents an OS signal.
@final
cdef class PySignal:
dtype = "C.os::Signal"
@staticmethod
cdef PySignal from_sig(Signal sig):
cdef PySignal pysig = PySignal.__new__(PySignal)
pysig.sig = sig
return pysig
property signo:
def __get__(PySignal pysig):
return pysig.sig.signo
def __str__(PySignal pysig):
return __pystr(pysig.sig.String())
def __repr__(PySignal pysig):
return ("os.Signal(%d)" % pysig.sig.signo)
# PySignal == PySignal
def __hash__(PySignal pysig):
return <Py_hash_t>pysig.sig.signo
# NOTE __ne__ not needed: PySignal does not have base class and for that
# case cython automatically generates __ne__ based on __eq__.
def __eq__(PySignal a, object rhs):
if not isinstance(rhs, PySignal):
return False
cdef PySignal b = rhs
return (a.sig == b.sig)
def _PySignal_from_int(int signo): # -> PySignal
return PySignal.from_sig(_Signal_from_int(signo))
...@@ -31,6 +31,17 @@ ...@@ -31,6 +31,17 @@
#include <unistd.h> #include <unistd.h>
#include <string.h> #include <string.h>
#include <signal.h>
// GLIBC >= 2.32 provides sigdescr_np but not sys_siglist in its headers
// GLIBC < 2.32 provides sys_siglist but not sigdescr_np in its headers
// cut this short
// (on darwing sys_siglist declaration is normally provided)
#ifndef __APPLE__
extern "C" {
extern const char * const sys_siglist[];
}
#endif
using golang::internal::_runtime; using golang::internal::_runtime;
namespace sys = golang::internal::syscall; namespace sys = golang::internal::syscall;
...@@ -237,4 +248,24 @@ static error _pathError(const char *op, const string &path, error err) { ...@@ -237,4 +248,24 @@ static error _pathError(const char *op, const string &path, error err) {
return fmt::errorf("%s %s: %w", op, path.c_str(), err); return fmt::errorf("%s %s: %w", op, path.c_str(), err);
} }
string Signal::String() const {
const Signal& sig = *this;
const char *sigstr = nil;
if (0 <= sig.signo && sig.signo < NSIG)
sigstr = ::sys_siglist[sig.signo]; // might be nil as well
if (sigstr != nil)
return string(sigstr);
return fmt::sprintf("signal%d", sig.signo);
}
Signal _Signal_from_int(int signo) {
Signal sig;
sig.signo = signo;
return sig;
}
}} // golang::os:: }} // golang::os::
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
// - `Open` opens file @path. // - `Open` opens file @path.
// - `Pipe` creates new pipe. // - `Pipe` creates new pipe.
// - `NewFile` wraps OS-level file-descriptor into File. // - `NewFile` wraps OS-level file-descriptor into File.
// - `Signal` represents OS-level signal.
// //
// See also https://golang.org/pkg/os for Go os package documentation. // See also https://golang.org/pkg/os for Go os package documentation.
...@@ -104,6 +105,37 @@ LIBGOLANG_API std::tuple<File, error> NewFile(int sysfd, const string& name); ...@@ -104,6 +105,37 @@ LIBGOLANG_API std::tuple<File, error> NewFile(int sysfd, const string& name);
// Pipe creates connected pair of files. // Pipe creates connected pair of files.
LIBGOLANG_API std::tuple</*r*/File, /*w*/File, error> Pipe(); LIBGOLANG_API std::tuple</*r*/File, /*w*/File, error> Pipe();
// Signal represents an OS signal.
//
// NOTE in Go os.Signal is interface while in pygolang os::Signal is concrete structure.
struct Signal {
int signo;
// String returns human-readable signal text.
LIBGOLANG_API string String() const;
// Signal == Signal
inline bool operator==(const Signal& sig2) const { return (signo == sig2.signo); }
inline bool operator!=(const Signal& sig2) const { return (signo != sig2.signo); }
};
// _Signal_from_int creates Signal from integer, for example from SIGINT.
LIBGOLANG_API Signal _Signal_from_int(int signo);
}} // golang::os:: }} // golang::os::
// std::
namespace std {
// std::hash<Signal>
template<> struct hash<golang::os::Signal> {
std::size_t operator()(const golang::os::Signal& sig) const noexcept {
return hash<int>()(sig.signo);
}
};
} // std::
#endif // _NXD_LIBGOLANG_OS_H #endif // _NXD_LIBGOLANG_OS_H
# cython: language_level=2
# Copyright (C) 2021-2022 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""Package os mirrors and amends Go package os.
See _os.pxd for package documentation.
"""
# redirect cimport: golang.os -> golang._os (see __init__.pxd for rationale)
from golang._os cimport *
# -*- coding: utf-8 -*-
# Copyright (C) 2021-2022 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""Package os mirrors Go package os.
- `Signal` represents OS-level signal.
See also https://golang.org/pkg/os for Go os package documentation.
"""
from __future__ import print_function, absolute_import
from golang._os import \
PySignal as Signal
# cython: language_level=2
# Copyright (C) 2021-2022 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""Package signal mirrors Go package signal.
- `Notify` arranges for signals to be delivered to channels.
- `Stop` unsubscribes a channel from signal delivery.
- `Ignore` requests signals to be ignored.
- `Reset` requests signals to be handled as by default.
See also https://golang.org/pkg/os/signal for Go signal package documentation.
"""
from golang cimport chan
from golang cimport os
from libcpp.vector cimport vector
cdef extern from "golang/os/signal.h" namespace "golang::os::signal" nogil:
void Notify(chan[os.Signal] ch, vector[os.Signal] sigv)
void Stop(chan[os.Signal] ch)
void Ignore(vector[os.Signal] sigv)
void Reset(vector[os.Signal] sigv)
# -*- coding: utf-8 -*-
# cython: language_level=2
# Copyright (C) 2021-2022 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""_signal.pyx implements signal.pyx - see _signal.pxd for package overview."""
from __future__ import print_function, absolute_import
from golang cimport pychan, topyexc
from golang cimport os
from libc.signal cimport SIGINT
from libcpp.vector cimport vector
# adjust Python behaviour to not raise KeyboardInterrupt on SIGINT by default
# if any Notify(SIGINT) is currently active.
cdef set _pyint_notifying = set() # {pychan}
cdef _pydefault_int_handler(sig, frame):
# with gil:
if 1:
if len(_pyint_notifying) != 0:
return
raise KeyboardInterrupt
import signal as pysignal
if pysignal.getsignal(SIGINT) is pysignal.default_int_handler:
pysignal.default_int_handler = _pydefault_int_handler
pysignal.signal(SIGINT, _pydefault_int_handler)
def PyNotify(pychan pych, *pysigv):
cdef chan[os.Signal] ch = pychan_osSignal_pyexc(pych)
sigv, has_sigint = _unwrap_pysigv(pysigv)
# with gil:
if 1:
if has_sigint:
_pyint_notifying.add(pych)
_Notify_pyexc(ch, sigv)
def PyStop(pychan pych):
cdef chan[os.Signal] ch = pychan_osSignal_pyexc(pych)
# with gil:
if 1:
try:
_pyint_notifying.remove(pych)
except KeyError:
pass
_Stop_pyexc(ch)
def PyIgnore(*pysigv):
sigv, has_sigint = _unwrap_pysigv(pysigv)
# with gil:
if 1:
if has_sigint:
_pyint_notifying.clear()
_Ignore_pyexc(sigv)
def PyReset(*pysigv):
sigv, has_sigint = _unwrap_pysigv(pysigv)
# with gil:
if 1:
if has_sigint:
_pyint_notifying.clear()
_Reset_pyexc(sigv)
# _unwrap_pysigv converts pysigv to sigv.
cdef (vector[os.Signal], bint) _unwrap_pysigv(pysigv) except *: # (sigv, has_sigint)
cdef vector[os.Signal] sigv
cdef bint has_sigint = (len(pysigv) == 0) # if all signals
for xpysig in pysigv:
if not isinstance(xpysig, os.PySignal):
raise TypeError("expect os.Signal, got %r" % (xpysig,))
pysig = <os.PySignal>xpysig
sigv.push_back(pysig.sig)
if pysig.sig.signo == SIGINT:
has_sigint = True
return (sigv, has_sigint)
cdef:
chan[os.Signal] pychan_osSignal_pyexc(pychan pych) except +topyexc:
return pych._chan_osSignal()
void _Notify_pyexc(chan[os.Signal] ch, const vector[os.Signal]& sigv) except +topyexc:
Notify(ch, sigv)
void _Stop_pyexc(chan[os.Signal] ch) except +topyexc:
Stop(ch)
void _Ignore_pyexc(const vector[os.Signal]& sigv) except +topyexc:
Ignore(sigv)
void _Reset_pyexc(const vector[os.Signal]& sigv) except +topyexc:
Reset(sigv)
// Copyright (C) 2021-2022 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
// Package signal mirrors Go package signal.
// See signal.h for package overview.
// Signal package organization
//
// Signals are delivered via regular OS mechanism to _os_sighandler. Because
// signal handler can use only limited subset of system and libc calls(*),
// _os_sighandler further delivers information about received signal to
// _sigrecv_loop running in regular goroutine, which further delivers received
// signals to subscribed clients via regular channel operations. The delivery
// from _os_sighandler to _sigrecv_loop is organized via special
// single-reader/multiple-writers queue implemented with only atomic operations
// and async-signal-safe functions.
//
//
// _sigrecv_loop ← _os_sighandler queue design
//
// The queue accumulates mask of pending received signals and allows the
// receiver to retrieve that mask flushing it to cleared state.
//
// The logic of the queue is explained in terms of its states:
//
// State "Idle":
// - the reader is not accessing the pending mask and is not blocked.
// - all writers are not accessing the pending mask.
// - the mask is clear
//
// State "TxPending":
// - the reader is not accessing the pending mask and is not blocked.
// - some writers have updated the pending mask.
// - the mask is not clear
//
// State "RxBlocked":
// - the reader accessed the pending mask, found it to be zero, and is in the
// progress to become blocking, or had already blocked waiting for wakeup
// from a writer.
// - all writers are not accessing the pending mask, no writer has woken up
// the reader.
//
// Reader block/wakeup is done via OS pipe. When the reader decides to block it
// calls sys_read on the reading side of the pipe. When a writer decides to
// wakeup the reader it writes one byte to the sending side of the pipe.
//
// (*) see https://man7.org/linux/man-pages/man7/signal-safety.7.html
// Interoperability with thirdparty code that installs signal handlers
//
// Signal package can be used together with other packages that install signal
// handlers. This interoperability is limited and works only when signal
// package installs signal handlers after thirdparty code. If signal package
// detects that signal handler, that it installed, was somehow changed, it
// raises "collision detected wrt thirdparty sigaction usage" panic.
#include "golang/cxx.h"
#include "golang/os.h"
#include "golang/os/signal.h"
#include "golang/sync.h"
#include "golang/time.h"
#include "golang/runtime/internal/syscall.h"
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <atomic>
#include <tuple>
#define DEBUG 0
#if DEBUG
# define debugf(format, ...) fprintf(stderr, format, ##__VA_ARGS__)
#else
# define debugf(format, ...) do {} while (0)
#endif
// golang::os::signal::
namespace golang {
namespace os {
namespace signal {
namespace sys = golang::internal::syscall;
using std::atomic;
using std::tie;
using std::vector;
using cxx::set;
static void _os_sighandler(int sig, siginfo_t *info, void *ucontext);
static void _notify(int signo);
static void _checksig(int signo);
static void _checkActEqual(const struct sigaction *a, const struct sigaction *b);
static void _spinwaitNextQueueCycle();
static void xsys_sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
static void xsigemptyset(sigset_t *sa_mask);
static bool _sigact_equal(const struct sigaction *a, const struct sigaction *b);
// queue for _sigrecv_loop <- _os_sighandler
enum _QState { _QIdle, _QTxPending, _QRxBlocked };
static atomic<_QState> _qstate (_QIdle);
static atomic<uint64_t> _pending (0); // signo corresponds to (signo-1)'th bit
#define _MAXSIG 64
static global<os::File> _wakerx = nil; // _sigrecv_loop blocks on _wakerx via read to wait for new signal
static int _waketx = -1; // _os_sighandler writes to _waketx to wakeup _sigrecv_loop
static atomic<int> _txrunning; // +1'ed during each running _os_sighandler
// subscribed clients are maintained in _registry.
//
// _registry is normally accessed with _regMu locked, or, atomically from
// _os_sighandler to get sigstate and prev_act for a signal.
enum _SigState {
_SigReset, // whether we installed previous signal handler (the case after Reset, or final Stop)
_SigIgnoring, // whether we installed SIG_IGN (the case after Ignore)
_SigNotifying, // whether we should be delivering the signal to channels (the case after Notify)
};
struct _SigHandler {
set<chan<os::Signal>> subscribers; // client channels
struct sigaction prev_act; // sigaction installed before us
atomic<_SigState> sigstate; // which kind of signal handler we installed
};
static sync::Mutex *_regMu; // allocated in _init
static atomic<_SigHandler*> _registry[_MAXSIG+1]; // {} signo -> _SigHandler; entry remains !nil once set
static bool _sigrecv_loop_started = false;
static struct sigaction _actIgnore; // sigaction corresponding to Ignore
static struct sigaction _actNotify; // sigaction corresponding to Notify
void _init() {
_regMu = new sync::Mutex();
// create _wakerx <-> _waketx pipe; set _waketx to nonblocking mode
int vfd[2];
if (sys::Pipe(vfd) < 0)
panic("pipe(_wakerx, _waketx)"); // TODO +syserr
if (sys::Fcntl(vfd[0], F_SETFD, FD_CLOEXEC) < 0)
panic("fcntl(_wakerx, FD_CLOEXEC)"); // TODO +syserr
error err;
tie(_wakerx, err) = os::NewFile(vfd[0], "_wakerx");
if (err != nil)
panic("os::newFile(_wakerx");
_waketx = vfd[1];
if (sys::Fcntl(_waketx, F_SETFL, O_NONBLOCK) < 0)
panic("fcntl(_waketx, O_NONBLOCK)"); // TODO +syserr
if (sys::Fcntl(_waketx, F_SETFD, FD_CLOEXEC) < 0)
panic("fcntl(_waketx, FD_CLOEXEC)"); // TODO +syserr
_actIgnore.sa_handler = SIG_IGN;
_actIgnore.sa_flags = 0;
xsigemptyset(&_actIgnore.sa_mask);
_actNotify.sa_sigaction = _os_sighandler;
_actNotify.sa_flags = SA_SIGINFO;
xsigemptyset(&_actNotify.sa_mask);
}
// _os_sighandler is called by OS on a signal.
static void _os_sighandler(int sig, siginfo_t *info, void *ucontext) {
_checksig(sig);
int syserr;
_txrunning.fetch_add(+1);
defer([]() {
_txrunning.fetch_add(-1);
});
debugf("\n");
debugf("SIGHANDLER: invoked with %d\n", sig);
_SigHandler *h = _registry[sig].load(); // should be !nil if we are here
_SigState sigstate = h->sigstate.load();
if (sigstate == _SigNotifying) {
_pending.fetch_or(1ULL << (sig-1));
while (1) {
_QState st = _qstate.load();
switch (st) {
case _QIdle:
debugf("SIGHANDLER: idle\n");
if (!_qstate.compare_exchange_strong(st, _QTxPending))
break;
goto done;
case _QTxPending:
debugf("SIGHANDLER: tx pending\n");
// another sighandler already transitioned the queue into this state
goto done;
case _QRxBlocked:
debugf("SIGHANDLER: rx blocked\n");
if (!_qstate.compare_exchange_strong(st, _QTxPending))
break;
debugf("SIGHANDLER: waking up\n");
// schedule reader wakeup
syserr = sys::Write(_waketx, "", 1);
if (syserr == -EAGAIN) // pipe buffer is full => the reader will be woken up anyway
syserr = 0;
if (syserr < 0)
panic("write(_waketx) failed"); // TODO +syserr
goto done;
default:
panic("bad _qstate");
}
}
}
done:
// also call previously-installed handler (e.g. one installed by python's stdlib signal)
if (sigstate != _SigIgnoring) {
if (h->prev_act.sa_flags & SA_SIGINFO) {
h->prev_act.sa_sigaction(sig, info, ucontext);
}
else {
auto sah = h->prev_act.sa_handler;
if (sah != SIG_IGN) {
if (sah != SIG_DFL) {
sah(sig);
}
else {
// SIG_DFL && _SigReset - reraise to die if the signal is fatal
if (sigstate == _SigReset) {
// raise will coredump/term on fatal signal, or ignored
// on signals whose default action is to ignore
raise(sig);
}
}
}
}
}
return;
}
// _sigrecv_loop retrieves received signals from _os_sighandler and sends them to subscribed clients.
// it is run in dedicated goroutine and never finishes.
static void _sigrecv_loop() {
while (1) {
_QState st = _qstate.load();
switch (st) {
case _QIdle:
debugf("LOOP: idle\n");
break;
case _QTxPending:
debugf("LOOP: tx pending\n");
if (!_qstate.compare_exchange_strong(st, _QIdle)) // must succeed - no writer is changing
panic("TxPending -> Idle failed"); // _qstate anymore in _QTxPending state
break;
default:
panic("bad _qstate");
}
auto sigp = _pending.exchange(0ULL);
if (sigp == 0) {
st = _QIdle;
if (!_qstate.compare_exchange_strong(st, _QRxBlocked))
continue;
debugf("LOOP: -> blocking ...\n");
char buf[1];
int n;
error err;
tie(n, err) = _wakerx->Read(buf, 1);
if (err != nil)
panic("read(_wakerx) failed"); // TODO panic(err) after we can
debugf("LOOP: woke up\n");
// by the time we get here _qstate must be reset back from _QRxBlocked
continue;
}
debugf("LOOP: sigp: %lux\n", sigp);
// deliver fetched signals
for (int sig = 1; sig <= _MAXSIG; sig++) {
if ((sigp & (1ULL<<(sig-1))) != 0)
_notify(sig);
}
}
}
static void _notify(int signo) {
_regMu->lock();
defer([&]() {
_regMu->unlock();
});
_SigHandler *h = _registry[signo].load();
if (h == nil)
return;
os::Signal sig; sig.signo = signo;
for (auto ch : h->subscribers) {
select({
_default, // 0
ch.sends(&sig), // 1
});
}
}
static int/*syserr*/ _Notify1(chan<os::Signal> ch, os::Signal sig) {
_checksig(sig.signo);
_regMu->lock();
defer([&]() {
_regMu->unlock();
});
// retrieve current signal action
struct sigaction cur;
int syserr = sys::Sigaction(sig.signo, nil, &cur);
if (syserr < 0) {
// TODO reenable once we can panic with any object
//return fmt::errorf("sigaction sig%d: %w", sig.signo, sys::NewErrno(syserr);
return syserr;
}
// retrieve/create sighandler
atomic<_SigHandler*> *regentry = &_registry[sig.signo];
_SigHandler *h = regentry->load();
if (h == nil) {
h = new _SigHandler();
h->sigstate.store(_SigReset);
h->prev_act = cur;
regentry->store(h);
}
// thirdparty code is allowed to install signal handlers after our Reset/Ignore/full-Stop,
// but not after active Notify.
_SigState sigstate = h->sigstate.load();
if (sigstate == _SigNotifying)
_checkActEqual(&cur, &_actNotify);
// register our signal handler for sig on first Notify
if (sigstate != _SigNotifying) {
// if thirdparty changed signal handler while we were inactive - adjust h.prev_act
// do the adjustment atomically not to race with _os_sighandler
struct sigaction *prev_act = (sigstate == _SigIgnoring ? &_actIgnore : &h->prev_act);
if (!_sigact_equal(&cur, prev_act)) {
_SigHandler *hold = h;
h = new _SigHandler();
h->prev_act = cur;
h->sigstate.store(_SigReset);
// h->subscribers remain empty
prev_act = &h->prev_act;
regentry->store(h);
// free old h after we are sure that currently running _os_sighandler is over
while (_txrunning.load() != 0)
time::sleep(0); // TODO -> runtime.Gosched
delete hold;
}
// register our sigaction
struct sigaction old;
syserr = sys::Sigaction(sig.signo, &_actNotify, &old);
if (syserr < 0) {
// TODO reenable once we can panic with any object
//return fmt::errorf("sigaction sig%d: %w", sig.signo, sys::NewErrno(syserr);
return syserr;
}
_checkActEqual(&old, prev_act);
// spawn _sigrecv_loop on first Notify request
if (!_sigrecv_loop_started) {
go(_sigrecv_loop);
_sigrecv_loop_started = true;
}
}
h->subscribers.insert(ch);
h->sigstate.store(_SigNotifying);
return 0;
}
void Stop(chan<os::Signal> ch) {
_regMu->lock();
defer([&]() {
_regMu->unlock();
});
for (int signo = 1; signo <= _MAXSIG; ++signo) {
atomic<_SigHandler*> *regentry = &_registry[signo];
_SigHandler *h = regentry->load();
if (h == nil || (h->sigstate.load() != _SigNotifying))
continue;
if (!h->subscribers.has(ch))
continue;
if (h->subscribers.size() == 1) { // stopping - ch was the only subscriber
// reset sigstate early so that _os_sighandler, if it will run (see
// below about lack of guarantees wrt running old signal handler
// after sigaction), executes default action by itself.
h->sigstate.store(_SigReset);
// sigaction to old handler
// NOTE sys_sigaction does not guarantee that previously-installed
// handler is not running after sys_sigaction completes.
struct sigaction act;
xsys_sigaction(signo, &h->prev_act, &act);
_checkActEqual(&act, &_actNotify);
// wait till signal queue delivers to ch if sig was/is already
// being handled by _os_sighandler or _sigrecv_loop.
//
// (sys_sigaction does not guarantee that previous signal handler
// is not being executed after sigaction completes; the old handler
// could be also started to run before our call to sys_sigaction)
_regMu->unlock();
_spinwaitNextQueueCycle();
_regMu->lock();
}
h->subscribers.erase(ch);
}
}
static int/*syserr*/ _Ignore1(os::Signal sig) {
_checksig(sig.signo);
_regMu->lock();
defer([&]() {
_regMu->unlock();
});
atomic<_SigHandler*> *regentry = &_registry[sig.signo];
_SigHandler *h = regentry->load();
// reset signal handler to SIG_IGN, but remember which handler it was previously there
// Reset will reset to that instead of hardcoded SIG_DFL
if (h == nil) {
h = new _SigHandler();
h->sigstate.store(_SigIgnoring);
int syserr = sys::Sigaction(sig.signo, nil, &h->prev_act);
if (syserr < 0) {
delete h;
return syserr; // TODO errctx
}
regentry->store(h);
}
h->sigstate.store(_SigIgnoring);
h->subscribers.clear();
int syserr = sys::Sigaction(sig.signo, &_actIgnore, nil);
if (syserr < 0)
return syserr; // TODO errctx
// no need to wait for delivery to channels to complete
return 0;
}
static int/*syserr*/ _Reset1(os::Signal sig) {
_checksig(sig.signo);
_regMu->lock();
defer([&]() {
_regMu->unlock();
});
// reset signal handler to what was there previously underlying signal package
atomic<_SigHandler*> *regentry = &_registry[sig.signo];
_SigHandler *h = regentry->load();
if (h == nil)
return 0;
_SigState sigstate = h->sigstate.load();
h->sigstate.store(_SigReset);
struct sigaction act;
int syserr = sys::Sigaction(sig.signo, &h->prev_act, &act);
if (syserr < 0)
return syserr; // TODO errctx
if (sigstate == _SigNotifying)
_checkActEqual(&act, &_actNotify);
// wait till signal queue delivers to ch if sig was/is already
// being handled by _os_sighandler or _sigrecv_loop.
// (see Stop for details)
_regMu->unlock();
_spinwaitNextQueueCycle();
_regMu->lock();
h->subscribers.clear();
return 0;
}
// _spinwaitNextQueueCycle waits for all currently-queued signals to complete
// delivery to subscribed channels.
static void _spinwaitNextQueueCycle() {
// make sure _os_sighandler cycle, if it was running, is complete.
// if _qstate is _QRxBlocked _os_sighandler transitions it at least once to another state
while (_txrunning.load() != 0)
time::sleep(0); // TODO -> runtime.Gosched
// make sure _sigrecv_loop cycle, if it was running, is complete.
while (_qstate.load() != _QRxBlocked)
time::sleep(0); // TODO -> runtime.Gosched
}
// public Notify/Ignore/Reset that accept set of signals.
// _for_all_signals calls f for every signal specified by sigv.
// TODO change f to return error instead of syserr once we can panic with formatted string.
static void _for_all_signals(const vector<os::Signal>& sigv, func<int(os::Signal)> f) {
if (sigv.size() != 0) {
for (auto sig : sigv) {
int syserr = f(sig);
if (syserr < 0)
panic("sigaction failed");
}
}
else {
int nok = 0;
for (int signo = 1; signo <= _MAXSIG; signo++) {
int syserr = f(os::_Signal_from_int(signo));
if (syserr < 0) {
if (syserr == -EINVAL) {
continue; // sigaction refuses to handle SIGKILL/SIGSTOP/32/...
}
panic("sigaction failed");
}
nok++;
}
if (nok == 0)
panic("sigaction failed for all signals");
}
}
void Notify(chan<os::Signal> ch, const vector<os::Signal>& sigv) {
_for_all_signals(sigv, [&](os::Signal sig) {
return _Notify1(ch, sig);
});
}
void Ignore(const vector<os::Signal>& sigv) {
_for_all_signals(sigv, [&](os::Signal sig) {
return _Ignore1(sig);
});
}
void Reset(const vector<os::Signal>& sigv) {
_for_all_signals(sigv, [&](os::Signal sig) {
return _Reset1(sig);
});
}
void Notify(chan<os::Signal> ch, const std::initializer_list<os::Signal>& sigv) {
Notify(ch, vector<os::Signal>(sigv));
}
void Ignore(const std::initializer_list<os::Signal>& sigv) {
Ignore(vector<os::Signal>(sigv));
}
void Reset(const std::initializer_list<os::Signal>& sigv) {
Reset(vector<os::Signal>(sigv));
}
static void _checkActEqual(const struct sigaction *a, const struct sigaction *b) {
if (_sigact_equal(a, b))
return;
//fprintf(stderr, "a: %p (%x)\n", a->sa_sigaction, a->sa_flags);
//fprintf(stderr, "b: %p (%x)\n", b->sa_sigaction, b->sa_flags);
panic("collision detected wrt thirdparty sigaction usage");
}
static void _checksig(int signo) {
if (!(1 <= signo && signo <= _MAXSIG))
panic("invalid signal");
}
static void xsigemptyset(sigset_t *sa_mask) {
if (sigemptyset(sa_mask) < 0)
panic("sigemptyset failed"); // must always succeed
}
static void xsys_sigaction(int signo, const struct sigaction *act, struct sigaction *oldact) {
int syserr = sys::Sigaction(signo, act, oldact);
if (syserr != 0)
panic("sigaction failed"); // TODO add errno detail
}
static bool _sigact_equal(const struct sigaction *a, const struct sigaction *b) {
// don't compare sigaction by memcmp - it will fail because struct sigaction
// has holes which glibc does not initialize when copying data from
// retrieved kernel sigaction struct.
//
// also don't compare sa_flags fully - glibc tinkers in SA_RESTORER, so
// retrieving sigaction after installing it might give not exactly the same result.
bool a_siginfo = (a->sa_flags & SA_SIGINFO);
bool b_siginfo = (b->sa_flags & SA_SIGINFO);
if (a_siginfo != b_siginfo) // approximation for a->sa_flags != b->sa_flags
return false;
if (a_siginfo & SA_SIGINFO) {
if (a->sa_sigaction != b->sa_sigaction)
return false;
}
else {
if (a->sa_handler != b->sa_handler)
return false;
}
// XXX no way to compare sigset_t portably -> let's ignore sa_mask for now
//return (a->sa_mask == b->sa_mask);
return true;
}
}}} // golang::os::signal::
#ifndef _NXD_LIBGOLANG_OS_SIGNAL_H
#define _NXD_LIBGOLANG_OS_SIGNAL_H
//
// Copyright (C) 2021-2022 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
// Package signal mirrors Go package signal.
//
// - `Notify` arranges for signals to be delivered to channels.
// - `Stop` unsubscribes a channel from signal delivery.
// - `Ignore` requests signals to be ignored.
// - `Reset` requests signals to be handled as by default.
//
// See also https://golang.org/pkg/os/signal for Go signal package documentation.
#include <golang/libgolang.h>
#include <golang/os.h>
#include <initializer_list>
#include <vector>
// golang::os::signal::
namespace golang {
namespace os {
namespace signal {
// Notify requests that specified signals, when received, are sent to channel ch.
//
// The sending will be done in non-blocking way. If, at the moment of signal
// reception, the channel is full and not being received-from, the signal won't
// be delivered.
//
// If the list of specified signals is empty, it means "all signals".
LIBGOLANG_API void Notify(chan<os::Signal> ch, const std::initializer_list<os::Signal>& sigv);
LIBGOLANG_API void Notify(chan<os::Signal> ch, const std::vector<os::Signal>& sigv);
// Stop undoes the effect of all previous calls to Notify with specified channel.
//
// After Stop completes, no more signals will be delivered to ch.
LIBGOLANG_API void Stop(chan<os::Signal> ch);
// Ignore requests specified signals to be ignored by the program.
//
// In particular it undoes the effect of all previous calls to Notify with
// specified signals.
//
// After Ignore completes specified signals won't be delivered to any channel.
//
// If the list of specified signals is empty, it means "all signals".
LIBGOLANG_API void Ignore(const std::initializer_list<os::Signal>& sigv);
LIBGOLANG_API void Ignore(const std::vector<os::Signal>& sigv);
// Reset resets specified signals to be handled as by default.
//
// In particular it undoes the effect of all previous calls to Notify with
// specified signals.
//
// After Reset completes specified signals won't be delivered to any channel.
//
// If the list of specified signals is empty, it means "all signals".
LIBGOLANG_API void Reset(const std::initializer_list<os::Signal>& sigv);
LIBGOLANG_API void Reset(const std::vector<os::Signal>& sigv);
}}} // golang::os::signal::
#endif // _NXD_LIBGOLANG_OS_SIGNAL_H
# cython: language_level=2
# Copyright (C) 2021-2022 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""Package signal mirrors and Go package signal.
See _signal.pxd for package documentation.
"""
# redirect cimport: golang.os.signal -> golang.os._signal (see __init__.pxd for rationale)
from golang.os._signal cimport *
# -*- coding: utf-8 -*-
# Copyright (C) 2021-2022 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""Package signal mirrors Go package signal.
- `Notify` arranges for signals to be delivered to channels.
- `Stop` unsubscribes a channel from signal delivery.
- `Ignore` requests signals to be ignored.
- `Reset` requests signals to be handled as by default.
See also https://golang.org/pkg/os/signal for Go signal package documentation.
"""
from __future__ import print_function, absolute_import
from golang.os._signal import \
PyNotify as Notify, \
PyStop as Stop, \
PyIgnore as Ignore, \
PyReset as Reset
# -*- coding: utf-8 -*-
# Copyright (C) 2021-2022 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
from __future__ import print_function, absolute_import
from golang import chan, func, defer
from golang import os as gos, syscall, time
from golang.os import signal
import os
from os.path import dirname
from golang.golang_test import panics, _pyrun
from pytest import raises
from subprocess import PIPE
# directories
dir_os = dirname(__file__) # .../pygolang/os
dir_testprog = dir_os + "/testprog" # .../pygolang/os/testprog
N = 1000
# test_signal verifies signal delivery to channels controlled by Notify/Stop/Ignore/Reset.
@func
def test_signal():
# Notify/Stop with wrong chan dtype -> panic
_ = panics("pychan: channel type mismatch")
with _: signal.Notify(chan(2), syscall.SIGUSR1)
with _: signal.Stop (chan(2))
with _: signal.Notify(chan(2, dtype='C.int'), syscall.SIGUSR1)
with _: signal.Stop (chan(2, dtype='C.int'))
# Notify/Ignore/Reset with wrong signal type
_ = raises(TypeError)
with _: signal.Notify(chan(dtype=gos.Signal), None)
with _: signal.Ignore(None)
with _: signal.Reset(None)
# subscribe ch1(USR1), ch12(USR1,USR2) and ch2(USR2)
ch1 = chan(2, dtype=gos.Signal)
ch12 = chan(2, dtype=gos.Signal)
ch2 = chan(2, dtype=gos.Signal)
signal.Notify(ch1, syscall.SIGUSR1)
signal.Notify(ch12, syscall.SIGUSR1, syscall.SIGUSR2)
signal.Notify(ch2, syscall.SIGUSR2)
def _():
signal.Reset()
defer(_)
for i in range(N):
# raise SIGUSR1 -> should be delivered to ch1 and ch12
assert len(ch1) == 0
assert len(ch12) == 0
assert len(ch2) == 0
killme(syscall.SIGUSR1)
waitfor(lambda: len(ch1) == 1 and len(ch12) == 1)
sig1 = ch1.recv()
sig12 = ch12.recv()
assert sig1 == syscall.SIGUSR1
assert sig12 == syscall.SIGUSR1
# raise SIGUSR2 -> should be delivered to ch12 and ch2
assert len(ch1) == 0
assert len(ch12) == 0
assert len(ch2) == 0
killme(syscall.SIGUSR2)
waitfor(lambda: len(ch12) == 1 and len(ch2) == 1)
sig12 = ch12.recv()
sig2 = ch2.recv()
assert sig12 == syscall.SIGUSR2
assert sig2 == syscall.SIGUSR2
# if SIGUSR2 will be eventually delivered to ch1 - it will break
# in SIGUSR1 check on next iteration.
# Stop(ch2) -> signals should not be delivered to ch2 anymore
signal.Stop(ch2)
for i in range(N):
# USR1 -> ch1, ch12
assert len(ch1) == 0
assert len(ch12) == 0
assert len(ch2) == 0
killme(syscall.SIGUSR1)
waitfor(lambda: len(ch1) == 1 and len(ch12) == 1)
sig1 = ch1.recv()
sig12 = ch12.recv()
assert sig1 == syscall.SIGUSR1
assert sig12 == syscall.SIGUSR1
# USR2 -> ch12, !ch2
assert len(ch1) == 0
assert len(ch12) == 0
assert len(ch2) == 0
killme(syscall.SIGUSR2)
waitfor(lambda: len(ch12) == 1)
sig12 = ch12.recv()
assert sig12 == syscall.SIGUSR2
# if SIGUSR2 will be eventually delivered to ch2 - it will break on
# next iteration.
# Ignore(USR1) -> ch1 should not be delivered to anymore, ch12 should be delivered only USR2
signal.Ignore(syscall.SIGUSR1)
for i in range(N):
# USR1 -> ø
assert len(ch1) == 0
assert len(ch12) == 0
assert len(ch2) == 0
killme(syscall.SIGUSR1)
time.sleep(1E-6)
# USR2 -> ch12
assert len(ch1) == 0
assert len(ch12) == 0
assert len(ch2) == 0
killme(syscall.SIGUSR2)
waitfor(lambda: len(ch12) == 1)
sig12 = ch12.recv()
assert sig12 == syscall.SIGUSR2
# if SIGUSR1 or SIGUSR2 will be eventually delivered to ch1 or ch2 - it
# will break on next iteration.
# Notify after Ignore
signal.Notify(ch1, syscall.SIGUSR1)
for i in range(N):
# USR1 -> ch1
assert len(ch1) == 0
assert len(ch12) == 0
assert len(ch2) == 0
killme(syscall.SIGUSR1)
waitfor(lambda: len(ch1) == 1)
sig1 = ch1.recv()
assert sig1 == syscall.SIGUSR1
# USR2 -> ch12
assert len(ch1) == 0
assert len(ch12) == 0
assert len(ch2) == 0
killme(syscall.SIGUSR2)
waitfor(lambda: len(ch12) == 1)
sig12 = ch12.recv()
assert sig12 == syscall.SIGUSR2
# if SIGUSR1 or SIGUSR2 will be eventually delivered to wrong place -
# it will break on next iteration.
# Reset is tested in test_stdlib_interop (it needs non-terminating default
# handler to verify behaviour)
# test_notify_reinstall verifies that repeated Notify correctly (re)installs _os_sighandler.
@func
def test_notify_reinstall():
ch = chan(10, dtype=gos.Signal)
def _():
signal.Stop(ch)
defer(_)
for i in range(N):
signal.Stop(ch)
signal.Notify(ch, syscall.SIGUSR1)
time.sleep(0.1*time.second)
assert len(ch) == 0
killme(syscall.SIGUSR1)
time.sleep(0.1*time.second)
assert len(ch) == 1
# test_signal_all verifies Notify(ø), Ignore(ø) and Reset(ø) that work on "all signals".
def test_signal_all():
retcode, out, _ = _pyrun([dir_testprog + "/signal_test_all.py"], stdout=PIPE)
assert b"ok (notify)" in out
assert b"ok (ignore)" in out
assert b"terminating ..." in out
assert retcode == -syscall.SIGTERM.signo
# test_stdlib_interop verifies that there is minimal compatibility in between
# golang.os.signal and stdlib signal modules: signal handlers installed by
# stdlib signal, before golang.os.signal becomes used, continue to be notified
# about received signals.
#
# NOTE: it does not work the other way - stdlib signal, if used after
# golang.os.signal, will effectively disable all signal handlers installed by
# gsignal.Notify. In other words stdlib signal installs signal handlers in
# non-cooperative way.
@func
def test_stdlib_interop():
import signal as pysig
ch1 = chan(2, dtype=object) # NOTE not gos.Signal nor 'C.os::Signal'
def _(signo, frame):
ch1.send("USR1")
pysig.signal(pysig.SIGUSR1, _)
def _():
pysig.signal(pysig.SIGUSR1, pysig.SIG_IGN)
defer(_)
# verify that plain pysig delivery works
for i in range(N):
assert len(ch1) == 0
killme(syscall.SIGUSR1)
waitfor(lambda: len(ch1) == 1)
obj1 = ch1.recv()
assert obj1 == "USR1"
# verify that combined pysig + golang.os.signal delivery works
ch2 = chan(2, dtype=gos.Signal)
signal.Notify(ch2, syscall.SIGUSR1)
def _():
signal.Stop(ch2)
defer(_)
for i in range(N):
assert len(ch1) == 0
assert len(ch2) == 0
killme(syscall.SIGUSR1)
waitfor(lambda: len(ch1) == 1 and len(ch2) == 1)
obj1 = ch1.recv()
sig2 = ch2.recv()
assert obj1 == "USR1"
assert sig2 == syscall.SIGUSR1
# Ignore stops delivery to both pysig and golang.os.signal
signal.Ignore(syscall.SIGUSR1)
for i in range(N):
assert len(ch1) == 0
assert len(ch2) == 0
killme(syscall.SIGUSR1)
time.sleep(1E-6)
time.sleep(0.1) # just in case
assert len(ch1) == 0
assert len(ch2) == 0
# after Reset pysig delivery is restored even after Ignore
signal.Reset(syscall.SIGUSR1)
for i in range(N):
assert len(ch1) == 0
assert len(ch2) == 0
killme(syscall.SIGUSR1)
waitfor(lambda: len(ch1) == 1)
assert len(ch2) == 0
obj1 = ch1.recv()
assert obj1 == "USR1"
# Reset stops delivery to golang.os.signal and restores pysig delivery
signal.Notify(ch2, syscall.SIGUSR1)
signal.Reset(syscall.SIGUSR1)
for i in range(N):
assert len(ch1) == 0
assert len(ch2) == 0
killme(syscall.SIGUSR1)
waitfor(lambda: len(ch1) == 1)
assert len(ch2) == 0
obj1 = ch1.recv()
assert obj1 == "USR1"
# test_stdlib_interop_KeyboardInterrupt verifies that signal.{Notify,Ignore} disable
# raising KeyboardInterrupt by default on SIGINT and signal.{Stop,Reset} reenable it back.
@func
def test_stdlib_interop_KeyboardInterrupt():
# KeyboardInterrupt is raised by default
with raises(KeyboardInterrupt):
killme(syscall.SIGINT)
time.sleep(1)
ch1 = chan(2, dtype=gos.Signal)
ch2 = chan(2, dtype=gos.Signal)
def _():
signal.Reset(syscall.SIGINT)
defer(_)
# Notify disables raising KeyboardInterrupt by default on SIGINT
signal.Notify(ch1, syscall.SIGINT)
try:
killme(syscall.SIGINT)
waitfor(lambda: len(ch1) == 1)
obj1 = ch1.recv()
assert obj1 == syscall.SIGINT
time.sleep(0.1) # just in case
except KeyboardInterrupt:
raise AssertionError("KeyboardInterrupt raised after signal.Notify +ch1")
signal.Notify(ch2, syscall.SIGINT)
try:
killme(syscall.SIGINT)
waitfor(lambda: len(ch1) == 1 and len(ch2) == 1)
obj1 = ch1.recv()
obj2 = ch2.recv()
assert obj1 == syscall.SIGINT
assert obj2 == syscall.SIGINT
time.sleep(0.1) # just in case
except KeyboardInterrupt:
raise AssertionError("KeyboardInterrupt raised after signal.Notify +ch1 +ch2")
# last Stop should reenable raising KeyboardInterrupt by default on SIGINT
signal.Stop(ch1)
try:
killme(syscall.SIGINT)
waitfor(lambda: len(ch2) == 1)
obj2 = ch2.recv()
assert obj2 == syscall.SIGINT
time.sleep(0.1) # just in case
assert len(ch1) == 0
except KeyboardInterrupt:
raise AssertionError("KeyboardInterrupt raised after signal.Notify +ch1 +ch2 -ch1")
signal.Stop(ch2)
with raises(KeyboardInterrupt):
killme(syscall.SIGINT)
time.sleep(1)
time.sleep(0.1) # just in case
assert len(ch1) == 0
assert len(ch2) == 0
# Ignore disables raising KeyboardInterrupt by default on SIGINT
signal.Ignore(syscall.SIGINT)
try:
killme(syscall.SIGINT)
time.sleep(0.1)
assert len(ch1) == 0
assert len(ch2) == 0
except KeyboardInterrupt:
raise AssertionError("KeyboardInterrupt raised after signal.Ignore")
# Reset restores original behaviour
signal.Reset(syscall.SIGINT)
with raises(KeyboardInterrupt):
killme(syscall.SIGINT)
time.sleep(1)
time.sleep(0.1) # just in case
assert len(ch1) == 0
assert len(ch2) == 0
# killme sends signal sig to own process.
def killme(sig):
mypid = os.getpid()
os.kill(mypid, sig.signo)
# wait for waits until cond() becomes true or timeout.
def waitfor(cond):
tstart = time.now()
while 1:
if cond():
return
t = time.now()
if (t - tstart) > 1*time.second:
raise AssertionError("timeout waiting")
time.sleep(1E-6) # NOTE sleep(0) consumes lot of CPU under gevent
#!/usr/bin/env python
# Copyright (C) 2021-2022 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""This program verifies signal.Notify(), Ignore() and Reset() with 'all signals' argument."""
from __future__ import print_function, absolute_import
from golang import chan
from golang import os as gos, syscall, time
from golang.os import signal
import os, sys
def main():
# build "all signals" list
allsigv = []
for attr in dir(syscall):
if attr.startswith("SIG") and "_" not in attr:
sig = getattr(syscall, attr)
if sig not in allsigv: # avoid e.g. SIGCHLD/SIGCLD dups
allsigv.append(sig)
allsigv.sort(key=lambda sig: sig.signo)
allsigv.remove(syscall.SIGKILL) # SIGKILL/SIGSTOP cannot be caught
allsigv.remove(syscall.SIGSTOP)
allsigv.remove(syscall.SIGBUS) # AddressSanitizer crashes on SIGBUS/SIGFPE/SIGSEGV
allsigv.remove(syscall.SIGFPE)
allsigv.remove(syscall.SIGSEGV)
# Notify() -> kill * -> should be notified
ch = chan(10, dtype=gos.Signal)
signal.Notify(ch) # all signals
for sig in allsigv:
emit("-> %d %s" % (sig.signo, sig))
killme(sig)
xsig = ch.recv()
emit("<- %d %s" % (xsig.signo, xsig))
if xsig != sig:
raise AssertionError("got %s, expected %s" % (xsig, sig))
emit("ok (notify)")
# Ignore() -> kill * -> should not be notified
emit()
signal.Ignore() # all signals
assert len(ch) == 0
for sig in allsigv:
emit("-> %d %s" % (sig.signo, sig))
killme(sig)
assert len(ch) == 0
time.sleep(0.3)
assert len(ch) == 0
emit("ok (ignore)")
# Reset() -> kill * should be handled by OS by default
emit()
signal.Reset() # all signals
emit("terminating ...")
killme(syscall.SIGTERM)
raise AssertionError("not terminated")
# killme sends signal sig to own process.
def killme(sig):
mypid = os.getpid()
os.kill(mypid, sig.signo)
def emit(msg=''):
print(msg)
sys.stdout.flush()
if __name__ == '__main__':
main()
...@@ -185,6 +185,7 @@ def _with_build_defaults(kw): # -> (pygo, kw') ...@@ -185,6 +185,7 @@ def _with_build_defaults(kw): # -> (pygo, kw')
'sync.h', 'sync.h',
'time.h', 'time.h',
'os.h', 'os.h',
'os/signal.h',
'pyx/runtime.h', 'pyx/runtime.h',
'_testing.h', '_testing.h',
]]) ]])
...@@ -227,6 +228,10 @@ def Extension(name, sources, **kw): ...@@ -227,6 +228,10 @@ def Extension(name, sources, **kw):
'_sync.pxd', '_sync.pxd',
'time.pxd', 'time.pxd',
'_time.pxd', '_time.pxd',
'os.pxd',
'_os.pxd',
'os/signal.pxd',
'os/_signal.pxd',
'pyx/runtime.pxd', 'pyx/runtime.pxd',
]]) ]])
kw['depends'] = dependv kw['depends'] = dependv
......
...@@ -119,12 +119,14 @@ const _libgolang_runtime_ops *_runtime = nil; ...@@ -119,12 +119,14 @@ const _libgolang_runtime_ops *_runtime = nil;
using internal::_runtime; using internal::_runtime;
namespace internal { namespace atomic { extern void _init(); } } namespace internal { namespace atomic { extern void _init(); } }
namespace os { namespace signal { extern void _init(); } }
void _libgolang_init(const _libgolang_runtime_ops *runtime_ops) { void _libgolang_init(const _libgolang_runtime_ops *runtime_ops) {
if (_runtime != nil) // XXX better check atomically if (_runtime != nil) // XXX better check atomically
panic("libgolang: double init"); panic("libgolang: double init");
_runtime = runtime_ops; _runtime = runtime_ops;
internal::atomic::_init(); internal::atomic::_init();
os::signal::_init();
} }
void _taskgo(void (*f)(void *), void *arg) { void _taskgo(void (*f)(void *), void *arg) {
......
# -*- coding: utf-8 -*-
# Copyright (C) 2021-2022 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""Package syscall mirrors Go package syscall.
- SIG* provide constants for signals.
See also https://golang.org/pkg/syscall for Go syscall package documentation.
"""
from __future__ import print_function, absolute_import
# create signal constants wrapped into os.Signal
# for example syscall.SIGTERM = _PySignal_from_int(SIGTERM)
def _():
from golang._os import _PySignal_from_int
import signal as _pysig
for attr in dir(_pysig):
if attr.startswith("SIG") and not attr.startswith("SIG_"):
signo = getattr(_pysig, attr)
sig = _PySignal_from_int(signo)
globals()[attr] = sig
_(); del _
...@@ -204,6 +204,7 @@ setup( ...@@ -204,6 +204,7 @@ setup(
'golang/fmt.cpp', 'golang/fmt.cpp',
'golang/io.cpp', 'golang/io.cpp',
'golang/os.cpp', 'golang/os.cpp',
'golang/os/signal.cpp',
'golang/strings.cpp', 'golang/strings.cpp',
'golang/sync.cpp', 'golang/sync.cpp',
'golang/time.cpp'], 'golang/time.cpp'],
...@@ -218,6 +219,7 @@ setup( ...@@ -218,6 +219,7 @@ setup(
'golang/fmt.h', 'golang/fmt.h',
'golang/io.h', 'golang/io.h',
'golang/os.h', 'golang/os.h',
'golang/os/signal.h',
'golang/strings.h', 'golang/strings.h',
'golang/sync.h', 'golang/sync.h',
'golang/time.h'], 'golang/time.h'],
...@@ -280,10 +282,15 @@ setup( ...@@ -280,10 +282,15 @@ setup(
Ext('golang._io', Ext('golang._io',
['golang/_io.pyx']), ['golang/_io.pyx']),
Ext('golang._os',
['golang/_os.pyx']),
Ext('golang._os_test', Ext('golang._os_test',
['golang/_os_test.pyx', ['golang/_os_test.pyx',
'golang/os_test.cpp']), 'golang/os_test.cpp']),
Ext('golang.os._signal',
['golang/os/_signal.pyx']),
Ext('golang._strings_test', Ext('golang._strings_test',
['golang/_strings_test.pyx', ['golang/_strings_test.pyx',
'golang/strings_test.cpp']), 'golang/strings_test.cpp']),
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment