adamwiggins-rush 0.6.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.
Files changed (50) hide show
  1. data/README.rdoc +87 -0
  2. data/Rakefile +61 -0
  3. data/VERSION +1 -0
  4. data/bin/rush +13 -0
  5. data/bin/rushd +7 -0
  6. data/lib/rush/access.rb +130 -0
  7. data/lib/rush/array_ext.rb +19 -0
  8. data/lib/rush/box.rb +112 -0
  9. data/lib/rush/commands.rb +55 -0
  10. data/lib/rush/config.rb +154 -0
  11. data/lib/rush/dir.rb +160 -0
  12. data/lib/rush/embeddable_shell.rb +26 -0
  13. data/lib/rush/entry.rb +185 -0
  14. data/lib/rush/exceptions.rb +31 -0
  15. data/lib/rush/file.rb +85 -0
  16. data/lib/rush/find_by.rb +39 -0
  17. data/lib/rush/fixnum_ext.rb +18 -0
  18. data/lib/rush/head_tail.rb +11 -0
  19. data/lib/rush/local.rb +402 -0
  20. data/lib/rush/process.rb +59 -0
  21. data/lib/rush/process_set.rb +62 -0
  22. data/lib/rush/remote.rb +156 -0
  23. data/lib/rush/search_results.rb +58 -0
  24. data/lib/rush/server.rb +117 -0
  25. data/lib/rush/shell.rb +187 -0
  26. data/lib/rush/ssh_tunnel.rb +122 -0
  27. data/lib/rush/string_ext.rb +3 -0
  28. data/lib/rush.rb +87 -0
  29. data/spec/access_spec.rb +134 -0
  30. data/spec/array_ext_spec.rb +15 -0
  31. data/spec/base.rb +24 -0
  32. data/spec/box_spec.rb +64 -0
  33. data/spec/commands_spec.rb +47 -0
  34. data/spec/config_spec.rb +108 -0
  35. data/spec/dir_spec.rb +164 -0
  36. data/spec/embeddable_shell_spec.rb +17 -0
  37. data/spec/entry_spec.rb +133 -0
  38. data/spec/file_spec.rb +83 -0
  39. data/spec/find_by_spec.rb +58 -0
  40. data/spec/fixnum_ext_spec.rb +19 -0
  41. data/spec/local_spec.rb +352 -0
  42. data/spec/process_set_spec.rb +50 -0
  43. data/spec/process_spec.rb +73 -0
  44. data/spec/remote_spec.rb +140 -0
  45. data/spec/rush_spec.rb +28 -0
  46. data/spec/search_results_spec.rb +44 -0
  47. data/spec/shell_spec.rb +23 -0
  48. data/spec/ssh_tunnel_spec.rb +122 -0
  49. data/spec/string_ext_spec.rb +23 -0
  50. metadata +141 -0
