rosh 0.9.5 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a9b04489aa21bdb87dd7a21a2696d33c0f1b29c45767593bca5bdc0b383b2a4
4
- data.tar.gz: a43d640d729094bb85640cfece07ebe2f1b82bb007723d528b628fb5a495a23a
3
+ metadata.gz: e3c244fe02aac976b4bc470e13f529e71bd0a0ec43cc2a389222b7dd7b9b2fc2
4
+ data.tar.gz: e4283b6e8c61adb188861184350959de02ba1c75ecc751bf1c8101c4f6934f31
5
5
  SHA512:
6
- metadata.gz: 35972089d8384a358f3e79795fbf774be00e38b811872f690491057ccb17134488d3a19d21cdd2bc5854acab669033273689f2d99bc1fedbdb324d1eff31982c
7
- data.tar.gz: 8a422f7936c8482d12b168ed6a5aab6dddda8691e98b327c206a676976f770ad3a02d4a0d19888eeb86188b0dea4431b370ed2e6989559758d99adaf2c696932
6
+ metadata.gz: 12de5a2533f7943d5adc5f29d5f675f97da16e692c615eb40e0357adbce3c649619d44536fd92f5278cb7157a798c32658207869875865b7bdf42ebf14536fcd
7
+ data.tar.gz: cb3c1055ad0e11fd5ad55c81268508ed795d165157606e0c16925fc3dd18e3ba0fb74c39b5104926766accf9b6d619e5e62f73561a83bc5336377fdbe1423ed7
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ vendor/bundle
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
@@ -1,3 +1,3 @@
1
1
  class Rosh
2
- VERSION = '0.9.5'
2
+ VERSION = '0.9.7'
3
3
  end
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
- @ssh_opts << "-o ServerAliveInterval=#{alive_interval}"
27
- @ssh_opts << "-o ServerAliveCountMax=1"
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
@@ -32,23 +35,18 @@ class Rosh
32
35
  if @verbose
33
36
  puts "ssh-config: #{config}"
34
37
  end
35
- @forward_opts = []
36
- @forwarding_disabled = false
37
38
  @oom_reported = false
38
39
  @last_exit_status = nil
39
- local_forwards(alias_name).each do |f|
40
- add_forward_option(:local, f)
41
- end
42
- remote_forwards(alias_name).each do |f|
43
- add_forward_option(:remote, f)
44
- end
40
+ @local_forward_specs = local_forwards(alias_name)
41
+ @remote_forward_specs = remote_forwards(alias_name)
45
42
  @host = config[:host_name] if config[:host_name]
46
- @ssh_opts << "-l #{config[:user]}" if config[:user]
47
- @ssh_opts << "-p #{config[:port]}" if config[:port]
48
- @ssh_opts << "-J #{config[:proxy].jump_proxies}" if config[:proxy]
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]
49
46
  if keys = config[:keys]
50
- keys.each{|k| @ssh_opts << "-i #{k}"}
47
+ keys.each{|k| @base_ssh_opts << "-i #{k}"}
51
48
  end
49
+ refresh_ssh_opts!
52
50
  if @verbose
53
51
  puts "host: #{@host}"
54
52
  puts "name: #{@name}"
@@ -60,25 +58,20 @@ class Rosh
60
58
  end
61
59
 
62
60
  def connect
63
- cmd = if @screen
64
- ["ssh", *@ssh_opts, resolv,
65
- '-t', "'screen -rx #{@name}'", '2>/dev/null']*' '
66
- else
67
- remote_cmd = single_quote(tmux_attach_command)
68
- ["ssh", *@ssh_opts, resolv,
69
- '-t', remote_cmd, '2>/dev/null']*' '
70
- end
71
61
  if @verbose
72
62
  puts "connecting to #{@host}..."
73
- puts cmd
74
63
  end
64
+ cmd = nil
75
65
  begin
76
66
  reconnect
67
+ cmd = attach_command
68
+ puts cmd if @verbose
77
69
  end until execute_attach(cmd)
78
70
  report_session_end(cmd)
79
71
  end
80
72
 
81
73
  def reconnect
74
+ refresh_ssh_opts!
82
75
  if @first_try
83
76
  session_exists = if @screen
84
77
  sh('-p 0 -X echo ok', '2>&1 >/dev/null')
@@ -109,6 +102,16 @@ class Rosh
109
102
  end
110
103
 
111
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
+
112
115
  def execute_attach(cmd)
113
116
  result = system cmd
114
117
  @last_exit_status = $?
@@ -120,26 +123,12 @@ private
120
123
  end
121
124
 
122
125
  def add_forward_option(kind, spec)
123
- return if forwarding_disabled?
124
126
  if kind == :local && !local_forward_available?(spec)
125
127
  puts "skip forwarding: #{spec} is already in use"
126
- disable_forwarding!
127
128
  return
128
129
  end
129
130
  opt = kind == :local ? "-L #{spec}" : "-R #{spec}"
130
131
  @ssh_opts << opt
131
- @forward_opts << opt
132
- end
133
-
134
- def disable_forwarding!
135
- return if @forwarding_disabled
136
- @forwarding_disabled = true
137
- @forward_opts.each { |opt| @ssh_opts.delete(opt) }
138
- @forward_opts.clear
139
- end
140
-
141
- def forwarding_disabled?
142
- @forwarding_disabled
143
132
  end
144
133
 
145
134
  def local_forward_available?(spec)
@@ -254,6 +243,17 @@ private
254
243
  system cmd
255
244
  end
256
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
+
257
257
  def local_forwards(host)
258
258
  file = File.expand_path("~/.ssh/config")
259
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-')
@@ -57,7 +73,56 @@ class RoshForwardingTest < Minitest::Test
57
73
  ssh_opts = rosh.instance_variable_get(:@ssh_opts)
58
74
 
59
75
  refute_includes ssh_opts, '-L 127.0.0.1:3131:localhost:3131'
60
- refute_includes ssh_opts, '-R 8082:localhost:8082'
76
+ assert_includes ssh_opts, '-R 8082:localhost:8082'
77
+ end
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'
61
126
  end
62
127
 
63
128
  def test_verbose_logs_when_session_is_missing
@@ -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.stub(:sh_has_session?, false) do
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.stub(:system, ->(cmd) { commands << cmd; true }) do
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.stub(:system, ->(cmd) { commands << cmd; results.next rescue true }) do
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.stub(:system, ->(cmd) { commands << cmd; true }) do
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.5
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: 2025-10-14 00:00:00.000000000 Z
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.15
110
+ rubygems_version: 3.4.20
111
111
  signing_key:
112
112
  specification_version: 4
113
113
  summary: Rosh is roaming shell