Merge pull request #19490 from Homebrew/fix_services_types

Fix services types
This commit is contained in:
Mike McQuaid 2025-03-14 17:24:10 +00:00 committed by GitHub
commit 8060ce8e54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 119 additions and 138 deletions

View File

@ -1,4 +1,4 @@
# typed: strict # typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true # frozen_string_literal: true
require "services/formula_wrapper" require "services/formula_wrapper"
@ -15,7 +15,7 @@ module Homebrew
sig { params(sudo_service_user: String).void } sig { params(sudo_service_user: String).void }
def self.sudo_service_user=(sudo_service_user) def self.sudo_service_user=(sudo_service_user)
@sudo_service_user = T.let(sudo_service_user, T.nilable(String)) @sudo_service_user = sudo_service_user
end end
# Binary name. # Binary name.
@ -41,7 +41,6 @@ module Homebrew
end end
# Check if formula has been found. # Check if formula has been found.
sig { params(targets: T.untyped).returns(TrueClass) }
def self.check(targets) def self.check(targets)
raise UsageError, "Formula(e) missing, please provide a formula name or use --all" if targets.empty? raise UsageError, "Formula(e) missing, please provide a formula name or use --all" if targets.empty?
@ -49,7 +48,6 @@ module Homebrew
end end
# Kill services that don't have a service file # Kill services that don't have a service file
sig { returns(T::Array[Services::FormulaWrapper]) }
def self.kill_orphaned_services def self.kill_orphaned_services
cleaned_labels = [] cleaned_labels = []
cleaned_services = [] cleaned_services = []
@ -67,7 +65,6 @@ module Homebrew
cleaned_labels cleaned_labels
end end
sig { returns(T::Array[T.untyped]) }
def self.remove_unused_service_files def self.remove_unused_service_files
cleaned = [] cleaned = []
Dir["#{System.path}homebrew.*.{plist,service}"].each do |file| Dir["#{System.path}homebrew.*.{plist,service}"].each do |file|
@ -82,7 +79,7 @@ module Homebrew
end end
# Run a service as defined in the formula. This does not clean the service file like `start` does. # Run a service as defined in the formula. This does not clean the service file like `start` does.
sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean)).void } sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T::Boolean).void }
def self.run(targets, verbose: false) def self.run(targets, verbose: false)
targets.each do |service| targets.each do |service|
if service.pid? if service.pid?
@ -102,7 +99,7 @@ module Homebrew
params( params(
targets: T::Array[Services::FormulaWrapper], targets: T::Array[Services::FormulaWrapper],
service_file: T.nilable(T.any(String, Pathname)), service_file: T.nilable(T.any(String, Pathname)),
verbose: T.nilable(T::Boolean), verbose: T::Boolean,
).void ).void
} }
def self.start(targets, service_file = nil, verbose: false) def self.start(targets, service_file = nil, verbose: false)
@ -121,11 +118,12 @@ module Homebrew
odie "Formula `#{service.name}` is not installed." unless service.installed? odie "Formula `#{service.name}` is not installed." unless service.installed?
file ||= if T.must(service.service_file).exist? || System.systemctl? file ||= if service.service_file.exist? || System.systemctl?
nil nil
elsif service.formula.opt_prefix.exist? && elsif service.formula.opt_prefix.exist? &&
(keg = Keg.for service.formula.opt_prefix) && keg.plist_installed? (keg = Keg.for service.formula.opt_prefix) &&
service_file = Dir["#{keg}/*#{T.must(service.service_file).extname}"].first keg.plist_installed?
service_file = Dir["#{keg}/*#{service.service_file.extname}"].first
Pathname.new service_file if service_file.present? Pathname.new service_file if service_file.present?
end end
@ -147,8 +145,8 @@ module Homebrew
sig { sig {
params( params(
targets: T::Array[Services::FormulaWrapper], targets: T::Array[Services::FormulaWrapper],
verbose: T.nilable(T::Boolean), verbose: T::Boolean,
no_wait: T.nilable(T::Boolean), no_wait: T::Boolean,
max_wait: T.nilable(T.any(Integer, Float)), max_wait: T.nilable(T.any(Integer, Float)),
).void ).void
} }
@ -208,7 +206,7 @@ module Homebrew
end end
# Stop a service but keep it registered. # Stop a service but keep it registered.
sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean)).void } sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T::Boolean).void }
def self.kill(targets, verbose: false) def self.kill(targets, verbose: false)
targets.each do |service| targets.each do |service|
if !service.pid? if !service.pid?
@ -233,7 +231,6 @@ module Homebrew
end end
# protections to avoid users editing root services # protections to avoid users editing root services
sig { params(service: T.untyped).returns(T.nilable(Integer)) }
def self.take_root_ownership(service) def self.take_root_ownership(service)
return unless System.root? return unless System.root?
return if sudo_service_user return if sudo_service_user
@ -302,7 +299,7 @@ module Homebrew
params( params(
service: Services::FormulaWrapper, service: Services::FormulaWrapper,
file: T.nilable(T.any(String, Pathname)), file: T.nilable(T.any(String, Pathname)),
enable: T.nilable(T::Boolean), enable: T::Boolean,
).void ).void
} }
def self.launchctl_load(service, file:, enable:) def self.launchctl_load(service, file:, enable:)
@ -310,13 +307,13 @@ module Homebrew
safe_system System.launchctl, "bootstrap", System.domain_target, file safe_system System.launchctl, "bootstrap", System.domain_target, file
end end
sig { params(service: Services::FormulaWrapper, enable: T.nilable(T::Boolean)).void } sig { params(service: Services::FormulaWrapper, enable: T::Boolean).void }
def self.systemd_load(service, enable:) def self.systemd_load(service, enable:)
System::Systemctl.run("start", T.must(service.service_name)) System::Systemctl.run("start", service.service_name)
System::Systemctl.run("enable", T.must(service.service_name)) if enable System::Systemctl.run("enable", service.service_name) if enable
end end
sig { params(service: Services::FormulaWrapper, enable: T.nilable(T::Boolean)).void } sig { params(service: Services::FormulaWrapper, enable: T::Boolean).void }
def self.service_load(service, enable:) def self.service_load(service, enable:)
if System.root? && !service.service_startup? if System.root? && !service.service_startup?
opoo "#{service.name} must be run as non-root to start at user login!" opoo "#{service.name} must be run as non-root to start at user login!"
@ -338,22 +335,21 @@ module Homebrew
ohai("Successfully #{function} `#{service.name}` (label: #{service.service_name})") ohai("Successfully #{function} `#{service.name}` (label: #{service.service_name})")
end end
sig { params(service: Services::FormulaWrapper, file: T.nilable(Pathname)).void }
def self.install_service_file(service, file) def self.install_service_file(service, file)
raise UsageError, "Formula `#{service.name}` is not installed" unless service.installed? raise UsageError, "Formula `#{service.name}` is not installed" unless service.installed?
unless T.must(service.service_file).exist? unless service.service_file.exist?
raise UsageError, raise UsageError,
"Formula `#{service.name}` has not implemented #plist, #service or installed a locatable service file" "Formula `#{service.name}` has not implemented #plist, #service or installed a locatable service file"
end end
temp = Tempfile.new(T.must(service.service_name)) temp = Tempfile.new(service.service_name)
temp << if T.must(file).blank? temp << if file.blank?
contents = T.must(service.service_file).read contents = service.service_file.read
if sudo_service_user && Services::System.launchctl? if sudo_service_user && System.launchctl?
# set the username in the new plist file # set the username in the new plist file
ohai "Setting username in #{service.service_name} to #{Services::System.user}" ohai "Setting username in #{service.service_name} to #{System.user}"
plist_data = Plist.parse_xml(contents, marshal: false) plist_data = Plist.parse_xml(contents, marshal: false)
plist_data["UserName"] = sudo_service_user plist_data["UserName"] = sudo_service_user
plist_data.to_plist plist_data.to_plist
@ -361,7 +357,7 @@ module Homebrew
contents contents
end end
else else
T.must(file).read file.read
end end
temp.flush temp.flush
@ -374,7 +370,7 @@ module Homebrew
chmod 0644, service.dest chmod 0644, service.dest
Services::System::Systemctl.run("daemon-reload") if System.systemctl? System::Systemctl.run("daemon-reload") if System.systemctl?
end end
end end
end end

