easy-serve 0.9 → 0.10

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
  SHA1:
3
- metadata.gz: 2d1ef7c230f25e932ca33c4acbf8836789aeba20
4
- data.tar.gz: 6003abf5645a32b1aadee49e711fbed4cfe8a8f4
3
+ metadata.gz: d74d8eda371150176555b9637f8e20b237edf068
4
+ data.tar.gz: ae3c58392fe3da0cb5e316f814a2a3b60e171087
5
5
  SHA512:
6
- metadata.gz: 0618e996740b75fefef23d2fda92fc2f39a1e13d8e6d66d6d18c61f613c44aefd7419e49587b909bbc2c1aa86e9e605588c4f4f59d6b1311dfe18a87da77c448
7
- data.tar.gz: 579718d353ad19562bb10a0dfdd0e9574476eeba4116844328a04e53ee0b13bf454cafa94a8194419a29eab912342e42297b2c778edf3c542cc623f0fbfcf2df
6
+ metadata.gz: 299b6abc59f540f9dd71f53d933ff5638cba9757a07fc9290c3bae5599868a3a9414e59c988d126582bd2f87a414fcff3dfcd7bfa3497f209bac8a894ae24919
7
+ data.tar.gz: fa63d61b24de5644289ac3711d15a25f1b869c547b46bbc733c5da17ca4849a2d280d7d44625f10648aadac6befda81fa35519c1d4a0bd5af6fd9489ce3c5a4d
data/README.md CHANGED
@@ -2,3 +2,16 @@ easy-serve
2
2
  ==========
3
3
 
4
4
  Framework for starting tcp/unix services and connected clients under one parent process and on remote hosts.
5
+
6
+ use cases
7
+ ---------
8
+
9
+ 1. start some procs with unix sockets established among them and
10
+ clean up afterwards [simple](examples/simple.rb) [multi](examples/multi.rb)
11
+
12
+ 2. ditto but with tcp and possibly [remote](examples/remote-eval.rb)
13
+
14
+ 3. ditto but through ssh [tunnels](examples/remote-eval.rb)
15
+
16
+ 4. ditto but where the tunnel is set up by the remote client, without
17
+ special assistance from the server [examples/tunnel](examples/tunnel)
@@ -0,0 +1,33 @@
1
+ require 'easy-serve/remote'
2
+
3
+ services_file = ARGV.shift
4
+
5
+ unless services_file
6
+ abort <<-END
7
+
8
+ Usage: #$0 services.yaml
9
+
10
+ Reads the yaml file and tunnels to the service. Note that
11
+ the filename may be remote, like host:path, so the lazy way
12
+ to run the example is:
13
+
14
+ host1$ ruby server.rb sv
15
+
16
+ host2$ ruby client.rb host1:path/to/sv
17
+
18
+ END
19
+ end
20
+
21
+ EasyServe.start services_file: services_file do |ez|
22
+ log = ez.log
23
+ log.level = Logger::INFO
24
+ log.formatter = nil if $VERBOSE
25
+
26
+ ez.tunnel_to_remote_services
27
+
28
+ ez.child "hello-service" do |conn|
29
+ log.progname = "client 1"
30
+ log.info conn.read
31
+ conn.write "hello from #{log.progname}"
32
+ end
33
+ end
@@ -0,0 +1,43 @@
1
+ require 'easy-serve'
2
+
3
+ services_file = ARGV.shift
4
+
5
+ unless services_file
6
+ abort <<-END
7
+
8
+ Usage: #$0 services.yaml
9
+
10
+ Creates the yaml file and sets up the service. The service is
11
+ listening only on localhost, so remote clients must use tunnels.
12
+ Does not set up any tunnels or remote clients. See client.rb.
13
+
14
+ END
15
+ end
16
+
17
+ EasyServe.start services_file: services_file do |ez|
18
+ log = ez.log
19
+ log.level = Logger::INFO
20
+ log.formatter = nil if $VERBOSE
21
+
22
+ ez.start_services do
23
+ host = "localhost" # no remote access, except by tunnel
24
+ ez.service "hello-service", :tcp, bind_host: host do |svr|
25
+ Thread.new do
26
+ loop do
27
+ Thread.new(svr.accept) do |conn|
28
+ log.info "accepted connection from #{conn.inspect}"
29
+ conn.write "hello from #{log.progname}"
30
+ log.info "wrote greeting"
31
+ conn.close_write
32
+ log.info "trying to read from #{conn.inspect}"
33
+ log.info "received: #{conn.read}"
34
+ conn.close
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ puts "PRESS RETURN TO STOP"
42
+ gets
43
+ end
@@ -6,7 +6,7 @@ require 'fileutils'
6
6
  require 'easy-serve/service'
