Class: Rush::Connection::Local

Inherits:
Object
  • Object
show all
Defined in:
lib/rush/local.rb

Overview

Rush::Box uses a connection object to execute all rush commands. If the box is local, Rush::Connection::Local is created. The local connection is the heart of rush’s internals. (Users of the rush shell or library need never access the connection object directly, so the docs herein are intended for developers wishing to modify rush.)

The local connection has a series of methods which do the actual work of modifying files, getting process lists, and so on. RushServer creates a local connection to handle incoming requests; the translation from a raw hash of parameters to an executed method is handled by Rush::Connection::Local#receive.

Defined Under Namespace

Classes: UnknownAction

Instance Method Summary collapse

Instance Method Details

#alive?Boolean

Local connections are always alive.

Returns:

  • (Boolean)


401
402
403
# File 'lib/rush/local.rb', line 401

def alive?
  true
end

#append_to_file(full_path, contents) ⇒ Object

Append contents to a file



25
26
27
28
# File 'lib/rush/local.rb', line 25

def append_to_file(full_path, contents)
  ::File.open(full_path, 'a') { |f| f.write contents }
  true
end

#bash(command, user = nil, background = false, reset_environment = false) ⇒ Object

Raises:



322
323
324
325
326
327
328
329
330
331
# File 'lib/rush/local.rb', line 322

def bash(command, user=nil, background=false, reset_environment=false)
  return bash_background(command, user, reset_environment) if background
  sh = Session::Bash.new
  shell = reset_environment ? "env -i bash" : "bash"
  out, err = sh.execute sudo(user, shell), :stdin => command
  retval = sh.status
  sh.close!
  raise(Rush::BashFailed, err) if retval != 0
  out
end

#bash_background(command, user, reset_environment) ⇒ Object



333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/rush/local.rb', line 333

def bash_background(command, user, reset_environment)
  pid = fork do
    inpipe, outpipe = IO.pipe
    outpipe.write command
    outpipe.close
    STDIN.reopen(inpipe)
    close_all_descriptors([inpipe.to_i])
    shell = reset_environment ? "env -i bash" : "bash"
    exec sudo(user, shell)
  end
  Process::detach pid
  pid
end

#chown(full_path, user = nil, group = nil, options = {}) ⇒ Object

A frontend for FileUtils::chown



155
156
157
158
159
160
161
# File 'lib/rush/local.rb', line 155

def chown( full_path, user=nil, group=nil, options={} )
  if options[:recursive]
    FileUtils.chown_R(user, group, full_path, options)
  else
    FileUtils.chown(user, group, full_path, options)
  end
end

#close_all_descriptors(keep_open = []) ⇒ Object



352
353
354
355
356
357
# File 'lib/rush/local.rb', line 352

def close_all_descriptors(keep_open = [])
  3.upto(256) do |fd|
    next if keep_open.include?(fd)
    IO::new(fd).close rescue nil
  end
end

#copy(src, dst) ⇒ Object

Copy ane entry from one path to another.

Raises:



72
73
74
75
76
77
# File 'lib/rush/local.rb', line 72

def copy(src, dst)
  raise(Rush::DoesNotExist, src) unless File.exists?(src)
  raise(Rush::DoesNotExist, File.dirname(dst)) unless File.exists?(File.dirname(dst))
  FileUtils.cp_r(src, dst)
  true
end

#create_dir(full_path) ⇒ Object

Create a dir.



56
57
58
59
# File 'lib/rush/local.rb', line 56

def create_dir(full_path)
  FileUtils.mkdir_p(full_path)
  true
end

#destroy(full_path) ⇒ Object

Destroy a file or dir.



38
39
40
41
42
# File 'lib/rush/local.rb', line 38

def destroy(full_path)
  raise "No." if full_path == '/'
  FileUtils.rm_rf(full_path)
  true
end

#ensure_tunnel(options = {}) ⇒ Object

No-op for duck typing with remote connection.



397
398
# File 'lib/rush/local.rb', line 397

def ensure_tunnel(options={})
end

#file_contents(full_path) ⇒ Object

Read raw bytes from a file.



31
32
33
34
35
# File 'lib/rush/local.rb', line 31

def file_contents(full_path)
  ::File.read(full_path)