data/lib/rush/local.rb ADDED
@@ -0,0 +1,402 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+ require 'timeout'
4
+
5
+ # Rush::Box uses a connection object to execute all rush commands. If the box
6
+ # is local, Rush::Connection::Local is created. The local connection is the
7
+ # heart of rush's internals. (Users of the rush shell or library need never
8
+ # access the connection object directly, so the docs herein are intended for
9
+ # developers wishing to modify rush.)
10
+ #
11
+ # The local connection has a series of methods which do the actual work of
12
+ # modifying files, getting process lists, and so on. RushServer creates a
13
+ # local connection to handle incoming requests; the translation from a raw hash
14
+ # of parameters to an executed method is handled by
15
+ # Rush::Connection::Local#receive.
16
+ class Rush::Connection::Local
17
+ # Write raw bytes to a file.
18
+ def write_file(full_path, contents)
19
+ ::File.open(full_path, 'w') do |f|
20
+ f.write contents
21
+ end
22
+ true
23
+ end
24
+
25
+ # Append contents to a file
26
+ def append_to_file(full_path, contents)
27
+ ::File.open(full_path, 'a') do |f|
28
+ f.write contents
29
+ end
30
+ true
31
+ end
32
+
33
+ # Read raw bytes from a file.
34
+ def file_contents(full_path)
35
+ ::File.read(full_path)
36
+ rescue Errno::ENOENT
37
+ raise Rush::DoesNotExist, full_path
38
+ end
39
+
40
+ # Destroy a file or dir.
41
+ def destroy(full_path)
42
+ raise "No." if full_path == '/'
43
+ FileUtils.rm_rf(full_path)
44
+ true
45
+ end
46
+
47
+ # Purge the contents of a dir.
48
+ def purge(full_path)
49
+ raise "No." if full_path == '/'
50
+ Dir.chdir(full_path) do
51
+ all = Dir.glob("*", File::FNM_DOTMATCH).reject { |f| f == '.' or f == '..' }
52
+ FileUtils.rm_rf all
53
+ end
54
+ true
55
+ end
56
+
57
+ # Create a dir.
58
+ def create_dir(full_path)
59
+ FileUtils.mkdir_p(full_path)
60
+ true
61
+ end
62
+
63
+ # Rename an entry within a dir.
64
+ def rename(path, name, new_name)
65
+ raise(Rush::NameCannotContainSlash, "#{path} rename #{name} to #{new_name}") if new_name.match(/\//)
66
+ old_full_path = "#{path}/#{name}"
67
+ new_full_path = "#{path}/#{new_name}"
68
+ raise(Rush::NameAlreadyExists, "#{path} rename #{name} to #{new_name}") if ::File.exists?(new_full_path)
69
+ FileUtils.mv(old_full_path, new_full_path)
70
+ true
71
+ end
72
+
73
+ # Copy ane entry from one path to another.
74
+ def copy(src, dst)
75
+ FileUtils.cp_r(src, dst)
76
+ true
77
+ rescue Errno::ENOENT
78
+ raise Rush::DoesNotExist, File.dirname(dst)
79
+ rescue RuntimeError
80
+ raise Rush::DoesNotExist, src
81
+ end
82
+
83
+ # Create an in-memory archive (tgz) of a file or dir, which can be
84
+ # transmitted to another server for a copy or move. Note that archive
85
+ # operations have the dir name implicit in the archive.
86
+ def read_archive(full_path)
87
+ `cd #{Rush.quote(::File.dirname(full_path))}; tar c #{Rush.quote(::File.basename(full_path))}`
88
+ end
89
+
90
+ # Extract an in-memory archive to a dir.
91
+ def write_archive(archive, dir)
92
+ IO.popen("cd #{Rush::quote(dir)}; tar x", "w") do |p|
93
+ p.write archive
94
+ end
95
+ end
96
+
97
+ # Get an index of files from the given path with the glob. Could return
98
+ # nested values if the glob contains a doubleglob. The return value is an
99
+ # array of full paths, with directories listed first.
100
+ def index(base_path, glob)
101
+ glob = '*' if glob == '' or glob.nil?
102
+ dirs = []
103
+ files = []
104
+ ::Dir.chdir(base_path) do
105
+ ::Dir.glob(glob).each do |fname|
106
+ if ::File.directory?(fname)
107
+ dirs << fname + '/'
108
+ else
109
+ files << fname
110
+ end
111
+ end
112
+ end
113
+ dirs.sort + files.sort
114
+ rescue Errno::ENOENT
115
+ raise Rush::DoesNotExist, base_path
116
+ end
117
+
118
+ # Fetch stats (size, ctime, etc) on an entry. Size will not be accurate for dirs.
119
+ def stat(full_path)
120
+ s = ::File.stat(full_path)
121
+ {
122
+ :size => s.size,
123
+ :ctime => s.ctime,
124
+ :atime => s.atime,
125
+ :mtime => s.mtime,
126
+ :mode => s.mode
127
+ }
128
+ rescue Errno::ENOENT
129
+ raise Rush::DoesNotExist, full_path
130
+ end
131
+
132
+ def set_access(full_path, access)
133
+ access.apply(full_path)
134
+ end
135
+
136
+ # Fetch the size of a dir, since a standard file stat does not include the
137
+ # size of the contents.
138
+ def size(full_path)
139
+ `du -sb #{Rush.quote(full_path)}`.match(/(\d+)/)[1].to_i
140
+ end
141
+
142
+ # Get the list of processes as an array of hashes.
143
+ def processes
144
+ if ::File.directory? "/proc"
145
+ resolve_unix_uids(linux_processes)
146
+ elsif ::File.directory? "C:/WINDOWS"
147
+ windows_processes
148
+ else
149
+ os_x_processes
150
+ end
151
+ end
152
+
153
+ # Process list on Linux using /proc.
154
+ def linux_processes
155
+ list = []
156
+ ::Dir["/proc/*/stat"].select { |file| file =~ /\/proc\/\d+\// }.each do |file|
157
+ begin
158
+ list << read_proc_file(file)
159
+ rescue
160
+ # process died between the dir listing and accessing the file
161
+ end
162
+ end
163
+ list
164
+ end
165
+
166
+ def resolve_unix_uids(list)
167
+ @uid_map = {} # reset the cache between uid resolutions.
168
+ list.each do |process|
169
+ process[:user] = resolve_unix_uid_to_user(process[:uid])
170
+ end
171
+ list
172
+ end
173
+
174
+ # resolve uid to user
175
+ def resolve_unix_uid_to_user(uid)
176
+ require 'etc'
177
+
178
+ @uid_map ||= {}
179
+ uid = uid.to_i
180
+
181
+ return @uid_map[uid] if !@uid_map[uid].nil?
182
+
183
+ begin
184
+ record = Etc.getpwuid(uid)
185
+ rescue ArgumentError
186
+ return nil
187
+ end
188
+
189
+ @uid_map[uid] = record.name
190
+ @uid_map[uid]
191
+ end
192
+
193
+ # Read a single file in /proc and store the parsed values in a hash suitable
194
+ # for use in the Rush::Process#new.
195
+ def read_proc_file(file)
196
+ data = ::File.read(file).split(" ")
197
+ uid = ::File.stat(file).uid
198
+ pid = data[0]
199
+ command = data[1].match(/^\((.*)\)$/)[1]
200
+ cmdline = ::File.read("/proc/#{pid}/cmdline").gsub(/\0/, ' ')
201
+ parent_pid = data[3].to_i
202
+ utime = data[13].to_i
203
+ ktime = data[14].to_i
204
+ vss = data[22].to_i / 1024
205
+ rss = data[23].to_i * 4
206
+ time = utime + ktime
207
+
208
+ {
209
+ :pid => pid,
210
+ :uid => uid,
211
+ :command => command,
212
+ :cmdline => cmdline,
213
+ :parent_pid => parent_pid,
214
+ :mem => rss,
215
+ :cpu => time,
216
+ }
217
+ end
218
+
219
+ # Process list on OS X or other unixes without a /proc.
220
+ def os_x_processes
221
+ raw = os_x_raw_ps.split("\n").slice(1, 99999)
222
+ raw.map do |line|
223
+ parse_ps(line)
224
+ end
225
+ end
226
+
227
+ # ps command used to generate list of processes on non-/proc unixes.
228
+ def os_x_raw_ps
229
+ `COLUMNS=9999 ps ax -o "pid uid ppid rss cpu command"`
230
+ end
231
+
232
+ # Parse a single line of the ps command and return the values in a hash
233
+ # suitable for use in the Rush::Process#new.
234
+ def parse_ps(line)
235
+ m = line.split(" ", 6)
236
+ params = {}
237
+ params[:pid] = m[0]
238
+ params[:uid] = m[1]
239
+ params[:parent_pid] = m[2].to_i
240
+ params[:mem] = m[3].to_i
241
+ params[:cpu] = m[4].to_i
242
+ params[:cmdline] = m[5]
243
+ params[:command] = params[:cmdline].split(" ").first
244
+ params
245
+ end
246
+
247
+ # Process list on Windows.
248
+ def windows_processes
249
+ require 'win32ole'
250
+ wmi = WIN32OLE.connect("winmgmts://")
251
+ wmi.ExecQuery("select * from win32_process").map do |proc_info|
252
+ parse_oleprocinfo(proc_info)
253
+ end
254
+ end
255
+
256
+ # Parse the Windows OLE process info.
257
+ def parse_oleprocinfo(proc_info)
258
+ command = proc_info.Name
259
+ pid = proc_info.ProcessId
260
+ uid = 0
261
+ cmdline = proc_info.CommandLine
262
+ rss = proc_info.MaximumWorkingSetSize
263
+ time = proc_info.KernelModeTime.to_i + proc_info.UserModeTime.to_i
264
+
265
+ {
266
+ :pid => pid,
267
+ :uid => uid,
268
+ :command => command,
269
+ :cmdline => cmdline,
270
+ :mem => rss,
271
+ :cpu => time,
272
+ }
273
+ end
274
+
275
+ # Returns true if the specified pid is running.
276
+ def process_alive(pid)
277
+ ::Process.kill(0, pid)
278
+ true
279
+ rescue Errno::ESRCH
280
+ false
281
+ end
282
+
283
+ # Terminate a process, by pid.
284
+ def kill_process(pid, options={})
285
+ # time to wait before terminating the process, in seconds
286
+ wait = options[:wait] || 3
287
+
288
+ if wait > 0
289
+ ::Process.kill('TERM', pid)
290
+
291
+ # keep trying until it's dead (technique borrowed from god)
292
+ begin
293
+ Timeout.timeout(wait) do
294
+ loop do
295
+ return if !process_alive(pid)
296
+ sleep 0.5
297
+ ::Process.kill('TERM', pid) rescue nil
298
+ end
299
+ end
300
+ rescue Timeout::Error
301
+ end
302
+ end
303
+
304
+ ::Process.kill('KILL', pid) rescue nil
305
+
306
+ rescue Errno::ESRCH
307
+ # if it's dead, great - do nothing
308
+ end
309
+
310
+ def bash(command, user=nil, background=false)
311
+ return bash_background(command, user) if background
312
+
313
+ require 'session'
314
+
315
+ sh = Session::Bash.new
316
+
317
+ if user and user != ""
318
+ out, err = sh.execute "cd /; sudo -H -u #{user} bash", :stdin => command
319
+ else
320
+ out, err = sh.execute command
321
+ end
322
+
323
+ retval = sh.status
324
+ sh.close!
325
+
326
+ raise(Rush::BashFailed, err) if retval != 0
327
+
328
+ out
329
+ end
330
+
331
+ def bash_background(command, user)
332
+ pid = fork do
333
+ inpipe, outpipe = IO.pipe
334
+
335
+ outpipe.write command
336
+ outpipe.close
337
+ STDIN.reopen(inpipe)
338
+
339
+ close_all_descriptors([inpipe.to_i])
340
+
341
+ if user and user != ''
342
+ exec "cd /; sudo -H -u #{user} bash"
343
+ else
344
+ exec "bash"
345
+ end
346
+ end
347
+
348
+ Process::detach pid
349
+
350
+ pid
351
+ end
352
+
353
+ def close_all_descriptors(keep_open = [])
354
+ 3.upto(256) do |fd|
355
+ next if keep_open.include?(fd)
356
+ IO::new(fd).close rescue nil
357
+ end
358
+ end
359
+
360
+ ####################################
361
+
362
+ # Raised when the action passed in by RushServer is not known.
363
+ class UnknownAction < Exception; end
364
+
365
+ # RushServer uses this method to transform a hash (:action plus parameters
366
+ # specific to that action type) into a method call on the connection. The
367
+ # returned value must be text so that it can be transmitted across the wire
368
+ # as an HTTP response.
369
+ def receive(params)
370
+ case params[:action]
371
+ when 'write_file' then write_file(params[:full_path], params[:payload])
372
+ when 'append_to_file' then append_to_file(params[:full_path], params[:payload])
373
+ when 'file_contents' then file_contents(params[:full_path])
374
+ when 'destroy' then destroy(params[:full_path])
375
+ when 'purge' then purge(params[:full_path])
376
+ when 'create_dir' then create_dir(params[:full_path])
377
+ when 'rename' then rename(params[:path], params[:name], params[:new_name])
378
+ when 'copy' then copy(params[:src], params[:dst])
379
+ when 'read_archive' then read_archive(params[:full_path])
380
+ when 'write_archive' then write_archive(params[:payload], params[:dir])
381
+ when 'index' then index(params[:base_path], params[:glob]).join("\n") + "\n"
382
+ when 'stat' then YAML.dump(stat(params[:full_path]))
383
+ when 'set_access' then set_access(params[:full_path], Rush::Access.from_hash(params))
384
+ when 'size' then size(params[:full_path])
385
+ when 'processes' then YAML.dump(processes)
386
+ when 'process_alive' then process_alive(params[:pid]) ? '1' : '0'
387
+ when 'kill_process' then kill_process(params[:pid].to_i, YAML.load(params[:payload]))
388
+ when 'bash' then bash(params[:payload], params[:user], params[:background] == 'true')
389
+ else
390
+ raise UnknownAction
391
+ end
392
+ end
393
+
394
+ # No-op for duck typing with remote connection.
395
+ def ensure_tunnel(options={})
396
+ end
397
+
398
+ # Local connections are always alive.
399
+ def alive?
400
+ true
401
+ end
402
+ end
@@ -0,0 +1,59 @@
1
+ # An array of these objects is returned by Rush::Box#processes.
2
+ class Rush::Process
3
+ attr_reader :box, :pid, :uid, :parent_pid, :command, :cmdline, :mem, :cpu, :user
4
+
5
+ # params is a hash returned by the system-specific method of looking up the
6
+ # process list.
7
+ def initialize(params, box)
8
+ @box = box
9
+
10
+ @pid = params[:pid].to_i
11
+ @uid = params[:uid].to_i
12
+ @user = params[:user]
13
+ @command = params[:command]
14
+ @cmdline = params[:cmdline]
15
+ @mem = params[:mem]
16
+ @cpu = params[:cpu]
17
+ @parent_pid = params[:parent_pid]
18
+ end
19
+
20
+ def to_s # :nodoc:
21
+ inspect
22
+ end
23
+
24
+ def inspect # :nodoc:
25
+ if box.to_s != 'localhost'
26
+ "#{box} #{@pid}: #{@cmdline}"
27
+ else
28
+ "#{@pid}: #{@cmdline}"
29
+ end
30
+ end
31
+
32
+ # Returns the Rush::Process parent of this process.
33
+ def parent
34
+ box.processes.select { |p| p.pid == parent_pid }.first
35
+ end
36
+
37
+ # Returns an array of child processes owned by this process.
38
+ def children
39
+ box.processes.select { |p| p.parent_pid == pid }
40
+ end
41
+
42
+ # Returns true if the process is currently running.
43
+ def alive?
44
+ box.connection.process_alive(pid)
45
+ end
46
+
47
+ # Terminate the process.
48
+ def kill(options={})
49
+ box.connection.kill_process(pid, options)
50
+ end
51
+
52
+ def ==(other) # :nodoc:
53
+ pid == other.pid and box == other.box
54
+ end
55
+
56
+ def self.all
57
+ Rush::Box.new('localhost').processes
58
+ end
59
+ end
@@ -0,0 +1,62 @@
1
+ # A container for processes that behaves like an array, and adds process-specific operations
2
+ # on the entire set, like kill.
3
+ #
4
+ # Example:
5
+ #
6
+ # processes.filter(:cmdline => /mongrel_rails/).kill
7
+ #
8
+ class Rush::ProcessSet
9
+ attr_reader :processes
10
+
11
+ def initialize(processes)
12
+ @processes = processes
13
+ end
14
+
15
+ # Filter by any field that the process responds to. Specify an exact value,
16
+ # or a regular expression. All conditions are put together as a boolean
17
+ # AND, so these two statements are equivalent:
18
+ #
19
+ # processes.filter(:uid => 501).filter(:cmdline => /ruby/)
20
+ # processes.filter(:uid => 501, :cmdline => /ruby/)
21
+ #
22
+ def filter(conditions)
23
+ Rush::ProcessSet.new(
24
+ processes.select do |p|
25
+ conditions.all? do |key, value|
26
+ value.class == Regexp ?
27
+ value.match(p.send(key)) :
28
+ p.send(key) == value
29
+ end
30
+ end
31
+ )
32
+ end
33
+
34
+ # Kill all processes in the set.
35
+ def kill(options={})
36
+ processes.each { |p| p.kill(options) }
37
+ end
38
+
39
+ # Check status of all processes in the set, returns an array of booleans.
40
+ def alive?
41
+ processes.map { |p| p.alive? }
42
+ end
43
+
44
+ include Enumerable
45
+
46
+ def each
47
+ processes.each { |p| yield p }
48
+ end
49
+
50
+ def ==(other)
51
+ if other.class == self.class
52
+ other.processes == processes
53
+ else
54
+ to_a == other
55
+ end
56
+ end
57
+
58
+ # All other messages (like size or first) are passed through to the array.
59
+ def method_missing(meth, *args)
60
+ processes.send(meth, *args)
61
+ end
62
+ end
@@ -0,0 +1,156 @@
1
+ require 'yaml'
2
+
3
+ # This class it the mirror of Rush::Connection::Local. A Rush::Box which is
4
+ # not localhost has a remote connection, which it can use to convert method
5
+ # calls to text suitable for transmission across the wire.
6
+ #
7
+ # This is an internal class that does not need to be accessed in normal use of
8
+ # the rush shell or library.
9
+ class Rush::Connection::Remote
10
+ attr_reader :host, :tunnel
11
+
12
+ def initialize(host)
13
+ @host = host
14
+ @tunnel = Rush::SshTunnel.new(host)
15
+ end
16
+
17
+ def write_file(full_path, contents)
18
+ transmit(:action => 'write_file', :full_path => full_path, :payload => contents)
19
+ end
20
+
21
+ def append_to_file(full_path, contents)
22
+ transmit(:action => 'append_to_file', :full_path => full_path, :payload => contents)
23
+ end
24
+
25
+ def file_contents(full_path)
26
+ transmit(:action => 'file_contents', :full_path => full_path)
27
+ end
28
+
29
+ def destroy(full_path)
30
+ transmit(:action => 'destroy', :full_path => full_path)
31
+ end
32
+
33
+ def purge(full_path)
34
+ transmit(:action => 'purge', :full_path => full_path)
35
+ end
36
+
37
+ def create_dir(full_path)
38
+ transmit(:action => 'create_dir', :full_path => full_path)
39
+ end
40
+
41
+ def rename(path, name, new_name)
42
+ transmit(:action => 'rename', :path => path, :name => name, :new_name => 'new_name')
43
+ end
44
+
45
+ def copy(src, dst)
46
+ transmit(:action => 'copy', :src => src, :dst => dst)
47
+ end
48
+
49
+ def read_archive(full_path)
50
+ transmit(:action => 'read_archive', :full_path => full_path)
51
+ end
52
+
53
+ def write_archive(archive, dir)
54
+ transmit(:action => 'write_archive', :dir => dir, :payload => archive)
55
+ end
56
+
57
+ def index(base_path, glob)
58
+ transmit(:action => 'index', :base_path => base_path, :glob => glob).split("\n")
59
+ end
60
+
61
+ def stat(full_path)
62
+ YAML.load(transmit(:action => 'stat', :full_path => full_path))
63
+ end
64
+
65
+ def set_access(full_path, access)
66
+ transmit access.to_hash.merge(:action => 'set_access', :full_path => full_path)
67
+ end
68
+
69
+ def size(full_path)
70
+ transmit(:action => 'size', :full_path => full_path).to_i
71
+ end
72
+
73
+ def processes
74
+ YAML.load(transmit(:action => 'processes'))
75
+ end
76
+
77
+ def process_alive(pid)
78
+ transmit(:action => 'process_alive', :pid => pid)
79
+ end
80
+
81
+ def kill_process(pid, options={})
82
+ transmit(:action => 'kill_process', :pid => pid, :payload => YAML.dump(options))
83
+ end
84
+
85
+ def bash(command, user, background)
86
+ transmit(:action => 'bash', :payload => command, :user => user, :background => background)
87
+ end
88
+
89
+ # Given a hash of parameters (converted by the method call on the connection
90
+ # object), send it across the wire to the RushServer listening on the other
91
+ # side. Uses http basic auth, with credentials fetched from the Rush::Config.
92
+ def transmit(params)
93
+ ensure_tunnel
94
+
95
+ require 'net/http'
96
+
97
+ payload = params.delete(:payload)
98
+
99
+ uri = "/?"
100
+ params.each do |key, value|
101
+ uri += "#{key}=#{value}&"
102
+ end
103
+
104
+ req = Net::HTTP::Post.new(uri)
105
+ req.basic_auth config.credentials_user, config.credentials_password
106
+
107
+ Net::HTTP.start(tunnel.host, tunnel.port) do |http|
108
+ res = http.request(req, payload)
109
+ process_result(res.code, res.body)
110
+ end
111
+ rescue EOFError
112
+ raise Rush::RushdNotRunning
113
+ end
114
+
115
+ # Take the http result of a transmit and raise an error, or return the body
116
+ # of the result when valid.
117
+ def process_result(code, body)
118
+ raise Rush::NotAuthorized if code == "401"
119
+
120
+ if code == "400"
121
+ klass, message = parse_exception(body)
122
+ raise klass, "#{host}:#{message}"
123
+ end
124
+
125
+ raise Rush::FailedTransmit if code != "200"
126
+
127
+ body
128
+ end
129
+
130
+ # Parse an exception returned from the server, with the class name on the
131
+ # first line and the message on the second.
132
+ def parse_exception(body)
133
+ klass, message = body.split("\n", 2)
134
+ raise "invalid exception class: #{klass}" unless klass.match(/^Rush::[A-Za-z]+$/)
135
+ klass = Object.module_eval(klass)
136
+ [ klass, message.strip ]
137
+ end
138
+
139
+ # Set up the tunnel if it is not already running.
140
+ def ensure_tunnel(options={})
141
+ tunnel.ensure_tunnel(options)
142
+ end
143
+
144
+ # Remote connections are alive when the box on the other end is responding
145
+ # to commands.
146
+ def alive?
147
+ index('/', 'alive_check')
148
+ true
149
+ rescue
150
+ false
151
+ end
152
+
153
+ def config
154
+ @config ||= Rush::Config.new
155
+ end
156
+ end