
The main thing is that this DSL allows us to provide an interface that can be serialized to the JSON API. Changes: - Homebrew::Service - Adds `#service_name` and `#plist_name` methods - Each is now included in the `#serialize` method as well - Eval block on instantiation - Before we lazy evaluated this but the cost is not significant and it complicated the code a bunch. This only gets called during install, when evaluating caveats and in the `brew service` command. It skips this evaluation if the service block isn't there. - Add `#command?` helper to avoid `#command.blank?` and `#command.present?` - Formula - `#service` now returns a service whenever it's called. This call is hidden behind a call to `#service?` most of the time anyway so this should be fine. - `#plist_name` and `#service_name` now call the methods of the same name on the service class. This should have already been in the service object to begin with and keeping these methods here helps preserve backwards compatibility with people who were overwriting these methods before. - Caveats - Prefer `service#command?` - Add helpers for checking on service commands - This duplicates some of the work in `brew services`. Maybe we should merge that repo in at some point. - Check for installed service at `#plist_name` or `#service_name`. I think this should be used instead of `Keg#plist_installed?` which checked for any plist file. We should think about deprecating `#plist_installed?` in the future. - Stop using `ps aux | grep #{formula.plist_name}` to check for service files because it was inaccurate (it always returns true on my machine) because the grep process is started before the ps process. - Note: The behavior is the same as it was before. This means that caveats only show up for custom service files on install or if they're already installed. Otherwise it won't show up in `brew info`. This is because it has to check first if the service file has been installed. - Utils::Service - Add utils for evaluating if a service is installed and running. This duplicates some of the work already found in `brew services`. We should seriously consider merging `brew services` with the main brew repo in the future since it's already tightly coupled to the code in the main repo. - Formulary.load_formula_from_api - Be more explicit about which types can be deserialized into run params since it is now possible for run params to be nil. - Update and add tests
655 lines
20 KiB
Ruby
655 lines
20 KiB
Ruby
# typed: true
|
|
# frozen_string_literal: true
|
|
|
|
require "extend/on_system"
|
|
|
|
module Homebrew
|
|
# The {Service} class implements the DSL methods used in a formula's
|
|
# `service` block and stores related instance variables. Most of these methods
|
|
# also return the related instance variable when no argument is provided.
|
|
class Service
|
|
extend Forwardable
|
|
include OnSystem::MacOSAndLinux
|
|
|
|
RUN_TYPE_IMMEDIATE = :immediate
|
|
RUN_TYPE_INTERVAL = :interval
|
|
RUN_TYPE_CRON = :cron
|
|
|
|
PROCESS_TYPE_BACKGROUND = :background
|
|
PROCESS_TYPE_STANDARD = :standard
|
|
PROCESS_TYPE_INTERACTIVE = :interactive
|
|
PROCESS_TYPE_ADAPTIVE = :adaptive
|
|
|
|
KEEP_ALIVE_KEYS = [:always, :successful_exit, :crashed, :path].freeze
|
|
|
|
# sig { params(formula: Formula).void }
|
|
def initialize(formula, &block)
|
|
@formula = formula
|
|
@run_type = RUN_TYPE_IMMEDIATE
|
|
@run_at_load = true
|
|
@environment_variables = {}
|
|
instance_eval(&block) if block
|
|
end
|
|
|
|
sig { returns(Formula) }
|
|
def f
|
|
@formula
|
|
end
|
|
|
|
sig { returns(String) }
|
|
def default_plist_name
|
|
"homebrew.mxcl.#{@formula.name}"
|
|
end
|
|
|
|
sig { params(name: T.nilable(String)).returns(String) }
|
|
def plist_name(name = nil)
|
|
case T.unsafe(name)
|
|
when nil
|
|
@plist_name ||= default_plist_name
|
|
when String
|
|
@plist_name = name
|
|
else
|
|
raise TypeError, "Service#plist_name expects a String"
|
|
end
|
|
end
|
|
|
|
sig { returns(String) }
|
|
def default_service_name
|
|
"homebrew.#{@formula.name}"
|
|
end
|
|
|
|
sig { params(name: T.nilable(String)).returns(String) }
|
|
def service_name(name = nil)
|
|
case T.unsafe(name)
|
|
when nil
|
|
@service_name ||= default_service_name
|
|
when String
|
|
@service_name = name
|
|
else
|
|
raise TypeError, "Service#service_name expects a String"
|
|
end
|
|
end
|
|
|
|
sig {
|
|
params(
|
|
command: T.nilable(T.any(T::Array[String], String, Pathname)),
|
|
macos: T.nilable(T.any(T::Array[String], String, Pathname)),
|
|
linux: T.nilable(T.any(T::Array[String], String, Pathname)),
|
|
).returns(T.nilable(Array))
|
|
}
|
|
def run(command = nil, macos: nil, linux: nil)
|
|
# Save parameters for serialization
|
|
if command
|
|
@run_params = command
|
|
elsif macos || linux
|
|
@run_params = { macos: macos, linux: linux }.compact
|
|
end
|
|
|
|
command ||= on_system_conditional(macos: macos, linux: linux)
|
|
case T.unsafe(command)
|
|
when nil
|
|
@run
|
|
when String, Pathname
|
|
@run = [command]
|
|
when Array
|
|
@run = command
|
|
else
|
|
raise TypeError, "Service#run expects an Array"
|
|
end
|
|
end
|
|
|
|
sig { params(path: T.nilable(T.any(String, Pathname))).returns(T.nilable(String)) }
|
|
def working_dir(path = nil)
|
|
case T.unsafe(path)
|
|
when nil
|
|
@working_dir
|
|
when String, Pathname
|
|
@working_dir = path.to_s
|
|
else
|
|
raise TypeError, "Service#working_dir expects a String"
|
|
end
|
|
end
|
|
|
|
sig { params(path: T.nilable(T.any(String, Pathname))).returns(T.nilable(String)) }
|
|
def root_dir(path = nil)
|
|
case T.unsafe(path)
|
|
when nil
|
|
@root_dir
|
|
when String, Pathname
|
|
@root_dir = path.to_s
|
|
else
|
|
raise TypeError, "Service#root_dir expects a String or Pathname"
|
|
end
|
|
end
|
|
|
|
sig { params(path: T.nilable(T.any(String, Pathname))).returns(T.nilable(String)) }
|
|
def input_path(path = nil)
|
|
case T.unsafe(path)
|
|
when nil
|
|
@input_path
|
|
when String, Pathname
|
|
@input_path = path.to_s
|
|
else
|
|
raise TypeError, "Service#input_path expects a String or Pathname"
|
|
end
|
|
end
|
|
|
|
sig { params(path: T.nilable(T.any(String, Pathname))).returns(T.nilable(String)) }
|
|
def log_path(path = nil)
|
|
case T.unsafe(path)
|
|
when nil
|
|
@log_path
|
|
when String, Pathname
|
|
@log_path = path.to_s
|
|
else
|
|
raise TypeError, "Service#log_path expects a String"
|
|
end
|
|
end
|
|
|
|
sig { params(path: T.nilable(T.any(String, Pathname))).returns(T.nilable(String)) }
|
|
def error_log_path(path = nil)
|
|
case T.unsafe(path)
|
|
when nil
|
|
@error_log_path
|
|
when String, Pathname
|
|
@error_log_path = path.to_s
|
|
else
|
|
raise TypeError, "Service#error_log_path expects a String"
|
|
end
|
|
end
|
|
|
|
sig {
|
|
params(value: T.nilable(T.any(T::Boolean, T::Hash[Symbol, T.untyped])))
|
|
.returns(T.nilable(T::Hash[Symbol, T.untyped]))
|
|
}
|
|
def keep_alive(value = nil)
|
|
case T.unsafe(value)
|
|
when nil
|
|
@keep_alive
|
|
when true, false
|
|
@keep_alive = { always: value }
|
|
when Hash
|
|
hash = T.cast(value, Hash)
|
|
unless (hash.keys - KEEP_ALIVE_KEYS).empty?
|
|
raise TypeError, "Service#keep_alive allows only #{KEEP_ALIVE_KEYS}"
|
|
end
|
|
|
|
@keep_alive = value
|
|
else
|
|
raise TypeError, "Service#keep_alive expects a Boolean or Hash"
|
|
end
|
|
end
|
|
|
|
sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) }
|
|
def require_root(value = nil)
|
|
case T.unsafe(value)
|
|
when nil
|
|
@require_root
|
|
when true, false
|
|
@require_root = value
|
|
else
|
|
raise TypeError, "Service#require_root expects a Boolean"
|
|
end
|
|
end
|
|
|
|
# Returns a `Boolean` describing if a service requires root access.
|
|
# @return [Boolean]
|
|
sig { returns(T::Boolean) }
|
|
def requires_root?
|
|
@require_root.present? && @require_root == true
|
|
end
|
|
|
|
sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) }
|
|
def run_at_load(value = nil)
|
|
case T.unsafe(value)
|
|
when nil
|
|
@run_at_load
|
|
when true, false
|
|
@run_at_load = value
|
|
else
|
|
raise TypeError, "Service#run_at_load expects a Boolean"
|
|
end
|
|
end
|
|
|
|
sig { params(value: T.nilable(String)).returns(T.nilable(T::Hash[Symbol, String])) }
|
|
def sockets(value = nil)
|
|
case T.unsafe(value)
|
|
when nil
|
|
@sockets
|
|
when String
|
|
match = T.must(value).match(%r{([a-z]+)://([a-z0-9.]+):([0-9]+)}i)
|
|
raise TypeError, "Service#sockets a formatted socket definition as <type>://<host>:<port>" if match.blank?
|
|
|
|
type, host, port = match.captures
|
|
@sockets = { host: host, port: port, type: type }
|
|
else
|
|
raise TypeError, "Service#sockets expects a String"
|
|
end
|
|
end
|
|
|
|
# Returns a `Boolean` describing if a service is set to be kept alive.
|
|
# @return [Boolean]
|
|
sig { returns(T::Boolean) }
|
|
def keep_alive?
|
|
@keep_alive.present? && @keep_alive[:always] != false
|
|
end
|
|
|
|
sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) }
|
|
def launch_only_once(value = nil)
|
|
case T.unsafe(value)
|
|
when nil
|
|
@launch_only_once
|
|
when true, false
|
|
@launch_only_once = value
|
|
else
|
|
raise TypeError, "Service#launch_only_once expects a Boolean"
|
|
end
|
|
end
|
|
|
|
sig { params(value: T.nilable(Integer)).returns(T.nilable(Integer)) }
|
|
def restart_delay(value = nil)
|
|
case T.unsafe(value)
|
|
when nil
|
|
@restart_delay
|
|
when Integer
|
|
@restart_delay = value
|
|
else
|
|
raise TypeError, "Service#restart_delay expects an Integer"
|
|
end
|
|
end
|
|
|
|
sig { params(value: T.nilable(Symbol)).returns(T.nilable(Symbol)) }
|
|
def process_type(value = nil)
|
|
case T.unsafe(value)
|
|
when nil
|
|
@process_type
|
|
when :background, :standard, :interactive, :adaptive
|
|
@process_type = value
|
|
when Symbol
|
|
raise TypeError, "Service#process_type allows: " \
|
|
"'#{PROCESS_TYPE_BACKGROUND}'/'#{PROCESS_TYPE_STANDARD}'/" \
|
|
"'#{PROCESS_TYPE_INTERACTIVE}'/'#{PROCESS_TYPE_ADAPTIVE}'"
|
|
else
|
|
raise TypeError, "Service#process_type expects a Symbol"
|
|
end
|
|
end
|
|
|
|
sig { params(value: T.nilable(Symbol)).returns(T.nilable(Symbol)) }
|
|
def run_type(value = nil)
|
|
case T.unsafe(value)
|
|
when nil
|
|
@run_type
|
|
when :immediate, :interval, :cron
|
|
@run_type = value
|
|
when Symbol
|
|
raise TypeError, "Service#run_type allows: '#{RUN_TYPE_IMMEDIATE}'/'#{RUN_TYPE_INTERVAL}'/'#{RUN_TYPE_CRON}'"
|
|
else
|
|
raise TypeError, "Service#run_type expects a Symbol"
|
|
end
|
|
end
|
|
|
|
sig { params(value: T.nilable(Integer)).returns(T.nilable(Integer)) }
|
|
def interval(value = nil)
|
|
case T.unsafe(value)
|
|
when nil
|
|
@interval
|
|
when Integer
|
|
@interval = value
|
|
else
|
|
raise TypeError, "Service#interval expects an Integer"
|
|
end
|
|
end
|
|
|
|
sig { params(value: T.nilable(String)).returns(T.nilable(Hash)) }
|
|
def cron(value = nil)
|
|
case T.unsafe(value)
|
|
when nil
|
|
@cron
|
|
when String
|
|
@cron = parse_cron(T.must(value))
|
|
else
|
|
raise TypeError, "Service#cron expects a String"
|
|
end
|
|
end
|
|
|
|
sig { returns(T::Hash[Symbol, T.any(Integer, String)]) }
|
|
def default_cron_values
|
|
{
|
|
Month: "*",
|
|
Day: "*",
|
|
Weekday: "*",
|
|
Hour: "*",
|
|
Minute: "*",
|
|
}
|
|
end
|
|
|
|
sig { params(cron_statement: String).returns(T::Hash[Symbol, T.any(Integer, String)]) }
|
|
def parse_cron(cron_statement)
|
|
parsed = default_cron_values
|
|
|
|
case cron_statement
|
|
when "@hourly"
|
|
parsed[:Minute] = 0
|
|
when "@daily"
|
|
parsed[:Minute] = 0
|
|
parsed[:Hour] = 0
|
|
when "@weekly"
|
|
parsed[:Minute] = 0
|
|
parsed[:Hour] = 0
|
|
parsed[:Weekday] = 0
|
|
when "@monthly"
|
|
parsed[:Minute] = 0
|
|
parsed[:Hour] = 0
|
|
parsed[:Day] = 1
|
|
when "@yearly", "@annually"
|
|
parsed[:Minute] = 0
|
|
parsed[:Hour] = 0
|
|
parsed[:Day] = 1
|
|
parsed[:Month] = 1
|
|
else
|
|
cron_parts = cron_statement.split
|
|
raise TypeError, "Service#parse_cron expects a valid cron syntax" if cron_parts.length != 5
|
|
|
|
[:Minute, :Hour, :Day, :Month, :Weekday].each_with_index do |selector, index|
|
|
parsed[selector] = Integer(cron_parts.fetch(index)) if cron_parts.fetch(index) != "*"
|
|
end
|
|
end
|
|
|
|
parsed
|
|
end
|
|
|
|
sig { params(variables: T::Hash[Symbol, String]).returns(T.nilable(T::Hash[Symbol, String])) }
|
|
def environment_variables(variables = {})
|
|
case T.unsafe(variables)
|
|
when Hash
|
|
@environment_variables = variables.transform_values(&:to_s)
|
|
else
|
|
raise TypeError, "Service#environment_variables expects a hash"
|
|
end
|
|
end
|
|
|
|
sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) }
|
|
def macos_legacy_timers(value = nil)
|
|
case T.unsafe(value)
|
|
when nil
|
|
@macos_legacy_timers
|
|
when true, false
|
|
@macos_legacy_timers = value
|
|
else
|
|
raise TypeError, "Service#macos_legacy_timers expects a Boolean"
|
|
end
|
|
end
|
|
|
|
delegate [:bin, :etc, :libexec, :opt_bin, :opt_libexec, :opt_pkgshare, :opt_prefix, :opt_sbin, :var] => :@formula
|
|
|
|
sig { returns(String) }
|
|
def std_service_path_env
|
|
"#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
end
|
|
|
|
sig { returns(T.nilable(T::Array[String])) }
|
|
def command
|
|
@run&.map(&:to_s)
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def command?
|
|
@run.present?
|
|
end
|
|
|
|
# Returns the `String` command to run manually instead of the service.
|
|
# @return [String]
|
|
sig { returns(String) }
|
|
def manual_command
|
|
vars = @environment_variables.except(:PATH)
|
|
.map { |k, v| "#{k}=\"#{v}\"" }
|
|
|
|
out = vars + command if command?
|
|
out.join(" ")
|
|
end
|
|
|
|
# Returns a `Boolean` describing if a service is timed.
|
|
# @return [Boolean]
|
|
sig { returns(T::Boolean) }
|
|
def timed?
|
|
@run_type == RUN_TYPE_CRON || @run_type == RUN_TYPE_INTERVAL
|
|
end
|
|
|
|
# Returns a `String` plist.
|
|
# @return [String]
|
|
sig { returns(String) }
|
|
def to_plist
|
|
# command needs to be first because it initializes all other values
|
|
base = {
|
|
Label: plist_name,
|
|
ProgramArguments: command,
|
|
RunAtLoad: @run_at_load == true,
|
|
}
|
|
|
|
base[:LaunchOnlyOnce] = @launch_only_once if @launch_only_once == true
|
|
base[:LegacyTimers] = @macos_legacy_timers if @macos_legacy_timers == true
|
|
base[:TimeOut] = @restart_delay if @restart_delay.present?
|
|
base[:ProcessType] = @process_type.to_s.capitalize if @process_type.present?
|
|
base[:StartInterval] = @interval if @interval.present? && @run_type == RUN_TYPE_INTERVAL
|
|
base[:WorkingDirectory] = @working_dir if @working_dir.present?
|
|
base[:RootDirectory] = @root_dir if @root_dir.present?
|
|
base[:StandardInPath] = @input_path if @input_path.present?
|
|
base[:StandardOutPath] = @log_path if @log_path.present?
|
|
base[:StandardErrorPath] = @error_log_path if @error_log_path.present?
|
|
base[:EnvironmentVariables] = @environment_variables unless @environment_variables.empty?
|
|
|
|
if keep_alive?
|
|
if (always = @keep_alive[:always].presence)
|
|
base[:KeepAlive] = always
|
|
elsif @keep_alive.key?(:successful_exit)
|
|
base[:KeepAlive] = { SuccessfulExit: @keep_alive[:successful_exit] }
|
|
elsif @keep_alive.key?(:crashed)
|
|
base[:KeepAlive] = { Crashed: @keep_alive[:crashed] }
|
|
elsif @keep_alive.key?(:path) && @keep_alive[:path].present?
|
|
base[:KeepAlive] = { PathState: @keep_alive[:path].to_s }
|
|
end
|
|
end
|
|
|
|
if @sockets.present?
|
|
base[:Sockets] = {}
|
|
base[:Sockets][:Listeners] = {
|
|
SockNodeName: @sockets[:host],
|
|
SockServiceName: @sockets[:port],
|
|
SockProtocol: @sockets[:type].upcase,
|
|
SockFamily: "IPv4v6",
|
|
}
|
|
end
|
|
|
|
if @cron.present? && @run_type == RUN_TYPE_CRON
|
|
base[:StartCalendarInterval] = @cron.reject { |_, value| value == "*" }
|
|
end
|
|
|
|
# Adding all session types has as the primary effect that if you initialise it through e.g. a Background session
|
|
# and you later "physically" sign in to the owning account (Aqua session), things shouldn't flip out.
|
|
# Also, we're not checking @process_type here because that is used to indicate process priority and not
|
|
# necessarily if it should run in a specific session type. Like database services could run with ProcessType
|
|
# Interactive so they have no resource limitations enforced upon them, but they aren't really interactive in the
|
|
# general sense.
|
|
base[:LimitLoadToSessionType] = %w[Aqua Background LoginWindow StandardIO System]
|
|
|
|
base.to_plist
|
|
end
|
|
|
|
# Returns a `String` systemd unit.
|
|
# @return [String]
|
|
sig { returns(String) }
|
|
def to_systemd_unit
|
|
unit = <<~EOS
|
|
[Unit]
|
|
Description=Homebrew generated unit for #{@formula.name}
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
|
|
[Service]
|
|
EOS
|
|
|
|
# command needs to be first because it initializes all other values
|
|
cmd = command&.join(" ")
|
|
|
|
options = []
|
|
options << "Type=#{(@launch_only_once == true) ? "oneshot" : "simple"}"
|
|
options << "ExecStart=#{cmd}"
|
|
|
|
options << "Restart=always" if @keep_alive.present? && @keep_alive[:always].present?
|
|
options << "RestartSec=#{restart_delay}" if @restart_delay.present?
|
|
options << "WorkingDirectory=#{@working_dir}" if @working_dir.present?
|
|
options << "RootDirectory=#{@root_dir}" if @root_dir.present?
|
|
options << "StandardInput=file:#{@input_path}" if @input_path.present?
|
|
options << "StandardOutput=append:#{@log_path}" if @log_path.present?
|
|
options << "StandardError=append:#{@error_log_path}" if @error_log_path.present?
|
|
options += @environment_variables.map { |k, v| "Environment=\"#{k}=#{v}\"" } if @environment_variables.present?
|
|
|
|
unit + options.join("\n")
|
|
end
|
|
|
|
# Returns a `String` systemd unit timer.
|
|
# @return [String]
|
|
sig { returns(String) }
|
|
def to_systemd_timer
|
|
timer = <<~EOS
|
|
[Unit]
|
|
Description=Homebrew generated timer for #{@formula.name}
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
|
|
[Timer]
|
|
Unit=#{service_name}
|
|
EOS
|
|
|
|
options = []
|
|
options << "Persistent=true" if @run_type == RUN_TYPE_CRON
|
|
options << "OnUnitActiveSec=#{@interval}" if @run_type == RUN_TYPE_INTERVAL
|
|
|
|
if @run_type == RUN_TYPE_CRON
|
|
minutes = (@cron[:Minute] == "*") ? "*" : format("%02d", @cron[:Minute])
|
|
hours = (@cron[:Hour] == "*") ? "*" : format("%02d", @cron[:Hour])
|
|
options << "OnCalendar=#{@cron[:Weekday]}-*-#{@cron[:Month]}-#{@cron[:Day]} #{hours}:#{minutes}:00"
|
|
end
|
|
|
|
timer + options.join("\n")
|
|
end
|
|
|
|
# Prepare the service hash for inclusion in the formula API JSON.
|
|
sig { returns(Hash) }
|
|
def serialize
|
|
custom_plist_name = plist_name if plist_name != default_plist_name
|
|
custom_service_name = service_name if service_name != default_service_name
|
|
|
|
unless command?
|
|
return {
|
|
plist_name: custom_plist_name,
|
|
service_name: custom_service_name,
|
|
}.compact
|
|
end
|
|
|
|
cron_string = if @cron.present?
|
|
[:Minute, :Hour, :Day, :Month, :Weekday]
|
|
.map { |key| @cron[key].to_s }
|
|
.join(" ")
|
|
end
|
|
|
|
sockets_string = "#{@sockets[:type]}://#{@sockets[:host]}:#{@sockets[:port]}" if @sockets.present?
|
|
|
|
{
|
|
plist_name: custom_plist_name,
|
|
service_name: custom_service_name,
|
|
run: @run_params,
|
|
run_type: @run_type,
|
|
interval: @interval,
|
|
cron: cron_string,
|
|
keep_alive: @keep_alive,
|
|
launch_only_once: @launch_only_once,
|
|
require_root: @require_root,
|
|
environment_variables: @environment_variables.presence,
|
|
working_dir: @working_dir,
|
|
root_dir: @root_dir,
|
|
input_path: @input_path,
|
|
log_path: @log_path,
|
|
error_log_path: @error_log_path,
|
|
restart_delay: @restart_delay,
|
|
process_type: @process_type,
|
|
macos_legacy_timers: @macos_legacy_timers,
|
|
sockets: sockets_string,
|
|
}.compact
|
|
end
|
|
|
|
# Turn the service API hash values back into what is expected by the formula DSL.
|
|
sig { params(api_hash: Hash).returns(Hash) }
|
|
def self.deserialize(api_hash)
|
|
hash = {}
|
|
|
|
%w[plist_name service_name].each do |key|
|
|
next if (value = api_hash[key]).nil?
|
|
|
|
hash[key.to_sym] = value
|
|
end
|
|
|
|
# The service hash might not have a "run" command if it only documents
|
|
# an existing service file with "plist_name" or "service_name".
|
|
return hash unless api_hash.key?("run")
|
|
|
|
hash[:run] =
|
|
case api_hash["run"]
|
|
when String
|
|
replace_placeholders(api_hash["run"])
|
|
when Array
|
|
api_hash["run"].map(&method(:replace_placeholders))
|
|
when Hash
|
|
api_hash["run"].to_h do |key, elem|
|
|
run_cmd = if elem.is_a?(Array)
|
|
elem.map(&method(:replace_placeholders))
|
|
else
|
|
replace_placeholders(elem)
|
|
end
|
|
|
|
[key.to_sym, run_cmd]
|
|
end
|
|
else
|
|
raise ArgumentError, "Unexpected run command: #{api_hash["run"]}"
|
|
end
|
|
|
|
hash[:keep_alive] = api_hash["keep_alive"].transform_keys(&:to_sym) if api_hash.key?("keep_alive")
|
|
|
|
if api_hash.key?("environment_variables")
|
|
hash[:environment_variables] = api_hash["environment_variables"].to_h do |key, value|
|
|
[key.to_sym, replace_placeholders(value)]
|
|
end
|
|
end
|
|
|
|
%w[run_type process_type].each do |key|
|
|
next unless (value = api_hash[key])
|
|
|
|
hash[key.to_sym] = value.to_sym
|
|
end
|
|
|
|
%w[working_dir root_dir input_path log_path error_log_path].each do |key|
|
|
next unless (value = api_hash[key])
|
|
|
|
hash[key.to_sym] = replace_placeholders(value)
|
|
end
|
|
|
|
%w[interval cron launch_only_once require_root restart_delay macos_legacy_timers sockets].each do |key|
|
|
next if (value = api_hash[key]).nil?
|
|
|
|
hash[key.to_sym] = value
|
|
end
|
|
|
|
hash
|
|
end
|
|
|
|
# Replace API path placeholders with local paths.
|
|
sig { params(string: String).returns(String) }
|
|
def self.replace_placeholders(string)
|
|
string.gsub(HOMEBREW_PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
|
|
.gsub(HOMEBREW_HOME_PLACEHOLDER, Dir.home)
|
|
end
|
|
end
|
|
end
|