necktie 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/bin/necktie +1 -1
  2. data/lib/necktie/rush.rb +11 -0
  3. data/lib/necktie/services.rb +31 -11
  4. data/lib/necktie.rb +10 -6
  5. data/necktie.gemspec +2 -2
  6. data/rush/README.rdoc +87 -0
  7. data/rush/Rakefile +60 -0
  8. data/rush/VERSION +1 -0
  9. data/rush/bin/rush +13 -0
  10. data/rush/bin/rushd +7 -0
  11. data/rush/lib/rush/access.rb +130 -0
  12. data/rush/lib/rush/array_ext.rb +19 -0
  13. data/rush/lib/rush/box.rb +112 -0
  14. data/rush/lib/rush/commands.rb +55 -0
  15. data/rush/lib/rush/config.rb +154 -0
  16. data/rush/lib/rush/dir.rb +160 -0
  17. data/rush/lib/rush/embeddable_shell.rb +26 -0
  18. data/rush/lib/rush/entry.rb +185 -0
  19. data/rush/lib/rush/exceptions.rb +31 -0
  20. data/rush/lib/rush/file.rb +85 -0
  21. data/rush/lib/rush/find_by.rb +39 -0
  22. data/rush/lib/rush/fixnum_ext.rb +18 -0
  23. data/rush/lib/rush/head_tail.rb +11 -0
  24. data/rush/lib/rush/local.rb +402 -0
  25. data/rush/lib/rush/process.rb +59 -0
  26. data/rush/lib/rush/process_set.rb +62 -0
  27. data/rush/lib/rush/remote.rb +156 -0
  28. data/rush/lib/rush/search_results.rb +58 -0
  29. data/rush/lib/rush/server.rb +117 -0
  30. data/rush/lib/rush/shell.rb +187 -0
  31. data/rush/lib/rush/ssh_tunnel.rb +122 -0
  32. data/rush/lib/rush/string_ext.rb +3 -0
  33. data/rush/lib/rush.rb +87 -0
  34. data/rush/rush.gemspec +113 -0
  35. data/rush/spec/access_spec.rb +134 -0
  36. data/rush/spec/array_ext_spec.rb +15 -0
  37. data/rush/spec/base.rb +24 -0
  38. data/rush/spec/box_spec.rb +64 -0
  39. data/rush/spec/commands_spec.rb +47 -0
  40. data/rush/spec/config_spec.rb +108 -0
  41. data/rush/spec/dir_spec.rb +164 -0
  42. data/rush/spec/embeddable_shell_spec.rb +17 -0
  43. data/rush/spec/entry_spec.rb +133 -0
  44. data/rush/spec/file_spec.rb +83 -0
  45. data/rush/spec/find_by_spec.rb +58 -0
  46. data/rush/spec/fixnum_ext_spec.rb +19 -0
  47. data/rush/spec/local_spec.rb +352 -0
  48. data/rush/spec/process_set_spec.rb +50 -0
  49. data/rush/spec/process_spec.rb +73 -0
  50. data/rush/spec/remote_spec.rb +140 -0
  51. data/rush/spec/rush_spec.rb +28 -0
  52. data/rush/spec/search_results_spec.rb +44 -0
  53. data/rush/spec/shell_spec.rb +23 -0
  54. data/rush/spec/ssh_tunnel_spec.rb +122 -0
  55. data/rush/spec/string_ext_spec.rb +23 -0
  56. metadata +53 -3
  57. data/lib/necktie/files.rb +0 -14
@@ -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