7
7
 
8
8
  class EasyServe
9
- VERSION = "0.9"
9
+ VERSION = "0.10"
10
10
 
11
11
  class EasyFormatter < Logger::Formatter
12
12
  Format = "%s: %s: %s\n"
@@ -35,6 +35,11 @@ class EasyServe
35
35
  attr_reader :services_file
36
36
  attr_reader :interactive
37
37
 
38
+ # Is this a sibling process, started by the same parent process that
39
+ # started the services, even if started remotely?
40
+ # Implies not owner, but not conversely.
41
+ attr_reader :sibling
42
+
38
43
  def self.start(log: default_logger, **opts)
39
44
  ez = new(**opts, log: log)
40
45
  yield ez
@@ -45,6 +50,24 @@ class EasyServe
45
50
  ez.cleanup if ez
46
51
  end
47
52
 
53
+ # Options:
54
+ #
55
+ # services_file: filename
56
+ #
57
+ # name of file that server addresses are written to (if this process
58
+ # is creating them) or read from (if this process is accessing them).
59
+ # If not specified, services will be available to child processes,
60
+ # but harder to access from other processes.
61
+ #
62
+ # If the filename has a ':' in it, we assume that it is a remote
63
+ # file, specified as [user@]host:path/to/file as in scp and rsync,
64
+ # and attempt to read its contents over an ssh connection.
65
+ #
66
+ # interactive: true|false
67
+ #
68
+ # true means do not propagate ^C to child processes.
69
+ # This is useful primarily when running in irb.
70
+ #
48
71
  def initialize **opts
49
72
  @services_file = opts[:services_file]
50
73
  @interactive = opts[:interactive]
@@ -52,6 +75,8 @@ class EasyServe
52
75
  @children = [] # pid
53
76
  @passive_children = [] # pid
54
77
  @owner = false
78
+ @sibling = true
79
+ @ssh_sessions = []
55
80
  @tmpdir = nil
56
81
  @services = opts[:services] # name => service
57
82
 
@@ -70,10 +95,21 @@ class EasyServe
70
95
  end
71
96
 
72
97
  def load_service_table
73
- File.open(services_file) do |f|
74
- YAML.load(f)
98
+ case services_file
99
+ when /\A(\S*):(.*)/
100
+ IO.popen ["ssh", $1, "cat #$2"], "r" do |f|
101
+ load_service_table_from_io f
102
+ end
103
+ else
104
+ File.open(services_file) do |f|
105
+ load_service_table_from_io f
106
+ end
75
107
  end
76
108
  end
109
+
110
+ def load_service_table_from_io io
111
+ YAML.load(io).tap {@sibling = false}
112
+ end
77
113
 
78
114
  def init_service_table
79
115
  @services ||= begin
@@ -161,6 +197,10 @@ class EasyServe
161
197
  end
162
198
 
163
199
  def host_name
200
+ EasyServe.host_name
201
+ end
202
+
203
+ def EasyServe.host_name
164
204
  @host_name ||= begin
165
205
  hn = Socket.gethostname
166
206
  begin
@@ -175,6 +215,10 @@ class EasyServe
175
215
  end
176
216
  end
177
217
  end
218
+
219
+ def EasyServe.ssh_supports_dynamic_ports_forwards
220
+ @ssh_6 ||= (Integer(`ssh -V 2>&1`[/OpenSSH_(\d)/i, 1]) >= 6 rescue false)
221
+ end
178
222
 
