service/src/main.cr

415 lines
7.3 KiB
Crystal
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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(pid_dir : String, 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.save_pid pid_dir, process.pid
end
# TODO:
# - Custom shutdown commands.
# - Should we wait for the process to die?
# - Shouldnt 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 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 <command> [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 "pid", "log"
elsif args[0] == "stop"
Service.new(args[1], args[2]?).stop "pid"
elsif args[0] == "status"
puts Service.new(args[1], args[2]?).status "pid"
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