rescue Errno::ENOENT
  raise Rush::DoesNotExist, full_path
end

#index(base_path, glob) ⇒ Object

Get an index of files from the given path with the glob. Could return nested values if the glob contains a doubleglob. The return value is an array of full paths, with directories listed first.



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/rush/local.rb', line 117

def index(base_path, glob)
  glob = '*' if glob == '' or glob.nil?
  dirs = []
  files = []
  ::Dir.chdir(base_path) do
    ::Dir.glob(glob).each do |fname|
      if ::File.directory?(fname)
        dirs << fname + '/'
      else
        files << fname
      end
    end
  end
  dirs.sort + files.sort
rescue Errno::ENOENT
  raise Rush::DoesNotExist, base_path
end

#kill_process(pid, options = {}) ⇒ Object

Terminate a process, by pid.



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/rush/local.rb', line 296

def kill_process(pid, options={})
  # time to wait before terminating the process, in seconds
  wait = options[:wait] || 3

  if wait > 0
    ::Process.kill('TERM', pid)

    # keep trying until it's dead (technique borrowed from god)
    begin
      Timeout.timeout(wait) do
        loop do
          return if !process_alive(pid)
          sleep 0.5
          ::Process.kill('TERM', pid) rescue nil
        end
      end
    rescue Timeout::Error
    end
  end

  ::Process.kill('KILL', pid) rescue nil

rescue Errno::ESRCH
  # if it's dead, great - do nothing
end

#linux_processesObject

Process list on Linux using /proc.



185
186
187
188
189
# File 'lib/rush/local.rb', line 185