View File

@ -12,8 +12,8 @@ module Homebrew
sig { sig {
params( params(
targets: T::Array[Services::FormulaWrapper], targets: T::Array[Services::FormulaWrapper],
verbose: T.nilable(T::Boolean), verbose: T::Boolean,
json: T.nilable(T::Boolean), json: T::Boolean,
).void ).void
} }
def self.run(targets, verbose:, json:) def self.run(targets, verbose:, json:)
@ -33,7 +33,7 @@ module Homebrew
sig { params(bool: T.nilable(T.any(String, T::Boolean))).returns(String) } sig { params(bool: T.nilable(T.any(String, T::Boolean))).returns(String) }
def self.pretty_bool(bool) def self.pretty_bool(bool)
return T.must(bool).to_s if !$stdout.tty? || Homebrew::EnvConfig.no_emoji? return bool.to_s if !$stdout.tty? || Homebrew::EnvConfig.no_emoji?
if bool if bool
"#{Tty.bold}#{Formatter.success("")}#{Tty.reset}" "#{Tty.bold}#{Formatter.success("")}#{Tty.reset}"
@ -42,7 +42,7 @@ module Homebrew
end end
end end
sig { params(hash: T.untyped, verbose: T.nilable(T::Boolean)).returns(String) } sig { params(hash: T::Hash[Symbol, T.untyped], verbose: T::Boolean).returns(String) }
def self.output(hash, verbose:) def self.output(hash, verbose:)
out = "#{Tty.bold}#{hash[:name]}#{Tty.reset} (#{hash[:service_name]})\n" out = "#{Tty.bold}#{hash[:name]}#{Tty.reset} (#{hash[:service_name]})\n"
out += "Running: #{pretty_bool(hash[:running])}\n" out += "Running: #{pretty_bool(hash[:running])}\n"

