Class: Terminalwire::V2::Server::Rack

Inherits:
Object
  • Object
show all
Defined in:
lib/terminalwire/v2/server/rack.rb

Overview

A Rack endpoint that serves a Terminalwire CLI over a WebSocket. Mounting it is the entire integration:

# config/routes.rb
mount Terminalwire::V2::Server::Rack.new(MyCLI), at: "/terminal"

It runs on threaded servers (Puma, and friends) and on async servers (Falcon) alike. The server runtime is thread-based, so each connection runs its CLI on its own thread; this class owns that thread, the WebSocket framing, and the teardown — the host app never sees any of it.

Two server worlds, picked per request by whether we're inside an async reactor (Async::Task.current?):

* Threaded (Puma): a raw RFC 6455 upgrade whose streaming body does the
framing on plain blocking socket I/O in threads — exactly what the
thread-based runtime and Puma's socket want (no async reactor fighting
Puma's write-timeout watchdog).
* Async (Falcon): async-websocket drives the connection in reactor fibers,
bridged to the runtime's threads via a queue + a wake pipe.

Opt-in require: require "terminalwire/v2/server/rack". The async stack is pulled in lazily and only on the Falcon path, so Puma deployments (and the frame parser in unit tests) never load async/async-websocket at all.

Defined Under Namespace

Modules: Frame Classes: Parser, ReactorBridge, ThreadBridge

Constant Summary collapse

WS_GUID =
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
SUBPROTOCOLS =

WebSocket subprotocols this server speaks, best first. The handshake echoes the first one the client also offered (RFC 6455 negotiation) — the v1 handler did this (protocols: ['ws']) and some edges/proxies (e.g. Fly) drop a WebSocket whose Sec-WebSocket-Protocol the server never echoes back.

%w[terminalwire.v2 ws].freeze

Instance Method Summary collapse

Constructor Details

#initialize(cli_class, verbose: false, report: nil) ⇒ Rack

Returns a new instance of Rack.

Parameters:

  • cli_class (Class)

    a Thor CLI that includes Terminalwire::V2::Server::Thor

  • verbose (Boolean) (defaults to: false)

    send full backtraces to the client (dev only)

  • report (#call, nil) (defaults to: nil)

    optional callable invoked with unexpected errors



49
50
51
# File 'lib/terminalwire/v2/server/rack.rb', line 49

def initialize(cli_class, verbose: false, report: nil)
  @handler = Handler.new(cli_class: cli_class, verbose: verbose, report: report)
end

Instance Method Details

#call(env) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/terminalwire/v2/server/rack.rb', line 53

def call(env)
  return upgrade_required unless websocket?(env)
  # The incoming connection profile, captured at the upgrade: host (so URL
  # helpers can build absolute URLs, as v1 did), real client IP, User-Agent,
  # and the raw headers. Threaded into the session for server code + `about`.
  request = request_info(env)

  if async_reactor?
    # Async server (Falcon): let async-websocket own the connection. Pull the
    # adapter in here — this is the only path that needs the async stack.
    # :nocov: Falcon transport wiring — exercised live by the conformance suite, not units.
    require "async/websocket/adapters/rack"
    Async::WebSocket::Adapters::Rack.open(env, protocols: SUBPROTOCOLS) { |connection| ReactorBridge.new(connection, @handler, request: request).run }
    # :nocov:
  else
    # Threaded server (Puma & friends): hand-roll the upgrade and stream.
    [101, upgrade_headers(env), ThreadBridge.new(@handler, request: request)]
  end
end