179
223
  MAX_TRIES = 10
180
224
 
@@ -246,4 +290,42 @@ class EasyServe
246
290
  def no_interrupt_if_interactive
247
291
  trap("INT") {} if interactive
248
292
  end
293
+
294
+ # Returns list of services that are accessible from +host+, setting
295
+ # up an ssh tunnel if specified. This is for the 'ssh -R' type of tunneling:
296
+ # a process, started remotely by some main process, needs to connect back to
297
+ # its siblings, other children of that main process. OpenSSH 6.0 or later is
298
+ # advised, but not necessary, for the tunnel option.
299
+ def accessible_services host, tunnel: false
300
+ tcp_svs = services.values.grep(TCPService)
301
+ return tcp_svs unless tunnel and host != "localhost" and host != "127.0.0.1"
302
+
303
+ require 'easy-serve/service/accessible'
304
+
305
+ tcp_svs.map do |service|
306
+ service, ssh_session = service.accessible(host, log)
307
+ @ssh_sessions << ssh_session # let GC close them
308
+ service
309
+ end
310
+ end
311
+
312
+ # Set up tunnels as needed and modify the service list so that connections
313
+ # will go to local endpoints in those cases. Call this method in non-sibling
314
+ # invocations, such as when the server file has been copied to a remote
315
+ # host and used to start a new client. This is for the 'ssh -L' type of
316
+ # tunneling: a process needs to connect to a cluster of remote EasyServe
317
+ # processes that already exist and do not know about this process.
318
+ def tunnel_to_remote_services
319
+ return if sibling
320
+
321
+ require 'easy-serve/service/tunnelled'
322
+
323
+ tunnelled_services = {}
324
+ services.each do |service_name, service|
325
+ service, ssh_session = service.tunnelled
326
+ tunnelled_services[service_name] = service
327
+ @ssh_sessions << ssh_session if ssh_session # let GC close them
328
+ end
329
+ @services = tunnelled_services
330
+ end
249
331
  end
@@ -7,15 +7,15 @@ class EasyServe
7
7
  raise ArgumentError, "no host specified" unless host
8
8
 
9
9
  if opts[:eval]
10
- require 'easy-serve/remote-eval'
10
+ require 'easy-serve/remote/eval'
11
11
  remote_eval(*service_names, host: host, **opts)
12
12
 
13
13
  elsif opts[:file]
14
- require 'easy-serve/remote-run'
14
+ require 'easy-serve/remote/run'
15
15
  remote_run(*service_names, host: host, **opts)
16
16
 
17
17
  elsif block_given?
18
- require 'easy-serve/remote-drb'
18
+ require 'easy-serve/remote/drb'
19
19
  remote_drb(*service_names, host: host, **opts, &Proc.new)
20
20
 
21
21
  else
@@ -1,5 +1,4 @@
1
1
  require 'msgpack'
2
- require 'easy-serve/accessible-services'
3
2
 
4
3
  class EasyServe
5
4
  # useful simple cases in testing and in production, but long eval strings
@@ -20,7 +19,7 @@ class EasyServe
20
19
 
21
20
  IO.popen [
22
21
  "ssh", host, "ruby",
23
- "-r", "easy-serve/remote-eval-mgr",
22
+ "-r", "easy-serve/remote/eval-mgr",
24
23
  "-e", "EasyServe.handle_remote_eval_messages"
25
24
  ],
26
25
  "w+" do |ssh|
@@ -1,5 +1,4 @@
1
1
  require 'msgpack'
2
- require 'easy-serve/accessible-services'
3
2
 
4
3
  class EasyServe
5
4
  # useful in production, though it requires remote lib files to be set up.
@@ -18,7 +17,7 @@ class EasyServe
18
17
 
19
18
  IO.popen [
20
19
  "ssh", host, "ruby",
21
- "-r", "easy-serve/remote-run-mgr",
20
+ "-r", "easy-serve/remote/run-mgr",
22
21
  "-e", "EasyServe.handle_remote_run_messages"
23
22
  ],
24
23
  "w+" do |ssh|
