2025-03-14 16:53:07 +00:00
|
|
|
# typed: true # rubocop:todo Sorbet/StrictSigil
|
2025-02-26 13:26:37 +01:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
# Wrapper for a formula to handle service-related stuff like parsing and
|
|
|
|
# generating the service/plist files.
|
2025-03-14 04:35:30 +00:00
|
|
|
module Homebrew
|
|
|
|
module Services
|
|
|
|
class FormulaWrapper
|
|
|
|
# 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
|
2025-02-26 13:26:37 +01:00
|
|
|
end
|
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# Initialize a new `Service` instance with supplied formula.
|
|
|
|
sig { params(formula: Formula).void }
|
|
|
|
def initialize(formula)
|
2025-03-14 16:53:07 +00:00
|
|
|
@formula = formula
|
2025-03-14 04:35:30 +00:00
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# Delegate access to `formula.name`.
|
|
|
|
sig { returns(String) }
|
2025-03-14 16:53:07 +00:00
|
|
|
def name
|
|
|
|
@name ||= formula.name
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# Delegate access to `formula.service?`.
|
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def service?
|
2025-03-14 16:53:07 +00:00
|
|
|
@service ||= @formula.service?
|
2025-03-14 04:35:30 +00:00
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# Delegate access to `formula.service.timed?`.
|
2025-03-14 16:53:07 +00:00
|
|
|
# TODO: this should either be T::Boolean or renamed to `timed`
|
2025-03-14 04:35:30 +00:00
|
|
|
sig { returns(T.nilable(T::Boolean)) }
|
|
|
|
def timed?
|
2025-03-14 16:53:07 +00:00
|
|
|
@timed ||= (load_service.timed? if service?)
|
2025-03-14 04:35:30 +00:00
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 16:53:07 +00:00
|
|
|
# Delegate access to `formula.service.keep_alive?`.
|
|
|
|
# TODO: this should either be T::Boolean or renamed to `keep_alive`
|
2025-03-14 04:35:30 +00:00
|
|
|
sig { returns(T.nilable(T::Boolean)) }
|
|
|
|
def keep_alive?
|
2025-03-14 16:53:07 +00:00
|
|
|
@keep_alive ||= (load_service.keep_alive? if service?)
|
2025-03-14 04:35:30 +00:00
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 16:53:07 +00:00
|
|
|
# service_name delegates with formula.plist_name or formula.service_name
|
|
|
|
# for systemd (e.g., `homebrew.<formula>`).
|
2025-03-14 04:35:30 +00:00
|
|
|
sig { returns(T.nilable(String)) }
|
2025-03-14 16:53:07 +00:00
|
|
|
def service_name
|
|
|
|
@service_name ||= if System.launchctl?
|
|
|
|
formula.plist_name
|
|
|
|
elsif System.systemctl?
|
|
|
|
formula.service_name
|
|
|
|
end
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# service_file delegates with formula.launchd_service_path or formula.systemd_service_path for systemd.
|
2025-03-14 16:53:07 +00:00
|
|
|
def service_file
|
|
|
|
@service_file ||= if System.launchctl?
|
|
|
|
formula.launchd_service_path
|
|
|
|
elsif System.systemctl?
|
|
|
|
formula.systemd_service_path
|
|
|
|
end
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# Whether the service should be launched at startup
|
2025-03-14 16:53:07 +00:00
|
|
|
sig { returns(T::Boolean) }
|
2025-03-14 04:35:30 +00:00
|
|
|
def service_startup?
|
2025-03-14 16:53:07 +00:00
|
|
|
@service_startup ||= if service?
|
|
|
|
load_service.requires_root?
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
2025-03-14 04:35:30 +00:00
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# Path to destination service directory. If run as root, it's `boot_path`, else `user_path`.
|
|
|
|
def dest_dir
|
2025-03-14 16:53:07 +00:00
|
|
|
System.root? ? System.boot_path : System.user_path
|
2025-03-14 04:35:30 +00:00
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# Path to destination service. If run as root, it's in `boot_path`, else `user_path`.
|
|
|
|
def dest
|
2025-03-14 16:53:07 +00:00
|
|
|
dest_dir + service_file.basename
|
2025-03-14 04:35:30 +00:00
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# Returns `true` if any version of the formula is installed.
|
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def installed?
|
|
|
|
formula.any_version_installed?
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# Returns `true` if the plist file exists.
|
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def plist?
|
|
|
|
return false unless installed?
|
2025-03-14 16:53:07 +00:00
|
|
|
return true if service_file.file?
|
2025-03-14 04:35:30 +00:00
|
|
|
return false unless formula.opt_prefix.exist?
|
|
|
|
return true if Keg.for(formula.opt_prefix).plist_installed?
|
|
|
|
|
|
|
|
false
|
|
|
|
rescue NotAKegError
|
|
|
|
false
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# Returns `true` if the service is loaded, else false.
|
2025-03-14 16:53:07 +00:00
|
|
|
# TODO: this should either be T::Boolean or renamed to `loaded`
|
2025-03-14 04:35:30 +00:00
|
|
|
sig { params(cached: T::Boolean).returns(T.nilable(T::Boolean)) }
|
|
|
|
def loaded?(cached: false)
|
|
|
|
if System.launchctl?
|
|
|
|
@status_output_success_type = nil unless cached
|
|
|
|
_, status_success, = status_output_success_type
|
|
|
|
status_success
|
|
|
|
elsif System.systemctl?
|
2025-03-14 16:53:07 +00:00
|
|
|
System::Systemctl.quiet_run("status", service_file.basename)
|
2025-03-14 04:35:30 +00:00
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
end
|
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# Returns `true` if service is present (e.g. .plist is present in boot or user service path), else `false`
|
2025-03-14 16:53:07 +00:00
|
|
|
# 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
|
2025-03-14 04:35:30 +00:00
|
|
|
boot_path_service_file_present?
|
2025-03-14 16:53:07 +00:00
|
|
|
when :user
|
2025-03-14 04:35:30 +00:00
|
|
|
user_path_service_file_present?
|
|
|
|
else
|
|
|
|
boot_path_service_file_present? || user_path_service_file_present?
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
end
|
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
sig { returns(T.nilable(String)) }
|
|
|
|
def owner
|
|
|
|
if System.launchctl? && dest.exist?
|
|
|
|
# read the username from the plist file
|
|
|
|
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?
|
2025-02-26 13:26:37 +01:00
|
|
|
end
|
2025-03-14 04:35:30 +00:00
|
|
|
return "root" if boot_path_service_file_present?
|
|
|
|
return System.user if user_path_service_file_present?
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
nil
|
2025-02-26 13:26:37 +01:00
|
|
|
end
|
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def pid?
|
2025-03-15 19:42:33 +00:00
|
|
|
pid.present? && pid.positive?
|
2025-03-14 04:35:30 +00:00
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 16:53:07 +00:00
|
|
|
sig { returns(T::Boolean) }
|
2025-03-14 04:35:30 +00:00
|
|
|
def error?
|
2025-03-14 16:53:07 +00:00
|
|
|
return false if pid?
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-15 19:42:33 +00:00
|
|
|
exit_code.present? && !exit_code.zero?
|
2025-03-14 04:35:30 +00:00
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 16:53:07 +00:00
|
|
|
sig { returns(T::Boolean) }
|
2025-03-14 04:35:30 +00:00
|
|
|
def unknown_status?
|
|
|
|
status_output.blank? && !pid?
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# Get current PID of daemon process from status output.
|
|
|
|
def pid
|
|
|
|
status_output, _, status_type = status_output_success_type
|
|
|
|
Regexp.last_match(1).to_i if status_output =~ pid_regex(status_type)
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# Get current exit code of daemon process from status output.
|
|
|
|
def exit_code
|
|
|
|
status_output, _, status_type = status_output_success_type
|
|
|
|
Regexp.last_match(1).to_i if status_output =~ exit_code_regex(status_type)
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 16:53:07 +00:00
|
|
|
sig { returns(T::Hash[Symbol, T.anything]) }
|
2025-03-14 04:35:30 +00:00
|
|
|
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,
|
2025-03-20 07:16:02 +00:00
|
|
|
registered: service_file_present?,
|
2025-03-14 04:35:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return hash unless service?
|
|
|
|
|
|
|
|
service = load_service
|
|
|
|
|
2025-03-14 16:53:07 +00:00
|
|
|
return hash if service.command.blank?
|
2025-03-14 04:35:30 +00:00
|
|
|
|
2025-03-14 16:53:07 +00:00
|
|
|
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
|
2025-03-14 04:35:30 +00:00
|
|
|
|
|
|
|
hash
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
private
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
# 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.
|
2025-03-14 16:53:07 +00:00
|
|
|
sig { returns(Homebrew::Service) }
|
2025-03-14 04:35:30 +00:00
|
|
|
def load_service
|
|
|
|
require "formula"
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
formula.service
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
def status_output_success_type
|
|
|
|
@status_output_success_type ||= if System.launchctl?
|
2025-03-25 14:16:29 +00:00
|
|
|
cmd = [System.launchctl.to_s, "list", "#{System.domain_target}/#{service_name}"]
|
2025-02-26 13:26:37 +01:00
|
|
|
output = Utils.popen_read(*cmd).chomp
|
2025-03-14 04:35:30 +00:00
|
|
|
if $CHILD_STATUS.present? && $CHILD_STATUS.success? && output.present?
|
|
|
|
success = true
|
|
|
|
odebug cmd.join(" "), output
|
|
|
|
[output, success, :launchctl_list]
|
|
|
|
else
|
|
|
|
cmd = [System.launchctl.to_s, "print", "#{System.domain_target}/#{service_name}"]
|
|
|
|
output = Utils.popen_read(*cmd).chomp
|
|
|
|
success = $CHILD_STATUS.present? && $CHILD_STATUS.success? && output.present?
|
|
|
|
odebug cmd.join(" "), output
|
|
|
|
[output, success, :launchctl_print]
|
|
|
|
end
|
|
|
|
elsif System.systemctl?
|
|
|
|
cmd = ["status", service_name]
|
|
|
|
output = System::Systemctl.popen_read(*cmd).chomp
|
2025-02-26 13:26:37 +01:00
|
|
|
success = $CHILD_STATUS.present? && $CHILD_STATUS.success? && output.present?
|
2025-03-14 04:35:30 +00:00
|
|
|
odebug [System::Systemctl.executable, System::Systemctl.scope, *cmd].join(" "), output
|
|
|
|
[output, success, :systemctl]
|
2025-02-26 13:26:37 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
sig { returns(T.nilable(String)) }
|
|
|
|
def status_output
|
|
|
|
status_output, = status_output_success_type
|
|
|
|
status_output
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
sig { returns(Symbol) }
|
|
|
|
def status_symbol
|
|
|
|
if pid?
|
|
|
|
:started
|
|
|
|
elsif !loaded?(cached: true)
|
|
|
|
:none
|
2025-03-14 16:53:07 +00:00
|
|
|
elsif exit_code.present? && exit_code.zero?
|
2025-03-14 04:35:30 +00:00
|
|
|
if timed?
|
|
|
|
:scheduled
|
|
|
|
else
|
|
|
|
:stopped
|
|
|
|
end
|
|
|
|
elsif error?
|
|
|
|
:error
|
|
|
|
elsif unknown_status?
|
|
|
|
:unknown
|
2025-02-26 13:26:37 +01:00
|
|
|
else
|
2025-03-14 04:35:30 +00:00
|
|
|
:other
|
2025-02-26 13:26:37 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
def exit_code_regex(status_type)
|
2025-03-14 16:53:07 +00:00
|
|
|
@exit_code_regex ||= {
|
2025-03-14 04:35:30 +00:00
|
|
|
launchctl_list: /"LastExitStatus"\ =\ ([0-9]*);/,
|
|
|
|
launchctl_print: /last exit code = ([0-9]+)/,
|
|
|
|
systemctl: /\(code=exited, status=([0-9]*)\)|\(dead\)/,
|
2025-03-14 16:53:07 +00:00
|
|
|
}
|
2025-03-14 04:35:30 +00:00
|
|
|
@exit_code_regex.fetch(status_type)
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
def pid_regex(status_type)
|
2025-03-14 16:53:07 +00:00
|
|
|
@pid_regex ||= {
|
2025-03-14 04:35:30 +00:00
|
|
|
launchctl_list: /"PID"\ =\ ([0-9]*);/,
|
|
|
|
launchctl_print: /pid = ([0-9]+)/,
|
|
|
|
systemctl: /Main PID: ([0-9]*) \((?!code=)/,
|
2025-03-14 16:53:07 +00:00
|
|
|
}
|
2025-03-14 04:35:30 +00:00
|
|
|
@pid_regex.fetch(status_type)
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def boot_path_service_file_present?
|
2025-03-14 16:53:07 +00:00
|
|
|
boot_path = System.boot_path
|
|
|
|
return false if boot_path.blank?
|
|
|
|
|
|
|
|
(boot_path + service_file.basename).exist?
|
2025-03-14 04:35:30 +00:00
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def user_path_service_file_present?
|
2025-03-14 16:53:07 +00:00
|
|
|
user_path = System.user_path
|
|
|
|
return false if user_path.blank?
|
|
|
|
|
|
|
|
(user_path + service_file.basename).exist?
|
2025-03-14 04:35:30 +00:00
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
|
2025-03-14 04:35:30 +00:00
|
|
|
sig { returns(Regexp) }
|
|
|
|
private_class_method def self.path_or_label_regex
|
|
|
|
/homebrew(?>\.mxcl)?\.([\w+-.@]+)(\.plist|\.service)?\z/
|
|
|
|
end
|
2025-02-26 13:26:37 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|