Process::MetricsSourceProcessMetricsGeneralProcessStatus

module ProcessStatus

General process information via the process status command (ps). Used on non-Linux platforms (e.g. Darwin) where there is no /proc; ps is the portable way to get pid, ppid, times, and memory in one pass.

Definitions

FIELDS = {...}

The fields that will be extracted from the ps command (order matches -o output).

Implementation

FIELDS = {
	pid: ->(value){value.to_i},
	ppid: ->(value){value.to_i},
	pgid: ->(value){value.to_i},
	pcpu: ->(value){value.to_f},
	vsz: ->(value){value.to_i * 1024},
	rss: ->(value){value.to_i * 1024},
	time: Process::Metrics.method(:duration),
	etime: Process::Metrics.method(:duration),
	command: ->(value){value},
}

def self.supported?

Whether process listing via ps is available on this system.

Implementation

def self.supported?
	system("which", PS, out: File::NULL, err: File::NULL)
end

def self.capture(pid: nil, ppid: nil, memory: Memory.supported?)

Capture process information using ps. If given a pid, captures that process; if given ppid, captures that process and all descendants. Specify both to capture a process and its children.

Signature

parameter pid Integer | Array(Integer)

Process ID(s) to capture.

parameter ppid Integer | Array(Integer)

Parent process ID(s) to include children for.

parameter memory Boolean

Whether to capture detailed memory metrics (default: Memory.supported?).

returns Hash<Integer, General>

Map of PID to General instance.

Implementation

def self.capture(pid: nil, ppid: nil, memory: Memory.supported?)
	spawned_pid = nil
	
	header, *lines = IO.pipe do |input, output|
		arguments = [PS]
		
		# When filtering by ppid we need the full process list to build the tree, so use "ax"; otherwise limit to -p.
		if pid && ppid.nil?
			arguments.push("-p", Array(pid).join(","))
		else
			arguments.push("ax")
		end
		
		arguments.push("-o", FIELDS.keys.join(","))
		
		spawned_pid = Process.spawn(*arguments, out: output)
		output.close
		
		input.readlines.map(&:strip)
	ensure
		input.close
		
		# Always kill and reap the ps subprocess so we never leave it hanging if the pipe closes early.
		if spawned_pid
			begin
				Process.kill(:KILL, spawned_pid)
				Process.wait(spawned_pid)
			rescue => error
				warn "Failed to cleanup ps process #{spawned_pid}:\n#{error.full_message}"
			end
		end
	end
	
	processes = {}
	
	lines.each do |line|
		next if line.empty?
		
		values = line.split(/\s+/, FIELDS.size)
		next if values.size < FIELDS.size
		
		record = FIELDS.keys.map.with_index{|key, i| FIELDS[key].call(values[i])}
		instance = General.new(*record, nil)
		processes[instance.process_id] = instance
	end
	
	# Restrict to the requested pid/ppid subtree; exclude our own ps process from the result.
	if ppid
		pids = Set.new
		hierarchy = General.build_tree(processes)
		General.expand_children(Array(pid), hierarchy, pids) if pid
		General.expand_children(Array(ppid), hierarchy, pids)
		processes.select!{|process_id, _| process_id != spawned_pid && pids.include?(process_id)}
	else
		processes.delete(spawned_pid) if spawned_pid
	end
	
	General.capture_memory(processes) if memory
	
	processes
end