async-signals 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +155 -0
- data/context/index.yaml +11 -0
- data/guides/getting-started/readme.md +155 -0
- data/guides/links.yaml +2 -0
- data/lib/async/signals/controller.rb +223 -0
- data/lib/async/signals/handlers.rb +61 -0
- data/lib/async/signals/version.rb +12 -0
- data/lib/async/signals.rb +52 -0
- data/license.md +21 -0
- data/readme.md +62 -0
- data/releases.md +5 -0
- data.tar.gz.sig +3 -0
- metadata +79 -0
- metadata.gz.sig +0 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: da817e6b406c01f5ac110ec773c2500ffe01bf6f1715163583a702291512dffc
|
|
4
|
+
data.tar.gz: 0f0b1eb81532e10ae0b06871b1607458716c021ebc180a100707db1127c6e4d3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a158082bb2d16860eb6ade8b1ff4dfbb5270feda5b38300b027563de348244af545f7b2e0e547ed134d3ebb0b0dedd14adec3be9477148e6bbfc8bea5ffef2c5
|
|
7
|
+
data.tar.gz: d22714bf8e6421f7f762c4819fe3fc73c7ac3f702e8ff77c9ee13209341b9fb26448c01149bf4aa88253695a9c612a106afccb0cf3b5b19c01f47720292870d6
|
checksums.yaml.gz.sig
ADDED
|
Binary file
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
This guide explains how to get started with `async-signals`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to your project:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
$ bundle add async-signals
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then require it in your application:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require "async/signals"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Core Concepts
|
|
20
|
+
|
|
21
|
+
Ruby signal handlers are process-wide. Calling `Signal.trap` for the same signal in two different parts of an application replaces the previous trap, which makes it easy for libraries and application code to accidentally interfere with each other.
|
|
22
|
+
|
|
23
|
+
`async-signals` provides a small coordination layer around `Signal.trap`:
|
|
24
|
+
|
|
25
|
+
- {ruby Async::Signals::Handlers} represents a configurable set of signal handlers for one consumer.
|
|
26
|
+
- {ruby Async::Signals::Controller} owns the process-wide `Signal.trap` entries while handler sets are installed.
|
|
27
|
+
- {ruby Async::Signals.install} installs a handler set using the default process-wide controller.
|
|
28
|
+
- {ruby Async::Signals.reset!} removes all active handlers and restores the previous signal traps.
|
|
29
|
+
|
|
30
|
+
Each handler set can trap or ignore signals independently. When multiple handler sets trap the same signal, `async-signals` installs one Ruby signal trap and dispatches the signal to each active handler.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
`async-signals` is useful when multiple parts of the same process need to observe or ignore signals without replacing each other's `Signal.trap` handlers.
|
|
35
|
+
|
|
36
|
+
### Handling Shutdown Signals
|
|
37
|
+
|
|
38
|
+
Use a handler set when one part of your application wants to respond to one or more signals without replacing traps installed by other code.
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
require "async/signals"
|
|
42
|
+
|
|
43
|
+
handlers = Async::Signals::Handlers.new
|
|
44
|
+
|
|
45
|
+
handlers.trap(:TERM) do |signal|
|
|
46
|
+
puts "Received signal: #{signal}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Async::Signals.install(handlers) do
|
|
50
|
+
# Signal handlers are active here.
|
|
51
|
+
sleep
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
When the block exits, the handler set is removed and any previous signal trap is restored.
|
|
56
|
+
|
|
57
|
+
### Multiple Consumers
|
|
58
|
+
|
|
59
|
+
Multiple parts of an application can listen for the same signal. This is useful when a service, supervisor, and application component each need to observe shutdown signals without taking ownership of the process-wide trap.
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
require "async/signals"
|
|
63
|
+
|
|
64
|
+
supervisor = Async::Signals::Handlers.new
|
|
65
|
+
supervisor.trap(:TERM) do
|
|
66
|
+
puts "Stopping supervisor..."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
application = Async::Signals::Handlers.new
|
|
70
|
+
application.trap(:TERM) do
|
|
71
|
+
puts "Stopping application..."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Async::Signals.install(supervisor) do
|
|
75
|
+
Async::Signals.install(application) do
|
|
76
|
+
Process.kill(:TERM, Process.pid)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Both handlers are invoked for the same signal while both handler sets are installed.
|
|
82
|
+
|
|
83
|
+
### Ignoring Signals
|
|
84
|
+
|
|
85
|
+
Use {ruby Async::Signals::Handlers#ignore} when one consumer needs a signal to be ignored while it is installed. Ignoring a signal does not suppress handlers installed by other handler sets for the same signal.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
require "async/signals"
|
|
89
|
+
|
|
90
|
+
ignored = Async::Signals::Handlers.new
|
|
91
|
+
ignored.ignore(:INT)
|
|
92
|
+
|
|
93
|
+
handled = Async::Signals::Handlers.new
|
|
94
|
+
handled.trap(:INT) do
|
|
95
|
+
puts "Still handled by another consumer."
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
Async::Signals.install(ignored) do
|
|
99
|
+
Async::Signals.install(handled) do
|
|
100
|
+
Process.kill(:INT, Process.pid)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If no active handler set traps the signal, the process-wide trap is set to ignore it for the duration of the installed ignore handler.
|
|
106
|
+
|
|
107
|
+
### Manual Registration
|
|
108
|
+
|
|
109
|
+
You can install handlers without a block when the handler lifetime is managed by a longer-lived object. The returned registration can be closed more than once.
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
require "async/signals"
|
|
113
|
+
|
|
114
|
+
handlers = Async::Signals::Handlers.new
|
|
115
|
+
handlers.trap(:HUP) do
|
|
116
|
+
puts "Reloading..."
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
registration = Async::Signals.install(handlers)
|
|
120
|
+
|
|
121
|
+
begin
|
|
122
|
+
# Run the application.
|
|
123
|
+
sleep
|
|
124
|
+
ensure
|
|
125
|
+
registration.close
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The installed handlers are snapshotted when they are installed. Later changes to the handler set do not affect an existing registration.
|
|
130
|
+
|
|
131
|
+
## Forking
|
|
132
|
+
|
|
133
|
+
Signal traps are inherited across `fork`. On Ruby implementations that support `Process._fork`, `async-signals` automatically resets inherited signal state in the forked child so the child does not keep handler registrations from the parent process.
|
|
134
|
+
|
|
135
|
+
If you need to clear all active handler sets explicitly, call:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
Async::Signals.reset!
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This restores the process-wide signal traps that were active before `async-signals` installed its handlers.
|
|
142
|
+
|
|
143
|
+
## Best Practices
|
|
144
|
+
|
|
145
|
+
Use block-form installation when possible so registrations are closed automatically. Use manual registrations only when another object clearly owns the handler lifetime.
|
|
146
|
+
|
|
147
|
+
Avoid calling `Signal.trap` for the same signals while `async-signals` handlers are installed. Direct calls to `Signal.trap` replace process-wide traps and can bypass the controller.
|
|
148
|
+
|
|
149
|
+
Keep signal handlers thread safe. Ruby implementations may dispatch signal traps from an implementation-specific thread, so handlers should avoid mutating shared state directly. Prefer doing minimal work in the handler and forwarding the event to a thread-safe mechanism such as `Thread::Queue`.
|
|
150
|
+
|
|
151
|
+
## Troubleshooting
|
|
152
|
+
|
|
153
|
+
If a handler is not invoked, check that the handler set is installed at the time the signal is delivered. Handler sets are only active inside the `Async::Signals.install` block, or until the returned registration is closed.
|
|
154
|
+
|
|
155
|
+
If a previous signal trap does not run after installation exits, make sure the registration was closed. Block-form installation handles this automatically.
|
data/context/index.yaml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Automatically generated context index for Utopia::Project guides.
|
|
2
|
+
# Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
|
|
3
|
+
---
|
|
4
|
+
description: Composable process signal handling for Ruby.
|
|
5
|
+
metadata:
|
|
6
|
+
documentation_uri: https://socketry.github.io/async-signals/
|
|
7
|
+
source_code_uri: https://github.com/socketry/async-signals.git
|
|
8
|
+
files:
|
|
9
|
+
- path: getting-started.md
|
|
10
|
+
title: Getting Started
|
|
11
|
+
description: This guide explains how to get started with `async-signals`.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
This guide explains how to get started with `async-signals`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to your project:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
$ bundle add async-signals
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then require it in your application:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require "async/signals"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Core Concepts
|
|
20
|
+
|
|
21
|
+
Ruby signal handlers are process-wide. Calling `Signal.trap` for the same signal in two different parts of an application replaces the previous trap, which makes it easy for libraries and application code to accidentally interfere with each other.
|
|
22
|
+
|
|
23
|
+
`async-signals` provides a small coordination layer around `Signal.trap`:
|
|
24
|
+
|
|
25
|
+
- {ruby Async::Signals::Handlers} represents a configurable set of signal handlers for one consumer.
|
|
26
|
+
- {ruby Async::Signals::Controller} owns the process-wide `Signal.trap` entries while handler sets are installed.
|
|
27
|
+
- {ruby Async::Signals.install} installs a handler set using the default process-wide controller.
|
|
28
|
+
- {ruby Async::Signals.reset!} removes all active handlers and restores the previous signal traps.
|
|
29
|
+
|
|
30
|
+
Each handler set can trap or ignore signals independently. When multiple handler sets trap the same signal, `async-signals` installs one Ruby signal trap and dispatches the signal to each active handler.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
`async-signals` is useful when multiple parts of the same process need to observe or ignore signals without replacing each other's `Signal.trap` handlers.
|
|
35
|
+
|
|
36
|
+
### Handling Shutdown Signals
|
|
37
|
+
|
|
38
|
+
Use a handler set when one part of your application wants to respond to one or more signals without replacing traps installed by other code.
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
require "async/signals"
|
|
42
|
+
|
|
43
|
+
handlers = Async::Signals::Handlers.new
|
|
44
|
+
|
|
45
|
+
handlers.trap(:TERM) do |signal|
|
|
46
|
+
puts "Received signal: #{signal}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Async::Signals.install(handlers) do
|
|
50
|
+
# Signal handlers are active here.
|
|
51
|
+
sleep
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
When the block exits, the handler set is removed and any previous signal trap is restored.
|
|
56
|
+
|
|
57
|
+
### Multiple Consumers
|
|
58
|
+
|
|
59
|
+
Multiple parts of an application can listen for the same signal. This is useful when a service, supervisor, and application component each need to observe shutdown signals without taking ownership of the process-wide trap.
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
require "async/signals"
|
|
63
|
+
|
|
64
|
+
supervisor = Async::Signals::Handlers.new
|
|
65
|
+
supervisor.trap(:TERM) do
|
|
66
|
+
puts "Stopping supervisor..."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
application = Async::Signals::Handlers.new
|
|
70
|
+
application.trap(:TERM) do
|
|
71
|
+
puts "Stopping application..."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Async::Signals.install(supervisor) do
|
|
75
|
+
Async::Signals.install(application) do
|
|
76
|
+
Process.kill(:TERM, Process.pid)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Both handlers are invoked for the same signal while both handler sets are installed.
|
|
82
|
+
|
|
83
|
+
### Ignoring Signals
|
|
84
|
+
|
|
85
|
+
Use {ruby Async::Signals::Handlers#ignore} when one consumer needs a signal to be ignored while it is installed. Ignoring a signal does not suppress handlers installed by other handler sets for the same signal.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
require "async/signals"
|
|
89
|
+
|
|
90
|
+
ignored = Async::Signals::Handlers.new
|
|
91
|
+
ignored.ignore(:INT)
|
|
92
|
+
|
|
93
|
+
handled = Async::Signals::Handlers.new
|
|
94
|
+
handled.trap(:INT) do
|
|
95
|
+
puts "Still handled by another consumer."
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
Async::Signals.install(ignored) do
|
|
99
|
+
Async::Signals.install(handled) do
|
|
100
|
+
Process.kill(:INT, Process.pid)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If no active handler set traps the signal, the process-wide trap is set to ignore it for the duration of the installed ignore handler.
|
|
106
|
+
|
|
107
|
+
### Manual Registration
|
|
108
|
+
|
|
109
|
+
You can install handlers without a block when the handler lifetime is managed by a longer-lived object. The returned registration can be closed more than once.
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
require "async/signals"
|
|
113
|
+
|
|
114
|
+
handlers = Async::Signals::Handlers.new
|
|
115
|
+
handlers.trap(:HUP) do
|
|
116
|
+
puts "Reloading..."
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
registration = Async::Signals.install(handlers)
|
|
120
|
+
|
|
121
|
+
begin
|
|
122
|
+
# Run the application.
|
|
123
|
+
sleep
|
|
124
|
+
ensure
|
|
125
|
+
registration.close
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The installed handlers are snapshotted when they are installed. Later changes to the handler set do not affect an existing registration.
|
|
130
|
+
|
|
131
|
+
## Forking
|
|
132
|
+
|
|
133
|
+
Signal traps are inherited across `fork`. On Ruby implementations that support `Process._fork`, `async-signals` automatically resets inherited signal state in the forked child so the child does not keep handler registrations from the parent process.
|
|
134
|
+
|
|
135
|
+
If you need to clear all active handler sets explicitly, call:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
Async::Signals.reset!
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This restores the process-wide signal traps that were active before `async-signals` installed its handlers.
|
|
142
|
+
|
|
143
|
+
## Best Practices
|
|
144
|
+
|
|
145
|
+
Use block-form installation when possible so registrations are closed automatically. Use manual registrations only when another object clearly owns the handler lifetime.
|
|
146
|
+
|
|
147
|
+
Avoid calling `Signal.trap` for the same signals while `async-signals` handlers are installed. Direct calls to `Signal.trap` replace process-wide traps and can bypass the controller.
|
|
148
|
+
|
|
149
|
+
Keep signal handlers thread safe. Ruby implementations may dispatch signal traps from an implementation-specific thread, so handlers should avoid mutating shared state directly. Prefer doing minimal work in the handler and forwarding the event to a thread-safe mechanism such as `Thread::Queue`.
|
|
150
|
+
|
|
151
|
+
## Troubleshooting
|
|
152
|
+
|
|
153
|
+
If a handler is not invoked, check that the handler set is installed at the time the signal is delivered. Handler sets are only active inside the `Async::Signals.install` block, or until the returned registration is closed.
|
|
154
|
+
|
|
155
|
+
If a previous signal trap does not run after installation exits, make sure the registration was closed. Block-form installation handles this automatically.
|
data/guides/links.yaml
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "thread"
|
|
7
|
+
|
|
8
|
+
require_relative "handlers"
|
|
9
|
+
|
|
10
|
+
module Async
|
|
11
|
+
module Signals
|
|
12
|
+
# Coordinates process-wide signal handlers for multiple consumers.
|
|
13
|
+
class Controller
|
|
14
|
+
# Represents the active handlers for a single process signal.
|
|
15
|
+
class State
|
|
16
|
+
# Initialize the signal state.
|
|
17
|
+
# @parameter previous [Object] The signal handler that was installed before this controller took ownership.
|
|
18
|
+
# @parameter handlers [Hash(Registration, Proc)] The active handlers for the signal.
|
|
19
|
+
# @parameter ignored [Hash(Registration, Nil)] The active ignored signals.
|
|
20
|
+
def initialize(previous, handlers = {}.compare_by_identity.freeze, ignored = {}.compare_by_identity.freeze)
|
|
21
|
+
@previous = previous
|
|
22
|
+
@handlers = handlers
|
|
23
|
+
@ignored = ignored
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @attribute [Object] The signal handler that was installed before this controller took ownership.
|
|
27
|
+
attr :previous
|
|
28
|
+
|
|
29
|
+
# @attribute [Hash(Registration, Proc)] The active handlers for the signal.
|
|
30
|
+
attr :handlers
|
|
31
|
+
|
|
32
|
+
# @attribute [Hash(Registration, Nil)] The active ignored signals.
|
|
33
|
+
attr :ignored
|
|
34
|
+
|
|
35
|
+
# Add a signal handler to this state.
|
|
36
|
+
# @parameter registration [Registration] The registration that owns the handler.
|
|
37
|
+
# @parameter handler [Proc | Nil] The handler to add, or `nil` to ignore the signal.
|
|
38
|
+
# @returns [State] The updated state.
|
|
39
|
+
def add(registration, handler)
|
|
40
|
+
if handler
|
|
41
|
+
handlers = @handlers.dup
|
|
42
|
+
handlers[registration] = handler
|
|
43
|
+
|
|
44
|
+
State.new(@previous, handlers.freeze, @ignored)
|
|
45
|
+
else
|
|
46
|
+
ignored = @ignored.dup
|
|
47
|
+
ignored[registration] = nil
|
|
48
|
+
|
|
49
|
+
State.new(@previous, @handlers, ignored.freeze)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Remove a signal handler from this state.
|
|
54
|
+
# @parameter registration [Registration] The registration that owns the handler.
|
|
55
|
+
# @returns [State] The updated state.
|
|
56
|
+
def remove(registration)
|
|
57
|
+
if @handlers.key?(registration)
|
|
58
|
+
handlers = @handlers.dup
|
|
59
|
+
handlers.delete(registration)
|
|
60
|
+
|
|
61
|
+
return State.new(@previous, handlers.freeze, @ignored)
|
|
62
|
+
else
|
|
63
|
+
ignored = @ignored.dup
|
|
64
|
+
ignored.delete(registration)
|
|
65
|
+
|
|
66
|
+
return State.new(@previous, @handlers, ignored.freeze)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Whether this state has any active handlers.
|
|
71
|
+
# @returns [Boolean] True if no handlers or ignored signals are active.
|
|
72
|
+
def empty?
|
|
73
|
+
@handlers.empty? && @ignored.empty?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The active callable signal handlers.
|
|
77
|
+
# @returns [Array(Proc)] The active handlers.
|
|
78
|
+
def callbacks
|
|
79
|
+
@handlers.values.freeze
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Represents an installed set of signal handlers.
|
|
84
|
+
class Registration
|
|
85
|
+
# Initialize the registration.
|
|
86
|
+
# @parameter controller [Controller] The controller that owns this registration.
|
|
87
|
+
# @parameter handlers [Hash(String, Proc | Nil)] The handlers that were installed.
|
|
88
|
+
def initialize(controller, handlers)
|
|
89
|
+
@controller = controller
|
|
90
|
+
@handlers = handlers
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Remove this registration from the controller.
|
|
94
|
+
def close
|
|
95
|
+
if handlers = @handlers
|
|
96
|
+
@handlers = nil
|
|
97
|
+
@controller.remove(self, handlers)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Initialize the controller.
|
|
103
|
+
def initialize
|
|
104
|
+
@mutex = ::Thread::Mutex.new
|
|
105
|
+
@states = {}
|
|
106
|
+
@dispatch = {}.freeze
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Install signal handlers.
|
|
110
|
+
# @parameter handlers [Handlers] The handlers to install.
|
|
111
|
+
# @yields {|handlers| ...} The block to run while the handlers are installed.
|
|
112
|
+
# @returns [Registration] The active registration.
|
|
113
|
+
def install(handlers)
|
|
114
|
+
installed_handlers = handlers.to_h.freeze
|
|
115
|
+
registration = Registration.new(self, installed_handlers)
|
|
116
|
+
|
|
117
|
+
@mutex.synchronize do
|
|
118
|
+
installed_handlers.each do |signal, handler|
|
|
119
|
+
add(signal, registration, handler)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
update_dispatch
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if block_given?
|
|
126
|
+
begin
|
|
127
|
+
return yield handlers
|
|
128
|
+
ensure
|
|
129
|
+
registration.close
|
|
130
|
+
end
|
|
131
|
+
else
|
|
132
|
+
return registration
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Dispatch a signal to all currently active handlers.
|
|
137
|
+
# @parameter signal [String] The signal name to dispatch.
|
|
138
|
+
def dispatch(signal)
|
|
139
|
+
number = ::Signal.list.fetch(signal)
|
|
140
|
+
|
|
141
|
+
@dispatch[signal]&.each do |handler|
|
|
142
|
+
begin
|
|
143
|
+
handler.call(number)
|
|
144
|
+
rescue Exception => error
|
|
145
|
+
warn "Async::Signals handler failed: #{error.class}: #{error.message}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Remove a set of installed handlers.
|
|
151
|
+
# @parameter registration [Registration] The registration that owns the handlers.
|
|
152
|
+
# @parameter handlers [Hash(String, Proc | Nil)] The handlers to remove.
|
|
153
|
+
def remove(registration, handlers)
|
|
154
|
+
@mutex.synchronize do
|
|
155
|
+
handlers.each_key do |signal|
|
|
156
|
+
remove_signal(signal, registration)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
update_dispatch
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Reset all installed signal handlers to their previous signal traps.
|
|
164
|
+
def reset!
|
|
165
|
+
@mutex.synchronize do
|
|
166
|
+
@states.each do |signal, state|
|
|
167
|
+
::Signal.trap(signal, state.previous)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
@states.clear
|
|
171
|
+
update_dispatch
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def add(signal, registration, handler)
|
|
178
|
+
unless state = @states[signal]
|
|
179
|
+
previous = ::Signal.trap(signal, "IGNORE")
|
|
180
|
+
state = State.new(previous)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
@states[signal] = state.add(registration, handler)
|
|
184
|
+
|
|
185
|
+
update_signal(signal)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def remove_signal(signal, registration)
|
|
189
|
+
if state = @states[signal]
|
|
190
|
+
state = state.remove(registration)
|
|
191
|
+
|
|
192
|
+
if state.empty?
|
|
193
|
+
::Signal.trap(signal, state.previous)
|
|
194
|
+
@states.delete(signal)
|
|
195
|
+
else
|
|
196
|
+
@states[signal] = state
|
|
197
|
+
update_signal(signal)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def update_signal(signal)
|
|
203
|
+
state = @states.fetch(signal)
|
|
204
|
+
|
|
205
|
+
if state.handlers.empty?
|
|
206
|
+
::Signal.trap(signal, "IGNORE")
|
|
207
|
+
else
|
|
208
|
+
::Signal.trap(signal) do
|
|
209
|
+
self.dispatch(signal)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def update_dispatch
|
|
215
|
+
@dispatch = @states.each_with_object({}) do |(signal, state), dispatch|
|
|
216
|
+
unless state.handlers.empty?
|
|
217
|
+
dispatch[signal] = state.callbacks
|
|
218
|
+
end
|
|
219
|
+
end.freeze
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module Signals
|
|
8
|
+
# Represents a configurable set of signal handlers.
|
|
9
|
+
class Handlers
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
# Initialize the handlers.
|
|
13
|
+
def initialize
|
|
14
|
+
@signals = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Trap a signal while these handlers are installed.
|
|
18
|
+
# @parameter signal [Symbol | String | Integer] The signal to trap.
|
|
19
|
+
def trap(signal, &block)
|
|
20
|
+
@signals[normalize(signal)] = block
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Ignore a signal while these handlers are installed.
|
|
24
|
+
# @parameter signal [Symbol | String | Integer] The signal to ignore.
|
|
25
|
+
def ignore(signal)
|
|
26
|
+
trap(signal)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Iterate over the configured signal handlers.
|
|
30
|
+
# @yields {|signal, handler| ...} The signal name and the handler, or `nil` if ignored.
|
|
31
|
+
def each(&block)
|
|
32
|
+
@signals.each(&block)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Normalize signals so the controller has one portable key per OS signal.
|
|
38
|
+
# This ensures equivalent forms like `:USR1`, `"USR1"` and `"SIGUSR1"` share
|
|
39
|
+
# the same installed trap and restoration lifecycle.
|
|
40
|
+
def normalize(signal)
|
|
41
|
+
case signal
|
|
42
|
+
when Integer
|
|
43
|
+
::Signal.list.invert.fetch(signal) do
|
|
44
|
+
raise ArgumentError, "unsupported signal number `#{signal}'"
|
|
45
|
+
end
|
|
46
|
+
when Symbol, String
|
|
47
|
+
name = signal.to_s
|
|
48
|
+
name = name.delete_prefix("SIG")
|
|
49
|
+
|
|
50
|
+
::Signal.list.fetch(name) do
|
|
51
|
+
raise ArgumentError, "unsupported signal `SIG#{name}'"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
name
|
|
55
|
+
else
|
|
56
|
+
raise ArgumentError, "bad signal type #{signal.class}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "signals/version"
|
|
7
|
+
require_relative "signals/handlers"
|
|
8
|
+
require_relative "signals/controller"
|
|
9
|
+
|
|
10
|
+
module Async
|
|
11
|
+
# Provides composable process signal handling.
|
|
12
|
+
module Signals
|
|
13
|
+
CONTROLLER = Controller.new
|
|
14
|
+
|
|
15
|
+
# The default process-wide signal controller.
|
|
16
|
+
# @returns [Controller] The default signal controller.
|
|
17
|
+
def self.controller
|
|
18
|
+
CONTROLLER
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Install signal handlers using the process-wide signal controller.
|
|
22
|
+
# @parameter handlers [Handlers] The handlers to install.
|
|
23
|
+
# @returns [Controller::Registration] The active registration.
|
|
24
|
+
def self.install(handlers, &block)
|
|
25
|
+
CONTROLLER.install(handlers, &block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Reset the process-wide signal controller.
|
|
29
|
+
# @returns [void]
|
|
30
|
+
def self.reset!
|
|
31
|
+
CONTROLLER.reset!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if ::Process.respond_to?(:_fork)
|
|
35
|
+
# Resets inherited signal state in forked children.
|
|
36
|
+
module ForkHook
|
|
37
|
+
# Fork the current process and reset inherited signal state in the child.
|
|
38
|
+
def _fork
|
|
39
|
+
pid = super
|
|
40
|
+
|
|
41
|
+
if pid == 0
|
|
42
|
+
Async::Signals.reset!
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
return pid
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
::Process.singleton_class.prepend(ForkHook)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/license.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright, 2026, by Samuel Williams.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/readme.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Async::Signals
|
|
2
|
+
|
|
3
|
+
Composable process signal handling for Ruby.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/socketry/async-signals/actions?workflow=Test)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Coordinates process-wide signal traps across multiple consumers.
|
|
10
|
+
- Supports overlapping signal handlers without replacing each other.
|
|
11
|
+
- Supports scoped ignore handlers for specific signals.
|
|
12
|
+
- Restores previous signal traps when handlers are removed.
|
|
13
|
+
- Resets inherited signal state in forked children on Ruby implementations with `Process._fork`.
|
|
14
|
+
- Documents thread-safe signal handler design for portable signal delivery.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
Please see the [project documentation](https://socketry.github.io/async-signals/) for more details.
|
|
19
|
+
|
|
20
|
+
- [Getting Started](https://socketry.github.io/async-signals/guides/getting-started/index) - This guide explains how to get started with `async-signals`.
|
|
21
|
+
|
|
22
|
+
## Releases
|
|
23
|
+
|
|
24
|
+
Please see the [project releases](https://socketry.github.io/async-signals/releases/index) for all releases.
|
|
25
|
+
|
|
26
|
+
### v0.1.0
|
|
27
|
+
|
|
28
|
+
- Initial release.
|
|
29
|
+
|
|
30
|
+
## Contributing
|
|
31
|
+
|
|
32
|
+
We welcome contributions to this project.
|
|
33
|
+
|
|
34
|
+
1. Fork it.
|
|
35
|
+
2. Create your feature branch (`git checkout -b my-new-feature`).
|
|
36
|
+
3. Commit your changes (`git commit -am 'Add some feature'`).
|
|
37
|
+
4. Push to the branch (`git push origin my-new-feature`).
|
|
38
|
+
5. Create new Pull Request.
|
|
39
|
+
|
|
40
|
+
### Running Tests
|
|
41
|
+
|
|
42
|
+
To run the test suite:
|
|
43
|
+
|
|
44
|
+
``` shell
|
|
45
|
+
bundle exec sus
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Making Releases
|
|
49
|
+
|
|
50
|
+
To make a new release:
|
|
51
|
+
|
|
52
|
+
``` shell
|
|
53
|
+
bundle exec bake gem:release:patch # or minor or major
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Developer Certificate of Origin
|
|
57
|
+
|
|
58
|
+
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
|
59
|
+
|
|
60
|
+
### Community Guidelines
|
|
61
|
+
|
|
62
|
+
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
|
data.tar.gz.sig
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
5
|
|
2
|
+
���}��yBYj����Ur��`�^���x3�+f*����q�m���&�KN�n�����Şap�>UfPա��s��9�T1f �+n۵rz3Î��Ԇ��d�a}�a[N�o�s�3��D/w¨F~2��"��>͎8���c�3�h�����d��
|
|
3
|
+
k�obI��E�F7yB���BF+C�:����,�ƥ�:k�ۻc�W�ry��QQRU�u� ��:�d��� 9w}q��2���6�@zy�.�<��?c�,n��H�`N6۹�/�m�7Y�B���z�SK5�$��T�~e��)-��-Kf����_%�O��<��L�u�K�1�;ޠS6:��-
|
metadata
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: async-signals
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Samuel Williams
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain:
|
|
10
|
+
- |
|
|
11
|
+
-----BEGIN CERTIFICATE-----
|
|
12
|
+
MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
|
|
13
|
+
ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
|
|
14
|
+
CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
|
|
15
|
+
MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
|
|
16
|
+
MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
|
|
17
|
+
bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
|
|
18
|
+
igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
|
|
19
|
+
9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
|
|
20
|
+
sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
|
|
21
|
+
e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
|
|
22
|
+
XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
|
|
23
|
+
RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
|
|
24
|
+
tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
|
|
25
|
+
zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
|
|
26
|
+
xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
|
|
27
|
+
BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
|
|
28
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
|
|
29
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
|
|
30
|
+
cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
|
|
31
|
+
xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
|
|
32
|
+
c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
|
|
33
|
+
8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
|
|
34
|
+
JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
|
|
35
|
+
eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
|
|
36
|
+
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
|
37
|
+
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
|
38
|
+
-----END CERTIFICATE-----
|
|
39
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
40
|
+
dependencies: []
|
|
41
|
+
executables: []
|
|
42
|
+
extensions: []
|
|
43
|
+
extra_rdoc_files: []
|
|
44
|
+
files:
|
|
45
|
+
- context/getting-started.md
|
|
46
|
+
- context/index.yaml
|
|
47
|
+
- guides/getting-started/readme.md
|
|
48
|
+
- guides/links.yaml
|
|
49
|
+
- lib/async/signals.rb
|
|
50
|
+
- lib/async/signals/controller.rb
|
|
51
|
+
- lib/async/signals/handlers.rb
|
|
52
|
+
- lib/async/signals/version.rb
|
|
53
|
+
- license.md
|
|
54
|
+
- readme.md
|
|
55
|
+
- releases.md
|
|
56
|
+
homepage: https://github.com/socketry/async-signals
|
|
57
|
+
licenses:
|
|
58
|
+
- MIT
|
|
59
|
+
metadata:
|
|
60
|
+
documentation_uri: https://socketry.github.io/async-signals/
|
|
61
|
+
source_code_uri: https://github.com/socketry/async-signals.git
|
|
62
|
+
rdoc_options: []
|
|
63
|
+
require_paths:
|
|
64
|
+
- lib
|
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '3.3'
|
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
requirements: []
|
|
76
|
+
rubygems_version: 4.0.10
|
|
77
|
+
specification_version: 4
|
|
78
|
+
summary: Composable process signal handling for Ruby.
|
|
79
|
+
test_files: []
|
metadata.gz.sig
ADDED
|
Binary file
|