def linux_processes
  ::Dir["/proc/*/stat"].
    select     { |file| file =~ /\/proc\/\d+\// }.
    inject([]) { |list, file| (list << read_proc_file(file)) rescue list }
end

#ln(src, dst, options = {}) ⇒ Object

Create a hard link to another file

Raises:



80
81
82
83
84
85
# File 'lib/rush/local.rb', line 80

def ln(src, dst, options = {})
  raise(Rush::DoesNotExist, src) unless File.exists?(src)
  raise(Rush::DoesNotExist, File.dirname(dst)) unless File.exists?(File.dirname(dst))
  FileUtils.ln(src, dst, options)
  true
end

#ln_s(src, dst, options = {}) ⇒ Object

Create a symbolic link to another file

Raises:



88
89
90
91
92
93
# File 'lib/rush/local.rb', line 88

def ln_s(src, dst, options = {})
  raise(Rush::DoesNotExist, src) unless File.exists?(src)
  raise(Rush::DoesNotExist, File.dirname(dst)) unless File.exists?(File.dirname(dst))
  FileUtils.ln_s(src, dst, options)
  true
end

#os_x_processesObject

Process list on OS X or other unixes without a /proc.



239
240
241
242
243
244
# File 'lib/rush/local.rb', line 239

def os_x_processes
  raw = os_x_raw_ps.split("\n").slice(1, 99999)
  raw.map do |line|
    parse_ps(line)
  end
end

#os_x_raw_psObject

ps command used to generate list of processes on non-/proc unixes.



247
248
249
# File 'lib/rush/local.rb', line 247

def os_x_raw_ps
  `COLUMNS=9999 ps ax -o "pid uid ppid rss cpu command"`
end

#parse_oleprocinfo(proc_info) ⇒ Object

Parse the Windows OLE process info.



276
277
278
279
280
281
282
283
284
285
# File 'lib/rush/local.rb', line 276

def parse_oleprocinfo(proc_info)
  {
    pid: proc_info.ProcessId,
    uid: 0,
    command: proc_info.Name,
    cmdline: proc_info.CommandLine,
    mem: proc_info.MaximumWorkingSetSize,
    cpu: proc_info.KernelModeTime.to_i + proc_info.UserModeTime.to_i
  }
end

#parse_ps(line) ⇒ Object

Parse a single line of the ps command and return the values in a hash suitable for use in the Rush::Process#new.



253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/rush/local.rb', line 253

def parse_ps(line)
  m = line.split(" ", 6)
  {
    pid: m[0],
    uid: m[1],
    parent_pid: m[2].to_i,
    mem: m[3].to_i,
    cpu: m[4].to_i,
    cmdline: m[5],
    command: m[5].split(' ').first
  }
end

#process_alive(pid) ⇒ Object

Returns true if the specified pid is running.



288
289
290
291
292
293
# File 'lib/rush/local.rb', line 288

def process_alive(pid)
  ::Process.kill(0, pid)
  true
rescue Errno::ESRCH
  false
end

#processesObject

Get the list of processes as an array of hashes.



174
175
176
177
178
179
180
181
182
# File 'lib/rush/local.rb', line 174

def processes
  if ::File.directory? "/proc"
    resolve_unix_uids(linux_processes)
  elsif ::File.directory? "C:/WINDOWS"
    windows_processes
  else
    os_x_processes
  end.uniq
end

#purge(full_path) ⇒ Object

Purge the contents of a dir.



45
46
47
48
49
50
51
52
53
# File 'lib/rush/local.rb', line 45

def purge(full_path)
  raise "No." if full_path == '/'
  Dir.chdir(full_path) do
    all = Dir.glob("*", File::FNM_DOTMATCH).
      reject { |f| f == '.' or f == '..' }
    FileUtils.rm_rf all
  end
  true
end

#read_archive(full_path) ⇒ Object

Create an in-memory archive (tgz) of a file or dir, which can be transmitted to another server for a copy or move. Note that archive operations have the dir name implicit in the archive.



103
104
105
# File 'lib/rush/local.rb', line 103

def read_archive(full_path)
  `cd #{Rush.quote(::File.dirname(full_path))}; tar c #{Rush.quote(::File.basename(full_path))}`
end

#read_proc_file(file) ⇒ Object

Read a single file in /proc and store the parsed values in a hash suitable for use in the Rush::Process#new.



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/rush/local.rb', line 212

def read_proc_file(file)
  stat_contents = ::File.read(file)
  stat_contents.gsub!(/\((.*)\)/, "")
  command = $1
  data = stat_contents.split(" ")
  uid = ::File.stat(file).uid
  pid = data[0]
  cmdline = ::File.read("/proc/#{pid}/cmdline").gsub(/\0/, ' ')
  parent_pid = data[2].to_i
  utime = data[12].to_i
  ktime = data[13].to_i
  vss = data[21].to_i / 1024
  rss = data[22].to_i * 4
  time = utime + ktime

  {
    :pid => pid,
    :uid => uid,
    :command => command,
    :cmdline => cmdline,
    :parent_pid => parent_pid,
    :mem => rss,
    :cpu => time
  }
end

#receive(params) ⇒ Object

RushServer uses this method to transform a hash (:action plus parameters specific to that action type) into a method call on the connection. The returned value must be text so that it can be transmitted across the wire as an HTTP response.



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/rush/local.rb', line 368

def receive(params)
  case params[:action]
    when 'write_file'     then write_file(params[:full_path], params[:payload])
    when 'append_to_file' then append_to_file(params[:full_path], params[:payload])
    when 'file_contents'  then file_contents(params[:full_path])
    when 'destroy'        then destroy(params[:full_path])
    when 'purge'          then purge(params[:full_path])
    when 'create_dir'     then create_dir(params[:full_path])
    when 'rename'         then rename(params[:path], params[:name], params[:new_name])
    when 'copy'           then copy(params[:src], params[:dst])
    when 'ln'             then ln(params[:src], params[:dst], params[:options])
    when 'ln_s'           then ln_s(params[:src], params[:dst], params[:options])
    when 'read_archive'   then read_archive(params[:full_path])
    when 'write_archive'  then write_archive(params[:payload], params[:dir])
    when 'index'          then index(params[:base_path], params[:glob]).join("\n") + "\n"
    when 'stat'           then YAML.dump(stat(params[:full_path]))
    when 'set_access'     then set_access(params[:full_path], Rush::Access.from_hash(params))
    when 'chown'          then chown(params[:full_path], params[:user], params[:group], params[:options])
    when 'size'           then size(params[:full_path])
    when 'processes'      then YAML.dump(processes)
    when 'process_alive'  then process_alive(params[:pid]) ? '1' : '0'
    when 'kill_process'   then kill_process(params[:pid].to_i, YAML.load(params[:payload]))
    when 'bash'           then bash(params[:payload], params[:user], params[:background] == 'true', params[:reset_environment] == 'true')
  else
    raise UnknownAction
  end
end

#rename(path, name, new_name) ⇒ Object

Rename an entry within a dir.



62
63
64
65
66
67
68
69
# File 'lib/rush/local.rb', line 62

def rename(path, name, new_name)
  raise(Rush::NameCannotContainSlash, "#{path} rename #{name} to #{new_name}") if new_name.match(/\//)
  old_full_path = "#{path}/#{name}"
  new_full_path = "#{path}/#{new_name}"
  raise(Rush::NameAlreadyExists, "#{path} rename #{name} to #{new_name}") if ::File.exists?(new_full_path)
  FileUtils.mv(old_full_path, new_full_path)
  true
end

#resolve_unix_uid_to_user(uid) ⇒ Object

resolve uid to user



200
201
202
203
204
205
206
207
208
# File 'lib/rush/local.rb', line 200

def resolve_unix_uid_to_user(uid)
  uid = uid.to_i
  require 'etc'
  @uid_map ||= {}
  return @uid_map[uid] if @uid_map[uid]
  record = Etc.getpwuid(uid) rescue (return nil)
  @uid_map.merge uid => record.name
  record.name
end

#resolve_unix_uids(list) ⇒ Object



191
192
193
194
195
196
197
# File 'lib/rush/local.rb', line 191

def resolve_unix_uids(list)
  @uid_map = {} # reset the cache between uid resolutions.
  list.each do |process|
    process[:user] = resolve_unix_uid_to_user(process[:uid])
  end
  list
end

#set_access(full_path, access) ⇒ Object



149
150
151
# File 'lib/rush/local.rb', line 149

def set_access(full_path, access)
  access.apply(full_path)
end

#size(full_path) ⇒ Object

Fetch the size of a dir, since a standard file stat does not include the size of the contents.



165
166
167
168
169
170
171
# File 'lib/rush/local.rb', line 165

def size(full_path)
  if RUBY_PLATFORM.match(/darwin/)
    `find #{Rush.quote(full_path)} -print0 | xargs -0 stat -f%z`.split(/\n/).map(&:to_i).reduce(:+)
  else
    `du -sb #{Rush.quote(full_path)}`.match(/(\d+)/)[1].to_i
  end
end

#stat(full_path) ⇒ Object

Fetch stats (size, ctime, etc) on an entry. Size will not be accurate for dirs.



136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/rush/local.rb', line 136

def stat(full_path)
  s = ::File.stat(full_path)
  {
    :size => s.size,
    :ctime => s.ctime,
    :atime => s.atime,
    :mtime => s.mtime,
    :mode => s.mode
  }
rescue Errno::ENOENT
  raise Rush::DoesNotExist, full_path
end

#sudo(user, shell) ⇒ Object



347
348
349
350
# File 'lib/rush/local.rb', line 347

def sudo(user, shell)
  return shell if user.nil? || user.empty?
  "cd /; sudo -H -u #{user} \"#{shell}\""
end

#symlink?(path) ⇒ Boolean

Is this path a symlink?

Returns:

  • (Boolean)


96
97
98
# File 'lib/rush/local.rb', line 96

def symlink?(path)
  File.symlink? path
end

#windows_processesObject

Process list on Windows.



267
268
269
270
271
272
273
# File 'lib/rush/local.rb', line 267

def windows_processes
  require 'win32ole'
  wmi = WIN32OLE.connect("winmgmts://")
  wmi.ExecQuery("select * from win32_process").map do |proc_info|
    parse_oleprocinfo(proc_info)
  end
end

#write_archive(archive, dir) ⇒ Object

Extract an in-memory archive to a dir.



108
109
110
111
112
# File 'lib/rush/local.rb', line 108

def write_archive(archive, dir)
  IO.popen("cd #{Rush::quote(dir)}; tar x", "w") do |p|
    p.write archive
  end
end

#write_file(full_path, contents) ⇒ Object

Write raw bytes to a file.



19
20
21
22
# File 'lib/rush/local.rb', line 19

def write_file(full_path, contents)
  ::File.open(full_path, 'w') { |f| f.write contents }
  true
end