View File

@ -9,7 +9,7 @@ module Homebrew
module Kill module Kill
TRIGGERS = %w[kill k].freeze TRIGGERS = %w[kill k].freeze
sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean)).void } sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T::Boolean).void }
def self.run(targets, verbose:) def self.run(targets, verbose:)
Services::Cli.check(targets) Services::Cli.check(targets)
Services::Cli.kill(targets, verbose:) Services::Cli.kill(targets, verbose:)

View File

@ -1,4 +1,4 @@
# typed: strict # typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true # frozen_string_literal: true
require "services/cli" require "services/cli"
@ -29,7 +29,6 @@ module Homebrew
# Print the JSON representation in the CLI # Print the JSON representation in the CLI
# @private # @private
sig { params(formulae: T.untyped).returns(NilClass) }
def self.print_json(formulae) def self.print_json(formulae)
services = formulae.map do |formula| services = formulae.map do |formula|
formula.slice(*JSON_FIELDS) formula.slice(*JSON_FIELDS)
@ -40,7 +39,6 @@ module Homebrew
# Print the table in the CLI # Print the table in the CLI
# @private # @private
sig { params(formulae: T::Array[T::Hash[T.untyped, T.untyped]]).void }
def self.print_table(formulae) def self.print_table(formulae)
services = formulae.map do |formula| services = formulae.map do |formula|
status = T.must(get_status_string(formula[:status])) status = T.must(get_status_string(formula[:status]))
@ -71,7 +69,7 @@ module Homebrew
# Get formula status output # Get formula status output
# @private # @private
sig { params(status: T.anything).returns(T.nilable(String)) } sig { params(status: Symbol).returns(T.nilable(String)) }
def self.get_status_string(status) def self.get_status_string(status)
case status case status
when :started, :scheduled then "#{Tty.green}#{status}#{Tty.reset}" when :started, :scheduled then "#{Tty.green}#{status}#{Tty.reset}"