@@ -3,25 +3,6 @@ class EasyServe
3
3
  # Encapsulates current location and identity, including pid and address. A
4
4
  # Service object can be serialized to a remote process so it can #connect to
5
5
  # the service.
6
- #
7
- # The scheme for referencing hosts is as follows:
8
- #
9
- # bind host | connect host
10
- # +------------------------------------------------------
11
- # | local remote TCP SSH tunnel
12
- # -----------+------------------------------------------------------
13
- #
14
- # localhost 'localhost' X 'localhost'
15
- #
16
- # 0.0.0.0 'localhost' hostname(*) 'localhost'
17
- #
18
- # hostname hostname hostname 'localhost'(**)
19
- #
20
- # * use hostname as best guess, can override; append ".local" if
21
- # hostname not qualified
22
- #
23
- # ** forwarding set up to hostname[.local] instead of localhost
24
- #
25
6
  class Service
26
7
  attr_reader :name
27
8
  attr_reader :pid
@@ -111,14 +92,33 @@ class EasyServe
111
92
  end
112
93
  end
113
94
 
95
+ # The scheme for referencing TCP hosts is as follows:
96
+ #
97
+ # bind host | connect host
98
+ # +------------------------------------------------------
99
+ # | local remote TCP SSH tunnel
100
+ # -----------+------------------------------------------------------
101
+ #
102
+ # localhost 'localhost' X 'localhost'
103
+ #
104
+ # 0.0.0.0 'localhost' hostname(*) 'localhost'
105
+ #
106
+ # hostname hostname hostname 'localhost'(**)
107
+ #
108
+ # * use hostname as best guess, can override; append ".local" if
109
+ # hostname not qualified
110
+ #
111
+ # ** forwarding set up to hostname[.local] instead of localhost
112
+ #
114
113
  class TCPService < Service
115
114
  SERVICE_CLASS[:tcp] = self
116
115
 
117
- attr_reader :bind_host, :connect_host, :port
116
+ attr_reader :bind_host, :connect_host, :host, :port
118
117
 
119
- def initialize name, bind_host: nil, connect_host: nil, port: 0
118
+ def initialize name, bind_host: nil, connect_host: nil, host: nil, port: 0
120
119
  super name
121
120
  @bind_host, @connect_host, @port = bind_host, connect_host, port
121
+ @host ||= EasyServe.host_name
122
122
  end
123
123
 
124
124
  def serve max_tries: 1, log: log
@@ -126,6 +126,7 @@ class EasyServe
126
126
  found_addr = svr.addr(false).values_at(2,1)
127
127
  log.debug "#{inspect} is listening at #{found_addr.join(":")}"
128
128
  @port = found_addr[1]
129
+ @bind_host ||= found_addr[0]
129
130
  end
130
131
  end
131
132
 
@@ -134,7 +135,7 @@ class EasyServe
134
135
  end
135
136
 
136
137
  def try_serve
137
- TCPServer.new(bind_host, port)
138
+ TCPServer.new(bind_host, port || 0) # new(nil, nil) ==> error
138
139
  end
139
140
 
140
141
  def bump!
