require "yaml" require "colorize" require "./libc.cr" def split_command(string) args = string.split /\ (?=([^\"]*\"[^\"]*\")*[^\"]*$)/ command = args[0] args.delete_at 0 return command, args end class Service getter environment : Environment getter providers = ProvidersList.new class Exception < ::Exception end class ProvidersList < Hash(String, String) def []?(name) super(name).try { |x| Service.get_by_id x} end end struct AsYAML YAML.mapping({ name: String, environment: String, consumes: { type: Array(YAMLConsumes), default: [] of YAMLConsumes } }) end struct YAMLConsumes YAML.mapping({ token: String, from: String }) end def initialize(name, environment_name : String?, @consumes = [] of YAMLConsumes) @reference = ServiceDefinition.get name @environment = if environment_name.nil? || environment_name == "" Environment.root else Environment.get environment_name end @consumes.each do |consume| @providers[consume.token] = consume.from end end def self.from_yaml(yaml) yaml = AsYAML.from_yaml yaml self.new yaml.name, yaml.environment, yaml.consumes end def to_yaml { name: name, environment: @environment.name }.to_yaml end def id if @environment.name == "root" name else "#{@environment.name}/#{name}" end end # FIXME: At this point, macros would be both more readable and shorter. def name @reference.name end def type @reference.type end def command @reference.command end def stop_command @reference.stop_command end def provides @reference.provides end def export_environment_variables ENV["SERVICE_ENVIRONMENT"] = @environment.name ENV["SERVICE_ENVIRONMENT_TYPE"] = @environment.type.to_s # FIXME: Parsing should probably be done… when parsing the file. # FIXME: Parsing is probably a bit primitive. Maybe this isn’t the right way of defining this. @reference.environment_variables.each do |string| # FIXME: Should probably deserve a warning. variable = string.match(/^[^=]*=/).not_nil![0] value = string[variable.size..string.size] variable = variable[0..variable.size-2] ENV[variable] = value end end private def evaluate(string) string.gsub /%{[a-zA-Z]+}/ do |match| match = match[2..match.size-2] if match.downcase == "environment" @environment.name else "" end end end def start(pid_dir : String, log_dir : String) export_environment_variables (@environment.checks + @reference.checks).each do |check| run_check = false check.file.try do |file| run_check = true if ! File.exists? evaluate file end check.directory.try do |directory| run_check = true if ! Dir.exists? evaluate directory end unless run_check next end puts " - #{check.name}" child = Process.fork do @reference.user.try do |user| unless System.become_user user STDERR << "service: child could not setuid() to user '#{user}'.\n" exit 1 end end Process.exec "sh", ["-c", evaluate check.command], output: Process::Redirect::Inherit, error: Process::Redirect::Inherit end.wait if child.exit_status != 0 raise Service::Exception.new "Child process exited with status “#{child.exit_status}”." break end end # FIXME: Should evaluate be used in split_command? What namespace should split_command use? command, args = split_command command args.map! do |arg| evaluate arg end process = Process.fork do base_log_name = "#{log_dir}/#{name}.#{@environment.name}" stdout_file = File.open "#{base_log_name}.out", "w" stderr_file = File.open "#{base_log_name}.err", "w" LibC.dup2 stdout_file.fd, 1 LibC.dup2 stderr_file.fd, 2 Process.exec command, args, chdir: @reference.directory end self.save_pid pid_dir, process.pid end # TODO: # - Custom shutdown commands. # - Should we wait for the process to die? # - Shouldn’t we remove the pid file? def stop(pid_dir : String) _pid = pid pid_dir if _pid command = stop_command if command command, args = split_command command Process.run(command, args) else Process.kill Signal::TERM, _pid end else # Already stopped or dead, nothing to be done here. end end def get_pid_file(pid_dir) "#{pid_dir}/#{name}.pid" end def pid(pid_dir) File.read(get_pid_file pid_dir).to_i rescue e # pid file missing, corrupted or otherwise not readable nil end def save_pid(pid_dir, new_pid) File.write get_pid_file(pid_dir), new_pid end enum Status Running Dead Stopped end def status(pid_dir) _pid = pid pid_dir if _pid if Process.exists? _pid Status::Running else Status::Dead end else Status::Stopped end end def running?(pid_dir) status(pid_dir) == Service::Status::Running end def to_s "#{name} (in #{@environment.name})" end def summary "Name: #{name}\n" + "Environment: #{environment.name} (#{environment.type.to_s.downcase})\n" + ( if provides.size > 0 "Provides:\n" + (provides.map { |x| " - " + x.token + "\n" }).join else "" end ) + ( if @reference.consumes.size > 0 "Consumes:\n" + (@reference.consumes.map { |x| provider = @providers[x.token]? if provider " - #{x.token} (from #{provider.id})\n" else " - #{x.token} " + "(NOT CURRENTLY PROVIDED)".colorize(:red).to_s + "\n" end }).join else "" end ) end class_getter all = [] of Service def self.load(path) Dir.each_child path do |child| unless child.match /\.yaml$/ next end @@all << Service.from_yaml File.read "#{path}/#{child}" end end def write(path) File.write "#{path}/#{name}.#{@environment.name}.yaml", to_yaml end def remove(path) File.delete "#{path}/#{name}.#{@environment.name}.yaml" end def self.get_by_id(id) matches = id.match /[^\/]*/ unless matches # Should not happen, above regex would always match. raise Exception.new "FIXME" end environment_name = matches[0] service_name = if environment_name == id environment_name = "root" id else id.sub 0..matches[0].size, "" end @@all.find do |service| service.name == service_name end end def is_id?(id) id == self.id || (@environment.name == "root" && id == "root/#{name}") end alias ServiceTree = Array(ServiceTree) | Service # Returns a dependency tree. # You’ll probably want to flatten and reverse it afterwards. def dependency_tree tree = [self] of ServiceTree @consumes.each do |token| service = Service.get_by_id token.from unless service # FIXME: Does it make the dep tree invalid? # FIXME: Only optional deps should be fine. next end tree << service.dependency_tree end tree end def reverse_dependency_tree tree = [self] of ServiceTree @@all.each do |service| service.providers.any? do |token, id| if self.is_id?(id) && ! tree.any? service tree << service end end end tree end end