View File

@ -14,7 +14,7 @@ module Homebrew
TRIGGERS = %w[restart relaunch reload r].freeze TRIGGERS = %w[restart relaunch reload r].freeze
sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean)).returns(NilClass) } sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T::Boolean).void }
def self.run(targets, verbose:) def self.run(targets, verbose:)
Services::Cli.check(targets) Services::Cli.check(targets)
@ -32,7 +32,6 @@ module Homebrew
Services::Cli.run(targets, verbose:) if ran.present? Services::Cli.run(targets, verbose:) if ran.present?
Services::Cli.start(started, verbose:) if started.present? Services::Cli.start(started, verbose:) if started.present?
nil
end end
end end
end end

View File

@ -9,7 +9,7 @@ module Homebrew
module Run module Run
TRIGGERS = ["run"].freeze TRIGGERS = ["run"].freeze
sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean)).void } sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T::Boolean).void }
def self.run(targets, verbose:) def self.run(targets, verbose:)
Services::Cli.check(targets) Services::Cli.check(targets)
Services::Cli.run(targets, verbose:) Services::Cli.run(targets, verbose:)

View File

@ -13,7 +13,7 @@ module Homebrew
params( params(
targets: T::Array[Services::FormulaWrapper], targets: T::Array[Services::FormulaWrapper],
custom_plist: T.nilable(String), custom_plist: T.nilable(String),
verbose: T.nilable(T::Boolean), verbose: T::Boolean,
).void ).void
} }
def self.run(targets, custom_plist, verbose:) def self.run(targets, custom_plist, verbose:)

View File

@ -12,8 +12,8 @@ module Homebrew
sig { sig {
params( params(
targets: T::Array[Services::FormulaWrapper], targets: T::Array[Services::FormulaWrapper],
verbose: T.nilable(T::Boolean), verbose: T::Boolean,
no_wait: T.nilable(T::Boolean), no_wait: T::Boolean,
max_wait: T.nilable(Float), max_wait: T.nilable(Float),
).void ).void
} }

View File

