mirror of
https://github.com/samsonjs/csc360-a1-shell.git
synced 2026-03-25 08:45:52 +00:00
Use fork and exec and implement bg builtin
This commit is contained in:
parent
0d0bb73114
commit
ba8bf8088f
5 changed files with 130 additions and 64 deletions
|
|
@ -2,7 +2,8 @@ class Shell
|
|||
class Builtins
|
||||
attr_reader :logger
|
||||
|
||||
def initialize(logger = ShellLogger.instance)
|
||||
def initialize(shell, logger = Logger.instance)
|
||||
@shell = shell
|
||||
@logger = logger
|
||||
end
|
||||
|
||||
|
|
@ -18,8 +19,14 @@ class Shell
|
|||
### Built-ins ###
|
||||
#################
|
||||
|
||||
def builtin_bg(args)
|
||||
cmd = args.shift
|
||||
@shell.exec_command(cmd, args, background: true)
|
||||
end
|
||||
|
||||
def builtin_cd(args)
|
||||
Dir.chdir args.first
|
||||
0
|
||||
end
|
||||
|
||||
def builtin_export(args)
|
||||
|
|
@ -30,10 +37,12 @@ class Shell
|
|||
else
|
||||
ENV[name] = value_parts.join('=').gsub(/\$\w+/) { |m| ENV[m[1..]] || '' }
|
||||
end
|
||||
0
|
||||
end
|
||||
|
||||
def bulitin_pwd(_args)
|
||||
puts Dir.pwd
|
||||
0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
class Shell
|
||||
Job = Struct.new(:id, :cmd)
|
||||
Job = Struct.new(:id, :pid, :cmd, :args)
|
||||
end
|
||||
|
|
|
|||
41
ruby/logger.rb
Normal file
41
ruby/logger.rb
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
require './colours'
|
||||
|
||||
class Shell
|
||||
# Queues up messages to be printed out when readline is waiting for input, to prevent
|
||||
# mixing shell output with command output.
|
||||
class Logger
|
||||
Log = Struct.new(:level, :message)
|
||||
|
||||
attr_reader :logs
|
||||
|
||||
def self.instance
|
||||
@instance ||= new
|
||||
end
|
||||
|
||||
def initialize
|
||||
clear
|
||||
end
|
||||
|
||||
def log(message)
|
||||
@logs << Log.new(:info, "#{WHITE}[INFO]#{CLEAR} #{message}")
|
||||
end
|
||||
alias info log
|
||||
|
||||
def warn(message)
|
||||
@logs << Log.new(:warning, "#{YELLOW}[WARN]#{CLEAR} #{message}")
|
||||
end
|
||||
|
||||
def error(message)
|
||||
@logs << Log.new(:error, "#{RED}[ERROR]#{CLEAR} #{message}")
|
||||
end
|
||||
|
||||
def verbose(message)
|
||||
@logs << Log.new(:verbose, "[VERBOSE] #{message}")
|
||||
end
|
||||
|
||||
def clear
|
||||
@logs = []
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
101
ruby/main.rb
101
ruby/main.rb
|
|
@ -7,33 +7,35 @@ require 'wordexp'
|
|||
|
||||
require './builtins'
|
||||
require './colours'
|
||||
require './shell_logger'
|
||||
require './logger'
|
||||
require './job'
|
||||
|
||||
class Shell
|
||||
attr_reader :logger, :options
|
||||
|
||||
def initialize(args = ARGV)
|
||||
@builtins = Builtins.new
|
||||
@builtins = Builtins.new(self)
|
||||
@jobs_by_pid = {}
|
||||
@logger = ShellLogger.instance
|
||||
@logger = Logger.instance
|
||||
@options = parse_options(args)
|
||||
logger.verbose "Options: #{options.inspect}"
|
||||
end
|
||||
|
||||
def main
|
||||
# this breaks Open3.capture3 so hold off until we fork + exec
|
||||
# trap_sigchld
|
||||
trap_sigchld
|
||||
if options[:command]
|
||||
logger.verbose "Executing command: #{options[:command]}"
|
||||
print_logs
|
||||
exit process_command(options[:command])
|
||||
elsif $stdin.isatty
|
||||
add_to_history = true
|
||||
status = 0
|
||||
loop do
|
||||
print_logs
|
||||
print "#{RED}#{status}#{CLEAR} " unless status.zero?
|
||||
line = Readline.readline(prompt(Dir.pwd), add_to_history)
|
||||
process_command(line)
|
||||
Readline::HISTORY.pop if line.nil? || line.strip.empty?
|
||||
status = process_command(line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -63,13 +65,17 @@ class Shell
|
|||
|
||||
def trap_sigchld
|
||||
# handler for SIGCHLD when a child's state changes
|
||||
Signal.trap('CHLD') do |signo|
|
||||
logger.info "SIGCHLD #{signo}"
|
||||
Signal.trap('CHLD') do |_signo|
|
||||
pid = Process.waitpid(-1, Process::WNOHANG)
|
||||
if (job = @jobs_by_pid[pid])
|
||||
logger.info "#{YELLOW}#{job.id}#{CLEAR}: #{WHITE}(pid #{pid})#{CLEAR} #{job.cmd}"
|
||||
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
|
||||
logger.warn "No job found for child with PID #{pid}"
|
||||
warn "\n#{YELLOW}[WARN]#{CLEAR} No job found for child with PID #{pid}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -88,10 +94,10 @@ class Shell
|
|||
end
|
||||
|
||||
def process_command(line)
|
||||
logger.verbose "Processing command: #{line.inspect}"
|
||||
exit 0 if line.nil? # EOF, ctrl-d
|
||||
return if line.empty? # no input, no-op
|
||||
return 0 if line.strip.empty? # no input, no-op
|
||||
|
||||
logger.verbose "Processing command: #{line.inspect}"
|
||||
args = Wordexp.expand(line)
|
||||
cmd = args.shift
|
||||
logger.verbose "Parsed command: #{cmd} #{args.inspect}"
|
||||
|
|
@ -100,20 +106,69 @@ class Shell
|
|||
@builtins.exec(cmd, args)
|
||||
else
|
||||
logger.verbose "Shelling out for #{cmd}"
|
||||
status = exec_command(cmd, args)
|
||||
print "#{RED}-#{status}-#{CLEAR} " unless status.zero?
|
||||
exec_command(cmd, args)
|
||||
end
|
||||
rescue Errno => e
|
||||
warn "#{RED}[ERROR]#{CLEAR} #{e.message}"
|
||||
-1
|
||||
end
|
||||
|
||||
def exec_command(cmd, args)
|
||||
# TODO: background execution using fork + exec, streaming output
|
||||
out, err, status = Open3.capture3(cmd + ' ' + args.join(' '))
|
||||
puts out.chomp unless out.empty?
|
||||
warn err.chomp unless err.empty?
|
||||
status.exitstatus
|
||||
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
|
||||
if pid
|
||||
# parent
|
||||
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?
|
||||
warn "#{YELLOW}[WARN]#{CLEAR} #{e.message} but child was just forked 🧐"
|
||||
-3
|
||||
end
|
||||
end
|
||||
else
|
||||
# child
|
||||
exec([path, cmd], *args)
|
||||
# if we make it here then exec failed
|
||||
-4
|
||||
end
|
||||
rescue StandardError => e
|
||||
logger.warn "#{RED}[ERROR]#{CLEAR} #{e.message}"
|
||||
1
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
require './colours'
|
||||
|
||||
# Queues up messages to be printed out when readline is waiting for input, to prevent
|
||||
# mixing shell output with command output.
|
||||
class ShellLogger
|
||||
Log = Struct.new(:level, :message)
|
||||
|
||||
attr_reader :logs
|
||||
|
||||
def self.instance
|
||||
@instance ||= new
|
||||
end
|
||||
|
||||
def initialize
|
||||
clear
|
||||
end
|
||||
|
||||
def log(message)
|
||||
@logs << Log.new(:info, "#{WHITE}[INFO]#{CLEAR} #{message}")
|
||||
end
|
||||
alias info log
|
||||
|
||||
def warn(message)
|
||||
@logs << Log.new(:warning, "#{YELLOW}[WARN]#{CLEAR} #{message}")
|
||||
end
|
||||
|
||||
def error(message)
|
||||
@logs << Log.new(:error, "#{RED}[ERROR]#{CLEAR} #{message}")
|
||||
end
|
||||
|
||||
def verbose(message)
|
||||
@logs << Log.new(:verbose, "[VERBOSE] #{message}")
|
||||
end
|
||||
|
||||
def clear
|
||||
@logs = []
|
||||
nil
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue