brew/Library/Homebrew/service.rb
valrus 698f4a8f69 Add process_type to service DSL
This enables mpd to be run with the "Interactive" value, which avoids
performance issues such as skipping when playing music. See this PR,
where this was done previously:
https://github.com/Homebrew/homebrew-core/pull/19410

The ProcessType plist key was lost in the conversion to the service DSL:
483ad1fdfa
2021-09-09 17:37:49 -07:00

258 lines
8.0 KiB
Ruby

# typed: true
# frozen_string_literal: true
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 T::Sig
extend Forwardable
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"
# sig { params(formula: Formula).void }
def initialize(formula, &block)
@formula = formula
@run_type = RUN_TYPE_IMMEDIATE
@environment_variables = {}
@service_block = block
end
sig { params(command: T.nilable(T.any(T::Array[String], String, Pathname))).returns(T.nilable(Array)) }
def run(command = nil)
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::Boolean)).returns(T.nilable(T::Boolean)) }
def keep_alive(value = nil)
case T.unsafe(value)
when nil
@keep_alive
when true, false
@keep_alive = value
else
raise TypeError, "Service#keep_alive 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(type: T.nilable(String)).returns(T.nilable(String)) }
def process_type(type = nil)
case T.unsafe(type)
when nil
@process_type
when PROCESS_TYPE_BACKGROUND, PROCESS_TYPE_STANDARD, PROCESS_TYPE_INTERACTIVE, PROCESS_TYPE_ADAPTIVE
@process_type = type.to_s
when String
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 String"
end
end
sig { params(type: T.nilable(T.any(String, Symbol))).returns(T.nilable(String)) }
def run_type(type = nil)
case T.unsafe(type)
when nil
@run_type
when "immediate", :immediate
@run_type = type.to_s
when RUN_TYPE_INTERVAL, RUN_TYPE_CRON
raise TypeError, "Service#run_type does not support timers"
when String
raise TypeError, "Service#run_type allows: '#{RUN_TYPE_IMMEDIATE}'/'#{RUN_TYPE_INTERVAL}'/'#{RUN_TYPE_CRON}'"
else
raise TypeError, "Service#run_type expects a string"
end
end
sig { params(variables: T::Hash[String, String]).returns(T.nilable(T::Hash[String, 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::Array[String]) }
def command
instance_eval(&@service_block)
@run.map(&:to_s)
end
sig { returns(String) }
def manual_command
instance_eval(&@service_block)
vars = @environment_variables.except(:PATH)
.map { |k, v| "#{k}=\"#{v}\"" }
out = vars + command
out.join(" ")
end
# Returns a `String` plist.
# @return [String]
sig { returns(String) }
def to_plist
base = {
Label: @formula.plist_name,
RunAtLoad: @run_type == RUN_TYPE_IMMEDIATE,
ProgramArguments: command,
}
base[:KeepAlive] = @keep_alive if @keep_alive == true
base[:LegacyTimers] = @macos_legacy_timers if @macos_legacy_timers == true
base[:TimeOut] = @restart_delay if @restart_delay.present?
base[:ProcessType] = @process_type.capitalize if @process_type.present?
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?
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=multi-user.target
[Service]
Type=simple
ExecStart=#{command.join(" ")}
EOS
options = []
options << "Restart=always" if @keep_alive == true
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
end
end