libtmux 0.1.0.alpha.1 → 0.1.0.alpha.2
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/README.md +19 -2
- data/assets/logo.png +0 -0
- data/assets/logo.svg +2 -0
- data/lib/libtmux/capture.rb +6 -8
- data/lib/libtmux/catalog.rb +5 -3
- data/lib/libtmux/entity.rb +6 -6
- data/lib/libtmux/metadata.rb +1 -1
- data/lib/libtmux/owned.rb +3 -0
- data/lib/libtmux/process.rb +15 -5
- data/lib/libtmux/server.rb +119 -3
- data/lib/libtmux/socket_readiness.rb +5 -1
- data/lib/libtmux/support.rb +37 -0
- data/lib/libtmux/tmux_version.rb +182 -0
- data/lib/libtmux/version.rb +1 -1
- data/lib/libtmux.rb +1 -0
- data/schema/where-v1.json +30 -9
- data/sig/fields.rbs +1 -1
- data/sig/libtmux.rbs +16 -3
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c42fbd138b8dcb5637f8a47ee771a288f318c9a34dfb83a8836ca9be6cec7a9
|
|
4
|
+
data.tar.gz: a3801bfd5c494129d0bee1872f756c03c2ec148262d245075336e9f54869ab95
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c8b889852d311651fdc65cff4a76776b7d3ab6dda81947d722ea4f268754361380e58095e99cc545c93c7a7d69b7ffc7ca4b76c09ab12ab4f620c32123f69b26
|
|
7
|
+
data.tar.gz: 98f5b864675439967d3377a465423b5719e8be045eb96e8ab53c06047e5d6c2bf3f7a6db54c6a87ab8382cfe8b272ddac1e16df444ee209144bd24579cad9406
|
data/README.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
<!-- libtmux-logo -->
|
|
2
|
+
<div align="center">
|
|
3
|
+
<picture>
|
|
4
|
+
<source srcset="assets/logo.svg" type="image/svg+xml">
|
|
5
|
+
<img src="assets/logo.png" width="128" height="128" alt="libtmux for Ruby">
|
|
6
|
+
</picture>
|
|
7
|
+
</div>
|
|
8
|
+
<!-- /libtmux-logo -->
|
|
9
|
+
|
|
10
|
+
<div align="center">
|
|
11
|
+
|
|
1
12
|
# libtmux
|
|
2
13
|
|
|
3
14
|
Ruby tmux orchestration core. `Server.open` borrows an
|
|
@@ -8,6 +19,12 @@ Owned startup uses Linux and Darwin readiness backends. The
|
|
|
8
19
|
[compatibility workflow](https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml)
|
|
9
20
|
retains exact per-revision platform and version results.
|
|
10
21
|
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
Requires tmux 3.3a or newer; unpatched 3.3 is below the minimum. A server
|
|
25
|
+
handle checks the connected daemon's release before any command is
|
|
26
|
+
dispatched, and refuses an older daemon even when its client is newer.
|
|
27
|
+
|
|
11
28
|
Install the alpha with `gem install libtmux --pre`, then require `libtmux`.
|
|
12
29
|
Imports do not
|
|
13
30
|
start tmux, a scheduler or an MCP server. See the repository's contribution
|
|
@@ -96,8 +113,8 @@ and retains `previous_generation`. Every new subscription begins with a
|
|
|
96
113
|
subscriptions stay closed, and requests are never replayed. Event sequences
|
|
97
114
|
describe one connection's observations, not durable pane history.
|
|
98
115
|
|
|
99
|
-
Typed command coverage includes hierarchy creation/listing,
|
|
100
|
-
|
|
116
|
+
Typed command coverage includes hierarchy creation/listing; rename, split,
|
|
117
|
+
resize, swap, join, break, respawn, and layout operations; link operations;
|
|
101
118
|
options/hooks/environment, capture/send/paste/pipe/buffers, copy commands,
|
|
102
119
|
display/source-file/wait-for. It does not establish complete flag parity or
|
|
103
120
|
every compatibility cell; consult the workflow results. RBS validation checks declarations;
|
data/assets/logo.png
ADDED
|
Binary file
|
data/assets/logo.svg
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 135.46667 135.46666" role="img" aria-label="libtmux for Ruby" class="libtmux-logo" data-language="ruby" data-variant="library" focusable="false"><title>libtmux for Ruby</title><g transform="translate(-210.28959,-409.98108)" data-part="artwork"><g transform="matrix(1.024,0,0,1.0240011,-5.0469507,-9.8399997)" data-part="primary-tile"><path d="M 227.78906,0 213.98242,3.6992188 V 460.62109 H 241.4082 V 15.564453 Z" transform="matrix(0.26458333,0,0,0.26458333,210.28959,409.98108)" fill="#2D0002" data-part="primary-edge"></path><path d="m 210.28981,417.13553 v 113.86425 l 60.26885,2.60089 V 409.98109 h -53.11441 c -3.95129,0 -7.15444,3.20315 -7.15444,7.15444 z" fill="#701516" data-part="primary"></path></g><g transform="matrix(1.024,0,0,1.0240011,-5.0469507,-9.8399997)" data-part="lower-tile"><path d="m 486.38086,246.03906 -5.24805,4.99805 v 207.2207 l 5.24805,-0.93164 L 500,465.18945 V 261.60352 Z" transform="matrix(0.26458333,0,0,0.26458333,210.28959,409.98108)" fill="#67221F" data-part="lower-edge"></path><path d="m 278.70874,475.07917 v 64.8875 l 60.26883,-8.98455 v -55.90295 z" fill="#984640" data-part="lower"></path></g><path d="m 210.28959,533.88585 v 4.23589 c 9e-5,4.04619 3.2803,7.32617 7.32649,7.32598 h 28.63105 l 81.26967,-1.0223 14.5495,-10.53906 v 0 z" fill="#231F20" data-part="base"></path><g transform="matrix(1.024,0,0,1.0240011,-5.0469507,-9.8399997)" data-part="upper-tile"><path d="m 459.33984,0 -25.33593,2.1074219 h 21.38281 c 14.93397,0 27.04101,12.1070061 27.04101,27.0410161 V 129.83008 l 6.17579,13.24609 L 500,112.43359 V 42.603516 C 499.99913,8.8176763 474.27408,-6.8031496e-4 459.33984,0 Z" transform="matrix(0.26458333,0,0,0.26458333,210.28959,409.98108)" fill="#854A46" data-part="upper-side"></path><path d="m 331.82331,409.98109 h -53.11442 v 56.93261 h 60.26885 v -49.77817 c 0,-3.95129 -3.20315,-7.15444 -7.15443,-7.15444 z" fill="#B6706A" data-part="upper"></path><path d="m 278.7089,466.9137 h 60.26885 l 3.6035,4.11782 h -60.26932 z" fill="#733A36" data-part="upper-bottom"></path></g><g transform="matrix(1.024,0,0,1.0240011,-5.0469507,-9.8399997)" data-part="gear-group"><path d="m 298.35661,419.37889 c -3.307,0 -6.01087,2.63649 -6.0946,5.94228 l -0.33026,13.04677 c -0.0503,1.98883 -1.3048,3.74588 -3.16843,4.44175 -1.63527,0.6105 -3.23561,1.29298 -4.79768,2.04293 -1.82786,0.87796 -4.00547,0.54134 -5.47527,-0.85609 l -9.17457,-8.72115 c -2.39692,-2.27863 -6.17361,-2.23122 -8.512,0.10759 l -14.3945,14.39447 c -1.27625,1.27604 -1.86544,2.98029 -1.77147,4.65491 -0.0118,0.22021 -0.0123,0.44029 0,0.66053 -0.0118,0.22023 -0.0123,0.44029 0,0.66053 -0.0118,0.22021 -0.0123,0.44029 0,0.66053 -0.0118,0.22024 -0.0123,0.44029 0,0.66053 -0.0118,0.22024 -0.0123,0.44029 0,0.66053 -0.0118,0.22024 -0.0123,0.44029 0,0.66053 -0.0118,0.22024 -0.0123,0.44029 0,0.66053 -0.0838,1.61437 0.46467,3.25669 1.6634,4.51768 l 8.99461,9.46208 c 0.23074,0.24278 0.42181,0.51038 0.59418,0.78721 -0.60023,1.35769 -1.15279,2.74067 -1.64984,4.15071 -0.6727,1.90958 -2.45019,3.20822 -4.47443,3.25942 l -12.65165,0.31971 c -3.30618,0.0837 -5.94279,2.7886 -5.94279,6.0956 v 0.66053 0.66053 0.66053 0.66056 2.64112 15.07361 0.66053 0.66053 0.66053 0.66053 0.66053 0.66053 0.66053 0.66053 c 0,3.30703 2.63661,6.01136 5.94279,6.09511 l 13.04677,0.32975 c 1.98885,0.0503 3.74539,1.30529 4.44124,3.16895 0.53531,1.43379 1.12957,2.83886 1.7725,4.21655 -0.17026,0.27145 -0.35881,0.53398 -0.58564,0.77264 l -8.72163,9.17405 c -1.19922,1.26148 -1.74723,2.90475 -1.66291,4.51969 -0.0124,0.22024 -0.0118,0.44029 0,0.66053 v 0.0457 h 0.0875 l 10.03868,-9.51839 29.63949,8.95286 h 43.47603 l 13.935,-16.69426 v -0.0216 c -8.04912,9.72841 -20.24315,15.90068 -33.88319,15.7844 -22.83086,-0.19494 -41.62302,-18.24181 -43.0251,-40.7806 1.34121,-22.94291 20.4588,-41.11551 43.78415,-40.91631 13.2902,0.11325 25.20451,6.28323 33.12414,15.86034 v -3.90741 -20.27843 -11.02398 l -3.97024,3.7742 c -1.44419,1.37264 -3.57681,1.72648 -5.38882,0.89729 -1.57465,-0.72024 -3.18724,-1.3729 -4.83384,-1.95295 -1.90955,-0.67268 -3.2077,-2.45012 -3.25892,-4.47392 l -0.32023,-12.65265 c -0.0836,-3.30579 -2.78757,-5.94228 -6.09457,-5.94228 z" fill="#999999" data-part="gear-edge"></path><path d="m 298.35715,419.3789 c -3.30703,0 -6.01139,2.63697 -6.09511,5.94277 l -0.33075,13.04677 c -0.0504,1.98885 -1.30431,3.74539 -3.16795,4.44126 -1.63521,0.6105 -3.23553,1.29298 -4.79716,2.04293 -1.8283,0.87755 -4.0065,0.54134 -5.47627,-0.85609 l -9.17408,-8.72115 c -2.39692,-2.27863 -6.17372,-2.23122 -8.51252,0.10759 l -14.39447,14.39498 c -2.33842,2.33839 -2.38624,6.1146 -0.10759,8.51152 l 8.9951,9.46211 c 1.37305,1.44416 1.72558,3.57678 0.89681,5.38881 -0.72068,1.57466 -1.37293,3.18676 -1.95295,4.83335 -0.67271,1.90958 -2.45022,3.20823 -4.47444,3.25942 l -12.65164,0.32023 c -3.30621,0.0837 -5.9428,2.78759 -5.9428,6.09459 v 20.35786 c 0,3.30703 2.63659,6.01087 5.9428,6.09459 l 13.04677,0.33027 c 1.98843,0.0504 3.74578,1.30479 4.44123,3.16846 0.61094,1.63521 1.29301,3.2356 2.04295,4.79765 0.87797,1.82789 0.54145,4.00591 -0.85559,5.47527 l -8.72164,9.17457 c -1.37514,1.44653 -1.88755,3.39384 -1.57592,5.22595 h 0.7465 24.32055 61.26465 3.60327 c 3.95126,0 7.15424,-3.20297 7.15424,-7.15427 v -2.056 -8.04952 c -8.05257,9.71866 -20.25002,15.87899 -33.88271,15.76278 -23.61284,-0.2016 -42.90943,-19.4986 -43.11104,-43.11106 -0.20729,-24.31847 19.55063,-44.07731 43.86912,-43.8696 13.28515,0.11319 25.2041,6.27146 33.12463,15.84172 v -18.88347 -11.02398 l -3.97073,3.77469 c -1.44417,1.37264 -3.57578,1.72599 -5.38782,0.8968 -1.57506,-0.72024 -3.18776,-1.37292 -4.83435,-1.95295 -1.90958,-0.67309 -3.20729,-2.45063 -3.25892,-4.47441 l -0.32023,-12.65167 c -0.0833,-3.3058 -2.78767,-5.94277 -6.09509,-5.94277 z" fill="#E6E6E6" data-part="gear"></path></g></g></svg>
|
data/lib/libtmux/capture.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "snapshot"
|
|
4
4
|
require_relative "metadata"
|
|
5
|
+
require_relative "tmux_version"
|
|
5
6
|
|
|
6
7
|
module LibTmux
|
|
7
8
|
module Internal
|
|
@@ -53,19 +54,16 @@ module LibTmux
|
|
|
53
54
|
end
|
|
54
55
|
version, pid, start_time = rows.first
|
|
55
56
|
version = version.dup.force_encoding(Encoding::UTF_8)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
supported = ([match[1].to_i, match[2].to_i, match[3]] <=> [3, 2, "a"]) >= 0
|
|
61
|
-
unless supported
|
|
62
|
-
raise UnsupportedFeatureError.new("captured metadata requires tmux 3.2a or newer", delivery: :observed, phase: :capture)
|
|
57
|
+
begin
|
|
58
|
+
TmuxVersion.require_supported(version)
|
|
59
|
+
rescue UnsupportedFeatureError => error
|
|
60
|
+
raise UnsupportedFeatureError.new(error.message, delivery: :observed, phase: :capture)
|
|
63
61
|
end
|
|
64
62
|
unless pid.match?(/\A[1-9][0-9]{0,9}\z/n) && start_time.match?(/\A-?[0-9]{1,19}\z/n)
|
|
65
63
|
raise ProtocolError.new("invalid capture server identity metadata", delivery: :observed, phase: :capture)
|
|
66
64
|
end
|
|
67
65
|
{version: version, pid: Integer(pid, 10), start_time: Integer(start_time, 10),
|
|
68
|
-
capabilities: {metadata: :byte_counted, field_baseline:
|
|
66
|
+
capabilities: {metadata: :byte_counted, field_baseline: TmuxVersion::MINIMUM}}
|
|
69
67
|
end
|
|
70
68
|
|
|
71
69
|
def acquire_rows(budget, attempt, clients)
|
data/lib/libtmux/catalog.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "tmux_version"
|
|
4
|
+
|
|
3
5
|
module LibTmux
|
|
4
6
|
module Internal
|
|
5
7
|
# Names and bounds are explicit: wire spelling is not Ruby case conversion.
|
|
@@ -23,7 +25,7 @@ module LibTmux
|
|
|
23
25
|
Field.new(id: id, name: name, wire_name: wire_name, format: format, type: type,
|
|
24
26
|
nullable: nullable, empty_is_null: empty_is_null, min: min, max: max,
|
|
25
27
|
operators: operators || {text: TEXT, integer: INTEGER, boolean: EQUALITY}.fetch(type),
|
|
26
|
-
min_version:
|
|
28
|
+
min_version: Internal::TmuxVersion::MINIMUM, scope: scope, capture_requirements: [scope].freeze)
|
|
27
29
|
end
|
|
28
30
|
private_class_method :field
|
|
29
31
|
|
|
@@ -40,7 +42,7 @@ module LibTmux
|
|
|
40
42
|
end
|
|
41
43
|
private_class_method :schema
|
|
42
44
|
|
|
43
|
-
#
|
|
45
|
+
# The minimum supported tmux is the capture baseline, not a claim about first introduction.
|
|
44
46
|
SCHEMAS = {
|
|
45
47
|
session: schema(:session, "session", [
|
|
46
48
|
field(:session, "session.id", :id, "id", "session_id", :text, operators: EQUALITY),
|
|
@@ -70,7 +72,7 @@ module LibTmux
|
|
|
70
72
|
field(:pane, "pane.id", :id, "id", "pane_id", :text, operators: EQUALITY),
|
|
71
73
|
field(:pane, "pane.window_id", :window_id, "windowId", "window_id", :text, operators: EQUALITY),
|
|
72
74
|
field(:pane, "pane.index", :index, "index", "pane_index", :integer, min: 0, max: UINT_MAX),
|
|
73
|
-
field(:pane, "pane.pid", :pid, "pid", "pane_pid", :integer, min: 0, max: INT_MAX),
|
|
75
|
+
field(:pane, "pane.pid", :pid, "pid", "pane_pid", :integer, nullable: true, empty_is_null: true, min: 0, max: INT_MAX),
|
|
74
76
|
field(:pane, "pane.current_command", :current_command, "currentCommand", "pane_current_command", :text),
|
|
75
77
|
field(:pane, "pane.current_path", :current_path, "currentPath", "pane_current_path", :text, nullable: true, empty_is_null: true),
|
|
76
78
|
field(:pane, "pane.title", :title, "title", "pane_title", :text),
|
data/lib/libtmux/entity.rb
CHANGED
|
@@ -135,16 +135,16 @@ module LibTmux
|
|
|
135
135
|
value = layout.is_a?(Symbol) ? layout.to_s.tr("_", "-") : layout
|
|
136
136
|
budget = server.__send__(:operation_budget, timeout, cancel)
|
|
137
137
|
# Older tmux can access an invalid pointer for a malformed layout header.
|
|
138
|
-
|
|
138
|
+
# tmux 3.8-rc2 and later report and accept saved layouts as JSON.
|
|
139
|
+
saved = /\A[0-9a-fA-F]{4},/.match?(value) ||
|
|
140
|
+
(value.start_with?("{") && server.__send__(:tmux_release) >= Internal::TmuxVersion.parse("3.8-rc2"))
|
|
141
|
+
unless LAYOUTS.include?(value) || saved
|
|
139
142
|
choices = (LAYOUTS + MIRRORED_LAYOUTS).select { |name| name.start_with?(value) }
|
|
140
143
|
raise ArgumentError, "layout must be a native name or a saved layout" if value.empty? || choices.empty?
|
|
141
144
|
|
|
142
145
|
if choices.any? { |name| MIRRORED_LAYOUTS.include?(name) }
|
|
143
|
-
|
|
144
|
-
release
|
|
145
|
-
raise ProtocolError.new("tmux version is invalid", phase: :decode, delivery: :observed) unless release
|
|
146
|
-
|
|
147
|
-
if ([release[1].to_i, release[2].to_i] <=> [3, 5]).negative?
|
|
146
|
+
release = server.__send__(:tmux_release)
|
|
147
|
+
if release < Internal::TmuxVersion.parse("3.5")
|
|
148
148
|
choices &= LAYOUTS
|
|
149
149
|
if choices.empty?
|
|
150
150
|
raise UnsupportedFeatureError.new("mirrored layouts require tmux 3.5+", phase: :admission)
|
data/lib/libtmux/metadata.rb
CHANGED
|
@@ -16,7 +16,7 @@ module LibTmux
|
|
|
16
16
|
def unquote(value)
|
|
17
17
|
escapes = {"a" => "\a", "b" => "\b", "t" => "\t", "n" => "\n", "v" => "\v",
|
|
18
18
|
"f" => "\f", "r" => "\r", "s" => " ", "E" => "\e"}
|
|
19
|
-
value.b.gsub(/\\([0-7]{3}|.)/
|
|
19
|
+
value.b.gsub(/\\([0-7]{3}|.)/nm) do
|
|
20
20
|
escaped = Regexp.last_match(1)
|
|
21
21
|
if escaped.match?(/\A[0-7]{3}\z/)
|
|
22
22
|
byte = escaped.to_i(8)
|
data/lib/libtmux/owned.rb
CHANGED
|
@@ -49,6 +49,9 @@ module LibTmux
|
|
|
49
49
|
if @endpoint.socket_path.bytesize > 103
|
|
50
50
|
raise UnsupportedFeatureError.new("owned Unix socket path exceeds the platform limit", phase: :startup)
|
|
51
51
|
end
|
|
52
|
+
Internal::TmuxVersion.executable(@endpoint.executable) do |argv|
|
|
53
|
+
ProcessExecutor.new.run(argv, timeout: deadline - clock, cancel: cancel)
|
|
54
|
+
end
|
|
52
55
|
@readiness = SocketReadiness.new(@directory)
|
|
53
56
|
@child = OwnedChild.new
|
|
54
57
|
pid = nil
|
data/lib/libtmux/process.rb
CHANGED
|
@@ -104,14 +104,17 @@ module LibTmux
|
|
|
104
104
|
@drain_timeout = drain_timeout
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
# +discard_stdout+ gives the child the null device for stdout, so the
|
|
108
|
+
# result reports no stdout and the call does not wait for a descriptor
|
|
109
|
+
# that a daemon older than tmux 3.2 keeps open after its client exits.
|
|
110
|
+
def run(argv, input: "".b, env: {}, timeout: 5.0, cancel: nil, discard_stdout: false)
|
|
111
|
+
Execution.new(argv, input, env, timeout, cancel, @limits, @cleanup_timeout, @drain_timeout, discard_stdout).call
|
|
109
112
|
end
|
|
110
113
|
|
|
111
114
|
class Execution
|
|
112
115
|
CHUNK_BYTES = 16_384
|
|
113
116
|
|
|
114
|
-
def initialize(argv, input, env, timeout, cancel, limits, cleanup_timeout, drain_timeout)
|
|
117
|
+
def initialize(argv, input, env, timeout, cancel, limits, cleanup_timeout, drain_timeout, discard_stdout)
|
|
115
118
|
@started = monotonic
|
|
116
119
|
unless argv.is_a?(Array) && !argv.empty? && argv.all? { |argument| argument.is_a?(String) && !argument.include?("\0") }
|
|
117
120
|
raise ArgumentError, "argv must contain strings without NUL"
|
|
@@ -130,6 +133,7 @@ module LibTmux
|
|
|
130
133
|
@limits = limits
|
|
131
134
|
@cleanup_timeout = cleanup_timeout
|
|
132
135
|
@drain_timeout = drain_timeout
|
|
136
|
+
@discard_stdout = discard_stdout
|
|
133
137
|
@process_wait = ProcessWait.new
|
|
134
138
|
@owned = []
|
|
135
139
|
@buffers = {stdout: +"".b, stderr: +"".b}
|
|
@@ -180,7 +184,12 @@ module LibTmux
|
|
|
180
184
|
|
|
181
185
|
def spawn
|
|
182
186
|
child_input, @input_writer = pipe
|
|
183
|
-
@
|
|
187
|
+
if @discard_stdout
|
|
188
|
+
child_output = File.open(File::NULL, "wb")
|
|
189
|
+
@owned << child_output
|
|
190
|
+
else
|
|
191
|
+
@output_reader, child_output = pipe
|
|
192
|
+
end
|
|
184
193
|
@error_reader, child_error = pipe
|
|
185
194
|
@child = OwnedChild.new(@process_wait)
|
|
186
195
|
@exit_reader = @child.reader
|
|
@@ -198,7 +207,8 @@ module LibTmux
|
|
|
198
207
|
end
|
|
199
208
|
|
|
200
209
|
def communicate
|
|
201
|
-
reading = {@
|
|
210
|
+
reading = {@error_reader => :stderr}
|
|
211
|
+
reading[@output_reader] = :stdout if @output_reader
|
|
202
212
|
loop do
|
|
203
213
|
return result if @status && reading.empty?
|
|
204
214
|
|
data/lib/libtmux/server.rb
CHANGED
|
@@ -4,9 +4,101 @@ require_relative "endpoint"
|
|
|
4
4
|
require_relative "process"
|
|
5
5
|
require_relative "metadata"
|
|
6
6
|
require_relative "entity"
|
|
7
|
+
require_relative "tmux_version"
|
|
7
8
|
|
|
8
9
|
module LibTmux
|
|
9
10
|
# Connects to an existing endpoint. Closing retires clients, never the daemon.
|
|
11
|
+
#
|
|
12
|
+
# A server is bound to one tmux executable and one Unix socket. It keeps a
|
|
13
|
+
# private link to the socket it bound, so commands never reach a different
|
|
14
|
+
# daemon that later takes over the same path. An {EntityRef} is valid only on
|
|
15
|
+
# the server that issued it; another server raises {TargetNotFoundError}.
|
|
16
|
+
#
|
|
17
|
+
# Methods that run a tmux command take `timeout:` and `cancel:`. The typed
|
|
18
|
+
# methods raise {CommandError} when tmux rejects a command, and a listing
|
|
19
|
+
# that fails raises, so an empty array means tmux answered with nothing to
|
|
20
|
+
# list. {#run} and {#run_group} answer their result whatever tmux's exit
|
|
21
|
+
# status. `max_requests:` bounds concurrent commands and `max_controls:`
|
|
22
|
+
# bounds control connections; a call past either limit raises
|
|
23
|
+
# {CapacityError} instead of waiting. After {#close} the server refuses new
|
|
24
|
+
# requests with {ClosedError}, and {#diagnostics} still answers. A forked
|
|
25
|
+
# child can close an inherited server but cannot use it.
|
|
26
|
+
#
|
|
27
|
+
# # What is on here
|
|
28
|
+
#
|
|
29
|
+
# The methods fall into the groups below. A {Session}, {Window}, or {Pane}
|
|
30
|
+
# carries the operations on that one thing.
|
|
31
|
+
#
|
|
32
|
+
# **Connecting.** {#initialize Server.new} and {Server.open} bind an endpoint
|
|
33
|
+
# that already exists; {Server.open} with a block closes the server when the
|
|
34
|
+
# block ends. {Server.start} starts a private daemon instead, creates no
|
|
35
|
+
# session, and owns the daemon's exit. {#endpoint} answers the {Endpoint}
|
|
36
|
+
# and {#owned?} says whether this server started its daemon. {#diagnostics}
|
|
37
|
+
# reports the request and control slots in use, and the limits. {#close}
|
|
38
|
+
# retires the server's clients; on an owned server it also stops the daemon.
|
|
39
|
+
# {#open_control} opens a {ControlConnection} to a session.
|
|
40
|
+
#
|
|
41
|
+
# **Finding one thing.** {#session}, {#window}, {#pane}, and {#window_link}
|
|
42
|
+
# wrap an {EntityRef} in a handle without running a command. Take the ref
|
|
43
|
+
# from a handle or a snapshot record. A ref of the wrong kind raises
|
|
44
|
+
# {TargetNotFoundError}; one whose entity has gone fails at its first
|
|
45
|
+
# command.
|
|
46
|
+
#
|
|
47
|
+
# **Listing everything.** {#list_sessions}, {#list_windows}, and
|
|
48
|
+
# {#list_panes} answer frozen arrays of handles in numeric ID order, and
|
|
49
|
+
# {#list_window_links} answers every {WindowLink}. {#list_clients} answers
|
|
50
|
+
# one hash per client. {#snapshot} captures the session, window, and pane
|
|
51
|
+
# tree, and clients with `clients: true`, as one validated {Snapshot}.
|
|
52
|
+
#
|
|
53
|
+
# **Querying things and getting around.** {#search_panes} filters the panes
|
|
54
|
+
# of a snapshot with `where:`, a hash of criteria or a {FilterExpr}, and
|
|
55
|
+
# answers a {Selection}; {#explain_panes} answers the plan for a query
|
|
56
|
+
# without running tmux. A {Selection} narrows with {Selection#where},
|
|
57
|
+
# {Selection#select}, {Selection#filter}, and {Selection#reject}, tests with
|
|
58
|
+
# {Selection#exists?}, and takes a single match with {Selection#one}, which
|
|
59
|
+
# raises {NoMatchError} or {MultipleMatchesError} otherwise, or
|
|
60
|
+
# {Selection#one_or_nil}. Build and compose criteria with
|
|
61
|
+
# {FilterExpr.build}, {FilterExpr#and}, {FilterExpr#or}, {FilterExpr#not},
|
|
62
|
+
# and {FilterExpr.from_json}.
|
|
63
|
+
#
|
|
64
|
+
# A {Snapshot} answers relational questions without another command:
|
|
65
|
+
# {Snapshot#sessions}, {Snapshot#windows}, {Snapshot#panes}, and
|
|
66
|
+
# {Snapshot#clients}. Its records follow relations by name: a
|
|
67
|
+
# {SessionSnapshot} reaches `windows`, `panes`, and `current_window`, a
|
|
68
|
+
# {WindowSnapshot} reaches `panes` and `active_pane`, and a {PaneSnapshot}
|
|
69
|
+
# reaches its `window`. {Entity#snapshot} reads one handle back as a record,
|
|
70
|
+
# and {CapturedRecord#ref} with {#session}, {#window}, or {#pane} turns a
|
|
71
|
+
# record into a handle. Live handles go down one command at a time with
|
|
72
|
+
# {Session#list_windows}, {Session#list_panes}, and {Window#list_panes}, and
|
|
73
|
+
# back up with {Entity#server}.
|
|
74
|
+
#
|
|
75
|
+
# **Changing things.** {#new_session} starts a detached session running
|
|
76
|
+
# `command:`, an argv array rather than a shell string, and answers the
|
|
77
|
+
# {Session} or, with `receipt: true`, a {CreationReceipt}. {#kill} stops the
|
|
78
|
+
# tmux server, which {#close} does only for an owned server.
|
|
79
|
+
# {#switch_client} points the client named by `client:` at a session.
|
|
80
|
+
#
|
|
81
|
+
# **Options and hooks.** {#options} answers {Options} at server scope, or at
|
|
82
|
+
# the global session or window scope. {#hooks} answers {Hooks} at the global
|
|
83
|
+
# session or window scope. {#environment}, {#set_environment}, and
|
|
84
|
+
# {#unset_environment} read, set, and unset one variable in the server's
|
|
85
|
+
# global environment.
|
|
86
|
+
#
|
|
87
|
+
# **Everything else tmux keeps.** Paste buffers: {#list_buffers},
|
|
88
|
+
# {#read_buffer}, {#write_buffer}, and {#delete_buffer}. {#source_file}
|
|
89
|
+
# loads a configuration file. {#display} expands a tmux format; the argument
|
|
90
|
+
# is a format, not literal text. {#wait_for} waits on, signals, locks, or
|
|
91
|
+
# unlocks a channel, as `action:` says.
|
|
92
|
+
#
|
|
93
|
+
# **Things that need a terminal.** {#attach} runs an interactive client on a
|
|
94
|
+
# TTY the caller supplies and keeps, blocks until that client exits, and
|
|
95
|
+
# answers a {TerminalResult}. Cancelling detaches the client and does not
|
|
96
|
+
# stop pane commands.
|
|
97
|
+
#
|
|
98
|
+
# Anything else tmux can do is reachable through {#run}, which runs one
|
|
99
|
+
# command from an argv array and keeps tmux's separators and format
|
|
100
|
+
# semantics, or {#run_group}, which runs several in one tmux invocation. A
|
|
101
|
+
# {GroupResult} cannot attribute the client's final status to a single step.
|
|
10
102
|
class Server
|
|
11
103
|
attr_reader :endpoint
|
|
12
104
|
|
|
@@ -42,7 +134,8 @@ module LibTmux
|
|
|
42
134
|
result
|
|
43
135
|
end
|
|
44
136
|
|
|
45
|
-
def initialize(endpoint: nil, socket_path: nil, socket_name: nil, executable: "tmux", max_requests: 32, max_controls: 4, close_timeout: 0.5
|
|
137
|
+
def initialize(endpoint: nil, socket_path: nil, socket_name: nil, executable: "tmux", max_requests: 32, max_controls: 4, close_timeout: 0.5,
|
|
138
|
+
version_timeout: 5.0)
|
|
46
139
|
if endpoint && (socket_path || socket_name || executable != "tmux")
|
|
47
140
|
raise ArgumentError, "endpoint cannot be combined with socket or executable options"
|
|
48
141
|
end
|
|
@@ -57,6 +150,10 @@ module LibTmux
|
|
|
57
150
|
raise ArgumentError, "close_timeout must be positive and finite"
|
|
58
151
|
end
|
|
59
152
|
|
|
153
|
+
unless version_timeout.is_a?(Numeric) && version_timeout.finite? && version_timeout.positive?
|
|
154
|
+
raise ArgumentError, "version_timeout must be positive and finite"
|
|
155
|
+
end
|
|
156
|
+
|
|
60
157
|
@endpoint = endpoint || Endpoint.new(socket_path: socket_path, socket_name: socket_name, executable: executable)
|
|
61
158
|
@owner_pid = Process.pid
|
|
62
159
|
@max_requests = max_requests
|
|
@@ -69,6 +166,19 @@ module LibTmux
|
|
|
69
166
|
@closed = false
|
|
70
167
|
@executor = Internal::ProcessExecutor.new
|
|
71
168
|
@pin = Internal::SocketIdentity.new(@endpoint)
|
|
169
|
+
begin
|
|
170
|
+
@release = Internal::TmuxVersion.connected(@pin.command_prefix) { |argv| @executor.run(argv, timeout: version_timeout, discard_stdout: true) }
|
|
171
|
+
argv, marker = Internal::TmuxVersion.fence_check_argv
|
|
172
|
+
Internal::TmuxVersion.verify_fence(@executor.run(@pin.command_prefix + argv, timeout: version_timeout), marker)
|
|
173
|
+
rescue Exception
|
|
174
|
+
@pin.close
|
|
175
|
+
raise
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The daemon's release text, such as "3.7d" or "next-3.9".
|
|
180
|
+
def tmux_version
|
|
181
|
+
@release.text
|
|
72
182
|
end
|
|
73
183
|
|
|
74
184
|
# Raw tmux arguments retain tmux's separators and format semantics.
|
|
@@ -79,8 +189,10 @@ module LibTmux
|
|
|
79
189
|
raise ArgumentError, "raw commands cannot override endpoint flags" if argv.first.start_with?("-")
|
|
80
190
|
raise ArgumentError, "timeout must be finite" unless timeout.is_a?(Numeric) && timeout.finite?
|
|
81
191
|
perform_request(cancel: cancel) do |view|
|
|
82
|
-
|
|
192
|
+
fence, marker = Internal::TmuxVersion.fence(@release)
|
|
193
|
+
result = @executor.run(@pin.command_prefix + fence + argv, input: input,
|
|
83
194
|
timeout: timeout - (monotonic - started), cancel: view)
|
|
195
|
+
Internal::TmuxVersion.check_fence(result, marker, @release)
|
|
84
196
|
end
|
|
85
197
|
end
|
|
86
198
|
|
|
@@ -283,6 +395,10 @@ module LibTmux
|
|
|
283
395
|
end
|
|
284
396
|
private_constant :CancellationView
|
|
285
397
|
|
|
398
|
+
def tmux_release
|
|
399
|
+
with_bound_endpoint { |_endpoint, _pin, release| release }
|
|
400
|
+
end
|
|
401
|
+
|
|
286
402
|
def monotonic
|
|
287
403
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
288
404
|
end
|
|
@@ -298,7 +414,7 @@ module LibTmux
|
|
|
298
414
|
ensure_owner
|
|
299
415
|
@mutex.synchronize do
|
|
300
416
|
ensure_open
|
|
301
|
-
yield @endpoint, @pin
|
|
417
|
+
yield @endpoint, @pin, @release
|
|
302
418
|
end
|
|
303
419
|
end
|
|
304
420
|
|
|
@@ -147,6 +147,10 @@ module LibTmux
|
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
class LogSocketReadiness
|
|
150
|
+
# tmux 3.8 parses more default key bindings under -v than 3.7, so the
|
|
151
|
+
# log reaches "server loop enter" past 1 MiB.
|
|
152
|
+
LIMIT = 1 << 22
|
|
153
|
+
|
|
150
154
|
attr_reader :bytes_read
|
|
151
155
|
|
|
152
156
|
def initialize(directory)
|
|
@@ -184,7 +188,7 @@ module LibTmux
|
|
|
184
188
|
end
|
|
185
189
|
while (bytes = @log.read(16_384))
|
|
186
190
|
@bytes_read += bytes.bytesize
|
|
187
|
-
if @bytes_read >
|
|
191
|
+
if @bytes_read > LIMIT
|
|
188
192
|
raise CapacityError.new("owned startup log exceeded byte limit", phase: :startup, pid: child.pid, delivery: :possibly_sent)
|
|
189
193
|
end
|
|
190
194
|
text = @tail + bytes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "libtmux/errors"
|
|
4
|
+
require "libtmux/tmux_version"
|
|
5
|
+
|
|
6
|
+
module LibTmux
|
|
7
|
+
# Here "support" means which tmux releases libtmux supports, not helper
|
|
8
|
+
# code. Every server handle checks its daemon's release before any command
|
|
9
|
+
# runs; this module exposes that check.
|
|
10
|
+
module Support
|
|
11
|
+
# The version check: is this tmux daemon supported at all?
|
|
12
|
+
module Versions
|
|
13
|
+
MINIMUM = Internal::TmuxVersion::MINIMUM
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# Returns the release text when +value+ is a supported release and
|
|
18
|
+
# raises UnsupportedFeatureError otherwise.
|
|
19
|
+
def require_supported(value)
|
|
20
|
+
Internal::TmuxVersion.require_supported(value).text
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The release text of the daemon behind +server+, such as "3.7d".
|
|
24
|
+
def server_version(server)
|
|
25
|
+
server.tmux_version
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
MINIMUM = Versions::MINIMUM
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def require_supported(value) = Versions.require_supported(value)
|
|
34
|
+
|
|
35
|
+
def server_version(server) = Versions.server_version(server)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "libtmux/errors"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module LibTmux
|
|
7
|
+
module Internal
|
|
8
|
+
# Version check for the tmux daemon a server handle is bound to.
|
|
9
|
+
#
|
|
10
|
+
# The connected daemon decides support, not the configured client. A
|
|
11
|
+
# release is read once through the retained route and every dispatch is
|
|
12
|
+
# then fenced against that exact text.
|
|
13
|
+
module TmuxVersion
|
|
14
|
+
MINIMUM = "3.3a"
|
|
15
|
+
# The forms tmux has reported: X.Y, X.Y plus a patch letter, X.Y-rc and
|
|
16
|
+
# X.Y-rcN candidates, and next-X.Y development builds. Anything else,
|
|
17
|
+
# including master and openbsd-*, is not a release number. The candidate
|
|
18
|
+
# number is bounded so that it can never outrank the release it precedes.
|
|
19
|
+
PATTERN = /\A(?:next-(?<next_major>\d+)\.(?<next_minor>\d+)|(?<major>\d+)\.(?<minor>\d+)(?:(?<patch>[a-z])|(?<candidate>-rc(?<number>\d{1,3})?))?)\z/
|
|
20
|
+
# tmux resolves a command-alias entry by exact name before it resolves a
|
|
21
|
+
# command, so the full names run-shell and if-shell can be hijacked by
|
|
22
|
+
# one alias, as the existing link guards avoid with builtin_spellings.
|
|
23
|
+
# These unique prefixes are out of reach of such an entry, and
|
|
24
|
+
# verify_fence proves at the version check that the fence still rejects
|
|
25
|
+
# its marker, so an alias named for a prefix, or a future command
|
|
26
|
+
# sharing it, refuses the server instead of silently disabling the fence.
|
|
27
|
+
FENCE_COMMAND = "if-shel"
|
|
28
|
+
PROBE_COMMAND = "run-shel"
|
|
29
|
+
ABSENT = /no server running|error connecting to .*\((?:No such file or directory|Connection refused)\)/
|
|
30
|
+
# What a daemon older than 3.2 answers to the probe. run-shell gained -C
|
|
31
|
+
# in 3.2, so 3.1 reports its usage line; a 3.0 daemon speaks a protocol
|
|
32
|
+
# the client refuses before any command runs.
|
|
33
|
+
PRE_3_2_ANSWER = /\A(?:usage: run-shell \[-b\] \[-t target-pane\] shell-command|server version is too old for client)\n\z/
|
|
34
|
+
REMEDY = "upgrade tmux and restart the server, or pin the previous libtmux release"
|
|
35
|
+
# Marks the refusals that mean "this daemon is not the supported one",
|
|
36
|
+
# so a caller that cannot show the full message can still name the floor.
|
|
37
|
+
EXPECTED = "tmux #{MINIMUM} or newer"
|
|
38
|
+
|
|
39
|
+
# `key` orders releases: major, minor, patch letter, then the build kind.
|
|
40
|
+
# A development build or release candidate ranks below the release it
|
|
41
|
+
# names: next-X.Y < X.Y-rc < X.Y-rcN < X.Y < X.Ya.
|
|
42
|
+
STABLE = 1 << 20
|
|
43
|
+
|
|
44
|
+
Release = Data.define(:text, :key) do
|
|
45
|
+
include Comparable
|
|
46
|
+
|
|
47
|
+
def <=>(other)
|
|
48
|
+
key <=> other.key if other.is_a?(Release)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
module_function
|
|
53
|
+
|
|
54
|
+
def parse(value, source: "the daemon")
|
|
55
|
+
match = value.is_a?(String) && PATTERN.match(value)
|
|
56
|
+
raise unrecognized(value, source) unless match
|
|
57
|
+
|
|
58
|
+
key = if match[:next_major]
|
|
59
|
+
[match[:next_major].to_i, match[:next_minor].to_i, 0, 0]
|
|
60
|
+
else
|
|
61
|
+
patch = match[:patch] ? match[:patch].ord - 96 : 0
|
|
62
|
+
kind = match[:candidate] ? 1 + match[:number].to_i : STABLE
|
|
63
|
+
[match[:major].to_i, match[:minor].to_i, patch, kind]
|
|
64
|
+
end
|
|
65
|
+
Release.new(text: value.dup.freeze, key: key.freeze)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def require_supported(value, minimum: MINIMUM, source: "the daemon")
|
|
69
|
+
release = parse(value, source: source)
|
|
70
|
+
if (release <=> parse(minimum)).negative?
|
|
71
|
+
reported = (source == "the daemon") ? "connected version" : "#{source} reports"
|
|
72
|
+
raise UnsupportedFeatureError.new("tmux #{minimum} or newer is required (#{reported} #{release.text}); #{REMEDY}",
|
|
73
|
+
phase: :admission, expected: EXPECTED)
|
|
74
|
+
end
|
|
75
|
+
release
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Names what the daemon or executable reported, shortened so that a
|
|
79
|
+
# long report cannot flood the message.
|
|
80
|
+
def unrecognized(value, source)
|
|
81
|
+
shown = value.is_a?(String) ? value.byteslice(0, 64).scrub("?") : value
|
|
82
|
+
UnsupportedFeatureError.new("tmux version is unrecognized (#{source} reports #{shown.inspect}); " \
|
|
83
|
+
"tmux #{MINIMUM} or newer is required; #{REMEDY}", phase: :admission, expected: EXPECTED)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Reads the daemon behind +prefix+. The block runs the argv and returns a
|
|
87
|
+
# CommandResult. Unlike display-message, run-shell -C fires no after-*
|
|
88
|
+
# hook and no command-error hook: tmux parses the expanded string after
|
|
89
|
+
# the command returned, so the private unknown command it reports has no
|
|
90
|
+
# effect and the version travels in the error text.
|
|
91
|
+
def connected(prefix)
|
|
92
|
+
marker = "libtmux-version-#{SecureRandom.hex(16)}-"
|
|
93
|
+
result = yield(prefix + [PROBE_COMMAND, "-C", marker + '#{version}'])
|
|
94
|
+
stderr = result.stderr.dup.force_encoding(Encoding::UTF_8)
|
|
95
|
+
if !result.success? && result.stdout.empty? && (match = /\Aunknown command: #{Regexp.escape(marker)}([^\n]+)\n\z/.match(stderr))
|
|
96
|
+
return require_supported(match[1])
|
|
97
|
+
end
|
|
98
|
+
if !result.success? && result.stdout.empty? && PRE_3_2_ANSWER.match?(stderr)
|
|
99
|
+
raise UnsupportedFeatureError.new("tmux #{MINIMUM} or newer is required (the connected daemon is older than 3.2); #{REMEDY}",
|
|
100
|
+
phase: :admission, expected: EXPECTED)
|
|
101
|
+
end
|
|
102
|
+
if ABSENT.match?(stderr)
|
|
103
|
+
raise TargetNotFoundError.new("no tmux server is running at the selected endpoint", phase: :admission)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
raise CommandError.new("tmux did not report a daemon version", result: result, phase: :admission, delivery: :not_sent)
|
|
107
|
+
rescue Error => error
|
|
108
|
+
raise not_sent(error), cause: nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Reads the executable that would start a new daemon. tmux 3.3a prints
|
|
112
|
+
# its own file name where later releases print "tmux".
|
|
113
|
+
def executable(path)
|
|
114
|
+
result = yield([path, "-V"])
|
|
115
|
+
unless result.success?
|
|
116
|
+
raise CommandError.new("tmux did not report its version", result: result, phase: :admission, delivery: :not_sent)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
names = ["tmux", File.basename(path)].uniq.map { |name| Regexp.escape(name.b) }.join("|")
|
|
120
|
+
match = /\A(?:#{names}) ([^\n]+)\n?\z/n.match(result.stdout)
|
|
121
|
+
raise unrecognized(result.stdout.strip, "the executable") unless match
|
|
122
|
+
|
|
123
|
+
require_supported(match[1], source: "the executable")
|
|
124
|
+
rescue Error => error
|
|
125
|
+
raise not_sent(error), cause: nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Leading commands for one dispatch. A daemon that no longer reports
|
|
129
|
+
# +release+ fails the if-shell condition and reaches the private marker,
|
|
130
|
+
# which tmux rejects before the commands that follow.
|
|
131
|
+
def fence(release)
|
|
132
|
+
marker = "libtmux-version-#{SecureRandom.hex(16)}"
|
|
133
|
+
[[FENCE_COMMAND, "-F", "\#{==:\#{version},#{release.text}}", "", marker, ";"], marker]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# A server runs this once when it is checked. A true if-shell condition
|
|
137
|
+
# runs a probe whose parse error names the marker, which shows that the
|
|
138
|
+
# fence command and the probe command are the real ones, with no failing
|
|
139
|
+
# command and so no hook.
|
|
140
|
+
def fence_check_argv
|
|
141
|
+
marker = "libtmux-fence-#{SecureRandom.hex(16)}"
|
|
142
|
+
[[FENCE_COMMAND, "-F", "1", "#{PROBE_COMMAND} -C #{marker}"], marker]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def verify_fence(result, marker)
|
|
146
|
+
return result if !result.success? && result.stdout.empty? && result.stderr.b == "unknown command: #{marker}\n".b
|
|
147
|
+
|
|
148
|
+
raise UnsupportedFeatureError.new("a tmux command-alias or command shadows #{FENCE_COMMAND} or " \
|
|
149
|
+
"#{PROBE_COMMAND}; remove it before opening a server", phase: :admission, delivery: :not_sent)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def check_fence(result, marker, release)
|
|
153
|
+
return result unless result.stderr.b.each_line.any? { |line| line.chomp == "unknown command: #{marker}".b }
|
|
154
|
+
|
|
155
|
+
raise UnsupportedFeatureError.new("the tmux daemon no longer reports #{release.text}; reopen the server",
|
|
156
|
+
phase: :admission, delivery: :not_sent, expected: EXPECTED)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# True for the refusals that name the supported floor: an unsupported,
|
|
160
|
+
# unreadable or changed daemon. Other unsupported features are not.
|
|
161
|
+
def refusal?(error)
|
|
162
|
+
error.is_a?(UnsupportedFeatureError) && error.expected == EXPECTED
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# A fixed line for results that cannot carry the full message.
|
|
166
|
+
def hint
|
|
167
|
+
"tmux #{MINIMUM} or newer is required, and the daemon must stay the " \
|
|
168
|
+
"one the server opened with; upgrade tmux and restart the server"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# A failed probe sent nothing, whatever stage it failed in, so only the
|
|
172
|
+
# delivery is forced; the error keeps its own phase.
|
|
173
|
+
def not_sent(error)
|
|
174
|
+
details = {delivery: :not_sent, phase: error.phase, pid: error.pid, cleanup_errors: error.cleanup_errors,
|
|
175
|
+
expected: error.expected}
|
|
176
|
+
return error.class.new(error.message, result: error.result, **details) if error.is_a?(CommandError)
|
|
177
|
+
|
|
178
|
+
error.class.new(error.message, **details)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
data/lib/libtmux/version.rb
CHANGED
data/lib/libtmux.rb
CHANGED
data/schema/where-v1.json
CHANGED
|
@@ -994,9 +994,16 @@
|
|
|
994
994
|
"pane.pid": {
|
|
995
995
|
"anyOf": [
|
|
996
996
|
{
|
|
997
|
-
"
|
|
998
|
-
|
|
999
|
-
|
|
997
|
+
"anyOf": [
|
|
998
|
+
{
|
|
999
|
+
"type": "integer",
|
|
1000
|
+
"minimum": 0,
|
|
1001
|
+
"maximum": 2147483647
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
"type": "null"
|
|
1005
|
+
}
|
|
1006
|
+
]
|
|
1000
1007
|
},
|
|
1001
1008
|
{
|
|
1002
1009
|
"type": "object",
|
|
@@ -1004,9 +1011,16 @@
|
|
|
1004
1011
|
"additionalProperties": false,
|
|
1005
1012
|
"properties": {
|
|
1006
1013
|
"equals": {
|
|
1007
|
-
"
|
|
1008
|
-
|
|
1009
|
-
|
|
1014
|
+
"anyOf": [
|
|
1015
|
+
{
|
|
1016
|
+
"type": "integer",
|
|
1017
|
+
"minimum": 0,
|
|
1018
|
+
"maximum": 2147483647
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
"type": "null"
|
|
1022
|
+
}
|
|
1023
|
+
]
|
|
1010
1024
|
},
|
|
1011
1025
|
"not": {
|
|
1012
1026
|
"$ref": "#/$defs/pane.pid"
|
|
@@ -1015,9 +1029,16 @@
|
|
|
1015
1029
|
"type": "array",
|
|
1016
1030
|
"maxItems": 1024,
|
|
1017
1031
|
"items": {
|
|
1018
|
-
"
|
|
1019
|
-
|
|
1020
|
-
|
|
1032
|
+
"anyOf": [
|
|
1033
|
+
{
|
|
1034
|
+
"type": "integer",
|
|
1035
|
+
"minimum": 0,
|
|
1036
|
+
"maximum": 2147483647
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
"type": "null"
|
|
1040
|
+
}
|
|
1041
|
+
]
|
|
1021
1042
|
}
|
|
1022
1043
|
},
|
|
1023
1044
|
"lt": {
|
data/sig/fields.rbs
CHANGED
data/sig/libtmux.rbs
CHANGED
|
@@ -139,11 +139,24 @@ module LibTmux
|
|
|
139
139
|
def send_keys: (*String, ?timeout: Numeric, ?cancel: untyped) -> CommandResult
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
+
module Support
|
|
143
|
+
MINIMUM: String
|
|
144
|
+
def self.require_supported: (String value) -> String
|
|
145
|
+
def self.server_version: (Server server) -> String
|
|
146
|
+
|
|
147
|
+
module Versions
|
|
148
|
+
MINIMUM: String
|
|
149
|
+
def self.require_supported: (String value) -> String
|
|
150
|
+
def self.server_version: (Server server) -> String
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
142
154
|
class Server
|
|
143
155
|
attr_reader endpoint: Endpoint
|
|
144
|
-
def self.open: [T] (?endpoint: Endpoint?, ?socket_path: String?, ?socket_name: String?, ?executable: String, ?max_requests: Integer, ?max_controls: Integer, ?close_timeout: Numeric) { (Server) -> T } -> T
|
|
145
|
-
| (?endpoint: Endpoint?, ?socket_path: String?, ?socket_name: String?, ?executable: String, ?max_requests: Integer, ?max_controls: Integer, ?close_timeout: Numeric) -> Server
|
|
146
|
-
def initialize: (?endpoint: Endpoint?, ?socket_path: String?, ?socket_name: String?, ?executable: String, ?max_requests: Integer, ?max_controls: Integer, ?close_timeout: Numeric) -> void
|
|
156
|
+
def self.open: [T] (?endpoint: Endpoint?, ?socket_path: String?, ?socket_name: String?, ?executable: String, ?max_requests: Integer, ?max_controls: Integer, ?close_timeout: Numeric, ?version_timeout: Numeric) { (Server) -> T } -> T
|
|
157
|
+
| (?endpoint: Endpoint?, ?socket_path: String?, ?socket_name: String?, ?executable: String, ?max_requests: Integer, ?max_controls: Integer, ?close_timeout: Numeric, ?version_timeout: Numeric) -> Server
|
|
158
|
+
def initialize: (?endpoint: Endpoint?, ?socket_path: String?, ?socket_name: String?, ?executable: String, ?max_requests: Integer, ?max_controls: Integer, ?close_timeout: Numeric, ?version_timeout: Numeric) -> void
|
|
159
|
+
def tmux_version: () -> String
|
|
147
160
|
def run: (Array[String], ?input: String, ?timeout: Numeric, ?cancel: untyped) -> CommandResult
|
|
148
161
|
def close: () -> nil
|
|
149
162
|
def diagnostics: () -> {admitted_requests: Integer, reserved_process_slots: Integer, control_connections: Integer, closed: bool, limits: {max_requests: Integer, max_controls: Integer, close_timeout: Numeric}}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: libtmux
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.alpha.
|
|
4
|
+
version: 0.1.0.alpha.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- libtmux contributors
|
|
@@ -87,6 +87,8 @@ extra_rdoc_files: []
|
|
|
87
87
|
files:
|
|
88
88
|
- LICENSE
|
|
89
89
|
- README.md
|
|
90
|
+
- assets/logo.png
|
|
91
|
+
- assets/logo.svg
|
|
90
92
|
- lib/libtmux.rb
|
|
91
93
|
- lib/libtmux/capture.rb
|
|
92
94
|
- lib/libtmux/catalog.rb
|
|
@@ -108,7 +110,9 @@ files:
|
|
|
108
110
|
- lib/libtmux/snapshot.rb
|
|
109
111
|
- lib/libtmux/socket_readiness.rb
|
|
110
112
|
- lib/libtmux/source_query.rb
|
|
113
|
+
- lib/libtmux/support.rb
|
|
111
114
|
- lib/libtmux/terminal.rb
|
|
115
|
+
- lib/libtmux/tmux_version.rb
|
|
112
116
|
- lib/libtmux/version.rb
|
|
113
117
|
- schema/where-v1.json
|
|
114
118
|
- sig/control.rbs
|
|
@@ -125,6 +129,7 @@ licenses:
|
|
|
125
129
|
- MIT
|
|
126
130
|
metadata:
|
|
127
131
|
source_code_uri: https://github.com/libtmux/libtmux-ruby
|
|
132
|
+
documentation_uri: https://libtmux.org/en/ruby/v0.1.0.alpha.2/
|
|
128
133
|
rdoc_options: []
|
|
129
134
|
require_paths:
|
|
130
135
|
- lib
|