lib/runit/config.rb (117 lines of code) (raw):
# frozen_string_literal: true
require 'erb'
require 'fileutils'
module Runit
class Config
attr_reader :gdk_root
# @deprecated we should move this to `GDK::Service` when cleaning up Procfile based services
TERM_SIGNAL = {
'webpack' => 'KILL'
}.freeze
# User read-write, group and global read-only
PERMISSION_READONLY = 0o644
# User read write and execute, group and global read and execute
PERMISSION_EXECUTION = 0o755
TEMPLATE_PATH = 'support/templates'
# @param [Pathname] gdk_root
def initialize(gdk_root)
@gdk_root = gdk_root
end
def log_dir
gdk_root.join('log')
end
def services_dir
gdk_root.join('services')
end
def sv_dir(service)
gdk_root.join('sv', service.name)
end
def render(services:)
FileUtils.mkdir_p(services_dir)
FileUtils.mkdir_p(log_dir)
max_service_length = services.map { |svc| svc.name.size }.max
services.each_with_index do |service, i|
create_runit_service(service)
create_runit_down(service)
create_runit_control_t(service)
create_runit_log_service(service)
create_runit_log_config(service, max_service_length, i)
enable_runit_service(service)
end
FileUtils.rm(stale_service_links(services))
end
def stale_service_links(services)
service_names = services.map(&:name)
dir_matcher = %w[. ..]
stale_entries = Dir.entries(services_dir).reject do |svc|
service_names.include?(svc) || dir_matcher.include?(svc)
end
stale_entries.filter_map do |entry|
path = services_dir.join(entry)
next unless File.symlink?(path)
path
end
end
# Create runit `run` executable
def create_runit_service(service)
run = render_template('runit/run.sh.erb', service_instance: service)
run_path = sv_dir(service).join('run')
write_executable_file(run_path, run)
end
# Create runit `down` file so that `runsvdir` won't boot this service
# until you request it with `gdk start`
#
# @param [GDK::Service::Base] service
def create_runit_down(service)
write_readonly_file(sv_dir(service).join('down'), '')
end
# Create runit `control/t` executable
#
# @param [GDK::Service::Base] service
def create_runit_control_t(service)
term_signal = TERM_SIGNAL.fetch(service.name, 'TERM')
pid_path = sv_dir(service).join('supervise/pid')
control_t = render_template('runit/control/t.rb.erb',
pid_path: pid_path,
term_signal: term_signal)
control_t_path = sv_dir(service).join('control/t')
write_executable_file(control_t_path, control_t)
end
# Create runit `log/run` executable
#
# @param [GDK::Service::Base] service
def create_runit_log_service(service)
service_log_dir = log_dir.join(service.name)
FileUtils.mkdir_p(service_log_dir)
log_run = render_template('runit/log/run.sh.erb', service_log_dir: service_log_dir)
log_run_path = sv_dir(service).join('log/run')
write_executable_file(log_run_path, log_run)
end
# Create runit `log/:service:/config` file
#
# @param [GDK::Service::Base] service
# @param [Integer] max_service_length
# @param [Integer] index
def create_runit_log_config(service, max_service_length, index)
log_prefix = GDK::Output.ansi(GDK::Output.color(index))
log_label = format("%-#{max_service_length}s : ", service.name)
reset_color = GDK::Output.reset_color
log_config = render_template('runit/log/config.erb',
log_prefix: log_prefix,
log_label: log_label,
reset_color: reset_color,
service_instance: service)
log_config_path = log_dir.join(service.name, 'config')
write_readonly_file(log_config_path, log_config)
end
def enable_runit_service(service)
# If the user removes this symlink, runit will stop managing this service.
FileUtils.ln_sf(sv_dir(service), services_dir.join(service.name))
rescue Errno::EEXIST
# Ignore this error because it's possible there is a race condition
# where multiple processes attempt to create this symlink.
end
# Return UNIX termination signal for given service
#
# @param [GDK::Service::Base] service
# @return [String] UNIX termination signal
def term_signal(service)
TERM_SIGNAL.fetch(service.name, 'TERM')
end
# Write content to a given file with execution permission
#
# @param [String] path of the file
# @param [String] content that will be written to the file
def write_executable_file(path, content)
write_file(path, content)
File.chmod(PERMISSION_EXECUTION, path)
end
def write_readonly_file(path, content)
write_file(path, content)
File.chmod(PERMISSION_READONLY, path)
end
# Write content to a given file with specified permissions
#
# @param [String] path of the file
# @param [String] content that will be written to the file
def write_file(path, content)
FileUtils.mkdir_p(File.dirname(path))
return if file_contains_content?(path, content)
File.write(path, content)
rescue Errno::ETXTBSY
nil
end
def file_contains_content?(path, content)
return false unless File.exist?(path)
File.read(path) == content
end
# Render a template to string with optional injected local variables
#
# @param [String] template_path partial path starting from the template root folder
# @param [Hash] locals any local variable that needs to be exposed in the template
# @return [String] rendered content
def render_template(template_path, **locals)
template_fullpath = gdk_root.join(TEMPLATE_PATH).join(template_path)
GDK::Templates::ErbRenderer.new(template_fullpath, **locals).render_to_string
end
end
end