Skip to content

WIP: User-defined signal handling #59147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions base/Base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ include("cmd.jl")
include("process.jl")
include("terminfo.jl")
include("secretbuffer.jl")
include("signals.jl")

# core math functions
include("floatfuncs.jl")
Expand Down
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export
Iterators,
Broadcast,
MathConstants,
Signals,

# Types
AbstractChannel,
Expand Down
143 changes: 143 additions & 0 deletions base/signals.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
module Signals

using Base: AsyncCondition
using Base.Intrinsics: atomic_pointerset

export signal_abbrev, signal_name
public register_handler, deregister_handler

"""
signal_abbrev(signum::Integer) -> Union{String,Nothing}

Returns the signal abbreviation (e.g. "TERM") associated with the signal number for standard
signals on this architecture. If the signal number is invalid `nothing` will be returned
instead.
"""
function signal_abbrev(signum::Integer)
abbrev = ccall(:jl_sigabbrev, Cstring, (Cint,), signum)
abbrev != C_NULL || return nothing
return @static Sys.isbsd() ? uppercase(unsafe_string(abbrev)) : unsafe_string(abbrev)
end

"""
signal_name(signum::Integer) -> Union{String,Nothing}

Returns the signal name (e.g. "SIGTERM") associated with the signal number for standard
signals on this architecture. If the signal number is invalid `nothing` will be returned
instead.
"""
function signal_name(signum::Integer)
abbrev = signal_abbrev(signum)
!isnothing(abbrev) || return nothing
return string("SIG", abbrev)
end

# Generate the `SIG*` constants for standard POSIX signals. We need to generate these
# constants as the associated signal numbers are architecture specific.
for signum in 1:31
signame = signal_name(signum)

if !isnothing(signame)
sigsym = Symbol(signame)
@eval begin
const $sigsym = $signum
export $sigsym
end
end
end

Base.kill(pid::Integer, signum::Integer) = ccall(:kill, Cvoid, (Cint, Cint), pid, signum)
Base.kill(signum::Integer) = kill(getpid(), signum)

const _SIGNAL_HANDLER_LOCK = Base.ReentrantLock()
const _SIGNAL_HANDLER = Dict{Cint,Base.Callable}()

const _SIGNAL_ROUTER_TASK_LOCK = Base.ReentrantLock()
const _SIGNAL_ROUTER_TASK = Ref{Task}()

function _initialize_signal_router()
condition = AsyncCondition()
signal_router_task = Threads.@spawn signal_router(condition)
errormonitor(signal_router_task)

# Allow C code to notify the `AsyncCondition`.
jl_signal_router_condition_ptr = cglobal(:jl_signal_router_condition, Ptr{Cvoid})
atomic_pointerset(jl_signal_router_condition_ptr, condition.handle, :release)

return signal_router_task
end

"""
signal_router() -> Nothing

Routes signals to user-defined signal handler functions via the `SIGNAL_CONDITION`.
Typically, this function is run in a separate thread and user-defined signal handlers
are run within that thread.
"""
function signal_router(condition::AsyncCondition)
while isopen(condition)
wait(condition) # Wait until notified by `jl_signal_router_condition`
signum = ccall(:jl_consume_user_signal, Cint, ())

# Process all queued signals while the thread is active
while signum != -1
signal_handler = @lock _SIGNAL_HANDLER_LOCK begin
get(_SIGNAL_HANDLER, signum, nothing)
end

if !isnothing(signal_handler)
invokelatest(signal_handler, signum)
end

signum = ccall(:jl_consume_user_signal, Cint, ())
end
end
return nothing
end
Comment on lines +77 to +96
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably automatically deregister the signal handler if anything in here throws, so that we don't infinitely accumulate signal messages in the runtime.


"""
register_handler(handler, signum::Integer) -> Nothing

Registers a Julia function as a signal handler for the given signal. The provided signal
handler will respond to process-directed signals.

The signals `SIGKILL` and `SIGSTOP` cannot be caught and attempting to register a handler
will throw an `ArgumentError`.
"""
function register_handler(handler, signum::Integer)
if signum == SIGKILL || signum == SIGSTOP
throw(ArgumentError("$(signal_name(signum)) is impossible to catch"))
end

