mirror of
https://github.com/Homebrew/brew.git
synced 2026-08-12 22:29:27 +04:00
- Since #23312, `brew services start` always regenerated the service definition from the formula `service` block. Formulae that bundle their own service file only declare `name` there, so the generated plist had empty `ProgramArguments` and `launchctl bootstrap` failed with `Input/output error`. - Read the installed service file instead unless the `service` block defines a command, the same gate `FormulaInstaller#install_service` uses when writing generated service files into the keg. - Fixes https://github.com/Homebrew/brew/issues/23408.
424 lines
13 KiB
Ruby
424 lines
13 KiB
Ruby
# typed: strict
|
|
# frozen_string_literal: true
|
|
|
|
require "utils/output"
|
|
|
|
# Wrapper for a formula to handle service-related stuff like parsing and
|
|
# generating the service/plist files.
|
|
module Homebrew
|
|
module Services
|
|
class FormulaWrapper
|
|
include Utils::Output::Mixin
|
|
|
|
# Access the `Formula` instance.
|
|
sig { returns(Formula) }
|
|
attr_reader :formula
|
|
|
|
# Create a new `Service` instance from either a path or label.
|
|
sig { params(path_or_label: T.any(Pathname, String)).returns(T.nilable(FormulaWrapper)) }
|
|
def self.from(path_or_label)
|
|
return unless path_or_label =~ path_or_label_regex
|
|
|
|
begin
|
|
new(Formulary.factory(T.must(Regexp.last_match(1))))
|
|
rescue
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Initialize a new `Service` instance with supplied formula.
|
|
sig { params(formula: Formula).void }
|
|
def initialize(formula)
|
|
@formula = formula
|
|
@status_output_success_type = T.let(nil, T.nilable(StatusOutputSuccessType))
|
|
|
|
return if System.launchctl? || System.systemctl?
|
|
|
|
raise UsageError, System::MISSING_DAEMON_MANAGER_EXCEPTION_MESSAGE
|
|
end
|
|
|
|
# Delegate access to `formula.name`.
|
|
sig { returns(String) }
|
|
def name
|
|
@name ||= T.let(formula.name, T.nilable(String))
|
|
end
|
|
|
|
# Delegate access to `formula.service?`.
|
|
sig { returns(T::Boolean) }
|
|
def service?
|
|
@service ||= T.let(formula.service?, T.nilable(T::Boolean))
|
|
end
|
|
|
|
# Delegate access to `formula.service.timed?`.
|
|
sig { returns(T::Boolean) }
|
|
def timed?
|
|
return @timed unless @timed.nil?
|
|
|
|
@timed = T.let(service? && load_service.timed?, T.nilable(T::Boolean))
|
|
@timed ||= false
|
|
end
|
|
|
|
# Delegate access to `formula.service.keep_alive?`.
|
|
sig { returns(T::Boolean) }
|
|
def keep_alive?
|
|
return @keep_alive unless @keep_alive.nil?
|
|
|
|
@keep_alive = T.let(service? && load_service.keep_alive?, T.nilable(T::Boolean))
|
|
@keep_alive ||= false
|
|
end
|
|
|
|
sig { returns(T::Array[Pathname]) }
|
|
def path_dirs
|
|
return [] unless service?
|
|
|
|
load_service.path_dirs
|
|
end
|
|
|
|
# service_name delegates with formula.plist_name or formula.service_name
|
|
# for systemd (e.g., `homebrew.<formula>`).
|
|
sig { returns(String) }
|
|
def service_name
|
|
@service_name ||= T.let(
|
|
if System.launchctl?
|
|
formula.plist_name
|
|
else # System.systemctl?
|
|
formula.service_name
|
|
end, T.nilable(String)
|
|
)
|
|
end
|
|
|
|
# service_file delegates with formula.launchd_service_path or formula.systemd_service_path for systemd.
|
|
sig { returns(Pathname) }
|
|
def service_file
|
|
@service_file ||= T.let(
|
|
if System.launchctl?
|
|
formula.launchd_service_path
|
|
else # System.systemctl?
|
|
formula.systemd_service_path
|
|
end, T.nilable(Pathname)
|
|
)
|
|
end
|
|
|
|
sig { returns(Pathname) }
|
|
def timer_file
|
|
@timer_file ||= T.let(formula.systemd_timer_path, T.nilable(Pathname))
|
|
end
|
|
|
|
sig { returns(String) }
|
|
def timer_name
|
|
@timer_name ||= T.let(timer_file.basename.to_s, T.nilable(String))
|
|
end
|
|
|
|
sig { returns(Pathname) }
|
|
def timer_dest
|
|
dest_dir + timer_file.basename
|
|
end
|
|
|
|
# Whether the service should be launched at startup
|
|
sig { returns(T::Boolean) }
|
|
def service_startup?
|
|
@service_startup ||= T.let(
|
|
if service?
|
|
load_service.requires_root?
|
|
else
|
|
false
|
|
end, T.nilable(T::Boolean)
|
|
)
|
|
end
|
|
|
|
# Path to destination service directory. If run as root, it's `boot_path`, else `user_path`.
|
|
sig { returns(Pathname) }
|
|
def dest_dir
|
|
System.root? ? System.boot_path : System.user_path
|
|
end
|
|
|
|
# Path to destination service. If run as root, it's in `boot_path`, else `user_path`.
|
|
sig { returns(Pathname) }
|
|
def dest
|
|
dest_dir + service_file.basename
|
|
end
|
|
|
|
# Returns `true` if any version of the formula is installed.
|
|
sig { returns(T::Boolean) }
|
|
def installed?
|
|
formula.any_version_installed?
|
|
end
|
|
|
|
sig { void }
|
|
def reset_cache!
|
|
@status_output_success_type = nil
|
|
end
|
|
|
|
# Returns `true` if the service is loaded, else false.
|
|
sig { params(cached: T::Boolean).returns(T::Boolean) }
|
|
def loaded?(cached: false)
|
|
if System.launchctl?
|
|
reset_cache! unless cached
|
|
status_success
|
|
else # System.systemctl?
|
|
System::Systemctl.quiet_run("status", timed? ? timer_name : service_file.basename)
|
|
end
|
|
end
|
|
|
|
# Returns `true` if service is present (e.g. .plist is present in boot or user service path), else `false`
|
|
# Accepts `type` with values `:root` for boot path or `:user` for user path.
|
|
sig { params(type: T.nilable(Symbol)).returns(T::Boolean) }
|
|
def service_file_present?(type: nil)
|
|
case type
|
|
when :root
|
|
boot_path_service_file_present?
|
|
when :user
|
|
user_path_service_file_present?
|
|
else
|
|
boot_path_service_file_present? || user_path_service_file_present?
|
|
end
|
|
end
|
|
|
|
sig { returns(T.nilable(String)) }
|
|
def owner
|
|
if System.launchctl? && dest.exist?
|
|
# read the username from the plist file
|
|
require "plist"
|
|
plist = begin
|
|
Plist.parse_xml(dest.read, marshal: false)
|
|
rescue
|
|
nil
|
|
end
|
|
plist_username = plist["UserName"] if plist
|
|
|
|
return plist_username if plist_username.present?
|
|
end
|
|
return "root" if boot_path_service_file_present?
|
|
return System.user if user_path_service_file_present?
|
|
|
|
nil
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def pid?
|
|
(pid = self.pid).present? && pid.positive?
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def error?
|
|
return false if pid?
|
|
|
|
(exit_code = self.exit_code).present? && !exit_code.zero?
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def unknown_status?
|
|
status_output.blank? && !pid?
|
|
end
|
|
|
|
# Get current PID of daemon process from status output.
|
|
sig { returns(T.nilable(Integer)) }
|
|
def pid
|
|
Regexp.last_match(1).to_i if status_output =~ pid_regex(status_type)
|
|
end
|
|
|
|
# Get current exit code of daemon process from status output.
|
|
sig { returns(T.nilable(Integer)) }
|
|
def exit_code
|
|
Regexp.last_match(1).to_i if status_output =~ exit_code_regex(status_type)
|
|
end
|
|
|
|
sig { returns(T.nilable(String)) }
|
|
def loaded_file
|
|
Regexp.last_match(1) if status_output =~ loaded_file_regex(status_type)
|
|
end
|
|
|
|
sig { returns(T::Hash[Symbol, T.anything]) }
|
|
def to_hash
|
|
hash = {
|
|
name:,
|
|
service_name:,
|
|
running: pid?,
|
|
loaded: loaded?(cached: true),
|
|
schedulable: timed?,
|
|
pid:,
|
|
exit_code:,
|
|
user: owner,
|
|
status: status_symbol,
|
|
file: service_file_present? ? dest : service_file,
|
|
registered: service_file_present?,
|
|
loaded_file:,
|
|
}
|
|
|
|
return hash unless service?
|
|
|
|
service = load_service
|
|
|
|
return hash if service.command.blank?
|
|
|
|
hash[:command] = service.manual_command
|
|
hash[:working_dir] = service.working_dir
|
|
hash[:root_dir] = service.root_dir
|
|
hash[:log_path] = service.log_path
|
|
hash[:error_log_path] = service.error_log_path
|
|
hash[:interval] = service.interval
|
|
hash[:cron] = service.cron.presence
|
|
|
|
hash
|
|
end
|
|
|
|
# Generate the service file content (plist or systemd unit),
|
|
# including any per-service user environment variable overrides,
|
|
# or read the package-provided service file if the formula's
|
|
# service block does not define a command.
|
|
sig { returns(String) }
|
|
def service_contents
|
|
if !service? || !load_service.command?
|
|
service_file.read
|
|
elsif System.launchctl?
|
|
load_service.to_plist
|
|
else
|
|
load_service.to_systemd_unit
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
# The purpose of this function is to lazy load the Homebrew::Service class
|
|
# and avoid nameclashes with the current Service module.
|
|
# It should be used instead of calling formula.service directly.
|
|
sig { returns(Homebrew::Service) }
|
|
def load_service
|
|
require "formula"
|
|
|
|
formula.service
|
|
end
|
|
|
|
sig { returns(StatusOutputSuccessType) }
|
|
def status_output_success_type
|
|
@status_output_success_type ||= if System.launchctl?
|
|
cmd = [System.launchctl.to_s, "print", "#{System.domain_target}/#{service_name}"]
|
|
output = Utils.popen_read(*cmd).chomp
|
|
if $CHILD_STATUS.present? && $CHILD_STATUS.success? && output.present?
|
|
success = true
|
|
type = :launchctl_print
|
|
else
|
|
cmd = [System.launchctl.to_s, "list", service_name]
|
|
output = Utils.popen_read(*cmd).chomp
|
|
success = T.cast($CHILD_STATUS.present? && $CHILD_STATUS.success? && output.present?, T::Boolean)
|
|
type = :launchctl_list
|
|
end
|
|
odebug cmd.join(" "), output
|
|
StatusOutputSuccessType.new(output, success, type)
|
|
else # System.systemctl?
|
|
cmd = ["status", service_name]
|
|
output = System::Systemctl.popen_read(*cmd).chomp
|
|
success = T.cast($CHILD_STATUS.present? && $CHILD_STATUS.success? && output.present?, T::Boolean)
|
|
odebug [System::Systemctl.executable, System::Systemctl.scope, *cmd].join(" "), output
|
|
StatusOutputSuccessType.new(output, success, :systemctl)
|
|
end
|
|
end
|
|
|
|
sig { returns(String) }
|
|
def status_output
|
|
status_output_success_type.output
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def status_success
|
|
status_output_success_type.success
|
|
end
|
|
|
|
sig { returns(Symbol) }
|
|
def status_type
|
|
status_output_success_type.type
|
|
end
|
|
|
|
sig { returns(Symbol) }
|
|
def status_symbol
|
|
if pid?
|
|
:started
|
|
elsif !loaded?(cached: true)
|
|
:none
|
|
elsif (exit_code = self.exit_code).present? && exit_code.zero?
|
|
if timed?
|
|
:scheduled
|
|
else
|
|
:stopped
|
|
end
|
|
elsif error?
|
|
:error
|
|
elsif unknown_status?
|
|
:unknown
|
|
else
|
|
:other
|
|
end
|
|
end
|
|
|
|
sig { params(status_type: Symbol).returns(Regexp) }
|
|
def exit_code_regex(status_type)
|
|
@exit_code_regex ||= T.let({
|
|
launchctl_list: /"LastExitStatus"\ =\ ([0-9]*);/,
|
|
launchctl_print: /last exit code = ([0-9]+)/,
|
|
systemctl: /\(code=exited, status=([0-9]*)\)|\(dead\)/,
|
|
}, T.nilable(T::Hash[Symbol, Regexp]))
|
|
@exit_code_regex.fetch(status_type)
|
|
end
|
|
|
|
sig { params(status_type: Symbol).returns(Regexp) }
|
|
def pid_regex(status_type)
|
|
@pid_regex ||= T.let({
|
|
launchctl_list: /"PID"\ =\ ([0-9]*);/,
|
|
launchctl_print: /pid = ([0-9]+)/,
|
|
systemctl: /Main PID: ([0-9]*) \((?!code=)/,
|
|
}, T.nilable(T::Hash[Symbol, Regexp]))
|
|
@pid_regex.fetch(status_type)
|
|
end
|
|
|
|
sig { params(status_type: Symbol).returns(Regexp) }
|
|
def loaded_file_regex(status_type)
|
|
@loaded_file_regex ||= T.let({
|
|
launchctl_list: //, # not available
|
|
launchctl_print: /path = (.*)/,
|
|
systemctl: /Loaded: .*? \((.*);/,
|
|
}, T.nilable(T::Hash[Symbol, Regexp]))
|
|
@loaded_file_regex.fetch(status_type)
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def boot_path_service_file_present?
|
|
boot_path = System.boot_path
|
|
return false if boot_path.blank?
|
|
|
|
(boot_path + service_file.basename).exist?
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def user_path_service_file_present?
|
|
user_path = System.user_path
|
|
return false if user_path.blank?
|
|
|
|
(user_path + service_file.basename).exist?
|
|
end
|
|
|
|
sig { returns(Regexp) }
|
|
private_class_method def self.path_or_label_regex
|
|
/homebrew(?>\.mxcl)?\.([\w+-.@]+)(\.plist|\.service)?\z/
|
|
end
|
|
|
|
class StatusOutputSuccessType
|
|
sig { returns(String) }
|
|
attr_reader :output
|
|
|
|
sig { returns(T::Boolean) }
|
|
attr_reader :success
|
|
|
|
sig { returns(Symbol) }
|
|
attr_reader :type
|
|
|
|
sig { params(output: String, success: T::Boolean, type: Symbol).void }
|
|
def initialize(output, success, type)
|
|
@output = output
|
|
@success = success
|
|
@type = type
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|