Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
tree: bf9aac9306
Fetching contributors…

Cannot retrieve contributors at this time

409 lines (351 sloc) 10.717 kB
require 'fileutils'
require 'yaml'
require 'timeout'
# 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.
class Rush::Connection::Local
# Write raw bytes to a file.
def write_file(full_path, contents)
::File.open(full_path, 'w') do |f|
f.write contents
end
true
end
# Append contents to a file
def append_to_file(full_path, contents)
::File.open(full_path, 'a') do |f|
f.write contents
end
true
end
# Read raw bytes from a file.
def file_contents(full_path)
::File.read(full_path)
rescue Errno::ENOENT
raise Rush::DoesNotExist, full_path
end
# Destroy a file or dir.
def destroy(full_path)
raise "No." if full_path == '/'
FileUtils.rm_rf(full_path)
true
end
# Purge the contents of a dir.
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
# Create a dir.
def create_dir(full_path)
FileUtils.mkdir_p(full_path)
true
end
# Rename an entry within a dir.
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
# Copy ane entry from one path to another.
def copy(src, dst)
FileUtils.cp_r(src, dst)
true
rescue Errno::ENOENT
raise Rush::DoesNotExist, File.dirname(dst)
rescue RuntimeError
raise Rush::DoesNotExist, src
end
# 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.
def read_archive(full_path)
`cd #{Rush.quote(::File.dirname(full_path))}; tar c #{Rush.quote(::File.basename(full_path))}`
end
# Extract an in-memory archive to a dir.
def write_archive(archive, dir)
IO.popen("cd #{Rush::quote(dir)}; tar x", "w") do |p|
p.write archive
end
end
# 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.
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
# Fetch stats (size, ctime, etc) on an entry. Size will not be accurate for dirs.
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
def set_access(full_path, access)
access.apply(full_path)
end
# Fetch the size of a dir, since a standard file stat does not include the
# size of the contents.
def size(full_path)
`du -sb #{Rush.quote(full_path)}`.match(/(\d+)/)[1].to_i
end
# Get the list of processes as an array of hashes.
def processes
if ::File.directory? "/proc"
resolve_unix_uids(linux_processes)
elsif ::File.directory? "C:/WINDOWS"
windows_processes
else
os_x_processes
end
end
# Process list on Linux using /proc.
def linux_processes
list = []
::Dir["/proc/*/stat"].select { |file| file =~ /\/proc\/\d+\// }.each do |file|
begin
list << read_proc_file(file)
rescue
# process died between the dir listing and accessing the file
end
end
list
end
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
# resolve uid to user
def resolve_unix_uid_to_user(uid)
require 'etc'
@uid_map ||= {}
uid = uid.to_i
return @uid_map[uid] if !@uid_map[uid].nil?
begin
record = Etc.getpwuid(uid)
rescue ArgumentError
return nil
end
@uid_map[uid] = record.name
@uid_map[uid]
end
# Read a single file in /proc and store the parsed values in a hash suitable
# for use in the Rush::Process#new.
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
# Process list on OS X or other unixes without a /proc.
def os_x_processes
raw = os_x_raw_ps.split("\n").slice(1, 99999)
raw.map do |line|
parse_ps(line)
end
end
# ps command used to generate list of processes on non-/proc unixes.
def os_x_raw_ps
`COLUMNS=9999 ps ax -o "pid uid ppid rss cpu command"`
end
# Parse a single line of the ps command and return the values in a hash
# suitable for use in the Rush::Process#new.
def parse_ps(line)
m = line.split(" ", 6)
params = {}
params[:pid] = m[0]
params[:uid] = m[1]
params[:parent_pid] = m[2].to_i
params[:mem] = m[3].to_i
params[:cpu] = m[4].to_i
params[:cmdline] = m[5]
params[:command] = params[:cmdline].split(" ").first
params
end
# Process list on Windows.
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
# Parse the Windows OLE process info.
def parse_oleprocinfo(proc_info)
command = proc_info.Name
pid = proc_info.ProcessId
uid = 0
cmdline = proc_info.CommandLine
rss = proc_info.MaximumWorkingSetSize
time = proc_info.KernelModeTime.to_i + proc_info.UserModeTime.to_i
{
:pid => pid,
:uid => uid,
:command => command,
:cmdline => cmdline,
:mem => rss,
:cpu => time,
}
end
# Returns true if the specified pid is running.
def process_alive(pid)
::Process.kill(0, pid)
true
rescue Errno::ESRCH
false
end
# Terminate a process, by pid.
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
def bash(command, user=nil, background=false, reset_environment=false)
return bash_background(command, user, reset_environment) if background
require 'session'
sh = Session::Bash.new
shell = reset_environment ? "env -i bash" : "bash"
if user and user != ""
out, err = sh.execute "cd /; sudo -H -u #{user} \"#{shell}\"", :stdin => command
else
out, err = sh.execute shell, :stdin => command
end
retval = sh.status
sh.close!
raise(Rush::BashFailed, err) if retval != 0
out
end
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"
if user and user != ''
exec "cd /; sudo -H -u #{user} \"#{shell}\""
else
exec shell
end
end
Process::detach pid
pid
end
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
####################################
# Raised when the action passed in by RushServer is not known.
class UnknownAction < Exception; end
# 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.
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 '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 '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
# No-op for duck typing with remote connection.
def ensure_tunnel(options={})
end
# Local connections are always alive.
def alive?
true
end
end
Jump to Line
Something went wrong with that request. Please try again.