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)


405
406
407
# File 'lib/rush/local.rb', line 405

def alive?
	true
end

#append_to_file(full_path, contents) ⇒ Object

Append contents to a file



26
27
28
29
30
31
# File 'lib/rush/local.rb', line 26

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

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

Raises:



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/rush/local.rb', line 312

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

#bash_background(command, user, reset_environment) ⇒ Object



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/rush/local.rb', line 335

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

#close_all_descriptors(keep_open = []) ⇒ Object



359
360
361
362
363
364
# File 'lib/rush/local.rb', line 359

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.



74
75
76
77
78
79
80
81
# File 'lib/rush/local.rb', line 74

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_dir(full_path) ⇒ Object

Create a dir.



58
59
60
61
# File 'lib/rush/local.rb', line 58

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

#destroy(full_path) ⇒ Object

Destroy a file or dir.



41
42
43
44
45
# File 'lib/rush/local.rb', line 41

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.



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

def ensure_tunnel(options={})
end

#file_contents(full_path) ⇒ Object

Read raw bytes from a file.



34
35
36
37
38
# File 'lib/rush/local.rb', line 34

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.



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/rush/local.rb', line 100

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.



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
# File 'lib/rush/local.rb', line 286

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.



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

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

#os_x_processesObject

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



222
223
224
225
226
227
# File 'lib/rush/local.rb', line 222

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.



230
231
232
# File 'lib/rush/local.rb', line 230

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.



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/rush/local.rb', line 259

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

#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.



236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/rush/local.rb', line 236

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_alive(pid) ⇒ Object

Returns true if the specified pid is running.



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

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.



143
144
145
146
147
148
149
150
151
# File 'lib/rush/local.rb', line 143

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

#purge(full_path) ⇒ Object

Purge the contents of a dir.



48
49
50
51
52
53
54
55
# File 'lib/rush/local.rb', line 48

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.



86
87
88
# File 'lib/rush/local.rb', line 86

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.



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
# File 'lib/rush/local.rb', line 195

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.



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/rush/local.rb', line 375

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

#rename(path, name, new_name) ⇒ Object

Rename an entry within a dir.



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

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



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/rush/local.rb', line 175

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

#resolve_unix_uids(list) ⇒ Object



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

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



132
133
134
# File 'lib/rush/local.rb', line 132

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.



138
139
140
# File 'lib/rush/local.rb', line 138

def size(full_path)
	`du -sb #{Rush.quote(full_path)}`.match(/(\d+)/)[1].to_i
end

#stat(full_path) ⇒ Object

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



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

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

#windows_processesObject

Process list on Windows.



250
251
252
253
254
255
256
# File 'lib/rush/local.rb', line 250

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.



91
92
93
94
95
# File 'lib/rush/local.rb', line 91

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.



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

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