forked from adamwiggins/rush
-
Notifications
You must be signed in to change notification settings - Fork 0
/
local.rb
408 lines (351 loc) · 10.5 KB
/
local.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
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
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
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
395
396
397
398
399
400
401
402
403
404
405
406
407
408
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