@ -1,4 +1,4 @@
# typed: strict # typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true # frozen_string_literal: true
# Wrapper for a formula to handle service-related stuff like parsing and # Wrapper for a formula to handle service-related stuff like parsing and
@ -25,75 +25,73 @@ module Homebrew
# Initialize a new `Service` instance with supplied formula. # Initialize a new `Service` instance with supplied formula.
sig { params(formula: Formula).void } sig { params(formula: Formula).void }
def initialize(formula) def initialize(formula)
@formula = T.let(formula, Formula) @formula = formula
@service = T.let(@formula.service? || false, T::Boolean)
@service_name = T.let(if System.launchctl?
formula.plist_name
elsif System.systemctl?
formula.service_name
end, T.nilable(String))
@service_file = T.let(if System.launchctl?
formula.launchd_service_path
elsif System.systemctl?
formula.systemd_service_path
end, T.nilable(Pathname))
@service_startup = T.let(
if service?
T.must(load_service).requires_root?
else
false
end, T.nilable(T::Boolean)
)
@name = T.let(formula.name, String)
end end
# Delegate access to `formula.name`. # Delegate access to `formula.name`.
sig { returns(String) } sig { returns(String) }
attr_reader :name def name
@name ||= formula.name
end
# Delegate access to `formula.service?`. # Delegate access to `formula.service?`.
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def service? def service?
@service @service ||= @formula.service?
end end
# Delegate access to `formula.service.timed?`. # Delegate access to `formula.service.timed?`.
# TODO: this should either be T::Boolean or renamed to `timed`
sig { returns(T.nilable(T::Boolean)) } sig { returns(T.nilable(T::Boolean)) }
def timed? def timed?
@timed ||= T.let(service? ? T.must(load_service).timed? : nil, T.nilable(T::Boolean)) @timed ||= (load_service.timed? if service?)
end end
# Delegate access to `formula.service.keep_alive?`.` # Delegate access to `formula.service.keep_alive?`.
# TODO: this should either be T::Boolean or renamed to `keep_alive`
sig { returns(T.nilable(T::Boolean)) } sig { returns(T.nilable(T::Boolean)) }
def keep_alive? def keep_alive?
@keep_alive ||= T.let(T.must(load_service).keep_alive?, T.nilable(T::Boolean)) if service? @keep_alive ||= (load_service.keep_alive? if service?)
end end
# service_name delegates with formula.plist_name or formula.service_name for systemd # service_name delegates with formula.plist_name or formula.service_name
# (e.g., `homebrew.<formula>`). # for systemd (e.g., `homebrew.<formula>`).
sig { returns(T.nilable(String)) } sig { returns(T.nilable(String)) }
attr_reader :service_name def service_name
@service_name ||= if System.launchctl?
formula.plist_name
elsif System.systemctl?
formula.service_name
end
end
# service_file delegates with formula.launchd_service_path or formula.systemd_service_path for systemd. # service_file delegates with formula.launchd_service_path or formula.systemd_service_path for systemd.
sig { returns(T.nilable(Pathname)) } def service_file
attr_reader :service_file @service_file ||= if System.launchctl?
formula.launchd_service_path
elsif System.systemctl?
formula.systemd_service_path
end
end
# Whether the service should be launched at startup # Whether the service should be launched at startup
sig { returns(T.nilable(T::Boolean)) } sig { returns(T::Boolean) }
def service_startup? def service_startup?
@service_startup ||= service? ? T.must(load_service).requires_root? : false @service_startup ||= if service?
load_service.requires_root?
else
false
end
end end
# Path to destination service directory. If run as root, it's `boot_path`, else `user_path`. # Path to destination service directory. If run as root, it's `boot_path`, else `user_path`.
sig { returns(Pathname) }
def dest_dir def dest_dir
System.root? ? T.must(System.boot_path) : T.must(System.user_path) System.root? ? System.boot_path : System.user_path
end end
# Path to destination service. If run as root, it's in `boot_path`, else `user_path`. # Path to destination service. If run as root, it's in `boot_path`, else `user_path`.
sig { returns(Pathname) }
def dest def dest
dest_dir + T.must(service_file).basename dest_dir + service_file.basename
end end
# Returns `true` if any version of the formula is installed. # Returns `true` if any version of the formula is installed.
@ -106,7 +104,7 @@ module Homebrew
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def plist? def plist?
return false unless installed? return false unless installed?
return true if T.must(service_file).file? return true if service_file.file?
return false unless formula.opt_prefix.exist? return false unless formula.opt_prefix.exist?
return true if Keg.for(formula.opt_prefix).plist_installed? return true if Keg.for(formula.opt_prefix).plist_installed?
@ -116,6 +114,7 @@ module Homebrew
end end
# Returns `true` if the service is loaded, else false. # Returns `true` if the service is loaded, else false.
# TODO: this should either be T::Boolean or renamed to `loaded`
sig { params(cached: T::Boolean).returns(T.nilable(T::Boolean)) } sig { params(cached: T::Boolean).returns(T.nilable(T::Boolean)) }
def loaded?(cached: false) def loaded?(cached: false)
if System.launchctl? if System.launchctl?
@ -123,17 +122,18 @@ module Homebrew
_, status_success, = status_output_success_type _, status_success, = status_output_success_type
status_success status_success
elsif System.systemctl? elsif System.systemctl?
System::Systemctl.quiet_run("status", T.must(service_file).basename) System::Systemctl.quiet_run("status", service_file.basename)
end end
end end
# Returns `true` if service is present (e.g. .plist is present in boot or user service path), else `false` # Returns `true` if service is present (e.g. .plist is present in boot or user service path), else `false`
# Accepts Hash option `:for` with values `:root` for boot path or `:user` for user path. # Accepts `type` with values `:root` for boot path or `:user` for user path.
sig { params(opts: T.untyped).returns(T::Boolean) } sig { params(type: T.nilable(Symbol)).returns(T::Boolean) }
def service_file_present?(opts = { for: false }) def service_file_present?(type: nil)
if opts[:for] && opts[:for] == :root case type
when :root
boot_path_service_file_present? boot_path_service_file_present?
elsif opts[:for] && opts[:for] == :user when :user
user_path_service_file_present? user_path_service_file_present?
else else
boot_path_service_file_present? || user_path_service_file_present? boot_path_service_file_present? || user_path_service_file_present?
@ -161,43 +161,34 @@ module Homebrew
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def pid? def pid?
return false if pid.nil? pid.present? && !pid.zero?
!T.must(pid).zero?
end end
sig { returns(T.nilable(T.any(T::Boolean, Integer))) } sig { returns(T::Boolean) }
def error? def error?
return false if pid? || pid.nil? return false if pid?
return exit_code if exit_code.nil?
T.must(exit_code).nonzero? exit_code.present? && exit_code.nonzero?
end end
sig { returns(T.nilable(T::Boolean)) } sig { returns(T::Boolean) }
def unknown_status? def unknown_status?
status_output.blank? && !pid? status_output.blank? && !pid?
end end
# Get current PID of daemon process from status output. # Get current PID of daemon process from status output.
sig { returns(T.nilable(Integer)) }
def pid def pid
status_output, _, status_type = status_output_success_type status_output, _, status_type = status_output_success_type
return if status_type.nil?
Regexp.last_match(1).to_i if status_output =~ pid_regex(status_type) Regexp.last_match(1).to_i if status_output =~ pid_regex(status_type)
end end
# Get current exit code of daemon process from status output. # Get current exit code of daemon process from status output.
sig { returns(T.nilable(Integer)) }
def exit_code def exit_code
status_output, _, status_type = status_output_success_type status_output, _, status_type = status_output_success_type
return if status_type.nil?
Regexp.last_match(1).to_i if status_output =~ exit_code_regex(status_type) Regexp.last_match(1).to_i if status_output =~ exit_code_regex(status_type)
end end
sig { returns(T::Hash[T.untyped, T.untyped]) } sig { returns(T::Hash[Symbol, T.anything]) }
def to_hash def to_hash
hash = { hash = {
name:, name:,
@ -216,15 +207,15 @@ module Homebrew
service = load_service service = load_service
return hash if T.must(service).command.blank? return hash if service.command.blank?
hash[:command] = T.must(service).manual_command hash[:command] = service.manual_command
hash[:working_dir] = T.must(service).working_dir hash[:working_dir] = service.working_dir
hash[:root_dir] = T.must(service).root_dir hash[:root_dir] = service.root_dir
hash[:log_path] = T.must(service).log_path hash[:log_path] = service.log_path
hash[:error_log_path] = T.must(service).error_log_path hash[:error_log_path] = service.error_log_path
hash[:interval] = T.must(service).interval hash[:interval] = service.interval
hash[:cron] = T.must(service).cron hash[:cron] = service.cron
hash hash
end end
@ -234,16 +225,14 @@ module Homebrew
# The purpose of this function is to lazy load the Homebrew::Service class # The purpose of this function is to lazy load the Homebrew::Service class
# and avoid nameclashes with the current Service module. # and avoid nameclashes with the current Service module.
# It should be used instead of calling formula.service directly. # It should be used instead of calling formula.service directly.
sig { returns(T.nilable(Homebrew::Service)) } sig { returns(Homebrew::Service) }
def load_service def load_service
require "formula" require "formula"
formula.service formula.service
end end
sig { returns(T.nilable(T::Array[T.untyped])) }
def status_output_success_type def status_output_success_type
@status_output_success_type ||= T.let(nil, T.nilable(T::Array[T.untyped]))
@status_output_success_type ||= if System.launchctl? @status_output_success_type ||= if System.launchctl?
cmd = [System.launchctl.to_s, "list", service_name] cmd = [System.launchctl.to_s, "list", service_name]
output = Utils.popen_read(*cmd).chomp output = Utils.popen_read(*cmd).chomp
@ -279,7 +268,7 @@ module Homebrew
:started :started
elsif !loaded?(cached: true) elsif !loaded?(cached: true)
:none :none
elsif T.must(exit_code).zero? elsif exit_code.present? && exit_code.zero?
if timed? if timed?
:scheduled :scheduled
else else
@ -294,34 +283,38 @@ module Homebrew
end end
end end
sig { params(status_type: Symbol).returns(Regexp) }
def exit_code_regex(status_type) def exit_code_regex(status_type)
@exit_code_regex ||= T.let({ @exit_code_regex ||= {
launchctl_list: /"LastExitStatus"\ =\ ([0-9]*);/, launchctl_list: /"LastExitStatus"\ =\ ([0-9]*);/,
launchctl_print: /last exit code = ([0-9]+)/, launchctl_print: /last exit code = ([0-9]+)/,
systemctl: /\(code=exited, status=([0-9]*)\)|\(dead\)/, systemctl: /\(code=exited, status=([0-9]*)\)|\(dead\)/,
}, T.nilable(T::Hash[T.untyped, Regexp])) }
@exit_code_regex.fetch(status_type) @exit_code_regex.fetch(status_type)
end end
sig { params(status_type: Symbol).returns(Regexp) }
def pid_regex(status_type) def pid_regex(status_type)
@pid_regex ||= T.let({ @pid_regex ||= {
launchctl_list: /"PID"\ =\ ([0-9]*);/, launchctl_list: /"PID"\ =\ ([0-9]*);/,
launchctl_print: /pid = ([0-9]+)/, launchctl_print: /pid = ([0-9]+)/,
systemctl: /Main PID: ([0-9]*) \((?!code=)/, systemctl: /Main PID: ([0-9]*) \((?!code=)/,
}, T.nilable(T::Hash[T.untyped, Regexp])) }
@pid_regex.fetch(status_type) @pid_regex.fetch(status_type)
end end
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def boot_path_service_file_present? def boot_path_service_file_present?
(T.must(System.boot_path) + T.must(service_file).basename).exist? boot_path = System.boot_path
return false if boot_path.blank?
(boot_path + service_file.basename).exist?
end end
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def user_path_service_file_present? def user_path_service_file_present?
(T.must(System.user_path) + T.must(service_file).basename).exist? user_path = System.user_path
return false if user_path.blank?
(user_path + service_file.basename).exist?
end end
sig { returns(Regexp) } sig { returns(Regexp) }

View File

@ -1,4 +1,4 @@
# typed: strict # typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true # frozen_string_literal: true
require "services/formula_wrapper" require "services/formula_wrapper"
@ -24,7 +24,6 @@ module Homebrew
end end
# List all available services with status, user, and path to the file. # List all available services with status, user, and path to the file.
sig { returns(T::Array[T::Hash[T.untyped, T.untyped]]) }
def self.services_list def self.services_list
available_services.map(&:to_hash) available_services.map(&:to_hash)
end end

View File

@ -1,4 +1,4 @@
# typed: strict # typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true # frozen_string_literal: true
module Homebrew module Homebrew
@ -20,22 +20,18 @@ module Homebrew
System.root? ? "--system" : "--user" System.root? ? "--system" : "--user"
end end
sig { params(args: T.nilable(T.any(String, Pathname))).void }
def self.run(*args) def self.run(*args)
_run(*args, mode: :default) _run(*args, mode: :default)
end end
sig { params(args: T.nilable(T.any(String, Pathname))).returns(T::Boolean) }
def self.quiet_run(*args) def self.quiet_run(*args)
_run(*args, mode: :quiet) _run(*args, mode: :quiet)
end end
sig { params(args: T.nilable(T.any(String, Pathname))).returns(String) }
def self.popen_read(*args) def self.popen_read(*args)
_run(*args, mode: :read) _run(*args, mode: :read)
end end
sig { params(args: T.nilable(T.any(String, Pathname)), mode: T.nilable(Symbol)).returns(T.untyped) }
private_class_method def self._run(*args, mode:) private_class_method def self._run(*args, mode:)
require "system_command" require "system_command"
result = SystemCommand.run(executable, result = SystemCommand.run(executable,

View File

@ -21,7 +21,7 @@ RSpec.describe Homebrew::Services::Commands::Restart do
expect(Homebrew::Services::Cli).not_to receive(:stop) expect(Homebrew::Services::Cli).not_to receive(:stop)
expect(Homebrew::Services::Cli).to receive(:start).once expect(Homebrew::Services::Cli).to receive(:start).once
service = instance_double(Homebrew::Services::FormulaWrapper, service_name: "name", loaded?: false) service = instance_double(Homebrew::Services::FormulaWrapper, service_name: "name", loaded?: false)
expect(described_class.run([service], verbose: false)).to be_nil expect { described_class.run([service], verbose: false) }.not_to raise_error
end end
it "starts if services are loaded with file" do it "starts if services are loaded with file" do
@ -30,7 +30,7 @@ RSpec.describe Homebrew::Services::Commands::Restart do
expect(Homebrew::Services::Cli).to receive(:stop).once expect(Homebrew::Services::Cli).to receive(:stop).once
service = instance_double(Homebrew::Services::FormulaWrapper, service_name: "name", loaded?: true, service = instance_double(Homebrew::Services::FormulaWrapper, service_name: "name", loaded?: true,
service_file_present?: true) service_file_present?: true)
expect(described_class.run([service], verbose: false)).to be_nil expect { described_class.run([service], verbose: false) }.not_to raise_error
end end
it "runs if services are loaded without file" do it "runs if services are loaded without file" do
@ -39,7 +39,7 @@ service_file_present?: true)
expect(Homebrew::Services::Cli).to receive(:stop).once expect(Homebrew::Services::Cli).to receive(:stop).once
service = instance_double(Homebrew::Services::FormulaWrapper, service_name: "name", loaded?: true, service = instance_double(Homebrew::Services::FormulaWrapper, service_name: "name", loaded?: true,
service_file_present?: false) service_file_present?: false)
expect(described_class.run([service], verbose: false)).to be_nil expect { described_class.run([service], verbose: false) }.not_to raise_error
end end
end end
end end

View File

@ -201,12 +201,12 @@ RSpec.describe Homebrew::Services::FormulaWrapper do
it "macOS - outputs if the service file is present for root" do it "macOS - outputs if the service file is present for root" do
allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false) allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false)
expect(service.service_file_present?(for: :root)).to be(false) expect(service.service_file_present?(type: :root)).to be(false)
end end
it "macOS - outputs if the service file is present for user" do it "macOS - outputs if the service file is present for user" do
allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false) allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false)
expect(service.service_file_present?(for: :user)).to be(false) expect(service.service_file_present?(type: :user)).to be(false)
end end
end end
@ -224,13 +224,13 @@ RSpec.describe Homebrew::Services::FormulaWrapper do
end end
end end
describe "#pid" do describe "#pid", :needs_systemd do
it "outputs nil because there is not pid" do it "outputs nil because there is not pid" do
expect(service.pid).to be_nil expect(service.pid).to be_nil
end end
end end
describe "#error?" do describe "#error?", :needs_systemd do
it "outputs false because there a no PID" do it "outputs false because there a no PID" do
allow(service).to receive(:pid).and_return(nil) allow(service).to receive(:pid).and_return(nil)
expect(service.error?).to be(false) expect(service.error?).to be(false)
@ -242,13 +242,13 @@ RSpec.describe Homebrew::Services::FormulaWrapper do
end end
end end
describe "#exit_code" do describe "#exit_code", :needs_systemd do
it "outputs nil because there is no exit code" do it "outputs nil because there is no exit code" do
expect(service.exit_code).to be_nil expect(service.exit_code).to be_nil
end end
end end
describe "#unknown_status?" do describe "#unknown_status?", :needs_systemd do
it "outputs true because there is no PID" do it "outputs true because there is no PID" do
expect(service.unknown_status?).to be(true) expect(service.unknown_status?).to be(true)
end end