Class: TTY::Command::Window::TrapManager

Inherits:
Object
  • Object
show all
Defined in:
lib/tty/command/window/trap_manager.rb

Overview

Process-global owner of the signals this gem cares about (+WINCH+, INT, TERM, HUP).

Ruby's Signal.trap is a process singleton: two coordinators installing their own trap for the same signal will silently clobber each other and lose the host application's previous handler. TrapManager mediates that by installing exactly one trap per signal (the first time anyone subscribes) and multiplexing to a subscriber list.

Ownership contract. Once a signal has been installed by TrapManager it remains installed for the life of the process (only TrapManager.reset!, which is test-only, uninstalls). This avoids clobbering a host-application handler that a caller installed after our first subscribe (follow-up review P1-6): restoring the snapshot we captured at first-install time would silently overwrite the host's newer handler.

Thread-safe. Trap-safe: the trap block reads a reference to the subscriber array under MRI's GVL and iterates a local snapshot; unsubscribe swaps the array reference so an in-flight trap keeps iterating the array it saw.

Class Method Summary collapse

Class Method Details

.reset!Object

Uninstall every subscriber and restore original handlers. Intended for tests and emergency teardown.



74
75
76
77
78
79
80
# File 'lib/tty/command/window/trap_manager.rb', line 74

def reset!
  @mutex.synchronize do
    # Materialize keys before iteration because uninstall mutates @originals.
    @subscribers.keys.to_a.each { |key| uninstall(key) }
    @subscribers.clear
  end
end

.subscribe(signal, callable) ⇒ #call

Register callable as a handler for signal.

Parameters:

  • signal (String, Symbol)

    e.g. "INT", "TERM", "WINCH"

  • callable (#call)

    invoked as callable.call(signal) in trap context

Returns:

  • (#call)

    the subscribed callable (pass back to unsubscribe)



39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/tty/command/window/trap_manager.rb', line 39

def subscribe(signal, callable)
  key = signal.to_s
  @mutex.synchronize do
    # Populate the subscriber list BEFORE installing the trap
    # so a signal arriving between Signal.trap returning and
    # the append cannot observe an empty list (follow-up
    # review P1-7).
    list = @subscribers[key] || []
    @subscribers[key] = list + [callable]
    install_once(key) unless @originals.key?(key)
  end
  callable
end

.subscribed?(signal) ⇒ Boolean

Returns:

  • (Boolean)


83
84
85
# File 'lib/tty/command/window/trap_manager.rb', line 83

def subscribed?(signal)
  @mutex.synchronize { !(@subscribers[signal.to_s] || []).empty? }
end

.unsubscribe(signal, callable) ⇒ void

This method returns an undefined value.

Remove a previously subscribed handler.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/tty/command/window/trap_manager.rb', line 56

def unsubscribe(signal, callable)
  key = signal.to_s
  @mutex.synchronize do
    list = @subscribers[key]
    next if list.nil?

    new_list = list.reject { |c| c.equal?(callable) }
    # Keep the installed trap even when the subscriber list
    # empties: uninstalling would restore the snapshot we took
    # at first-install time and clobber a host handler
    # installed after that snapshot (follow-up review P1-6).
    # reset! (test-only) is the one path that uninstalls.
    @subscribers[key] = new_list
  end
end