Project organisation for WIP separate binaries.
- The idea would be to allow running specific commands from SUID binaries when unpriviledged users should be able to run them. Such commands include getting services status, which need root priviledges to be implemented but shouldn’t be required by users. - Because the repository now builds several binaries, src/main.cr is now src/service.cr and a WIP src/status.cr has been added. - The `status` binary will likely be installed in libexec in the future, with the SUID bit set.
This commit is contained in:
parent
8a6829d803
commit
5eb0849abd
@ -9,6 +9,8 @@ description: |
|
||||
|
||||
targets:
|
||||
service:
|
||||
main: src/main.cr
|
||||
main: src/service.cr
|
||||
status:
|
||||
main: src/status.cr
|
||||
|
||||
license: MIT
|
||||
|
8
src/config.cr
Normal file
8
src/config.cr
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
# Those are set to local to make testing easier. Will change at some point.
|
||||
PID_DIRECTORY = "./pid"
|
||||
RC_DIRECTORY = "./rc"
|
||||
LOG_DIRECTORY = "./log"
|
||||
SERVICES_DIRECTORY = "./services"
|
||||
ENVIRONMENTS_DIRECTORY = "./environments"
|
||||
|
141
src/main.cr
141
src/main.cr
@ -1,141 +0,0 @@
|
||||
require "option_parser"
|
||||
require "yaml"
|
||||
require "colorize"
|
||||
|
||||
# Those are set to local to make testing easier.
|
||||
PID_DIRECTORY = "./pid"
|
||||
RC_DIRECTORY = "./rc"
|
||||
LOG_DIRECTORY = "./log"
|
||||
SERVICES_DIRECTORY = "./services"
|
||||
ENVIRONMENTS_DIRECTORY = "./environments"
|
||||
|
||||
# TODO:
|
||||
# - Be more declarative about the definition of commands.
|
||||
|
||||
require "./service_definition.cr"
|
||||
require "./environment.cr"
|
||||
require "./service.cr"
|
||||
|
||||
args = [] of String
|
||||
|
||||
parser = OptionParser.parse! do |parser|
|
||||
parser.banner = "usage: service <command> [options]\n" +
|
||||
"\n" +
|
||||
"commands:\n" +
|
||||
" start Starts a stopped or dead service.\n" +
|
||||
" stop Stops a running service.\n" +
|
||||
" status Shows the current state of a service.\n" +
|
||||
" show Describe a service in detail.\n" +
|
||||
" add Add a service to an environment.\n" +
|
||||
" del Remove a service from an environment.\n" +
|
||||
" list Lists registered services.\n" +
|
||||
" list-environments Lists registered 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_DIRECTORY
|
||||
Environment.load ENVIRONMENTS_DIRECTORY
|
||||
Service.load RC_DIRECTORY
|
||||
|
||||
begin
|
||||
if args[0] == "help"
|
||||
puts parser
|
||||
elsif args[0] == "add"
|
||||
Service.new(args[1], args[2]?).write RC_DIRECTORY
|
||||
elsif args[0] == "del"
|
||||
Service.new(args[1], args[2]?).remove RC_DIRECTORY
|
||||
elsif args[0] == "start"
|
||||
services = args[1..args.size].map do |arg|
|
||||
service = Service.get_by_id(arg)
|
||||
|
||||
unless service
|
||||
raise Service::Exception.new "Service '#{arg}' does not exist."
|
||||
end
|
||||
|
||||
service
|
||||
end
|
||||
|
||||
services.each do |service|
|
||||
service.dependency_tree.flatten.reverse.each do |service|
|
||||
next if service.running? PID_DIRECTORY
|
||||
|
||||
puts "starting #{service.to_s}"
|
||||
service.start PID_DIRECTORY, LOG_DIRECTORY
|
||||
end
|
||||
end
|
||||
elsif args[0] == "stop"
|
||||
services = args[1..args.size].map do |arg|
|
||||
service = Service.get_by_id(arg)
|
||||
|
||||
unless service
|
||||
raise Service::Exception.new "Service '#{arg}' does not exist."
|
||||
end
|
||||
|
||||
service
|
||||
end
|
||||
|
||||
services.each do |service|
|
||||
# FIXME: Build revdep tree and stop services started as dependencies?
|
||||
next if ! service.running? PID_DIRECTORY
|
||||
# FIXME: Should we remove duplicate services from the
|
||||
# tree once flattened?
|
||||
service.reverse_dependency_tree.flatten.reverse.each do |service|
|
||||
next if ! service.running? PID_DIRECTORY
|
||||
|
||||
puts "stopping #{service.to_s}"
|
||||
service.stop PID_DIRECTORY
|
||||
end
|
||||
end
|
||||
elsif args[0] == "status"
|
||||
puts Service.new(args[1], args[2]?).status PID_DIRECTORY
|
||||
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 : Service::Exception
|
||||
STDERR << e.message << "\n"
|
||||
exit 2
|
||||
end
|
||||
|
435
src/service.cr
435
src/service.cr
@ -1,347 +1,134 @@
|
||||
require "option_parser"
|
||||
require "yaml"
|
||||
require "colorize"
|
||||
|
||||
require "./libc.cr"
|
||||
require "./config.cr"
|
||||
|
||||
def split_command(string)
|
||||
args = string.split /\ (?=([^\"]*\"[^\"]*\")*[^\"]*$)/
|
||||
# TODO:
|
||||
# - Be more declarative about the definition of commands.
|
||||
|
||||
command = args[0]
|
||||
args.delete_at 0
|
||||
require "./service/*"
|
||||
|
||||
return command, args
|
||||
args = [] of String
|
||||
|
||||
parser = OptionParser.parse! do |parser|
|
||||
parser.banner = "usage: service <command> [options]\n" +
|
||||
"\n" +
|
||||
"commands:\n" +
|
||||
" start Starts a stopped or dead service.\n" +
|
||||
" stop Stops a running service.\n" +
|
||||
" status Shows the current state of a service.\n" +
|
||||
" show Describe a service in detail.\n" +
|
||||
" add Add a service to an environment.\n" +
|
||||
" del Remove a service from an environment.\n" +
|
||||
" list Lists registered services.\n" +
|
||||
" list-environments Lists registered 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
|
||||
|
||||
class Service
|
||||
getter environment : Environment
|
||||
getter providers = ProvidersList.new
|
||||
command = args[0]?
|
||||
if command.nil?
|
||||
STDERR << parser << "\n"
|
||||
exit 1
|
||||
end
|
||||
|
||||
class Exception < ::Exception
|
||||
end
|
||||
ServiceDefinition.load SERVICES_DIRECTORY
|
||||
Environment.load ENVIRONMENTS_DIRECTORY
|
||||
Service.load RC_DIRECTORY
|
||||
|
||||
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
|
||||
begin
|
||||
if args[0] == "help"
|
||||
puts parser
|
||||
elsif args[0] == "add"
|
||||
Service.new(args[1], args[2]?).write RC_DIRECTORY
|
||||
elsif args[0] == "del"
|
||||
Service.new(args[1], args[2]?).remove RC_DIRECTORY
|
||||
elsif args[0] == "start"
|
||||
services = args[1..args.size].map do |arg|
|
||||
service = Service.get_by_id(arg)
|
||||
|
||||
unless service
|
||||
# FIXME: Does it make the dep tree invalid?
|
||||
# FIXME: Only optional deps should be fine.
|
||||
next
|
||||
raise Service::Exception.new "Service '#{arg}' does not exist."
|
||||
end
|
||||
|
||||
tree << service.dependency_tree
|
||||
service
|
||||
end
|
||||
|
||||
tree
|
||||
end
|
||||
services.each do |service|
|
||||
service.dependency_tree.flatten.reverse.each do |service|
|
||||
next if service.running? PID_DIRECTORY
|
||||
|
||||
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
|
||||
puts "starting #{service.to_s}"
|
||||
service.start PID_DIRECTORY, LOG_DIRECTORY
|
||||
end
|
||||
end
|
||||
elsif args[0] == "stop"
|
||||
services = args[1..args.size].map do |arg|
|
||||
service = Service.get_by_id(arg)
|
||||
|
||||
tree
|
||||
unless service
|
||||
raise Service::Exception.new "Service '#{arg}' does not exist."
|
||||
end
|
||||
|
||||
service
|
||||
end
|
||||
|
||||
services.each do |service|
|
||||
# FIXME: Build revdep tree and stop services started as dependencies?
|
||||
next if ! service.running? PID_DIRECTORY
|
||||
# FIXME: Should we remove duplicate services from the
|
||||
# tree once flattened?
|
||||
service.reverse_dependency_tree.flatten.reverse.each do |service|
|
||||
next if ! service.running? PID_DIRECTORY
|
||||
|
||||
puts "stopping #{service.to_s}"
|
||||
service.stop PID_DIRECTORY
|
||||
end
|
||||
end
|
||||
elsif args[0] == "status"
|
||||
puts Service.new(args[1], args[2]?).status PID_DIRECTORY
|
||||
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 : Service::Exception
|
||||
STDERR << e.message << "\n"
|
||||
exit 2
|
||||
end
|
||||
|
||||
|
||||
|
349
src/service/service.cr
Normal file
349
src/service/service.cr
Normal file
@ -0,0 +1,349 @@
|
||||
require "yaml"
|
||||
require "colorize"
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
16
src/status.cr
Normal file
16
src/status.cr
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
require "./service/service.cr"
|
||||
|
||||
require "./config.cr"
|
||||
|
||||
ServiceDefinition.load SERVICES_DIRECTORY
|
||||
Environment.load ENVIRONMENTS_DIRECTORY
|
||||
Service.load RC_DIRECTORY
|
||||
|
||||
Service.get_by_id(ARGV[0]).try do |service|
|
||||
puts service.status PID_DIRECTORY
|
||||
exit 0
|
||||
end
|
||||
|
||||
exit 1
|
||||
|
Loading…
Reference in New Issue
Block a user