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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1399835146cfcd6615a17868694c426b4c06b25649fab1c02192bf8f4534fe1d
4
- data.tar.gz: 835a84fe3a697e322fb28aa6c67e16f0ad1cbf832616b23991c65f0333fde8f9
3
+ metadata.gz: e3c244fe02aac976b4bc470e13f529e71bd0a0ec43cc2a389222b7dd7b9b2fc2
4
+ data.tar.gz: e4283b6e8c61adb188861184350959de02ba1c75ecc751bf1c8101c4f6934f31
5
5
  SHA512:
6
- metadata.gz: ecaffa32659f87db955a586bced2a742f15735cc75c5d76b00cc7113a9378673cfb54294b7d33ffa29eefdda0213cee56ea09f07de4caae68b3239875f4d9d7c
7
- data.tar.gz: 66a075c156779a0d33907187ada1195f8d7ebce66fb59de8136e585f7e3b4ce1d0e9f641961d3be12c6b956ae8215f0071e60e16a2a46ea153f0b5c8f377fbd0
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.6'
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
@@ -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).each do |f|
38
- add_forward_option(:local, f)
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
- @ssh_opts << "-l #{config[:user]}" if config[:user]
45
- @ssh_opts << "-p #{config[:port]}" if config[:port]
46
- @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]
47
46
  if keys = config[:keys]
48
- keys.each{|k| @ssh_opts << "-i #{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.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.6
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-12-28 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