@@ -0,0 +1,81 @@
1
+ require 'easy-serve/service'
2
+
3
+ class EasyServe::TCPService
4
+ # Returns [service, ssh_session]. The service is modified based on self
5
+ # with tunneling from remote_host and ssh_session is the associated ssh pipe.
6
+ def accessible remote_host, log
7
+ service_host =
8
+ case bind_host
9
+ when nil, "localhost", "127.0.0.1", "0.0.0.0", /\A<any>\z/i
10
+ "localhost"
11
+ else
12
+ bind_host
13
+ end
14
+
15
+ fwd = "0:#{service_host}:#{port}"
16
+ remote_port = nil
17
+ ssh = nil
18
+ tries = 10
19
+
20
+ 1.times do
21
+ if EasyServe.ssh_supports_dynamic_ports_forwards
22
+ remote_port = Integer(`ssh -O forward -R #{fwd} #{remote_host}`)
23
+ else
24
+ log.warn "Unable to set up dynamic ssh port forwarding. " +
25
+ "Please check if ssh -v is at least 6.0. " +
26
+ "Falling back to new ssh session."
27
+
28
+ code = <<-CODE
29
+ require 'socket'
30
+ svr = TCPServer.new "localhost", 0 # no rescue; error here is fatal
31
+ puts svr.addr[1]
32
+ svr.close
33
+ CODE
34
+
35
+ remote_port =
36
+ IO.popen ["ssh", remote_host, "ruby"], "w+" do |ruby|
37
+ ruby.puts code
38
+ ruby.close_write
39
+ Integer(ruby.gets)
40
+ end
41
+
42
+ cmd = [
43
+ "ssh", remote_host,
44
+ "-R", "#{remote_port}:#{service_host}:#{port}",
45
+ "echo ok && cat"
46
+ ]
47
+ ssh = IO.popen cmd, "w+"
48
+ ## how to tell if port in use and retry? ssh doesn't seem to fail,
49
+ ## or maybe it fails by printing a message on the remote side
50
+
51
+ ssh.sync = true
52
+ line = ssh.gets
53
+ unless line and line.chomp == "ok" # wait for forwarding
54
+ raise "Could not start ssh forwarding: #{cmd.join(" ")}"
55
+ end
56
+ end
57
+
58
+ if remote_port == 0
59
+ log.warn "race condition in ssh selection of remote_port"
60
+ tries -= 1
61
+ if tries > 0
62
+ sleep 0.1
63
+ log.info "retrying ssh selection of remote_port"
64
+ redo
65
+ end
66
+ raise "ssh did not assign remote_port"
67
+ end
68
+ end
69
+
70
+ # This breaks with multiple forward requests, and it would be too hard
71
+ # to coordinate among all requesting processes, so let's leave the
72
+ # forwarding open:
73
+ #at_exit {system "ssh -O cancel -R #{fwd} #{remote_host}"}
74
+
75
+ service =
76
+ self.class.new name, host: host,
77
+ bind_host: bind_host, connect_host: "localhost", port: remote_port
78
+
79
+ return [service, ssh]
80
+ end
81
+ end
@@ -0,0 +1,53 @@
1
+ require 'easy-serve/service'
2
+
3
+ class EasyServe
4
+ class Service
5
+ def tunnelled(*)
6
+ [self, nil]
7
+ end
8
+ end
9
+
10
+ class TCPService
11
+ # Returns [service, ssh_session|nil]. The service is self and ssh_session
12
+ # is nil, unless tunneling is appropriate, in which case the returned
13
+ # service is the tunnelled one, and the ssh_session is the associated ssh
14
+ # pipe. This is for the 'ssh -L' type of tunneling: a process needs to
15
+ # connect to a cluster of remote EasyServe processes.
16
+ def tunnelled
17
+ return [self, nil] if
18
+ ["localhost", "127.0.0.1", EasyServe.host_name].include? host
19
+
20
+ if ["localhost", "127.0.0.1", "0.0.0.0"].include? bind_host
21
+ rhost = "localhost"
22
+ else
23
+ rhost = bind_host
24
+ end
25
+
26
+ svr = TCPServer.new "localhost", 0 # no rescue; error here is fatal
27
+ lport = svr.addr[1]
28
+ svr.close
29
+ ## why doesn't `ssh -L 0:host:port` work?
30
+
31
+ # possible alternative: ssh -f -N -o ExitOnForwardFailure: yes
32
+ cmd = [
33
+ "ssh", host,
34
+ "-L", "#{lport}:#{rhost}:#{port}",
35
+ "echo ok && cat"
36
+ ]
37
+ ssh = IO.popen cmd, "w+"
38
+ ## how to tell if lport in use and retry? ssh doesn't seem to fail,
39
+ ## or maybe it fails by printing a message on the remote side
40
+
41
+ ssh.sync = true
42
+ line = ssh.gets
43
+ unless line and line.chomp == "ok" # wait for forwarding
44
+ raise "Could not start ssh forwarding: #{cmd.join(" ")}"
45
+ end
46
+
47
+ service = TCPService.new name,
48
+ bind_host: bind_host, connect_host: 'localhost', host: host, port: lport
49
+
50
+ return [service, ssh]
51
+ end
52
+ end
53
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easy-serve
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.9'
4
+ version: '0.10'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel VanderWerf
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-12-18 00:00:00.000000000 Z
11
+ date: 2013-12-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -35,25 +35,28 @@ extra_rdoc_files:
35
35
  files:
