commit 2dd3fe7dd6da76b465111f111124d87f661bab66 Author: Luka Vandervelden Date: Sat Jun 8 02:21:31 2019 +0200 Initial commit. diff --git a/src/main.cr b/src/main.cr new file mode 100644 index 0000000..27b407b --- /dev/null +++ b/src/main.cr @@ -0,0 +1,413 @@ +require "option_parser" +require "yaml" + +# TODO: +# - Be more declarative about the definition of commands. + +def split_command(string) + args = string.split /\ (?=([^\"]*\"[^\"]*\")*[^\"]*$)/ + + command = args[0] + args.delete_at 0 + + return command, args +end + +class ServiceDefinition + struct Consumes + YAML.mapping({ + token: String, + optional: { + type: Bool, + default: false + } + }) + end + struct Provides + YAML.mapping({ + token: String + }) + end + YAML.mapping({ + name: String, + command: String, + stop_command: { + type: String?, + key: "stop-command" + }, + directory: String?, # Place to chdir to before running @command. + environment: { + type: Environment, + default: Environment.root + }, + provides: { + type: Array(Provides), + default: [] of Provides + }, + consumes: { + type: Array(Consumes), + default: [] of Consumes + } + }) + + def self.new(name) + Service.from_yaml File.read "#{name}.yaml" + end + + class_getter all = [] of ServiceDefinition + + def self.load(path) + Dir.each_child path do |child| + unless child.match /\.yaml$/ + next + end + + @@all << ServiceDefinition.from_yaml File.read "#{path}/#{child}" + end + end + + def self.get(name) : ServiceDefinition + _def = @@all.find &.name.==(name) + + if _def.nil? + raise Exception.new "Service '#{name}' does not exist." + end + + _def + end + + def to_s + name + end +end + +class Environment + enum Type + Native + RootFileSystem + end + + YAML.mapping({ + name: String, + type: { + type: Type, + default: Type::Native + }, + domain_name: { + type: String?, + key: "domain-name" + } + }) + + def initialize() + @name = "root" + @type = Type::Native + end + + class_getter root = Environment.new + class_getter all = [@@root] of Environment + + def self.load(path) + Dir.each_child path do |child| + unless child.match /\.yaml$/ + next + end + + @@all << Environment.from_yaml File.read "#{path}/#{child}" + end + end + + def self.get(name) + _def = @@all.find &.name.==(name) + + if _def.nil? + raise Exception.new "Environment '#{name}' does not exist." + end + + _def + end + + def to_s + "#{name} (#{type.to_s.downcase})" + end +end + +class Service + getter environment : Environment + getter providers = ProvidersList.new + + 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.map 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 + + # 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 start(log_dir : String) + command, args = split_command command + + 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.pid = process.pid + end + + # TODO: + # - Custom shutdown commands. + # - Should we wait for the process to die? + def stop + _pid = pid + + 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 + "#{name}.pid" + end + + def pid + File.read(get_pid_file).to_i + rescue e # pid file missing, corrupted or otherwise not readable + nil + end + + def pid=(new_pid) + File.write get_pid_file, new_pid + end + + enum Status + Running + Dead + Stopped + end + + def status + _pid = pid + + if _pid + if Process.exists? _pid + Status::Running + else + Status::Dead + end + else + Status::Stopped + end + end + + def to_s + "#{name} (in #{@environment.name})" + end + + def summary + "Name: #{name}\n" + + "Environment: #{environment.name} (#{environment.type.to_s.downcase})\n" + + "Provides:\n" + + (provides.map { |x| " - " + x.token + "\n" }).join + + "Consumes:\n" + + (@reference.consumes.map { |x| + provider = @providers[x.token]? + if provider + " - #{x.token} (from #{provider.to_s})\n" + else + " - #{x.token} (NOT CURRENTLY PROVIDED)\n" + end + }).join + 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 + raise Exception.new "FIXME" + end + environment_name = matches[0] + service_name = id.sub 0..matches[0].size, "" + + @@all.find do |service| + service.name == service_name + end + end +end + +args = [] of String + +parser = OptionParser.parse! do |parser| + parser.banner = "usage: yunoservice [options]\n" + + "\n" + + "commands:\n" + + " start\n" + + " stop\n" + + " status\n" + + " show\n" + + " add\n" + + " del\n" + + " list\n" + + " list-environments\n" + + "\n" + + "options:\n" + + parser.on "-h", "--help", "Prints this help message." do + puts parser + exit 0 + end + parser.unknown_args do |x| + args = x + end +end + +command = args[0]? +if command.nil? + STDERR << parser << "\n" + exit 1 +end + +ServiceDefinition.load "services" +Environment.load "environments" +Service.load "rc" + +begin + if args[0] == "help" + puts parser + elsif args[0] == "add" + Service.new(args[1], args[2]?).write "rc" + elsif args[0] == "del" + Service.new(args[1], args[2]?).remove "rc" + elsif args[0] == "start" + Service.new(args[1], args[2]?).start "log" + elsif args[0] == "stop" + Service.new(args[1], args[2]?).stop + elsif args[0] == "status" + puts Service.new(args[1], args[2]?).status + elsif args[0] == "show" + service = Service.all.find do |service| + unless service.name == args[1] + next false + end + + env = args[2]? || "root" + if service.environment.name != env + next false + end + + true + end + if service + puts service.summary + else + STDERR << "No such service is registered.\n" + exit 2 + end + elsif args[0] == "list" + Service.all.map do |service| + puts service.to_s + end + elsif args[0] == "list-environments" + Environment.all.map do |env| + puts env.to_s + end + else + STDERR << parser << "\n" + exit 1 + end +rescue e + STDERR << e.message << "\n" + exit 2 +end +