rosh 0.9.6 → 0.9.7
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/.gitignore +1 -0
- data/README.md +5 -0
- data/lib/rosh/version.rb +1 -1
- data/lib/rosh.rb +37 -21
- data/test/rosh_forwarding_test.rb +69 -4
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3c244fe02aac976b4bc470e13f529e71bd0a0ec43cc2a389222b7dd7b9b2fc2
|
|
4
|
+
data.tar.gz: e4283b6e8c61adb188861184350959de02ba1c75ecc751bf1c8101c4f6934f31
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12de5a2533f7943d5adc5f29d5f675f97da16e692c615eb40e0357adbce3c649619d44536fd92f5278cb7157a798c32658207869875865b7bdf42ebf14536fcd
|
|
7
|
+
data.tar.gz: cb3c1055ad0e11fd5ad55c81268508ed795d165157606e0c16925fc3dd18e3ba0fb74c39b5104926766accf9b6d619e5e62f73561a83bc5336377fdbe1423ed7
|
data/.gitignore
CHANGED
data/README.md
CHANGED
|
@@ -30,6 +30,11 @@ Or install it yourself as:
|
|
|
30
30
|
|
|
31
31
|
If ~/.ssh/config contains LocalForward or RemoteForward for the host, the same
|
|
32
32
|
forwarding options are passed to `ssh` automatically.
|
|
33
|
+
|
|
34
|
+
If a `LocalForward` is skipped because the local port is actually in use,
|
|
35
|
+
rosh retries that forwarding on later reconnect attempts instead of dropping it
|
|
36
|
+
for the rest of the process lifetime.
|
|
37
|
+
|
|
33
38
|
To detach the outer screen session,
|
|
34
39
|
|
|
35
40
|
^t d
|
data/lib/rosh/version.rb
CHANGED
data/lib/rosh.rb
CHANGED
|
@@ -9,6 +9,9 @@ class Rosh
|
|
|
9
9
|
def initialize(*args)
|
|
10
10
|
@interval = 3
|
|
11
11
|
@ssh_opts = []
|
|
12
|
+
@base_ssh_opts = []
|
|
13
|
+
@local_forward_specs = []
|
|
14
|
+
@remote_forward_specs = []
|
|
12
15
|
alive_interval = 5
|
|
13
16
|
@escape = '^t'
|
|
14
17
|
@tmux_socket_name = nil
|
|
@@ -23,8 +26,8 @@ class Rosh
|
|
|
23
26
|
end.parse! args
|
|
24
27
|
@host, @name = *args, :default
|
|
25
28
|
abort 'hostname is required' if @host == :default
|
|
26
|
-
@
|
|
27
|
-
@
|
|
29
|
+
@base_ssh_opts << "-o ServerAliveInterval=#{alive_interval}"
|
|
30
|
+
@base_ssh_opts << "-o ServerAliveCountMax=1"
|
|
28
31
|
|
|
29
32
|
# check ~/.ssh/config to resolve alias name
|
|
30
33
|
alias_name = @host
|
|
@@ -34,19 +37,16 @@ class Rosh
|
|
|
34
37
|
end
|
|
35
38
|
@oom_reported = false
|
|
36
39
|
@last_exit_status = nil
|
|
37
|
-
local_forwards(alias_name)
|
|
38
|
-
|
|
39
|
-
end
|
|
40
|
-
remote_forwards(alias_name).each do |f|
|
|
41
|
-
add_forward_option(:remote, f)
|
|
42
|
-
end
|
|
40
|
+
@local_forward_specs = local_forwards(alias_name)
|
|
41
|
+
@remote_forward_specs = remote_forwards(alias_name)
|
|
43
42
|
@host = config[:host_name] if config[:host_name]
|
|
44
|
-
@
|
|
45
|
-
@
|
|
46
|
-
@
|
|
43
|
+
@base_ssh_opts << "-l #{config[:user]}" if config[:user]
|
|
44
|
+
@base_ssh_opts << "-p #{config[:port]}" if config[:port]
|
|
45
|
+
@base_ssh_opts << "-J #{config[:proxy].jump_proxies}" if config[:proxy]
|
|
47
46
|
if keys = config[:keys]
|
|
48
|
-
keys.each{|k| @
|
|
47
|
+
keys.each{|k| @base_ssh_opts << "-i #{k}"}
|
|
49
48
|
end
|
|
49
|
+
refresh_ssh_opts!
|
|
50
50
|
if @verbose
|
|
51
51
|
puts "host: #{@host}"
|
|
52
52
|
puts "name: #{@name}"
|
|
@@ -58,25 +58,20 @@ class Rosh
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def connect
|
|
61
|
-
cmd = if @screen
|
|
62
|
-
["ssh", *@ssh_opts, resolv,
|
|
63
|
-
'-t', "'screen -rx #{@name}'", '2>/dev/null']*' '
|
|
64
|
-
else
|
|
65
|
-
remote_cmd = single_quote(tmux_attach_command)
|
|
66
|
-
["ssh", *@ssh_opts, resolv,
|
|
67
|
-
'-t', remote_cmd, '2>/dev/null']*' '
|
|
68
|
-
end
|
|
69
61
|
if @verbose
|
|
70
62
|
puts "connecting to #{@host}..."
|
|
71
|
-
puts cmd
|
|
72
63
|
end
|
|
64
|
+
cmd = nil
|
|
73
65
|
begin
|
|
74
66
|
reconnect
|
|
67
|
+
cmd = attach_command
|
|
68
|
+
puts cmd if @verbose
|
|
75
69
|
end until execute_attach(cmd)
|
|
76
70
|
report_session_end(cmd)
|
|
77
71
|
end
|
|
78
72
|
|
|
79
73
|
def reconnect
|
|
74
|
+
refresh_ssh_opts!
|
|
80
75
|
if @first_try
|
|
81
76
|
session_exists = if @screen
|
|
82
77
|
sh('-p 0 -X echo ok', '2>&1 >/dev/null')
|
|
@@ -107,6 +102,16 @@ class Rosh
|
|
|
107
102
|
end
|
|
108
103
|
|
|
109
104
|
private
|
|
105
|
+
def refresh_ssh_opts!
|
|
106
|
+
@ssh_opts = @base_ssh_opts.dup
|
|
107
|
+
@local_forward_specs.each do |spec|
|
|
108
|
+
add_forward_option(:local, spec)
|
|
109
|
+
end
|
|
110
|
+
@remote_forward_specs.each do |spec|
|
|
111
|
+
add_forward_option(:remote, spec)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
110
115
|
def execute_attach(cmd)
|
|
111
116
|
result = system cmd
|
|
112
117
|
@last_exit_status = $?
|
|
@@ -238,6 +243,17 @@ private
|
|
|
238
243
|
system cmd
|
|
239
244
|
end
|
|
240
245
|
|
|
246
|
+
def attach_command
|
|
247
|
+
if @screen
|
|
248
|
+
["ssh", *@ssh_opts, resolv,
|
|
249
|
+
'-t', "'screen -rx #{@name}'", '2>/dev/null']*' '
|
|
250
|
+
else
|
|
251
|
+
remote_cmd = single_quote(tmux_attach_command)
|
|
252
|
+
["ssh", *@ssh_opts, resolv,
|
|
253
|
+
'-t', remote_cmd, '2>/dev/null']*' '
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
241
257
|
def local_forwards(host)
|
|
242
258
|
file = File.expand_path("~/.ssh/config")
|
|
243
259
|
return [] unless File.readable?(file)
|
|
@@ -5,6 +5,22 @@ require 'fileutils'
|
|
|
5
5
|
require_relative '../lib/rosh'
|
|
6
6
|
|
|
7
7
|
class RoshForwardingTest < Minitest::Test
|
|
8
|
+
def with_stub(obj, method_name, callable)
|
|
9
|
+
singleton = class << obj; self; end
|
|
10
|
+
original_defined = obj.respond_to?(method_name, true)
|
|
11
|
+
original_name = :"__orig_#{method_name}_for_test"
|
|
12
|
+
|
|
13
|
+
singleton.alias_method(original_name, method_name) if original_defined
|
|
14
|
+
singleton.define_method(method_name, &callable)
|
|
15
|
+
yield
|
|
16
|
+
ensure
|
|
17
|
+
singleton.remove_method(method_name) rescue nil
|
|
18
|
+
if original_defined
|
|
19
|
+
singleton.alias_method(method_name, original_name)
|
|
20
|
+
singleton.remove_method(original_name) rescue nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
8
24
|
def setup
|
|
9
25
|
@original_home = ENV['HOME']
|
|
10
26
|
@tmp_home = Dir.mktmpdir('rosh-test-home-')
|
|
@@ -60,6 +76,55 @@ class RoshForwardingTest < Minitest::Test
|
|
|
60
76
|
assert_includes ssh_opts, '-R 8082:localhost:8082'
|
|
61
77
|
end
|
|
62
78
|
|
|
79
|
+
def test_reconnect_retries_local_forwarding_when_port_becomes_available
|
|
80
|
+
klass = Class.new(Rosh) do
|
|
81
|
+
def initialize(*args)
|
|
82
|
+
@availability_checks = [false, true]
|
|
83
|
+
super
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def local_forward_available?(_spec)
|
|
87
|
+
@availability_checks.empty? ? true : @availability_checks.shift
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
rosh = klass.new('grav')
|
|
92
|
+
refute_includes rosh.instance_variable_get(:@ssh_opts), '-L 127.0.0.1:3131:localhost:3131'
|
|
93
|
+
|
|
94
|
+
rosh.instance_variable_set(:@first_try, false)
|
|
95
|
+
capture_io { rosh.reconnect }
|
|
96
|
+
|
|
97
|
+
assert_includes rosh.instance_variable_get(:@ssh_opts), '-L 127.0.0.1:3131:localhost:3131'
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_connect_rebuilds_attach_command_after_reconnect_refreshes_forwarding
|
|
101
|
+
klass = Class.new(Rosh) do
|
|
102
|
+
def initialize(*args)
|
|
103
|
+
@availability_checks = [false, false, true]
|
|
104
|
+
super
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def local_forward_available?(_spec)
|
|
108
|
+
@availability_checks.empty? ? true : @availability_checks.shift
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
rosh = klass.new('grav')
|
|
113
|
+
rosh.instance_variable_set(:@first_try, false)
|
|
114
|
+
|
|
115
|
+
commands = []
|
|
116
|
+
results = [false, true]
|
|
117
|
+
|
|
118
|
+
with_stub(rosh, :execute_attach, ->(cmd) { commands << cmd; results.shift }) do
|
|
119
|
+
with_stub(rosh, :report_session_end, ->(_cmd) { nil }) do
|
|
120
|
+
capture_io { rosh.connect }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
refute_includes commands.first, '-L 127.0.0.1:3131:localhost:3131'
|
|
125
|
+
assert_includes commands.last, '-L 127.0.0.1:3131:localhost:3131'
|
|
126
|
+
end
|
|
127
|
+
|
|
63
128
|
def test_verbose_logs_when_session_is_missing
|
|
64
129
|
rosh = Rosh.new('grav')
|
|
65
130
|
rosh.instance_variable_set(:@verbose, true)
|
|
@@ -69,7 +134,7 @@ class RoshForwardingTest < Minitest::Test
|
|
|
69
134
|
rosh.instance_variable_set(:@last_exit_status, status)
|
|
70
135
|
|
|
71
136
|
out, _ = capture_io do
|
|
72
|
-
rosh
|
|
137
|
+
with_stub(rosh, :sh_has_session?, -> { false }) do
|
|
73
138
|
rosh.send(:log_session_end, 'ssh grav')
|
|
74
139
|
end
|
|
75
140
|
end
|
|
@@ -98,7 +163,7 @@ class RoshForwardingTest < Minitest::Test
|
|
|
98
163
|
def test_tmux_new_session_disables_destroy_unattached
|
|
99
164
|
rosh = Rosh.new('grav', 'grav')
|
|
100
165
|
commands = []
|
|
101
|
-
rosh
|
|
166
|
+
with_stub(rosh, :system, ->(cmd) { commands << cmd; true }) do
|
|
102
167
|
assert rosh.send(:sh_new_session?)
|
|
103
168
|
end
|
|
104
169
|
|
|
@@ -111,7 +176,7 @@ class RoshForwardingTest < Minitest::Test
|
|
|
111
176
|
commands = []
|
|
112
177
|
results = [false, true].each
|
|
113
178
|
|
|
114
|
-
rosh
|
|
179
|
+
with_stub(rosh, :system, ->(cmd) { commands << cmd; results.next rescue true }) do
|
|
115
180
|
assert rosh.send(:sh_new_session?)
|
|
116
181
|
end
|
|
117
182
|
|
|
@@ -127,7 +192,7 @@ class RoshForwardingTest < Minitest::Test
|
|
|
127
192
|
assert_includes rosh.send(:tmux_attach_command), "-L #{socket_name}"
|
|
128
193
|
|
|
129
194
|
commands = []
|
|
130
|
-
rosh
|
|
195
|
+
with_stub(rosh, :system, ->(cmd) { commands << cmd; true }) do
|
|
131
196
|
rosh.send(:sh_has_session?)
|
|
132
197
|
end
|
|
133
198
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rosh
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Genki Takiuchi
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-04-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: net-ssh
|
|
@@ -107,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
107
107
|
- !ruby/object:Gem::Version
|
|
108
108
|
version: '0'
|
|
109
109
|
requirements: []
|
|
110
|
-
rubygems_version: 3.4.
|
|
110
|
+
rubygems_version: 3.4.20
|
|
111
111
|
signing_key:
|
|
112
112
|
specification_version: 4
|
|
113
113
|
summary: Rosh is roaming shell
|