36
36
  - README.md
37
37
  - COPYING
38
- - lib/easy-serve.rb
39
- - lib/easy-serve/remote-eval-mgr.rb
40
- - lib/easy-serve/remote-run.rb
41
- - lib/easy-serve/remote-drb.rb
42
38
  - lib/easy-serve/remote.rb
43
- - lib/easy-serve/accessible-services.rb
44
- - lib/easy-serve/remote-eval.rb
45
- - lib/easy-serve/remote-run-mgr.rb
39
+ - lib/easy-serve/service/accessible.rb
40
+ - lib/easy-serve/service/tunnelled.rb
46
41
  - lib/easy-serve/service.rb
47
- - examples/simple.rb
48
- - examples/remote-manual.rb
49
- - examples/multi.rb
50
- - examples/remote-eval-passive.rb
42
+ - lib/easy-serve/remote/eval.rb
43
+ - lib/easy-serve/remote/run-mgr.rb
44
+ - lib/easy-serve/remote/run.rb
45
+ - lib/easy-serve/remote/drb.rb
46
+ - lib/easy-serve/remote/eval-mgr.rb
47
+ - lib/easy-serve.rb
51
48
  - examples/remote-run.rb
52
- - examples/remote-drb.rb
53
49
  - examples/remote-multi-server.rb
50
+ - examples/passive.rb
51
+ - examples/remote-eval-passive.rb
52
+ - examples/remote-drb.rb
54
53
  - examples/remote-run-script.rb
54
+ - examples/multi.rb
55
+ - examples/remote-manual.rb
56
+ - examples/simple.rb
57
+ - examples/tunnel/client.rb
58
+ - examples/tunnel/server.rb
55
59
  - examples/remote-eval.rb
56
- - examples/passive.rb
57
60
  homepage: https://github.com/vjoel/easy-serve
58
61
  licenses:
59
62
  - BSD
@@ -1,51 +0,0 @@
1
- class EasyServe
2
- # Returns list of services that are accessible from +host+, setting
3
- # up an ssh tunnel if specified. Note that OpenSSH 6.0 or later is required
4
- # for the tunnel option.
5
- def accessible_services host, tunnel: false
6
- tcp_svs = services.values.grep(TCPService)
7
- return tcp_svs unless tunnel and host != "localhost" and host != "127.0.0.1"
8
-
9
- tcp_svs.map do |service|
10
- service_host =
11
- case service.bind_host
12
- when nil, "localhost", "127.0.0.1", "0.0.0.0", /\A<any>\z/i
13
- "localhost"
14
- else
15
- service.bind_host
16
- end
17
-
18
- fwd = "0:#{service_host}:#{service.port}"
19
- remote_port = nil
20
- tries = 10
21
- 1.times do
22
- out = `ssh -O forward -R #{fwd} #{host}`
23
- begin
24
- remote_port = Integer(out)
25
- rescue
26
- log.error "Unable to set up dynamic ssh port forwarding. " +
27
- "Please check if ssh -v is at least 6.0."
28
- raise
29
- end
30
-
31
- if remote_port == 0
32
- log.warn "race condition in ssh selection of remote_port"
33
- tries -= 1
34
- if tries > 0
35
- sleep 0.1
36
- log.info "retrying ssh selection of remote_port"
37
- redo
38
- end
39
- raise "ssh did not assign remote_port"
40
- end
41
- end
42
-
43
- # This breaks with multiple forward requests, and it would be too hard
44
- # to coordinate among all requesting processes, so let's leave the
45
- # forwarding open:
46
- #at_exit {system "ssh -O cancel -R #{fwd} #{host}"}
47
-
48
- TCPService.new service.name, connect_host: "localhost", port: remote_port
49
- end
50
- end
51
- end