require "yaml" require "colorize" require "file_utils" require "./service_definition.cr" require "./environment.cr" 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 property domain : String? getter ports = Hash(String, Int32).new # The place we’ll store configuration and data. @root : String? class Exception < ::Exception end class ProvidersList < Hash(String, String) def []?(name) super(name).try { |x| Service.get_by_id x} end end struct Consumer getter token : String getter from : String def initialize(@token, @from) end end def initialize(name, environment_name : String?) @reference = ServiceDefinition.get name @environment = if environment_name.nil? || environment_name == "" Environment.root else Environment.get environment_name end end def initialize(specs : SpecParser) assignments = specs.assignments @reference = ServiceDefinition.get assignments["name"].as_s @domain = assignments["domain"]?.try &.as_s env = assignments["environment"]?.try &.as_s @environment = if env.nil? || env == "" Environment.root else Environment.get env end @root = assignments["root"]?.try &.as_s assignments["ports"]?.try &.as_a_or_s.each do |port_string| match = port_string.match /(.*)=(.*)/ if match.nil? STDERR.puts "warning: '#{id}' has invalid port strings" next end _, name, number = match number = number.to_i @ports[name] = number end specs.sections.select(&.name.==("consumes")).each do |section| env, provider = Service.parse_id section.content["from"].as_s @providers[section.options[0]] = "#{env}/#{provider}" end end def to_spec file = [ "name: #{@reference.name}", "environment: #{@environment.name}" ] if @ports.size > 0 ports_list = @ports.map do |name, number| "#{name}=#{number}" end.join ", " file << "ports: #{ports_list}" end if @root file << "root: #{@root}" end if @domain file << "domain: #{@domain}" end @providers.each do |token, provider| file << "%consumes #{token}" file << " from: #{provider}" end file.join("\n") + "\n" end def full_id "#{@environment.name}/#{name}" end def id if @environment.name == "root" name else full_id 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 consumes @reference.consumes end def requires_domain @reference.requires_domain end def port_definitions @reference.port_definitions end def root @root || "#{@environment.root}/#{name}" end def provides?(token) provides.any? do |provider| provider.token == token end end private def build_environment env = {} of String => String env["SERVICE_ROOT"] = root env["SERVICE_ID"] = full_id env["ENVIRONMENT"] = @environment.name env["ENVIRONMENT_TYPE"] = @environment.type.to_s @providers.each do |token, provider| service_provider = Service.get_by_id provider # FIXME: Warning? next if service_provider.nil? env["#{token.upcase}_PROVIDER"] = provider env["#{token.upcase}_ENVIRONMENT"] = service_provider.environment.name env["#{token.upcase}_ROOT"] = service_provider.root end env["SERVICE_TOKENS"] = @providers.to_a.map(&.[0]).join ':' # 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 env end # FIXME: Is working on ${} really a good idea? private def evaluate(string) string.gsub /\${[a-zA-Z_]+}/ do |match| match = match[2..match.size-2] if match.downcase == "environment" @environment.name elsif match.downcase == "service_root" root else "" end end end def pre_start_hooks @environment.pre_start_hooks + @reference.pre_start_hooks end def start(pid_dir : String, log_dir : String) pre_start_hooks.each do |hook| run_hook = false hook.unless_file.try do |file| file = evaluate file run_hook = true if ! File.exists? file end hook.unless_directory.try do |directory| directory = evaluate directory run_hook = true if ! Dir.exists? directory end unless run_hook next end puts " - #{hook.name}" child = Process.fork do Process.exec "sh", ["-c", hook.command], output: Process::Redirect::Inherit, error: Process::Redirect::Inherit, env: build_environment 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 @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 command, args, chdir: (@reference.directory.try { |x| evaluate x } || root), env: build_environment 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}.#{environment.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) FileUtils.mkdir_p pid_dir 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) return unless Dir.exists? path Dir.each_child path do |child| unless child.match /\.spec$/ next end begin specs = SpecParser.parse(File.read "#{path}/#{child}").not_nil! rescue next end @@all << Service.new specs end end def write(path) FileUtils.mkdir_p "#{path}" File.write "#{path}/#{name}.#{@environment.name}.spec", to_spec end def remove(path) File.delete "#{path}/#{name}.#{@environment.name}.spec" 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, service_name = Service.parse_id id @@all.find do |service| service.name == service_name && service.environment.name == environment_name end end def is_id?(id) id == self.id || (@environment.name == "root" && id == "root/#{name}") end def self.parse_id(id) : Tuple(String, String) s = id.split '/' environment = s[0] service = s[1]? if service.nil? service = environment environment = "root" end {environment, service} end def self.get_used_ports(other_reservations = Array(Int32).new) Service.all.map(&.ports.to_a) .flatten .map { |k, v| v } .+(other_reservations) end def self.get_free_port(other_reservations = Array(Int32).new) port = 49152 used_ports = Service.get_used_ports other_reservations while used_ports.any? &.==(port) port = port + 1 end port end def self.is_port_used(port, other_reservations = Array(Int32).new) used_ports = Service.get_used_ports other_reservations used_ports.any? &.==(port) 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 @providers.each do |token, provider_id| service = Service.get_by_id provider_id 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_dependencies rdeps = [self] i = 0 while i < rdeps.size item = rdeps[i] @@all.each do |service| service.providers.any? do |token, id| if item.is_id?(id) && ! rdeps.any? service rdeps << service end end end i += 1 end rdeps end def get_default_provider(token) : String? @environment.get_provider(token) || Environment.root.get_provider(token) end def consumes?(token, origin) providers.select do |_token, provider| token == _token && origin.is_id?(provider) end.size > 0 end def get_consumers(token) Service.all.select(&.consumes?(token, self)) end end