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 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.
@@ -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,2 @@
1
+ getting-started:
2
+ order: 1
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ # @namespace
7
+ module Async
8
+ # @namespace
9
+ module Signals
10
+ VERSION = "0.1.0"
11
+ end
12
+ 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
+ [![Development Status](https://github.com/socketry/async-signals/workflows/Test/badge.svg)](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/releases.md ADDED
@@ -0,0 +1,5 @@
1
+ # Releases
2
+
3
+ ## v0.1.0
4
+
5
+ - Initial release.
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