# Initialize the `signal_router` task if this is the first time a user signal handler
# is being registered
@lock _SIGNAL_ROUTER_TASK_LOCK begin
if !isassigned(_SIGNAL_ROUTER_TASK)
_SIGNAL_ROUTER_TASK[] = _initialize_signal_router()
end
end

@lock _SIGNAL_HANDLER_LOCK begin
_SIGNAL_HANDLER[signum] = handler
end
ccall(:jl_register_user_signal, Cvoid, (Cint,), signum)
return nothing
end

"""
deregister_handler(signum::Integer) -> Nothing

Disassociates the Julia function from being triggered when this process receives the given
signal and restores the default Julia signal handler.
"""
function deregister_handler(signum::Integer)
@lock _SIGNAL_HANDLER_LOCK begin
if haskey(_SIGNAL_HANDLER, signum)
delete!(_SIGNAL_HANDLER, signum)
end
end
ccall(:jl_deregister_user_signal, Cvoid, (Cint,), signum)
return nothing
end

end
5 changes: 5 additions & 0 deletions src/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ JL_DLLEXPORT void jl_raise(int signo)
fflush(NULL);
#ifdef _OS_WINDOWS_
if (signo == SIGABRT) {
// TODO: Look into adding user-defined signal handlers here
signal(signo, SIG_DFL);
abort();
}
Expand All @@ -214,7 +215,10 @@ JL_DLLEXPORT void jl_raise(int signo)
TerminateProcess(GetCurrentProcess(), 3); // aka _exit
abort(); // prior call does not return, because we passed GetCurrentProcess()
#else
// TODO: Why set the default signal handler when we unblock this signal for the current
// thread right away?
signal(signo, SIG_DFL);

sigset_t sset;
sigemptyset(&sset);
sigaddset(&sset, signo);
Expand Down Expand Up @@ -714,6 +718,7 @@ JL_DLLEXPORT void jl_init_(jl_image_buf_t sysimage)
jl_init_uv();
init_stdio();
restore_fp_env();
init_signal_router();
if (jl_options.handle_signals == JL_OPTIONS_HANDLE_SIGNALS_ON)
restore_signals();

Expand Down
1 change: 1 addition & 0 deletions src/jl_exported_data.inc
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
XX(jl_readonlymemory_exception) \
XX(jl_ref_type) \
XX(jl_returnnode_type) \
XX(jl_signal_router_condition) \
XX(jl_signed_type) \
XX(jl_simplevector_type) \
XX(jl_slotnumber_type) \
Expand Down
1 change: 1 addition & 0 deletions src/jl_exported_funcs.inc
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@
XX(jl_threadpoolid) \
XX(jl_get_ptls_rng) \
XX(jl_set_ptls_rng) \
XX(jl_sigabbrev) \
XX(jl_throw) \
XX(jl_throw_out_of_memory_error) \
XX(jl_too_few_args) \
Expand Down
1 change: 1 addition & 0 deletions src/julia_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,7 @@ JL_CALLABLE(jl_f_tuple);
void jl_install_default_signal_handlers(void);
void restore_signals(void);
void jl_install_thread_signal_handler(jl_ptls_t ptls);
void init_signal_router(void);

extern uv_loop_t *jl_io_loop;
JL_DLLEXPORT void jl_uv_flush(uv_stream_t *stream);
Expand Down
2 changes: 2 additions & 0 deletions src/signal-handling.c
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,8 @@ void jl_critical_error(int sig, int si_code, bt_context_t *context, jl_task_t *c
if (sig != SIGINT)
sigaddset(&sset, sig);
pthread_sigmask(SIG_UNBLOCK, &sset, NULL);

// TODO: Need to hook in here as well
#endif
if (si_code)
jl_safe_printf("\n[%d] signal %d (%d): %s\n", getpid(), sig, si_code, strsignal(sig));
Expand Down
Loading