csc360-a1-shell/ruby/job_control.rb

82 lines
2.4 KiB
Ruby

require './colours'
require './job'
class Shell
class JobControl
include Colours
attr_reader :logger
def initialize(logger = Logger.instance)
@jobs_by_pid = {}
@logger = logger
end
def trap_sigchld
# handler for SIGCHLD when a child's state changes
Signal.trap('CHLD') do |_signo|
pid = Process.waitpid(-1, Process::WNOHANG)
if pid.nil?
# no-op
elsif (job = @jobs_by_pid[pid])
puts "\n#{YELLOW}#{job.id}#{CLEAR}: " \
"#{WHITE}(pid #{pid})#{CLEAR} " \
"#{GREEN}#{job.cmd}#{CLEAR} " \
"#{job.args.inspect}"
else
warn "\n#{YELLOW}[WARN]#{CLEAR} No job found for child with PID #{pid}"
end
end
end
def exec_command(cmd, args, background: false)
unless (path = resolve_executable(cmd))
warn "#{RED}[ERROR]#{CLEAR} command not found: #{cmd}"
return -2
end
pid = fork { exec([path, cmd], *args) }
if background
job = Job.new(next_job_id, pid, cmd, args)
@jobs_by_pid[pid] = job
puts "Background job #{job.id} (pid #{pid})"
Process.waitpid(pid, Process::WNOHANG)
0
else
begin
Process.waitpid(pid)
$CHILD_STATUS.exitstatus
rescue Errno::ECHILD => e
# FIXME: why does this happen? doesn't seem to be a real problem
logger.verbose "#{YELLOW}#{e.message}#{CLEAR} but child was just forked 🧐"
0
end
end
rescue StandardError => e
warn "#{RED}[ERROR]#{CLEAR} #{e.message} #{e.inspect}"
-5
end
# Return absolute and relative paths directly, or searches PATH for a
# matching executable with the given filename and returns its path.
# Returns nil when no such command was found.
def resolve_executable(path_or_filename)
# process absolute and relative paths directly
return path_or_filename if path_or_filename['/'] && \
File.executable?(path_or_filename)
filename = path_or_filename
ENV['PATH'].split(':').each do |dir|
path = File.join(dir, filename)
next unless File.exist?(path)
return path if File.executable?(path)
logger.warn "Found #{path} but it's not executable"
end
nil
end
def next_job_id
(@jobs_by_pid.values.map(&:id).max || 0) + 1
end
end
end