muxr 0.1.10 → 0.2.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 +4 -4
- data/CHANGELOG.md +275 -1
- data/README.md +763 -387
- data/bin/muxr +68 -33
- data/bin/muxr-mcp +172 -16
- data/lib/muxr/application.rb +639 -35
- data/lib/muxr/client.rb +26 -4
- data/lib/muxr/command_dispatcher.rb +69 -0
- data/lib/muxr/config.rb +156 -0
- data/lib/muxr/control_server.rb +301 -3
- data/lib/muxr/history_row.rb +264 -0
- data/lib/muxr/image_store.rb +61 -0
- data/lib/muxr/input_handler.rb +169 -17
- data/lib/muxr/layout_manager.rb +83 -57
- data/lib/muxr/mouse_report.rb +24 -0
- data/lib/muxr/pane.rb +128 -2
- data/lib/muxr/pane_picker.rb +49 -0
- data/lib/muxr/pane_transfer.rb +172 -0
- data/lib/muxr/protocol.rb +51 -5
- data/lib/muxr/pty_process.rb +54 -7
- data/lib/muxr/remote_pane.rb +263 -0
- data/lib/muxr/renderer.rb +297 -74
- data/lib/muxr/session.rb +19 -2
- data/lib/muxr/session_directory.rb +102 -0
- data/lib/muxr/terminal.rb +903 -87
- data/lib/muxr/version.rb +1 -1
- data/lib/muxr/width_probe.rb +121 -0
- data/lib/muxr/window.rb +50 -3
- data/lib/muxr.rb +8 -0
- data/muxr.gemspec +10 -0
- data/skills/muxr-control/SKILL.md +158 -8
- metadata +18 -1
data/bin/muxr
CHANGED
|
@@ -7,6 +7,10 @@ require "fileutils"
|
|
|
7
7
|
require "rbconfig"
|
|
8
8
|
require "socket"
|
|
9
9
|
|
|
10
|
+
def configured_prefix_letter
|
|
11
|
+
Muxr::Renderer.prefix_letter(Muxr::Config.load.prefix)
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
if ARGV.include?("-v") || ARGV.include?("--version")
|
|
11
15
|
puts "muxr #{Muxr::VERSION}"
|
|
12
16
|
exit 0
|
|
@@ -18,27 +22,31 @@ if ARGV.include?("-l") || ARGV.include?("--list")
|
|
|
18
22
|
exit 0
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
install_skill_arg = ARGV.find { |a| a == "--install-skill" || a.start_with?("--install-skill=") }
|
|
26
|
+
|
|
27
|
+
if install_skill_arg
|
|
28
|
+
requested_strategy = install_skill_arg.split("=", 2)[1]
|
|
29
|
+
unless [nil, "copy", "link"].include?(requested_strategy)
|
|
30
|
+
$stderr.puts "muxr: --install-skill takes `copy` or `link` (got #{requested_strategy.inspect})"
|
|
31
|
+
exit 1
|
|
32
|
+
end
|
|
28
33
|
src = File.expand_path("../skills/muxr-control", __dir__)
|
|
29
34
|
unless File.directory?(src)
|
|
30
35
|
$stderr.puts "muxr: skill source not found at #{src} (was the gem packaged without skills/?)"
|
|
31
36
|
exit 1
|
|
32
37
|
end
|
|
38
|
+
running_from_source_checkout = File.directory?(File.expand_path("../.git", __dir__))
|
|
39
|
+
strategy = requested_strategy || (running_from_source_checkout ? "link" : "copy")
|
|
33
40
|
claude_skills = File.expand_path("~/.claude/skills")
|
|
34
41
|
dst = File.join(claude_skills, "muxr-control")
|
|
35
42
|
FileUtils.mkdir_p(claude_skills)
|
|
36
|
-
|
|
37
|
-
if
|
|
38
|
-
FileUtils.
|
|
43
|
+
FileUtils.rm_rf(dst) if File.symlink?(dst) || File.exist?(dst)
|
|
44
|
+
if strategy == "link"
|
|
45
|
+
FileUtils.ln_s(src, dst)
|
|
46
|
+
else
|
|
47
|
+
FileUtils.mkdir_p(dst)
|
|
48
|
+
FileUtils.cp_r(File.join(src, "."), dst)
|
|
39
49
|
end
|
|
40
|
-
FileUtils.mkdir_p(dst)
|
|
41
|
-
FileUtils.cp_r(File.join(src, "."), dst)
|
|
42
50
|
|
|
43
51
|
# Pick the most stable absolute path we can point claude's mcp config at:
|
|
44
52
|
#
|
|
@@ -49,13 +57,31 @@ if ARGV.include?("--install-skill")
|
|
|
49
57
|
# sits next to the running `muxr` binary.
|
|
50
58
|
bin_stub = File.join(Gem.bindir, "muxr-mcp") rescue nil
|
|
51
59
|
bridge = (bin_stub && File.exist?(bin_stub)) ? bin_stub : File.expand_path("muxr-mcp", __dir__)
|
|
60
|
+
puts(
|
|
61
|
+
if strategy == "link"
|
|
62
|
+
<<~LINKED
|
|
63
|
+
✓ Linked skill: #{dst}
|
|
64
|
+
-> #{src}
|
|
65
|
+
|
|
66
|
+
Edits to the skill are live in new claude sessions with no re-run.
|
|
67
|
+
Force a copy instead with `muxr --install-skill=copy`.
|
|
68
|
+
LINKED
|
|
69
|
+
else
|
|
70
|
+
<<~COPIED
|
|
71
|
+
✓ Copied skill to: #{dst}
|
|
72
|
+
(source: #{src})
|
|
73
|
+
|
|
74
|
+
Re-run `muxr --install-skill` after `gem update muxr` to refresh the
|
|
75
|
+
skill contents. An installed gem is copied rather than linked because
|
|
76
|
+
RubyGems prunes the old versioned gem directory on upgrade, which
|
|
77
|
+
would leave a symlink dangling. If you know this path is stable, force
|
|
78
|
+
a symlink with `muxr --install-skill=link`.
|
|
79
|
+
COPIED
|
|
80
|
+
end
|
|
81
|
+
)
|
|
82
|
+
puts
|
|
83
|
+
letter = configured_prefix_letter
|
|
52
84
|
puts <<~MSG
|
|
53
|
-
✓ Copied skill to: #{dst}
|
|
54
|
-
(source: #{src})
|
|
55
|
-
|
|
56
|
-
Re-run `muxr --install-skill` after `gem update muxr` to refresh the
|
|
57
|
-
skill contents.
|
|
58
|
-
|
|
59
85
|
Next, register the muxr MCP bridge with Claude Code at USER scope
|
|
60
86
|
(so every claude session sees it, not just ones started from your
|
|
61
87
|
home directory):
|
|
@@ -71,35 +97,44 @@ if ARGV.include?("--install-skill")
|
|
|
71
97
|
claude mcp add muxr muxr-mcp --scope user
|
|
72
98
|
|
|
73
99
|
Then restart any running Claude Code sessions so they pick up the new
|
|
74
|
-
config
|
|
75
|
-
|
|
100
|
+
config. Any claude you launch from inside muxr — a tiled pane or the
|
|
101
|
+
Claude drawer (Ctrl-#{letter} C) — auto-detects its session via MUXR_SESSION /
|
|
102
|
+
MUXR_CONTROL_SOCKET, which muxr injects into every PTY it spawns.
|
|
103
|
+
Outside muxr the bridge stays quiet and offers no tools.
|
|
104
|
+
|
|
105
|
+
Panes that were already running before this install won't have those
|
|
106
|
+
vars — open a new pane (Ctrl-#{letter} c) to pick them up.
|
|
76
107
|
MSG
|
|
77
108
|
exit 0
|
|
78
109
|
end
|
|
79
110
|
|
|
80
111
|
if ARGV.include?("-h") || ARGV.include?("--help")
|
|
112
|
+
letter = configured_prefix_letter
|
|
113
|
+
source = letter == "a" ? "" : ", from #{Muxr::Config.path_from_env.sub(Dir.home, "~")}"
|
|
81
114
|
puts <<~USAGE
|
|
82
115
|
muxr #{Muxr::VERSION} — a tiling terminal multiplexer
|
|
83
116
|
|
|
84
117
|
Usage:
|
|
85
|
-
muxr attach the
|
|
118
|
+
muxr attach the session for the current directory (auto-spawn if needed)
|
|
86
119
|
muxr <name> attach (or start) the named session
|
|
87
120
|
muxr -s <name> same as above
|
|
88
121
|
muxr --list list running sessions and exit
|
|
89
|
-
muxr --install-skill
|
|
122
|
+
muxr --install-skill install the MCP skill into ~/.claude/skills and exit
|
|
123
|
+
(symlinks from a source checkout, copies from an
|
|
124
|
+
installed gem; force with --install-skill=copy|link)
|
|
90
125
|
muxr --version print version and exit
|
|
91
126
|
muxr --help this help
|
|
92
127
|
|
|
93
|
-
Keybindings (Ctrl
|
|
94
|
-
C
|
|
95
|
-
C
|
|
96
|
-
C
|
|
97
|
-
C
|
|
98
|
-
C
|
|
99
|
-
C
|
|
100
|
-
C
|
|
101
|
-
C
|
|
102
|
-
C
|
|
128
|
+
Keybindings (Ctrl-#{letter} prefix#{source}):
|
|
129
|
+
C-#{letter} c new pane C-#{letter} n / p next / prev pane
|
|
130
|
+
C-#{letter} a toggle last pane C-#{letter} 1..9 jump to pane by number
|
|
131
|
+
C-#{letter} x close pane C-#{letter} Tab cycle layout
|
|
132
|
+
C-#{letter} ~ toggle drawer C-#{letter} Enter promote to master
|
|
133
|
+
C-#{letter} C Claude drawer (as is claude in any pane; needs `muxr --install-skill`)
|
|
134
|
+
C-#{letter} : command prompt C-#{letter} ? toggle help
|
|
135
|
+
C-#{letter} d detach (server stays running)
|
|
136
|
+
C-#{letter} q kill session (with y/n confirmation)
|
|
137
|
+
C-#{letter} C-#{letter} send literal C-#{letter}
|
|
103
138
|
USAGE
|
|
104
139
|
exit 0
|
|
105
140
|
end
|
|
@@ -131,7 +166,7 @@ end
|
|
|
131
166
|
def session_name_from(argv)
|
|
132
167
|
idx = argv.index("-s") || argv.index("--session")
|
|
133
168
|
return argv[idx + 1] if idx && argv[idx + 1]
|
|
134
|
-
argv.find { |a| !a.start_with?("-") } ||
|
|
169
|
+
argv.find { |a| !a.start_with?("-") } || Muxr::Application.default_session_name
|
|
135
170
|
end
|
|
136
171
|
|
|
137
172
|
def probe_socket(path)
|
data/bin/muxr-mcp
CHANGED
|
@@ -9,10 +9,15 @@
|
|
|
9
9
|
#
|
|
10
10
|
# 1. $MUXR_CONTROL_SOCKET (absolute path) — exact override
|
|
11
11
|
# 2. $MUXR_SESSION (session name) — resolved to standard path
|
|
12
|
-
# 3.
|
|
12
|
+
# 3. whichever live session lists $MUXR_PANE as a pane it owns, when the
|
|
13
|
+
# two above name a session that no longer has us (see below)
|
|
14
|
+
# 4. otherwise: no tools, and an explanatory error if called anyway
|
|
13
15
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
+
# muxr injects all three vars into every PTY it spawns, so end users never set
|
|
17
|
+
# them by hand. They are a snapshot of where the pane was when its shell
|
|
18
|
+
# started, which is why (3) exists: a pane can be moved to another session
|
|
19
|
+
# while its shell keeps running, and a running process's environment cannot
|
|
20
|
+
# be rewritten from outside.
|
|
16
21
|
#
|
|
17
22
|
# Typical claude-code mcp config:
|
|
18
23
|
#
|
|
@@ -63,7 +68,7 @@ class MuxrMcpBridge
|
|
|
63
68
|
# ids from muxr_panes_list.
|
|
64
69
|
PANE_REF = {
|
|
65
70
|
"type" => "string",
|
|
66
|
-
"description" => "Stable 6-hex pane id from muxr_panes_list (e.g. \"a3f9b2\"). Ids survive splits, kills, and promote_to_master — always prefer them over the positional slot numbers shown in the status bar."
|
|
71
|
+
"description" => "Stable 6-hex pane id from muxr_panes_list (e.g. \"a3f9b2\"), or the name the human gave the pane with :rename (e.g. \"api\"). Ids survive splits, kills, and promote_to_master — always prefer them over the positional slot numbers shown in the status bar."
|
|
67
72
|
}.freeze
|
|
68
73
|
|
|
69
74
|
TOOL_SCHEMAS = [
|
|
@@ -238,15 +243,22 @@ class MuxrMcpBridge
|
|
|
238
243
|
}
|
|
239
244
|
].freeze
|
|
240
245
|
|
|
246
|
+
SELF_FORBIDDEN_PANE_METHODS = ["pane.read", "pane.send_input", "pane.run", "pane.kill"].freeze
|
|
247
|
+
|
|
248
|
+
DEFAULT_SOCKETS_DIR = File.join(Dir.home, ".muxr", "sockets")
|
|
249
|
+
DISCOVERY_TIMEOUT = 0.5
|
|
250
|
+
OWNERSHIP_TTL = 10.0
|
|
251
|
+
|
|
241
252
|
def initialize
|
|
242
253
|
@socket = nil
|
|
243
254
|
@next_request_id = 0
|
|
244
255
|
@read_buffer = +""
|
|
245
256
|
@in_drawer = ENV["MUXR_DRAWER_SELF"] == "1"
|
|
257
|
+
@own_pane = ENV["MUXR_PANE"].to_s
|
|
258
|
+
@verified_at = nil
|
|
246
259
|
end
|
|
247
260
|
|
|
248
261
|
def run
|
|
249
|
-
connect_to_muxr
|
|
250
262
|
loop do
|
|
251
263
|
line = $stdin.gets
|
|
252
264
|
break if line.nil?
|
|
@@ -280,7 +292,7 @@ class MuxrMcpBridge
|
|
|
280
292
|
when "initialized", "notifications/initialized"
|
|
281
293
|
# notification, no reply
|
|
282
294
|
when "tools/list"
|
|
283
|
-
respond(id, { "tools" => TOOL_SCHEMAS })
|
|
295
|
+
respond(id, { "tools" => session_reachable? ? TOOL_SCHEMAS : [] })
|
|
284
296
|
when "tools/call"
|
|
285
297
|
handle_tool_call(id, params)
|
|
286
298
|
when "ping"
|
|
@@ -310,6 +322,17 @@ class MuxrMcpBridge
|
|
|
310
322
|
return
|
|
311
323
|
end
|
|
312
324
|
|
|
325
|
+
unless ensure_owning_connection
|
|
326
|
+
respond_tool_error(id, connection_error)
|
|
327
|
+
return
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
if targets_own_pane?(muxr_method, args)
|
|
331
|
+
respond_tool_error(id, "#{muxr_method} is unavailable on pane #{@own_pane} — that is the pane this claude " \
|
|
332
|
+
"session is running in. Use muxr_panes_list to pick a different pane.")
|
|
333
|
+
return
|
|
334
|
+
end
|
|
335
|
+
|
|
313
336
|
response = send_muxr_request(muxr_method, args)
|
|
314
337
|
if response.nil?
|
|
315
338
|
respond_tool_error(id, "muxr server closed the connection")
|
|
@@ -329,7 +352,7 @@ class MuxrMcpBridge
|
|
|
329
352
|
# Send one NDJSON request to the muxr control socket and block until the
|
|
330
353
|
# response with the matching id arrives. Server-pushed events (no id, or
|
|
331
354
|
# id mismatch) are silently dropped — the v1 bridge doesn't forward them.
|
|
332
|
-
def send_muxr_request(method, params)
|
|
355
|
+
def send_muxr_request(method, params, retrying: false)
|
|
333
356
|
@next_request_id += 1
|
|
334
357
|
rid = @next_request_id
|
|
335
358
|
payload = JSON.generate({ "id" => rid, "method" => method, "params" => params }) + "\n"
|
|
@@ -337,11 +360,22 @@ class MuxrMcpBridge
|
|
|
337
360
|
|
|
338
361
|
loop do
|
|
339
362
|
line = read_muxr_line
|
|
340
|
-
|
|
363
|
+
break unless line
|
|
341
364
|
msg = JSON.parse(line)
|
|
342
365
|
next unless msg["id"] == rid
|
|
343
366
|
return msg
|
|
344
367
|
end
|
|
368
|
+
|
|
369
|
+
reconnect_and_retry(method, params, retrying: retrying)
|
|
370
|
+
rescue SystemCallError, IOError
|
|
371
|
+
reconnect_and_retry(method, params, retrying: retrying)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def reconnect_and_retry(method, params, retrying:)
|
|
375
|
+
drop_connection
|
|
376
|
+
return nil if retrying
|
|
377
|
+
return nil unless ensure_connection
|
|
378
|
+
send_muxr_request(method, params, retrying: true)
|
|
345
379
|
end
|
|
346
380
|
|
|
347
381
|
# Line-oriented read on the muxr socket. Reads in chunks and emits one
|
|
@@ -360,17 +394,139 @@ class MuxrMcpBridge
|
|
|
360
394
|
nil
|
|
361
395
|
end
|
|
362
396
|
|
|
363
|
-
def
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
397
|
+
def targets_own_pane?(muxr_method, args)
|
|
398
|
+
return false if @own_pane.empty?
|
|
399
|
+
return false unless SELF_FORBIDDEN_PANE_METHODS.include?(muxr_method)
|
|
400
|
+
ref = args["pane"].to_s
|
|
401
|
+
ref == @own_pane || names_own_pane?(ref)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def names_own_pane?(ref)
|
|
405
|
+
panes = send_muxr_request("panes.list", {})&.dig("result", "panes") || []
|
|
406
|
+
return false if panes.any? { |pane| pane["id"].to_s == ref }
|
|
407
|
+
panes.any? { |pane| pane["id"].to_s == @own_pane && pane["name"] == ref }
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def control_socket_path
|
|
411
|
+
path = ENV["MUXR_CONTROL_SOCKET"]
|
|
412
|
+
path = derive_socket_from_session if path.nil? || path.empty?
|
|
413
|
+
path
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def env_socket_path
|
|
417
|
+
path = control_socket_path
|
|
418
|
+
return nil if path.nil? || path.empty? || !File.exist?(path)
|
|
419
|
+
path
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def resolve_control_socket
|
|
423
|
+
env_path = env_socket_path
|
|
424
|
+
return env_path if @own_pane.empty?
|
|
425
|
+
return env_path if env_path && socket_owns_own_pane?(env_path)
|
|
426
|
+
discover_owning_socket(skip: env_path) || env_path
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def sockets_dir
|
|
430
|
+
path = control_socket_path
|
|
431
|
+
return DEFAULT_SOCKETS_DIR if path.nil? || path.empty?
|
|
432
|
+
File.dirname(path)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def discover_owning_socket(skip: nil)
|
|
436
|
+
dir = sockets_dir
|
|
437
|
+
return nil unless File.directory?(dir)
|
|
438
|
+
Dir.glob(File.join(dir, "*.ctrl.sock")).sort.find do |path|
|
|
439
|
+
next false if path == skip
|
|
440
|
+
socket_owns_own_pane?(path)
|
|
368
441
|
end
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def socket_owns_own_pane?(path)
|
|
445
|
+
sock = UNIXSocket.new(path)
|
|
446
|
+
sock.write(JSON.generate("id" => 1, "method" => "panes.list", "params" => {}) + "\n")
|
|
447
|
+
deadline = monotonic + DISCOVERY_TIMEOUT
|
|
448
|
+
buffer = +""
|
|
449
|
+
loop do
|
|
450
|
+
remaining = deadline - monotonic
|
|
451
|
+
return false if remaining <= 0
|
|
452
|
+
return false unless IO.select([sock], nil, nil, remaining)
|
|
453
|
+
buffer << sock.readpartial(64 * 1024)
|
|
454
|
+
while (nl = buffer.index("\n"))
|
|
455
|
+
line = buffer.slice!(0..nl)
|
|
456
|
+
msg = JSON.parse(line) rescue nil
|
|
457
|
+
next unless msg && msg["id"] == 1
|
|
458
|
+
return owns_own_pane?(msg["result"])
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
rescue SystemCallError, IOError, EOFError
|
|
462
|
+
false
|
|
463
|
+
ensure
|
|
464
|
+
sock&.close rescue nil
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def owns_own_pane?(result)
|
|
468
|
+
panes = result.is_a?(Hash) ? result["panes"] : nil
|
|
469
|
+
return false unless panes.is_a?(Array)
|
|
470
|
+
panes.any? { |pane| pane["id"].to_s == @own_pane && !mirror?(pane) }
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def mirror?(pane)
|
|
474
|
+
!pane["origin"].nil?
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def monotonic
|
|
478
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def session_reachable?
|
|
482
|
+
!resolve_control_socket.nil?
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def connection_error
|
|
486
|
+
path = control_socket_path
|
|
487
|
+
if path.nil? || path.empty?
|
|
488
|
+
"not running inside muxr — no MUXR_CONTROL_SOCKET or MUXR_SESSION in the environment. " \
|
|
489
|
+
"Start a pane with `muxr <session>` and run claude from inside it."
|
|
490
|
+
elsif @own_pane.empty?
|
|
491
|
+
"muxr control socket not found at #{path} — is the server running? Start it with `muxr <session>`."
|
|
492
|
+
else
|
|
493
|
+
"no live muxr session owns pane #{@own_pane} (#{path} is gone, and no other session claims it) — " \
|
|
494
|
+
"is the server running? Start it with `muxr <session>`."
|
|
372
495
|
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def ensure_connection
|
|
499
|
+
return true if @socket && !@socket.closed?
|
|
500
|
+
path = resolve_control_socket
|
|
501
|
+
return false if path.nil?
|
|
373
502
|
@socket = UNIXSocket.new(path)
|
|
503
|
+
@read_buffer.clear
|
|
504
|
+
@verified_at = monotonic
|
|
505
|
+
true
|
|
506
|
+
rescue SystemCallError, IOError
|
|
507
|
+
@socket = nil
|
|
508
|
+
false
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def ensure_owning_connection
|
|
512
|
+
return false unless ensure_connection
|
|
513
|
+
return true if @own_pane.empty?
|
|
514
|
+
return true if @verified_at && monotonic - @verified_at < OWNERSHIP_TTL
|
|
515
|
+
|
|
516
|
+
if owns_own_pane?(send_muxr_request("panes.list", {})&.fetch("result", nil))
|
|
517
|
+
@verified_at = monotonic
|
|
518
|
+
return true
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
drop_connection
|
|
522
|
+
ensure_connection
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def drop_connection
|
|
526
|
+
@socket&.close rescue nil
|
|
527
|
+
@socket = nil
|
|
528
|
+
@read_buffer.clear
|
|
529
|
+
@verified_at = nil
|
|
374
530
|
end
|
|
375
531
|
|
|
376
532
|
def derive_socket_from_session
|