libtmux 0.1.0.alpha.1
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +109 -0
- data/lib/libtmux/capture.rb +146 -0
- data/lib/libtmux/catalog.rb +116 -0
- data/lib/libtmux/child.rb +156 -0
- data/lib/libtmux/control.rb +885 -0
- data/lib/libtmux/criteria.rb +410 -0
- data/lib/libtmux/endpoint.rb +213 -0
- data/lib/libtmux/entity.rb +262 -0
- data/lib/libtmux/errors.rb +54 -0
- data/lib/libtmux/group.rb +59 -0
- data/lib/libtmux/metadata.rb +85 -0
- data/lib/libtmux/operations.rb +412 -0
- data/lib/libtmux/options.rb +180 -0
- data/lib/libtmux/owned.rb +252 -0
- data/lib/libtmux/process.rb +327 -0
- data/lib/libtmux/process_wait.rb +66 -0
- data/lib/libtmux/selection.rb +95 -0
- data/lib/libtmux/server.rb +534 -0
- data/lib/libtmux/snapshot.rb +428 -0
- data/lib/libtmux/socket_readiness.rb +232 -0
- data/lib/libtmux/source_query.rb +57 -0
- data/lib/libtmux/terminal.rb +197 -0
- data/lib/libtmux/version.rb +5 -0
- data/lib/libtmux.rb +18 -0
- data/schema/where-v1.json +2291 -0
- data/sig/control.rbs +75 -0
- data/sig/criteria.rbs +36 -0
- data/sig/fields.rbs +72 -0
- data/sig/group.rbs +14 -0
- data/sig/libtmux.rbs +186 -0
- data/sig/operations.rbs +91 -0
- data/sig/owned.rbs +7 -0
- data/sig/snapshot.rbs +70 -0
- data/sig/terminal.rbs +15 -0
- metadata +145 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "criteria"
|
|
4
|
+
|
|
5
|
+
module LibTmux
|
|
6
|
+
module Internal
|
|
7
|
+
# Capturing all relation evidence is the conservative source plan.
|
|
8
|
+
class SourceQuery
|
|
9
|
+
COLLECTIONS = {session: :sessions, window: :windows, pane: :panes,
|
|
10
|
+
window_link: :window_links, client: :clients}.freeze
|
|
11
|
+
private_constant :COLLECTIONS
|
|
12
|
+
|
|
13
|
+
def initialize(entity, where:, pushdown: :auto)
|
|
14
|
+
unless %i[auto never required].include?(pushdown)
|
|
15
|
+
raise ArgumentError, "pushdown must be :auto, :never or :required"
|
|
16
|
+
end
|
|
17
|
+
@expression = FilterExpr.build(entity, where)
|
|
18
|
+
@entity = entity
|
|
19
|
+
@collection = COLLECTIONS.fetch(entity)
|
|
20
|
+
@pushdown = pushdown
|
|
21
|
+
reasons = pushdown == :never ? [] : ["no tmux predicate compiler has passed differential verification"]
|
|
22
|
+
@explanation = freeze_tree({entity: entity, requested_pushdown: pushdown,
|
|
23
|
+
capture: :full_graph, capture_requirements: %i[session window window_link pane] + (entity == :client ? [:client] : []),
|
|
24
|
+
pushed: nil, residual: @expression.to_h, executable: pushdown != :required,
|
|
25
|
+
rejected_optimization_reasons: reasons})
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def explain
|
|
30
|
+
@explanation
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def inspect
|
|
34
|
+
"#<#{self.class} entity=#{@entity} capture=full_graph pushdown=#{@pushdown}>"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def execute(server, **options)
|
|
38
|
+
if @pushdown == :required
|
|
39
|
+
raise UnsupportedFeatureError.new("required pushdown has no verified exact compiler; use :auto or :never", phase: :plan)
|
|
40
|
+
end
|
|
41
|
+
options = options.merge(clients: true) if @entity == :client
|
|
42
|
+
server.snapshot(**options).public_send(@collection).where(@expression)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def freeze_tree(value)
|
|
48
|
+
case value
|
|
49
|
+
when Hash then value.to_h { |key, child| [key, freeze_tree(child)] }.freeze
|
|
50
|
+
when Array then value.map { |child| freeze_tree(child) }.freeze
|
|
51
|
+
when String then value.dup.freeze
|
|
52
|
+
else value
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
require "libtmux/server"
|
|
5
|
+
require "libtmux/child"
|
|
6
|
+
require "libtmux/operations"
|
|
7
|
+
|
|
8
|
+
module LibTmux
|
|
9
|
+
# Terminal bytes go directly to the caller's TTY and are not captured.
|
|
10
|
+
class TerminalResult
|
|
11
|
+
attr_reader :status, :pid, :elapsed_seconds
|
|
12
|
+
|
|
13
|
+
def initialize(status:, pid:, elapsed_seconds:)
|
|
14
|
+
@status, @pid, @elapsed_seconds = status, pid, elapsed_seconds
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
private_class_method :new
|
|
18
|
+
|
|
19
|
+
def success?
|
|
20
|
+
status.success?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def delivery
|
|
24
|
+
:observed
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def inspect
|
|
28
|
+
"#<#{self.class} pid=#{pid} exitstatus=#{status.exitstatus}>"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Server
|
|
33
|
+
# Resolves the literal selector at dispatch; it is not a client reference.
|
|
34
|
+
def switch_client(client:, session:, timeout: 5.0, cancel: nil)
|
|
35
|
+
budget = operation_budget(timeout, cancel)
|
|
36
|
+
session_id = target(session, :session)
|
|
37
|
+
unless client.is_a?(String) && client.bytesize.between?(1, 1024) && !client.b.include?("\0")
|
|
38
|
+
raise ArgumentError, "client must be a nonempty String of at most 1024 bytes without NUL"
|
|
39
|
+
end
|
|
40
|
+
client = client.b.freeze
|
|
41
|
+
command = builtin_spellings("switch-client", budget: budget).fetch("switch-client")
|
|
42
|
+
execute_typed([command, "-E", "-c", client, "-t", session_id], **budget.options)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Blocks until the owned interactive client exits. The caller supplies and
|
|
46
|
+
# retains its TTY. Cancellation detaches this client, not pane commands.
|
|
47
|
+
def attach(session:, terminal:, term:, read_only: false, timeout: nil, cancel: nil)
|
|
48
|
+
started = monotonic
|
|
49
|
+
session_id = target(session, :session)
|
|
50
|
+
unless terminal.is_a?(IO) && !terminal.closed? && terminal.tty?
|
|
51
|
+
raise ArgumentError, "terminal must be an open caller-owned TTY"
|
|
52
|
+
end
|
|
53
|
+
unless term.is_a?(String) && /\A[A-Za-z0-9][A-Za-z0-9_.+-]{0,127}\z/.match?(term)
|
|
54
|
+
raise ArgumentError, "term must name a terminal type"
|
|
55
|
+
end
|
|
56
|
+
raise ArgumentError, "read_only must be Boolean" unless [true, false].include?(read_only)
|
|
57
|
+
unless timeout.nil? || (timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?)
|
|
58
|
+
raise ArgumentError, "terminal timeout must be positive and finite, or nil"
|
|
59
|
+
end
|
|
60
|
+
argv = @pin.command_prefix + ["attach-session", "-E", *(read_only ? ["-r"] : []), "-t", session_id]
|
|
61
|
+
perform_request(cancel: cancel) do |view|
|
|
62
|
+
Internal.const_get(:TerminalExecution, false).new(argv, terminal, term,
|
|
63
|
+
timeout && timeout - (monotonic - started), view).call
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
module Internal
|
|
69
|
+
class TerminalExecution
|
|
70
|
+
def initialize(argv, terminal, term, timeout, cancel)
|
|
71
|
+
@argv, @terminal, @term, @cancel = argv, terminal, term.dup.freeze, cancel
|
|
72
|
+
@started = clock
|
|
73
|
+
@deadline = timeout && @started + timeout
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def call
|
|
77
|
+
failure = result = nil
|
|
78
|
+
begin
|
|
79
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
80
|
+
begin
|
|
81
|
+
check_cancel
|
|
82
|
+
check_deadline
|
|
83
|
+
@tty = @terminal.dup
|
|
84
|
+
@tty.close_on_exec = true
|
|
85
|
+
@mode = @tty.console_mode
|
|
86
|
+
@child = OwnedChild.new
|
|
87
|
+
begin
|
|
88
|
+
@pid = Process.spawn({"TMUX" => nil, "TMUX_PANE" => nil, "TERM" => @term},
|
|
89
|
+
*@argv, in: @tty, out: @tty, err: @tty, close_others: true)
|
|
90
|
+
rescue SystemCallError, IOError => error
|
|
91
|
+
raise TransportError.new("terminal client could not start (#{error.class})", **details(:spawn)), cause: nil
|
|
92
|
+
ensure
|
|
93
|
+
@child.spawned(@pid)
|
|
94
|
+
end
|
|
95
|
+
Thread.handle_interrupt(Exception => :immediate) { result = await_exit }
|
|
96
|
+
rescue Exception => error
|
|
97
|
+
failure = error
|
|
98
|
+
ensure
|
|
99
|
+
errors = retire
|
|
100
|
+
if failure.is_a?(Error)
|
|
101
|
+
failure.__send__(:attach_cleanup_errors, errors)
|
|
102
|
+
elsif !failure && !errors.empty?
|
|
103
|
+
failure = TransportError.new("terminal cleanup failed", **details(:retire), cleanup_errors: errors)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
rescue Exception => deferred
|
|
108
|
+
failure ||= deferred
|
|
109
|
+
end
|
|
110
|
+
raise failure, cause: nil if failure
|
|
111
|
+
|
|
112
|
+
result
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def await_exit
|
|
118
|
+
loop do
|
|
119
|
+
if @child.observed?
|
|
120
|
+
@retire_deadline ||= clock + 0.5
|
|
121
|
+
@child.finish_signalling
|
|
122
|
+
end
|
|
123
|
+
raise @child.observation_error if @child.observation_error
|
|
124
|
+
if @child.complete?
|
|
125
|
+
raise @child.retirement_error if @child.retirement_error
|
|
126
|
+
|
|
127
|
+
return TerminalResult.__send__(:new, status: @child.status, pid: @pid, elapsed_seconds: clock - @started)
|
|
128
|
+
end
|
|
129
|
+
check_cancel unless @retire_deadline
|
|
130
|
+
deadline = @retire_deadline || @deadline
|
|
131
|
+
if deadline && clock >= deadline
|
|
132
|
+
raise DeadlineExceeded.new("terminal client exceeded its deadline", **details(:terminal))
|
|
133
|
+
end
|
|
134
|
+
readers = [@child.reader]
|
|
135
|
+
readers << @cancel.reader unless @retire_deadline
|
|
136
|
+
if IO.select(readers, nil, nil, deadline && [deadline - clock, 0].max)
|
|
137
|
+
@child.reader.read_nonblock(16, exception: false)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
rescue SystemCallError, IOError => error
|
|
141
|
+
raise TransportError.new("terminal process observation failed (#{error.class})", **details(:terminal)), cause: nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def retire
|
|
145
|
+
errors = []
|
|
146
|
+
deadline = clock + 0.5
|
|
147
|
+
if @child
|
|
148
|
+
attempt(errors, "terminal client retirement") do
|
|
149
|
+
begin
|
|
150
|
+
unless @child.observed? || @child.complete?
|
|
151
|
+
@child.signal("TERM")
|
|
152
|
+
@child.wait_observed(0.05)
|
|
153
|
+
@child.signal("KILL") unless @child.observed? || @child.complete?
|
|
154
|
+
end
|
|
155
|
+
ensure
|
|
156
|
+
@child.finish_signalling
|
|
157
|
+
end
|
|
158
|
+
unless @child.join((deadline - clock).clamp(0, 0.5))
|
|
159
|
+
raise DeadlineExceeded.new("terminal client did not retire", **details(:retire))
|
|
160
|
+
end
|
|
161
|
+
raise @child.retirement_error if @child.retirement_error
|
|
162
|
+
raise @child.observation_error if @child.observation_error
|
|
163
|
+
end
|
|
164
|
+
attempt(errors, "observer close") { @child.close } if @child.complete?
|
|
165
|
+
end
|
|
166
|
+
attempt(errors, "terminal mode restoration") { @tty.console_mode = @mode } if @mode
|
|
167
|
+
attempt(errors, "terminal descriptor close") { @tty.close unless @tty.closed? } if @tty
|
|
168
|
+
errors
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def attempt(errors, operation)
|
|
172
|
+
yield
|
|
173
|
+
rescue Exception => error
|
|
174
|
+
errors << "#{operation} failed (#{error.class})"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def check_cancel
|
|
178
|
+
raise Cancelled.new("terminal attachment cancelled", **details(:terminal)) if @cancel.cancelled?
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def check_deadline
|
|
182
|
+
if @deadline && clock >= @deadline
|
|
183
|
+
raise DeadlineExceeded.new("terminal attachment deadline elapsed", **details(:admission))
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def details(phase)
|
|
188
|
+
{phase: phase, pid: @pid, delivery: @pid ? :possibly_sent : :not_sent}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def clock
|
|
192
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
private_constant :TerminalExecution
|
|
196
|
+
end
|
|
197
|
+
end
|
data/lib/libtmux.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "libtmux/version"
|
|
4
|
+
require "libtmux/errors"
|
|
5
|
+
require "libtmux/process"
|
|
6
|
+
require "libtmux/endpoint"
|
|
7
|
+
require "libtmux/metadata"
|
|
8
|
+
require "libtmux/selection"
|
|
9
|
+
require "libtmux/entity"
|
|
10
|
+
require "libtmux/server"
|
|
11
|
+
require "libtmux/operations"
|
|
12
|
+
require "libtmux/criteria"
|
|
13
|
+
require "libtmux/capture"
|
|
14
|
+
require "libtmux/source_query"
|
|
15
|
+
require "libtmux/control"
|
|
16
|
+
require "libtmux/group"
|
|
17
|
+
require "libtmux/owned"
|
|
18
|
+
require "libtmux/terminal"
|