Move Services module to Homebrew namespace

This commit is contained in:
Bo Anderson 2025-03-14 04:35:30 +00:00 committed by Mike McQuaid
parent 25b9000c77
commit 02cd7a63c8
No known key found for this signature in database
23 changed files with 1162 additions and 1088 deletions

View File

@ -3,7 +3,6 @@
require "abstract_command"
require "services/system"
require "services/cli"
require "services/commands/list"
require "services/commands/cleanup"
require "services/commands/info"
@ -73,36 +72,40 @@ module Homebrew
# Keep this after the .parse to keep --help fast.
require "utils"
if !::Services::System.launchctl? && !::Services::System.systemctl?
if !Homebrew::Services::System.launchctl? && !Homebrew::Services::System.systemctl?
raise UsageError,
"`brew services` is supported only on macOS or Linux (with systemd)!"
end
if (sudo_service_user = args.sudo_service_user)
unless ::Services::System.root?
unless Homebrew::Services::System.root?
raise UsageError,
"`brew services` is supported only when running as root!"
end
unless ::Services::System.launchctl?
unless Homebrew::Services::System.launchctl?
raise UsageError,
"`brew services --sudo-service-user` is currently supported only on macOS " \
"(but we'd love a PR to add Linux support)!"
end
::Services::Cli.sudo_service_user = sudo_service_user
Homebrew::Services::Cli.sudo_service_user = sudo_service_user
end
# Parse arguments.
subcommand, formula, = args.named
if [*::Services::Commands::List::TRIGGERS, *::Services::Commands::Cleanup::TRIGGERS].include?(subcommand)
no_named_formula_commands = [
*Homebrew::Services::Commands::List::TRIGGERS,
*Homebrew::Services::Commands::Cleanup::TRIGGERS,
]
if no_named_formula_commands.include?(subcommand)
raise UsageError, "The `#{subcommand}` subcommand does not accept a formula argument!" if formula
raise UsageError, "The `#{subcommand}` subcommand does not accept the --all argument!" if args.all?
end
if args.file
if ::Services::Commands::Start::TRIGGERS.exclude?(subcommand)
if Homebrew::Services::Commands::Start::TRIGGERS.exclude?(subcommand)
raise UsageError, "The `#{subcommand}` subcommand does not accept the --file= argument!"
elsif args.all?
raise UsageError, "The start subcommand does not accept the --all and --file= arguments at the same time!"
@ -113,14 +116,20 @@ module Homebrew
targets = if args.all?
if subcommand == "start"
::Services::Formulae.available_services(loaded: false, skip_root: !::Services::System.root?)
Homebrew::Services::Formulae.available_services(
loaded: false,
skip_root: !Homebrew::Services::System.root?,
)
elsif subcommand == "stop"
::Services::Formulae.available_services(loaded: true, skip_root: !::Services::System.root?)
Homebrew::Services::Formulae.available_services(
loaded: true,
skip_root: !Homebrew::Services::System.root?,
)
else
::Services::Formulae.available_services
Homebrew::Services::Formulae.available_services
end
elsif formula
[::Services::FormulaWrapper.new(Formulary.factory(formula))]
[Homebrew::Services::FormulaWrapper.new(Formulary.factory(formula))]
else
[]
end
@ -128,30 +137,30 @@ module Homebrew
# Exit successfully if --all was used but there is nothing to do
return if args.all? && targets.empty?
if ::Services::System.systemctl?
if Homebrew::Services::System.systemctl?
ENV["DBUS_SESSION_BUS_ADDRESS"] = ENV.fetch("HOMEBREW_DBUS_SESSION_BUS_ADDRESS", nil)
ENV["XDG_RUNTIME_DIR"] = ENV.fetch("HOMEBREW_XDG_RUNTIME_DIR", nil)
end
# Dispatch commands and aliases.
case subcommand.presence
when *::Services::Commands::List::TRIGGERS
::Services::Commands::List.run(json: args.json?)
when *::Services::Commands::Cleanup::TRIGGERS
::Services::Commands::Cleanup.run
when *::Services::Commands::Info::TRIGGERS
::Services::Commands::Info.run(targets, verbose: args.verbose?, json: args.json?)
when *::Services::Commands::Restart::TRIGGERS
::Services::Commands::Restart.run(targets, verbose: args.verbose?)
when *::Services::Commands::Run::TRIGGERS
::Services::Commands::Run.run(targets, verbose: args.verbose?)
when *::Services::Commands::Start::TRIGGERS
::Services::Commands::Start.run(targets, args.file, verbose: args.verbose?)
when *::Services::Commands::Stop::TRIGGERS
when *Homebrew::Services::Commands::List::TRIGGERS
Homebrew::Services::Commands::List.run(json: args.json?)
when *Homebrew::Services::Commands::Cleanup::TRIGGERS
Homebrew::Services::Commands::Cleanup.run
when *Homebrew::Services::Commands::Info::TRIGGERS
Homebrew::Services::Commands::Info.run(targets, verbose: args.verbose?, json: args.json?)
when *Homebrew::Services::Commands::Restart::TRIGGERS
Homebrew::Services::Commands::Restart.run(targets, verbose: args.verbose?)
when *Homebrew::Services::Commands::Run::TRIGGERS
Homebrew::Services::Commands::Run.run(targets, verbose: args.verbose?)
when *Homebrew::Services::Commands::Start::TRIGGERS
Homebrew::Services::Commands::Start.run(targets, args.file, verbose: args.verbose?)
when *Homebrew::Services::Commands::Stop::TRIGGERS
max_wait = args.max_wait.to_f
::Services::Commands::Stop.run(targets, verbose: args.verbose?, no_wait: args.no_wait?, max_wait:)
when *::Services::Commands::Kill::TRIGGERS
::Services::Commands::Kill.run(targets, verbose: args.verbose?)
Homebrew::Services::Commands::Stop.run(targets, verbose: args.verbose?, no_wait: args.no_wait?, max_wait:)
when *Homebrew::Services::Commands::Kill::TRIGGERS
Homebrew::Services::Commands::Kill.run(targets, verbose: args.verbose?)
else
raise UsageError, "unknown subcommand: `#{subcommand}`"
end

View File

@ -1,368 +1,381 @@
# typed: strict
# frozen_string_literal: true
module Services
module Cli
extend FileUtils
require "services/formula_wrapper"
sig { returns(T.nilable(String)) }
def self.sudo_service_user
@sudo_service_user
end
module Homebrew
module Services
module Cli
extend FileUtils
sig { params(sudo_service_user: String).void }
def self.sudo_service_user=(sudo_service_user)
@sudo_service_user = T.let(sudo_service_user, T.nilable(String))
end
# Binary name.
sig { returns(String) }
def self.bin
"brew services"
end
# Find all currently running services via launchctl list or systemctl list-units.
sig { returns(T::Array[String]) }
def self.running
if System.launchctl?
Utils.popen_read(System.launchctl, "list")
else
System::Systemctl.popen_read("list-units",
"--type=service",
"--state=running",
"--no-pager",
"--no-legend")
end.chomp.split("\n").filter_map do |svc|
Regexp.last_match(0) if svc =~ /homebrew(?>\.mxcl)?\.([\w+-.@]+)/
sig { returns(T.nilable(String)) }
def self.sudo_service_user
@sudo_service_user
end
end
# Check if formula has been found.
sig { params(targets: T.untyped).returns(TrueClass) }
def self.check(targets)
raise UsageError, "Formula(e) missing, please provide a formula name or use --all" if targets.empty?
sig { params(sudo_service_user: String).void }
def self.sudo_service_user=(sudo_service_user)
@sudo_service_user = T.let(sudo_service_user, T.nilable(String))
end
true
end
# Binary name.
sig { returns(String) }
def self.bin
"brew services"
end
# Kill services that don't have a service file
sig { returns(T::Array[Services::FormulaWrapper]) }
def self.kill_orphaned_services
cleaned_labels = []
cleaned_services = []
running.each do |label|
if (service = FormulaWrapper.from(label))
unless service.dest.file?
cleaned_labels << label
cleaned_services << service
end
# Find all currently running services via launchctl list or systemctl list-units.
sig { returns(T::Array[String]) }
def self.running
if System.launchctl?
Utils.popen_read(System.launchctl, "list")
else
opoo "Service #{label} not managed by `#{bin}` => skipping"
System::Systemctl.popen_read("list-units",
"--type=service",
"--state=running",
"--no-pager",
"--no-legend")
end.chomp.split("\n").filter_map do |svc|
Regexp.last_match(0) if svc =~ /homebrew(?>\.mxcl)?\.([\w+-.@]+)/
end
end
kill(cleaned_services)
cleaned_labels
end
sig { returns(T::Array[T.untyped]) }
def self.remove_unused_service_files
cleaned = []
Dir["#{System.path}homebrew.*.{plist,service}"].each do |file|
next if running.include?(File.basename(file).sub(/\.(plist|service)$/i, ""))
# Check if formula has been found.
sig { params(targets: T.untyped).returns(TrueClass) }
def self.check(targets)
raise UsageError, "Formula(e) missing, please provide a formula name or use --all" if targets.empty?
puts "Removing unused service file #{file}"
rm file
cleaned << file
true
end
cleaned
end
# 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 }
def self.run(targets, verbose: false)
targets.each do |service|
if service.pid?
puts "Service `#{service.name}` already running, use `#{bin} restart #{service.name}` to restart."
next
elsif System.root?
puts "Service `#{service.name}` cannot be run (but can be started) as root."
next
end
service_load(service, enable: false)
end
end
# Start a service.
sig {
params(targets: T::Array[Services::FormulaWrapper], service_file: T.nilable(T.any(String, Pathname)),
verbose: T.nilable(T::Boolean)).void
}
def self.start(targets, service_file = nil, verbose: false)
file = T.let(nil, T.nilable(Pathname))
if service_file.present?
file = Pathname.new service_file
raise UsageError, "Provided service file does not exist" unless file.exist?
end
targets.each do |service|
if service.pid?
puts "Service `#{service.name}` already started, use `#{bin} restart #{service.name}` to restart."
next
end
odie "Formula `#{service.name}` is not installed." unless service.installed?
file ||= if T.must(service.service_file).exist? || System.systemctl?
nil
elsif service.formula.opt_prefix.exist? && (keg = Keg.for service.formula.opt_prefix) && keg.plist_installed?
service_file = Dir["#{keg}/*#{T.must(service.service_file).extname}"].first
Pathname.new service_file if service_file.present?
end
install_service_file(service, file)
if file.blank? && verbose
ohai "Generated service file for #{service.formula.name}:"
puts " #{service.dest.read.gsub("\n", "\n ")}"
puts
end
next if take_root_ownership(service).nil? && System.root?
service_load(service, enable: true)
end
end
# Stop a service and unload it.
sig {
params(targets: T::Array[Services::FormulaWrapper],
verbose: T.nilable(T::Boolean),
no_wait: T.nilable(T::Boolean),
max_wait: T.nilable(T.any(Integer, Float))).void
}
def self.stop(targets, verbose: false, no_wait: false, max_wait: 0)
targets.each do |service|
unless service.loaded?
rm service.dest if service.dest.exist? # get rid of installed service file anyway, dude
if service.service_file_present?
odie <<~EOS
Service `#{service.name}` is started as `#{service.owner}`. Try:
#{"sudo " unless System.root?}#{bin} stop #{service.name}
EOS
elsif System.launchctl? &&
quiet_system(System.launchctl, "bootout", "#{System.domain_target}/#{service.service_name}")
ohai "Successfully stopped `#{service.name}` (label: #{service.service_name})"
# Kill services that don't have a service file
sig { returns(T::Array[Services::FormulaWrapper]) }
def self.kill_orphaned_services
cleaned_labels = []
cleaned_services = []
running.each do |label|
if (service = FormulaWrapper.from(label))
unless service.dest.file?
cleaned_labels << label
cleaned_services << service
end
else
opoo "Service `#{service.name}` is not started."
opoo "Service #{label} not managed by `#{bin}` => skipping"
end
next
end
kill(cleaned_services)
cleaned_labels
end
sig { returns(T::Array[T.untyped]) }
def self.remove_unused_service_files
cleaned = []
Dir["#{System.path}homebrew.*.{plist,service}"].each do |file|
next if running.include?(File.basename(file).sub(/\.(plist|service)$/i, ""))
puts "Removing unused service file #{file}"
rm file
cleaned << file
end
systemctl_args = []
if no_wait
systemctl_args << "--no-block"
puts "Stopping `#{service.name}`..."
else
puts "Stopping `#{service.name}`... (might take a while)"
cleaned
end
# 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 }
def self.run(targets, verbose: false)
targets.each do |service|
if service.pid?
puts "Service `#{service.name}` already running, use `#{bin} restart #{service.name}` to restart."
next
elsif System.root?
puts "Service `#{service.name}` cannot be run (but can be started) as root."
next
end
service_load(service, enable: false)
end
end
# Start a service.
sig {
params(
targets: T::Array[Services::FormulaWrapper],
service_file: T.nilable(T.any(String, Pathname)),
verbose: T.nilable(T::Boolean),
).void
}
def self.start(targets, service_file = nil, verbose: false)
file = T.let(nil, T.nilable(Pathname))
if service_file.present?
file = Pathname.new service_file
raise UsageError, "Provided service file does not exist" unless file.exist?
end
targets.each do |service|
if service.pid?
puts "Service `#{service.name}` already started, use `#{bin} restart #{service.name}` to restart."
next
end
odie "Formula `#{service.name}` is not installed." unless service.installed?
file ||= if T.must(service.service_file).exist? || System.systemctl?
nil
elsif service.formula.opt_prefix.exist? &&
(keg = Keg.for service.formula.opt_prefix) && keg.plist_installed?
service_file = Dir["#{keg}/*#{T.must(service.service_file).extname}"].first
Pathname.new service_file if service_file.present?
end
install_service_file(service, file)
if file.blank? && verbose
ohai "Generated service file for #{service.formula.name}:"
puts " #{service.dest.read.gsub("\n", "\n ")}"
puts
end
next if take_root_ownership(service).nil? && System.root?
service_load(service, enable: true)
end
end
# Stop a service and unload it.
sig {
params(
targets: T::Array[Services::FormulaWrapper],
verbose: T.nilable(T::Boolean),
no_wait: T.nilable(T::Boolean),
max_wait: T.nilable(T.any(Integer, Float)),
).void
}
def self.stop(targets, verbose: false, no_wait: false, max_wait: 0)
targets.each do |service|
unless service.loaded?
rm service.dest if service.dest.exist? # get rid of installed service file anyway, dude
if service.service_file_present?
odie <<~EOS
Service `#{service.name}` is started as `#{service.owner}`. Try:
#{"sudo " unless System.root?}#{bin} stop #{service.name}
EOS
elsif System.launchctl? &&
quiet_system(System.launchctl, "bootout", "#{System.domain_target}/#{service.service_name}")
ohai "Successfully stopped `#{service.name}` (label: #{service.service_name})"
else
opoo "Service `#{service.name}` is not started."
end
next
end
systemctl_args = []
if no_wait
systemctl_args << "--no-block"
puts "Stopping `#{service.name}`..."
else
puts "Stopping `#{service.name}`... (might take a while)"
end
if System.systemctl?
System::Systemctl.quiet_run(*systemctl_args, "disable", "--now", service.service_name)
elsif System.launchctl?
quiet_system System.launchctl, "bootout", "#{System.domain_target}/#{service.service_name}"
unless no_wait
time_slept = 0
sleep_time = 1
max_wait = T.must(max_wait)
while ($CHILD_STATUS.to_i == 9216 || service.loaded?) && (max_wait.zero? || time_slept < max_wait)
sleep(sleep_time)
time_slept += sleep_time
quiet_system System.launchctl, "bootout", "#{System.domain_target}/#{service.service_name}"
end
end
quiet_system System.launchctl, "stop", "#{System.domain_target}/#{service.service_name}" if service.pid?
end
rm service.dest if service.dest.exist?
# Run daemon-reload on systemctl to finish unloading stopped and deleted service.
System::Systemctl.run(*systemctl_args, "daemon-reload") if System.systemctl?
if service.pid? || service.loaded?
opoo "Unable to stop `#{service.name}` (label: #{service.service_name})"
else
ohai "Successfully stopped `#{service.name}` (label: #{service.service_name})"
end
end
end
# Stop a service but keep it registered.
sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean)).void }
def self.kill(targets, verbose: false)
targets.each do |service|
if !service.pid?
puts "Service `#{service.name}` is not started."
elsif service.keep_alive?
puts "Service `#{service.name}` is set to automatically restart and can't be killed."
else
puts "Killing `#{service.name}`... (might take a while)"
if System.systemctl?
System::Systemctl.quiet_run("stop", service.service_name)
elsif System.launchctl?
quiet_system System.launchctl, "stop", "#{System.domain_target}/#{service.service_name}"
end
if service.pid?
opoo "Unable to kill `#{service.name}` (label: #{service.service_name})"
else
ohai "Successfully killed `#{service.name}` (label: #{service.service_name})"
end
end
end
end
# protections to avoid users editing root services
sig { params(service: T.untyped).returns(T.nilable(Integer)) }
def self.take_root_ownership(service)
return unless System.root?
return if sudo_service_user
root_paths = T.let([], T::Array[Pathname])
if System.systemctl?
System::Systemctl.quiet_run(*systemctl_args, "disable", "--now", service.service_name)
group = "root"
elsif System.launchctl?
quiet_system System.launchctl, "bootout", "#{System.domain_target}/#{service.service_name}"
unless no_wait
time_slept = 0
sleep_time = 1
max_wait = T.must(max_wait)
while ($CHILD_STATUS.to_i == 9216 || service.loaded?) && (max_wait.zero? || time_slept < max_wait)
sleep(sleep_time)
time_slept += sleep_time
quiet_system System.launchctl, "bootout", "#{System.domain_target}/#{service.service_name}"
group = "admin"
chown "root", group, service.dest
plist_data = service.dest.read
plist = begin
Plist.parse_xml(plist_data, marshal: false)
rescue
nil
end
return unless plist
program_location = plist["ProgramArguments"]&.first
key = "first ProgramArguments value"
if program_location.blank?
program_location = plist["Program"]
key = "Program"
end
if program_location.present?
Dir.chdir("/") do
if File.exist?(program_location)
program_location_path = Pathname(program_location).realpath
root_paths += [
program_location_path,
program_location_path.parent.realpath,
]
else
opoo <<~EOS
#{service.name}: the #{key} does not exist:
#{program_location}
EOS
end
end
end
quiet_system System.launchctl, "stop", "#{System.domain_target}/#{service.service_name}" if service.pid?
end
if (formula = service.formula)
root_paths += [
formula.opt_prefix,
formula.linked_keg,
formula.bin,
formula.sbin,
]
end
root_paths = root_paths.sort.uniq.select(&:exist?)
opoo <<~EOS
Taking root:#{group} ownership of some #{service.formula} paths:
#{root_paths.join("\n ")}
This will require manual removal of these paths using `sudo rm` on
brew upgrade/reinstall/uninstall.
EOS
chown "root", group, root_paths
chmod "+t", root_paths
end
sig {
params(
service: Services::FormulaWrapper,
file: T.nilable(T.any(String, Pathname)),
enable: T.nilable(T::Boolean),
).void
}
def self.launchctl_load(service, file:, enable:)
safe_system System.launchctl, "enable", "#{System.domain_target}/#{service.service_name}" if enable
safe_system System.launchctl, "bootstrap", System.domain_target, file
end
sig { params(service: Services::FormulaWrapper, enable: T.nilable(T::Boolean)).void }
def self.systemd_load(service, enable:)
System::Systemctl.run("start", T.must(service.service_name))
System::Systemctl.run("enable", T.must(service.service_name)) if enable
end
sig { params(service: Services::FormulaWrapper, enable: T.nilable(T::Boolean)).void }
def self.service_load(service, enable:)
if System.root? && !service.service_startup?
opoo "#{service.name} must be run as non-root to start at user login!"
elsif !System.root? && service.service_startup?
opoo "#{service.name} must be run as root to start at system startup!"
end
if System.launchctl?
file = enable ? service.dest : service.service_file
launchctl_load(service, file:, enable:)
elsif System.systemctl?
# Systemctl loads based upon location so only install service
# file when it is not installed. Used with the `run` command.
install_service_file(service, nil) unless service.dest.exist?
systemd_load(service, enable:)
end
function = enable ? "started" : "ran"
ohai("Successfully #{function} `#{service.name}` (label: #{service.service_name})")
end
sig { params(service: Services::FormulaWrapper, file: T.nilable(Pathname)).void }
def self.install_service_file(service, file)
raise UsageError, "Formula `#{service.name}` is not installed" unless service.installed?
unless T.must(service.service_file).exist?
raise UsageError,
"Formula `#{service.name}` has not implemented #plist, #service or installed a locatable service file"
end
temp = Tempfile.new(T.must(service.service_name))
temp << if T.must(file).blank?
contents = T.must(service.service_file).read
if sudo_service_user && Services::System.launchctl?
# set the username in the new plist file
ohai "Setting username in #{service.service_name} to #{Services::System.user}"
plist_data = Plist.parse_xml(contents, marshal: false)
plist_data["UserName"] = sudo_service_user
plist_data.to_plist
else
contents
end
else
T.must(file).read
end
temp.flush
rm service.dest if service.dest.exist?
# Run daemon-reload on systemctl to finish unloading stopped and deleted service.
System::Systemctl.run(*systemctl_args, "daemon-reload") if System.systemctl?
service.dest_dir.mkpath unless service.dest_dir.directory?
cp T.must(temp.path), service.dest
if service.pid? || service.loaded?
opoo "Unable to stop `#{service.name}` (label: #{service.service_name})"
else
ohai "Successfully stopped `#{service.name}` (label: #{service.service_name})"
end
# Clear tempfile.
temp.close
chmod 0644, service.dest
Services::System::Systemctl.run("daemon-reload") if System.systemctl?
end
end
# Stop a service but keep it registered.
sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean)).void }
def self.kill(targets, verbose: false)
targets.each do |service|
if !service.pid?
puts "Service `#{service.name}` is not started."
elsif service.keep_alive?
puts "Service `#{service.name}` is set to automatically restart and can't be killed."
else
puts "Killing `#{service.name}`... (might take a while)"
if System.systemctl?
System::Systemctl.quiet_run("stop", service.service_name)
elsif System.launchctl?
quiet_system System.launchctl, "stop", "#{System.domain_target}/#{service.service_name}"
end
if service.pid?
opoo "Unable to kill `#{service.name}` (label: #{service.service_name})"
else
ohai "Successfully killed `#{service.name}` (label: #{service.service_name})"
end
end
end
end
# protections to avoid users editing root services
sig { params(service: T.untyped).returns(T.nilable(Integer)) }
def self.take_root_ownership(service)
return unless System.root?
return if sudo_service_user
root_paths = T.let([], T::Array[Pathname])
if System.systemctl?
group = "root"
elsif System.launchctl?
group = "admin"
chown "root", group, service.dest
plist_data = service.dest.read
plist = begin
Plist.parse_xml(plist_data, marshal: false)
rescue
nil
end
return unless plist
program_location = plist["ProgramArguments"]&.first
key = "first ProgramArguments value"
if program_location.blank?
program_location = plist["Program"]
key = "Program"
end
if program_location.present?
Dir.chdir("/") do
if File.exist?(program_location)
program_location_path = Pathname(program_location).realpath
root_paths += [
program_location_path,
program_location_path.parent.realpath,
]
else
opoo <<~EOS
#{service.name}: the #{key} does not exist:
#{program_location}
EOS
end
end
end
end
if (formula = service.formula)
root_paths += [
formula.opt_prefix,
formula.linked_keg,
formula.bin,
formula.sbin,
]
end
root_paths = root_paths.sort.uniq.select(&:exist?)
opoo <<~EOS
Taking root:#{group} ownership of some #{service.formula} paths:
#{root_paths.join("\n ")}
This will require manual removal of these paths using `sudo rm` on
brew upgrade/reinstall/uninstall.
EOS
chown "root", group, root_paths
chmod "+t", root_paths
end
sig {
params(service: Services::FormulaWrapper, file: T.nilable(T.any(String, Pathname)),
enable: T.nilable(T::Boolean)).void
}
def self.launchctl_load(service, file:, enable:)
safe_system System.launchctl, "enable", "#{System.domain_target}/#{service.service_name}" if enable
safe_system System.launchctl, "bootstrap", System.domain_target, file
end
sig { params(service: Services::FormulaWrapper, enable: T.nilable(T::Boolean)).void }
def self.systemd_load(service, enable:)
System::Systemctl.run("start", T.must(service.service_name))
System::Systemctl.run("enable", T.must(service.service_name)) if enable
end
sig { params(service: Services::FormulaWrapper, enable: T.nilable(T::Boolean)).void }
def self.service_load(service, enable:)
if System.root? && !service.service_startup?
opoo "#{service.name} must be run as non-root to start at user login!"
elsif !System.root? && service.service_startup?
opoo "#{service.name} must be run as root to start at system startup!"
end
if System.launchctl?
file = enable ? service.dest : service.service_file
launchctl_load(service, file:, enable:)
elsif System.systemctl?
# Systemctl loads based upon location so only install service
# file when it is not installed. Used with the `run` command.
install_service_file(service, nil) unless service.dest.exist?
systemd_load(service, enable:)
end
function = enable ? "started" : "ran"
ohai("Successfully #{function} `#{service.name}` (label: #{service.service_name})")
end
sig { params(service: Services::FormulaWrapper, file: T.nilable(Pathname)).void }
def self.install_service_file(service, file)
raise UsageError, "Formula `#{service.name}` is not installed" unless service.installed?
unless T.must(service.service_file).exist?
raise UsageError,
"Formula `#{service.name}` has not implemented #plist, #service or installed a locatable service file"
end
temp = Tempfile.new(T.must(service.service_name))
temp << if T.must(file).blank?
contents = T.must(service.service_file).read
if sudo_service_user && Services::System.launchctl?
# set the username in the new plist file
ohai "Setting username in #{service.service_name} to #{Services::System.user}"
plist_data = Plist.parse_xml(contents, marshal: false)
plist_data["UserName"] = sudo_service_user
plist_data.to_plist
else
contents
end
else
T.must(file).read
end
temp.flush
rm service.dest if service.dest.exist?
service.dest_dir.mkpath unless service.dest_dir.directory?
cp T.must(temp.path), service.dest
# Clear tempfile.
temp.close
chmod 0644, service.dest
Services::System::Systemctl.run("daemon-reload") if System.systemctl?
end
end
end

View File

@ -1,19 +1,23 @@
# typed: strict
# frozen_string_literal: true
module Services
module Commands
module Cleanup
TRIGGERS = %w[cleanup clean cl rm].freeze
require "services/cli"
sig { void }
def self.run
cleaned = []
module Homebrew
module Services
module Commands
module Cleanup
TRIGGERS = %w[cleanup clean cl rm].freeze
cleaned += Services::Cli.kill_orphaned_services
cleaned += Services::Cli.remove_unused_service_files
sig { void }
def self.run
cleaned = []
puts "All #{System.root? ? "root" : "user-space"} services OK, nothing cleaned..." if cleaned.empty?
cleaned += Services::Cli.kill_orphaned_services
cleaned += Services::Cli.remove_unused_service_files
puts "All #{System.root? ? "root" : "user-space"} services OK, nothing cleaned..." if cleaned.empty?
end
end
end
end

View File

@ -1,65 +1,69 @@
# typed: strict
# frozen_string_literal: true
require "services/formula_wrapper"
require "services/cli"
module Services
module Commands
module Info
TRIGGERS = %w[info i].freeze
module Homebrew
module Services
module Commands
module Info
TRIGGERS = %w[info i].freeze
sig {
params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean),
json: T.nilable(T::Boolean)).void
}
def self.run(targets, verbose:, json:)
Services::Cli.check(targets)
sig {
params(
targets: T::Array[Services::FormulaWrapper],
verbose: T.nilable(T::Boolean),
json: T.nilable(T::Boolean),
).void
}
def self.run(targets, verbose:, json:)
Services::Cli.check(targets)
output = targets.map(&:to_hash)
output = targets.map(&:to_hash)
if json
puts JSON.pretty_generate(output)
return
if json
puts JSON.pretty_generate(output)
return
end
output.each do |hash|
puts output(hash, verbose:)
end
end
output.each do |hash|
puts output(hash, verbose:)
sig { params(bool: T.nilable(T.any(String, T::Boolean))).returns(String) }
def self.pretty_bool(bool)
return T.must(bool).to_s if !$stdout.tty? || Homebrew::EnvConfig.no_emoji?
if bool
"#{Tty.bold}#{Formatter.success("")}#{Tty.reset}"
else
"#{Tty.bold}#{Formatter.error("")}#{Tty.reset}"
end
end
end
sig { params(bool: T.nilable(T.any(String, T::Boolean))).returns(String) }
def self.pretty_bool(bool)
return T.must(bool).to_s if !$stdout.tty? || Homebrew::EnvConfig.no_emoji?
sig { params(hash: T.untyped, verbose: T.nilable(T::Boolean)).returns(String) }
def self.output(hash, verbose:)
out = "#{Tty.bold}#{hash[:name]}#{Tty.reset} (#{hash[:service_name]})\n"
out += "Running: #{pretty_bool(hash[:running])}\n"
out += "Loaded: #{pretty_bool(hash[:loaded])}\n"
out += "Schedulable: #{pretty_bool(hash[:schedulable])}\n"
out += "User: #{hash[:user]}\n" unless hash[:pid].nil?
out += "PID: #{hash[:pid]}\n" unless hash[:pid].nil?
return out unless verbose
if bool
"#{Tty.bold}#{Formatter.success("")}#{Tty.reset}"
else
"#{Tty.bold}#{Formatter.error("")}#{Tty.reset}"
out += "File: #{hash[:file]} #{pretty_bool(hash[:file].present?)}\n"
out += "Command: #{hash[:command]}\n" unless hash[:command].nil?
out += "Working directory: #{hash[:working_dir]}\n" unless hash[:working_dir].nil?
out += "Root directory: #{hash[:root_dir]}\n" unless hash[:root_dir].nil?
out += "Log: #{hash[:log_path]}\n" unless hash[:log_path].nil?
out += "Error log: #{hash[:error_log_path]}\n" unless hash[:error_log_path].nil?
out += "Interval: #{hash[:interval]}s\n" unless hash[:interval].nil?
out += "Cron: #{hash[:cron]}\n" unless hash[:cron].nil?
out
end
end
sig { params(hash: T.untyped, verbose: T.nilable(T::Boolean)).returns(String) }
def self.output(hash, verbose:)
out = "#{Tty.bold}#{hash[:name]}#{Tty.reset} (#{hash[:service_name]})\n"
out += "Running: #{pretty_bool(hash[:running])}\n"
out += "Loaded: #{pretty_bool(hash[:loaded])}\n"
out += "Schedulable: #{pretty_bool(hash[:schedulable])}\n"
out += "User: #{hash[:user]}\n" unless hash[:pid].nil?
out += "PID: #{hash[:pid]}\n" unless hash[:pid].nil?
return out unless verbose
out += "File: #{hash[:file]} #{pretty_bool(hash[:file].present?)}\n"
out += "Command: #{hash[:command]}\n" unless hash[:command].nil?
out += "Working directory: #{hash[:working_dir]}\n" unless hash[:working_dir].nil?
out += "Root directory: #{hash[:root_dir]}\n" unless hash[:root_dir].nil?
out += "Log: #{hash[:log_path]}\n" unless hash[:log_path].nil?
out += "Error log: #{hash[:error_log_path]}\n" unless hash[:error_log_path].nil?
out += "Interval: #{hash[:interval]}s\n" unless hash[:interval].nil?
out += "Cron: #{hash[:cron]}\n" unless hash[:cron].nil?
out
end
end
end
end

View File

@ -1,15 +1,19 @@
# typed: strict
# frozen_string_literal: true
module Services
module Commands
module Kill
TRIGGERS = %w[kill k].freeze
require "services/cli"
sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean)).void }
def self.run(targets, verbose:)
Services::Cli.check(targets)
Services::Cli.kill(targets, verbose:)
module Homebrew
module Services
module Commands
module Kill
TRIGGERS = %w[kill k].freeze
sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean)).void }
def self.run(targets, verbose:)
Services::Cli.check(targets)
Services::Cli.kill(targets, verbose:)
end
end
end
end

View File

@ -1,83 +1,85 @@
# typed: strict
# frozen_string_literal: true
require "services/formulae"
require "services/cli"
require "services/formulae"
module Services
module Commands
module List
TRIGGERS = [nil, "list", "ls"].freeze
module Homebrew
module Services
module Commands
module List
TRIGGERS = [nil, "list", "ls"].freeze
sig { params(json: T::Boolean).void }
def self.run(json: false)
formulae = Formulae.services_list
if formulae.blank?
opoo "No services available to control with `#{Services::Cli.bin}`" if $stderr.tty?
return
sig { params(json: T::Boolean).void }
def self.run(json: false)
formulae = Formulae.services_list
if formulae.blank?
opoo "No services available to control with `#{Services::Cli.bin}`" if $stderr.tty?
return
end
if json
print_json(formulae)
else
print_table(formulae)
end
end
if json
print_json(formulae)
else
print_table(formulae)
end
end
JSON_FIELDS = [:name, :status, :user, :file, :exit_code].freeze
JSON_FIELDS = [:name, :status, :user, :file, :exit_code].freeze
# Print the JSON representation in the CLI
# @private
sig { params(formulae: T.untyped).returns(NilClass) }
def self.print_json(formulae)
services = formulae.map do |formula|
formula.slice(*JSON_FIELDS)
end
# Print the JSON representation in the CLI
# @private
sig { params(formulae: T.untyped).returns(NilClass) }
def self.print_json(formulae)
services = formulae.map do |formula|
formula.slice(*JSON_FIELDS)
puts JSON.pretty_generate(services)
end
puts JSON.pretty_generate(services)
end
# Print the table in the CLI
# @private
sig { params(formulae: T::Array[T::Hash[T.untyped, T.untyped]]).void }
def self.print_table(formulae)
services = formulae.map do |formula|
status = T.must(get_status_string(formula[:status]))
status += formula[:exit_code].to_s if formula[:status] == :error
file = formula[:file].to_s.gsub(Dir.home, "~").presence if formula[:loaded]
# Print the table in the CLI
# @private
sig { params(formulae: T::Array[T::Hash[T.untyped, T.untyped]]).void }
def self.print_table(formulae)
services = formulae.map do |formula|
status = T.must(get_status_string(formula[:status]))
status += formula[:exit_code].to_s if formula[:status] == :error
file = formula[:file].to_s.gsub(Dir.home, "~").presence if formula[:loaded]
{ name: formula[:name], status:, user: formula[:user], file: }
end
{ name: formula[:name], status:, user: formula[:user], file: }
longest_name = [*services.map { |service| service[:name].length }, 4].max
longest_status = [*services.map { |service| service[:status].length }, 15].max
longest_user = [*services.map { |service| service[:user]&.length }, 4].compact.max
# `longest_status` includes 9 color characters from `Tty.color` and `Tty.reset`.
# We don't have these in the header row, so we don't need to add the extra padding.
headers = "#{Tty.bold}%-#{longest_name}.#{longest_name}<name>s " \
"%-#{longest_status - 9}.#{longest_status - 9}<status>s " \
"%-#{longest_user}.#{longest_user}<user>s %<file>s#{Tty.reset}"
row = "%-#{longest_name}.#{longest_name}<name>s " \
"%-#{longest_status}.#{longest_status}<status>s " \
"%-#{longest_user}.#{longest_user}<user>s %<file>s"
puts format(headers, name: "Name", status: "Status", user: "User", file: "File")
services.each do |service|
puts format(row, **service)
end
end
longest_name = [*services.map { |service| service[:name].length }, 4].max
longest_status = [*services.map { |service| service[:status].length }, 15].max
longest_user = [*services.map { |service| service[:user]&.length }, 4].compact.max
# `longest_status` includes 9 color characters from `Tty.color` and `Tty.reset`.
# We don't have these in the header row, so we don't need to add the extra padding.
headers = "#{Tty.bold}%-#{longest_name}.#{longest_name}<name>s " \
"%-#{longest_status - 9}.#{longest_status - 9}<status>s " \
"%-#{longest_user}.#{longest_user}<user>s %<file>s#{Tty.reset}"
row = "%-#{longest_name}.#{longest_name}<name>s " \
"%-#{longest_status}.#{longest_status}<status>s " \
"%-#{longest_user}.#{longest_user}<user>s %<file>s"
puts format(headers, name: "Name", status: "Status", user: "User", file: "File")
services.each do |service|
puts format(row, **service)
end
end
# Get formula status output
# @private
sig { params(status: T.anything).returns(T.nilable(String)) }
def self.get_status_string(status)
case status
when :started, :scheduled then "#{Tty.green}#{status}#{Tty.reset}"
when :stopped, :none then "#{Tty.default}#{status}#{Tty.reset}"
when :error then "#{Tty.red}error #{Tty.reset}"
when :unknown then "#{Tty.yellow}unknown#{Tty.reset}"
when :other then "#{Tty.yellow}other#{Tty.reset}"
# Get formula status output
# @private
sig { params(status: T.anything).returns(T.nilable(String)) }
def self.get_status_string(status)
case status
when :started, :scheduled then "#{Tty.green}#{status}#{Tty.reset}"
when :stopped, :none then "#{Tty.default}#{status}#{Tty.reset}"
when :error then "#{Tty.red}error #{Tty.reset}"
when :unknown then "#{Tty.yellow}unknown#{Tty.reset}"
when :other then "#{Tty.yellow}other#{Tty.reset}"
end
end
end
end

View File

@ -1,35 +1,39 @@
# typed: strict
# frozen_string_literal: true
module Services
module Commands
module Restart
# NOTE: The restart command is used to update service files
# after a package gets updated through `brew upgrade`.
# This works by removing the old file with `brew services stop`
# and installing the new one with `brew services start|run`.
require "services/cli"
TRIGGERS = %w[restart relaunch reload r].freeze
module Homebrew
module Services
module Commands
module Restart
# NOTE: The restart command is used to update service files
# after a package gets updated through `brew upgrade`.
# This works by removing the old file with `brew services stop`
# and installing the new one with `brew services start|run`.
sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean)).returns(NilClass) }
def self.run(targets, verbose:)
Services::Cli.check(targets)
TRIGGERS = %w[restart relaunch reload r].freeze
ran = []
started = []
targets.each do |service|
if service.loaded? && !service.service_file_present?
ran << service
else
# group not-started services with started ones for restart
started << service
sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean)).returns(NilClass) }
def self.run(targets, verbose:)
Services::Cli.check(targets)
ran = []
started = []
targets.each do |service|
if service.loaded? && !service.service_file_present?
ran << service
else
# group not-started services with started ones for restart
started << service
end
Services::Cli.stop([service], verbose:) if service.loaded?
end
Services::Cli.stop([service], verbose:) if service.loaded?
end
Services::Cli.run(targets, verbose:) if ran.present?
Services::Cli.start(started, verbose:) if started.present?
nil
Services::Cli.run(targets, verbose:) if ran.present?
Services::Cli.start(started, verbose:) if started.present?
nil
end
end
end
end

View File

@ -1,15 +1,19 @@
# typed: strict
# frozen_string_literal: true
module Services
module Commands
module Run
TRIGGERS = ["run"].freeze
require "services/cli"
sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean)).void }
def self.run(targets, verbose:)
Services::Cli.check(targets)
Services::Cli.run(targets, verbose:)
module Homebrew
module Services
module Commands
module Run
TRIGGERS = ["run"].freeze
sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T.nilable(T::Boolean)).void }
def self.run(targets, verbose:)
Services::Cli.check(targets)
Services::Cli.run(targets, verbose:)
end
end
end
end

View File

@ -1,18 +1,25 @@
# typed: strict
# frozen_string_literal: true
module Services
module Commands
module Start
TRIGGERS = %w[start launch load s l].freeze
require "services/cli"
sig {
params(targets: T::Array[Services::FormulaWrapper], custom_plist: T.nilable(String),
verbose: T.nilable(T::Boolean)).void
}
def self.run(targets, custom_plist, verbose:)
Services::Cli.check(targets)
Services::Cli.start(targets, custom_plist, verbose:)
module Homebrew
module Services
module Commands
module Start
TRIGGERS = %w[start launch load s l].freeze
sig {
params(
targets: T::Array[Services::FormulaWrapper],
custom_plist: T.nilable(String),
verbose: T.nilable(T::Boolean),
).void
}
def self.run(targets, custom_plist, verbose:)
Services::Cli.check(targets)
Services::Cli.start(targets, custom_plist, verbose:)
end
end
end
end

View File

@ -1,20 +1,26 @@
# typed: strict
# frozen_string_literal: true
module Services
module Commands
module Stop
TRIGGERS = %w[stop unload terminate term t u].freeze
require "services/cli"
sig {
params(targets: T::Array[Services::FormulaWrapper],
verbose: T.nilable(T::Boolean),
no_wait: T.nilable(T::Boolean),
max_wait: T.nilable(Float)).void
}
def self.run(targets, verbose:, no_wait:, max_wait:)
Services::Cli.check(targets)
Services::Cli.stop(targets, verbose:, no_wait:, max_wait:)
module Homebrew
module Services
module Commands
module Stop
TRIGGERS = %w[stop unload terminate term t u].freeze
sig {
params(
targets: T::Array[Services::FormulaWrapper],
verbose: T.nilable(T::Boolean),
no_wait: T.nilable(T::Boolean),
max_wait: T.nilable(Float),
).void
}
def self.run(targets, verbose:, no_wait:, max_wait:)
Services::Cli.check(targets)
Services::Cli.stop(targets, verbose:, no_wait:, max_wait:)
end
end
end
end

View File

@ -3,330 +3,331 @@
# Wrapper for a formula to handle service-related stuff like parsing and
# generating the service/plist files.
module Services
class FormulaWrapper
# Access the `Formula` instance.
sig { returns(Formula) }
attr_reader :formula
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
# 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 = T.let(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
# Delegate access to `formula.name`.
sig { returns(String) }
attr_reader :name
# Delegate access to `formula.service?`.
sig { returns(T::Boolean) }
def service?
@service
end
# Delegate access to `formula.service.timed?`.
sig { returns(T.nilable(T::Boolean)) }
def timed?
@timed ||= T.let(service? ? T.must(load_service).timed? : nil, T.nilable(T::Boolean))
end
# Delegate access to `formula.service.keep_alive?`.`
sig { returns(T.nilable(T::Boolean)) }
def keep_alive?
@keep_alive ||= T.let(T.must(load_service).keep_alive?, T.nilable(T::Boolean)) if service?
end
# service_name delegates with formula.plist_name or formula.service_name for systemd (e.g., `homebrew.<formula>`).
sig { returns(T.nilable(String)) }
attr_reader :service_name
# service_file delegates with formula.launchd_service_path or formula.systemd_service_path for systemd.
sig { returns(T.nilable(Pathname)) }
attr_reader :service_file
# Whether the service should be launched at startup
sig { returns(T.nilable(T::Boolean)) }
def service_startup?
@service_startup ||= service? ? T.must(load_service).requires_root? : false
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? ? T.must(System.boot_path) : T.must(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 + T.must(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
# Returns `true` if the plist file exists.
sig { returns(T::Boolean) }
def plist?
return false unless installed?
return true if T.must(service_file).file?
return false unless formula.opt_prefix.exist?
return true if Keg.for(formula.opt_prefix).plist_installed?
false
rescue NotAKegError
false
end
# Returns `true` if the service is loaded, else false.
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?
System::Systemctl.quiet_run("status", T.must(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 Hash option `:for` with values `:root` for boot path or `:user` for user path.
sig { params(opts: T.untyped).returns(T::Boolean) }
def service_file_present?(opts = { for: false })
if opts[:for] && opts[:for] == :root
boot_path_service_file_present?
elsif opts[:for] && opts[:for] == :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
plist = begin
Plist.parse_xml(dest.read, marshal: false)
begin
new(Formulary.factory(T.must(Regexp.last_match(1))))
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
# Initialize a new `Service` instance with supplied formula.
sig { params(formula: Formula).void }
def initialize(formula)
@formula = T.let(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
sig { returns(T::Boolean) }
def pid?
return false if pid.nil?
# Delegate access to `formula.name`.
sig { returns(String) }
attr_reader :name
!T.must(pid).zero?
end
# Delegate access to `formula.service?`.
sig { returns(T::Boolean) }
def service?
@service
end
sig { returns(T.nilable(T.any(T::Boolean, Integer))) }
def error?
return false if pid? || pid.nil?
return exit_code if exit_code.nil?
# Delegate access to `formula.service.timed?`.
sig { returns(T.nilable(T::Boolean)) }
def timed?
@timed ||= T.let(service? ? T.must(load_service).timed? : nil, T.nilable(T::Boolean))
end
T.must(exit_code).nonzero?
end
# Delegate access to `formula.service.keep_alive?`.`
sig { returns(T.nilable(T::Boolean)) }
def keep_alive?
@keep_alive ||= T.let(T.must(load_service).keep_alive?, T.nilable(T::Boolean)) if service?
end
sig { returns(T.nilable(T::Boolean)) }
def unknown_status?
status_output.blank? && !pid?
end
# service_name delegates with formula.plist_name or formula.service_name for systemd
# (e.g., `homebrew.<formula>`).
sig { returns(T.nilable(String)) }
attr_reader :service_name
# Get current PID of daemon process from status output.
sig { returns(T.nilable(Integer)) }
def pid
status_output, _, status_type = status_output_success_type
return if status_type.nil?
# service_file delegates with formula.launchd_service_path or formula.systemd_service_path for systemd.
sig { returns(T.nilable(Pathname)) }
attr_reader :service_file
Regexp.last_match(1).to_i if status_output =~ pid_regex(status_type)
end
# Whether the service should be launched at startup
sig { returns(T.nilable(T::Boolean)) }
def service_startup?
@service_startup ||= service? ? T.must(load_service).requires_root? : false
end
# Get current exit code of daemon process from status output.
sig { returns(T.nilable(Integer)) }
def exit_code
status_output, _, status_type = status_output_success_type
return if status_type.nil?
# Path to destination service directory. If run as root, it's `boot_path`, else `user_path`.
sig { returns(Pathname) }
def dest_dir
System.root? ? T.must(System.boot_path) : T.must(System.user_path)
end
Regexp.last_match(1).to_i if status_output =~ exit_code_regex(status_type)
end
# Path to destination service. If run as root, it's in `boot_path`, else `user_path`.
sig { returns(Pathname) }
def dest
dest_dir + T.must(service_file).basename
end
sig { returns(T::Hash[T.untyped, T.untyped]) }
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,
}
# Returns `true` if any version of the formula is installed.
sig { returns(T::Boolean) }
def installed?
formula.any_version_installed?
end
return hash unless service?
# Returns `true` if the plist file exists.
sig { returns(T::Boolean) }
def plist?
return false unless installed?
return true if T.must(service_file).file?
return false unless formula.opt_prefix.exist?
return true if Keg.for(formula.opt_prefix).plist_installed?
service = load_service
false
rescue NotAKegError
false
end
return hash if T.must(service).command.blank?
# Returns `true` if the service is loaded, else false.
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?
System::Systemctl.quiet_run("status", T.must(service_file).basename)
end
end
hash[:command] = T.must(service).manual_command
hash[:working_dir] = T.must(service).working_dir
hash[:root_dir] = T.must(service).root_dir
hash[:log_path] = T.must(service).log_path
hash[:error_log_path] = T.must(service).error_log_path
hash[:interval] = T.must(service).interval
hash[:cron] = T.must(service).cron
hash
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(T.nilable(Homebrew::Service)) }
def load_service
require "formula"
formula.service
end
sig { returns(T.nilable(T::Array[T.untyped])) }
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?
cmd = [System.launchctl.to_s, "list", service_name]
output = Utils.popen_read(*cmd).chomp
if $CHILD_STATUS.present? && $CHILD_STATUS.success? && output.present?
success = true
odebug cmd.join(" "), output
[output, success, :launchctl_list]
# 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.
sig { params(opts: T.untyped).returns(T::Boolean) }
def service_file_present?(opts = { for: false })
if opts[:for] && opts[:for] == :root
boot_path_service_file_present?
elsif opts[:for] && opts[:for] == :user
user_path_service_file_present?
else
cmd = [System.launchctl.to_s, "print",
"#{System.domain_target}/#{service_name}"]
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
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?
return false if pid.nil?
!T.must(pid).zero?
end
sig { returns(T.nilable(T.any(T::Boolean, Integer))) }
def error?
return false if pid? || pid.nil?
return exit_code if exit_code.nil?
T.must(exit_code).nonzero?
end
sig { returns(T.nilable(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
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)
end
# Get current exit code of daemon process from status output.
sig { returns(T.nilable(Integer)) }
def exit_code
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)
end
sig { returns(T::Hash[T.untyped, T.untyped]) }
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,
}
return hash unless service?
service = load_service
return hash if T.must(service).command.blank?
hash[:command] = T.must(service).manual_command
hash[:working_dir] = T.must(service).working_dir
hash[:root_dir] = T.must(service).root_dir
hash[:log_path] = T.must(service).log_path
hash[:error_log_path] = T.must(service).error_log_path
hash[:interval] = T.must(service).interval
hash[:cron] = T.must(service).cron
hash
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(T.nilable(Homebrew::Service)) }
def load_service
require "formula"
formula.service
end
sig { returns(T.nilable(T::Array[T.untyped])) }
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?
cmd = [System.launchctl.to_s, "list", service_name]
output = Utils.popen_read(*cmd).chomp
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
success = $CHILD_STATUS.present? && $CHILD_STATUS.success? && output.present?
odebug cmd.join(" "), output
[output, success, :launchctl_print]
odebug [System::Systemctl.executable, System::Systemctl.scope, *cmd].join(" "), output
[output, success, :systemctl]
end
elsif System.systemctl?
cmd = ["status", service_name]
output = System::Systemctl.popen_read(*cmd).chomp
success = $CHILD_STATUS.present? && $CHILD_STATUS.success? && output.present?
odebug [System::Systemctl.executable, System::Systemctl.scope, *cmd].join(" "),
output
[output, success, :systemctl]
end
end
sig { returns(T.nilable(String)) }
def status_output
status_output, = status_output_success_type
status_output
end
sig { returns(T.nilable(String)) }
def status_output
status_output, = status_output_success_type
status_output
end
sig { returns(Symbol) }
def status_symbol
if pid?
:started
elsif !loaded?(cached: true)
:none
elsif T.must(exit_code).zero?
if timed?
:scheduled
sig { returns(Symbol) }
def status_symbol
if pid?
:started
elsif !loaded?(cached: true)
:none
elsif T.must(exit_code).zero?
if timed?
:scheduled
else
:stopped
end
elsif error?
:error
elsif unknown_status?
:unknown
else
:stopped
:other
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[T.untyped, Regexp]))
@exit_code_regex.fetch(status_type)
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[T.untyped, 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[T.untyped, Regexp]))
@pid_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[T.untyped, Regexp]))
@pid_regex.fetch(status_type)
end
sig { returns(T::Boolean) }
def boot_path_service_file_present?
(T.must(System.boot_path) + T.must(service_file).basename).exist?
end
sig { returns(T::Boolean) }
def boot_path_service_file_present?
(T.must(System.boot_path) + T.must(service_file).basename).exist?
end
sig { returns(T::Boolean) }
def user_path_service_file_present?
(T.must(System.user_path) + T.must(service_file).basename).exist?
end
sig { returns(T::Boolean) }
def user_path_service_file_present?
(T.must(System.user_path) + T.must(service_file).basename).exist?
end
sig { returns(Regexp) }
private_class_method def self.path_or_label_regex
/homebrew(?>\.mxcl)?\.([\w+-.@]+)(\.plist|\.service)?\z/
sig { returns(Regexp) }
private_class_method def self.path_or_label_regex
/homebrew(?>\.mxcl)?\.([\w+-.@]+)(\.plist|\.service)?\z/
end
end
end
end

View File

@ -3,29 +3,31 @@
require "services/formula_wrapper"
module Services
module Formulae
# All available services, with optional filters applied
# @private
sig { params(loaded: T.nilable(T::Boolean), skip_root: T::Boolean).returns(T::Array[Services::FormulaWrapper]) }
def self.available_services(loaded: nil, skip_root: false)
require "formula"
module Homebrew
module Services
module Formulae
# All available services, with optional filters applied
# @private
sig { params(loaded: T.nilable(T::Boolean), skip_root: T::Boolean).returns(T::Array[Services::FormulaWrapper]) }
def self.available_services(loaded: nil, skip_root: false)
require "formula"
formulae = Formula.installed
.map { |formula| FormulaWrapper.new(formula) }
.select(&:service?)
.sort_by(&:name)
formulae = Formula.installed
.map { |formula| FormulaWrapper.new(formula) }
.select(&:service?)
.sort_by(&:name)
formulae = formulae.select { |formula| formula.loaded? == loaded } unless loaded.nil?
formulae = formulae.reject { |formula| formula.owner == "root" } if skip_root
formulae = formulae.select { |formula| formula.loaded? == loaded } unless loaded.nil?
formulae = formulae.reject { |formula| formula.owner == "root" } if skip_root
formulae
end
formulae
end
# 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
available_services.map(&:to_hash)
# 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
available_services.map(&:to_hash)
end
end
end
end

View File

@ -3,99 +3,102 @@
require_relative "system/systemctl"
module Services
module System
extend FileUtils
module Homebrew
module Services
module System
extend FileUtils
# Path to launchctl binary.
sig { returns(T.nilable(Pathname)) }
def self.launchctl
@launchctl ||= T.let(which("launchctl"), T.nilable(Pathname))
end
# Is this a launchctl system
sig { returns(T::Boolean) }
def self.launchctl?
launchctl.present?
end
# Is this a systemd system
sig { returns(T::Boolean) }
def self.systemctl?
Systemctl.executable.present?
end
# Woohoo, we are root dude!
sig { returns(T::Boolean) }
def self.root?
Process.euid.zero?
end
# Current user running `[sudo] brew services`.
sig { returns(T.nilable(String)) }
def self.user
@user ||= T.let(ENV["USER"].presence || Utils.safe_popen_read("/usr/bin/whoami").chomp, T.nilable(String))
end
sig { params(pid: T.nilable(Integer)).returns(T.nilable(String)) }
def self.user_of_process(pid)
if pid.nil? || pid.zero?
user
else
Utils.safe_popen_read("ps", "-o", "user", "-p", pid.to_s).lines.second&.chomp
# Path to launchctl binary.
sig { returns(T.nilable(Pathname)) }
def self.launchctl
@launchctl ||= T.let(which("launchctl"), T.nilable(Pathname))
end
end
# Run at boot.
sig { returns(T.nilable(Pathname)) }
def self.boot_path
if launchctl?
Pathname.new("/Library/LaunchDaemons")
elsif systemctl?
Pathname.new("/usr/lib/systemd/system")
# Is this a launchctl system
sig { returns(T::Boolean) }
def self.launchctl?
launchctl.present?
end
end
# Run at login.
sig { returns(T.nilable(Pathname)) }
def self.user_path
if launchctl?
Pathname.new("#{Dir.home}/Library/LaunchAgents")
elsif systemctl?
Pathname.new("#{Dir.home}/.config/systemd/user")
# Is this a systemd system
sig { returns(T::Boolean) }
def self.systemctl?
Systemctl.executable.present?
end
end
# If root, return `boot_path`, else return `user_path`.
sig { returns(T.nilable(Pathname)) }
def self.path
root? ? boot_path : user_path
end
# Woohoo, we are root dude!
sig { returns(T::Boolean) }
def self.root?
Process.euid.zero?
end
sig { returns(String) }
def self.domain_target
if root?
"system"
elsif (ssh_tty = ENV.fetch("HOMEBREW_SSH_TTY", nil).present? && File.stat("/dev/console").uid != Process.uid) ||
(sudo_user = ENV.fetch("HOMEBREW_SUDO_USER", nil).present?) ||
(Process.uid != Process.euid)
if @output_warning.blank? && ENV.fetch("HOMEBREW_SERVICES_NO_DOMAIN_WARNING", nil).blank?
if ssh_tty
opoo "running over SSH without /dev/console ownership, using user/* instead of gui/* domain!"
elsif sudo_user
opoo "running through sudo, using user/* instead of gui/* domain!"
else
opoo "uid and euid do not match, using user/* instead of gui/* domain!"
end
unless Homebrew::EnvConfig.no_env_hints?
puts "Hide this warning by setting HOMEBREW_SERVICES_NO_DOMAIN_WARNING."
puts "Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`)."
end
@output_warning = T.let(true, T.nilable(TrueClass))
# Current user running `[sudo] brew services`.
sig { returns(T.nilable(String)) }
def self.user
@user ||= T.let(ENV["USER"].presence || Utils.safe_popen_read("/usr/bin/whoami").chomp, T.nilable(String))
end
sig { params(pid: T.nilable(Integer)).returns(T.nilable(String)) }
def self.user_of_process(pid)
if pid.nil? || pid.zero?
user
else
Utils.safe_popen_read("ps", "-o", "user", "-p", pid.to_s).lines.second&.chomp
end
end
# Run at boot.
sig { returns(T.nilable(Pathname)) }
def self.boot_path
if launchctl?
Pathname.new("/Library/LaunchDaemons")
elsif systemctl?
Pathname.new("/usr/lib/systemd/system")
end
end
# Run at login.
sig { returns(T.nilable(Pathname)) }
def self.user_path
if launchctl?
Pathname.new("#{Dir.home}/Library/LaunchAgents")
elsif systemctl?
Pathname.new("#{Dir.home}/.config/systemd/user")
end
end
# If root, return `boot_path`, else return `user_path`.
sig { returns(T.nilable(Pathname)) }
def self.path
root? ? boot_path : user_path
end
sig { returns(String) }
def self.domain_target
if root?
"system"
elsif (ssh_tty = ENV.fetch("HOMEBREW_SSH_TTY", nil).present? &&
File.stat("/dev/console").uid != Process.uid) ||
(sudo_user = ENV.fetch("HOMEBREW_SUDO_USER", nil).present?) ||
(Process.uid != Process.euid)
if @output_warning.blank? && ENV.fetch("HOMEBREW_SERVICES_NO_DOMAIN_WARNING", nil).blank?
if ssh_tty
opoo "running over SSH without /dev/console ownership, using user/* instead of gui/* domain!"
elsif sudo_user
opoo "running through sudo, using user/* instead of gui/* domain!"
else
opoo "uid and euid do not match, using user/* instead of gui/* domain!"
end
unless Homebrew::EnvConfig.no_env_hints?
puts "Hide this warning by setting HOMEBREW_SERVICES_NO_DOMAIN_WARNING."
puts "Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`)."
end
@output_warning = T.let(true, T.nilable(TrueClass))
end
"user/#{Process.euid}"
else
"gui/#{Process.uid}"
end
"user/#{Process.euid}"
else
"gui/#{Process.uid}"
end
end
end

View File

@ -1,52 +1,54 @@
# typed: strict
# frozen_string_literal: true
module Services
module System
module Systemctl
sig { returns(T.nilable(Pathname)) }
def self.executable
@executable ||= T.let(which("systemctl"), T.nilable(Pathname))
end
module Homebrew
module Services
module System
module Systemctl
sig { returns(T.nilable(Pathname)) }
def self.executable
@executable ||= T.let(which("systemctl"), T.nilable(Pathname))
end
sig { void }
def self.reset_executable!
@executable = nil
end
sig { void }
def self.reset_executable!
@executable = nil
end
sig { returns(String) }
def self.scope
System.root? ? "--system" : "--user"
end
sig { returns(String) }
def self.scope
System.root? ? "--system" : "--user"
end
sig { params(args: T.nilable(T.any(String, Pathname))).void }
def self.run(*args)
_run(*args, mode: :default)
end
sig { params(args: T.nilable(T.any(String, Pathname))).void }
def self.run(*args)
_run(*args, mode: :default)
end
sig { params(args: T.nilable(T.any(String, Pathname))).returns(T::Boolean) }
def self.quiet_run(*args)
_run(*args, mode: :quiet)
end
sig { params(args: T.nilable(T.any(String, Pathname))).returns(T::Boolean) }
def self.quiet_run(*args)
_run(*args, mode: :quiet)
end
sig { params(args: T.nilable(T.any(String, Pathname))).returns(String) }
def self.popen_read(*args)
_run(*args, mode: :read)
end
sig { params(args: T.nilable(T.any(String, Pathname))).returns(String) }
def self.popen_read(*args)
_run(*args, mode: :read)
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:)
require "system_command"
result = SystemCommand.run(executable,
args: [scope, *args.map(&:to_s)],
print_stdout: mode == :default,
print_stderr: mode == :default,
must_succeed: mode == :default,
reset_uid: true)
if mode == :read
result.stdout
elsif mode == :quiet
result.success?
sig { params(args: T.nilable(T.any(String, Pathname)), mode: T.nilable(Symbol)).returns(T.untyped) }
private_class_method def self._run(*args, mode:)
require "system_command"
result = SystemCommand.run(executable,
args: [scope, *args.map(&:to_s)],
print_stdout: mode == :default,
print_stderr: mode == :default,
must_succeed: mode == :default,
reset_uid: true)
if mode == :read
result.stdout
elsif mode == :quiet
result.success?
end
end
end
end

View File

@ -4,7 +4,7 @@ require "services/cli"
require "services/system"
require "services/formula_wrapper"
RSpec.describe Services::Cli do
RSpec.describe Homebrew::Services::Cli do
subject(:services_cli) { described_class }
let(:service_string) { "service" }
@ -17,7 +17,7 @@ RSpec.describe Services::Cli do
describe "#running" do
it "macOS - returns the currently running services" do
allow(Services::System).to receive_messages(launchctl?: true, systemctl?: false)
allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false)
allow(Utils).to receive(:popen_read).and_return <<~EOS
77513 50 homebrew.mxcl.php
495 0 homebrew.mxcl.node_exporter
@ -31,8 +31,8 @@ RSpec.describe Services::Cli do
end
it "systemD - returns the currently running services" do
allow(Services::System).to receive(:launchctl?).and_return(false)
allow(Services::System::Systemctl).to receive(:popen_read).and_return <<~EOS
allow(Homebrew::Services::System).to receive(:launchctl?).and_return(false)
allow(Homebrew::Services::System::Systemctl).to receive(:popen_read).and_return <<~EOS
homebrew.php.service loaded active running Homebrew PHP service
systemd-udevd.service loaded active running Rule-based Manager for Device Events and Files
udisks2.service loaded active running Disk Manager
@ -74,7 +74,7 @@ RSpec.describe Services::Cli do
keep_alive?: false,
)
allow(service).to receive(:service_name)
allow(Services::FormulaWrapper).to receive(:from).and_return(service)
allow(Homebrew::Services::FormulaWrapper).to receive(:from).and_return(service)
allow(services_cli).to receive(:running).and_return(["example_service"])
expect do
services_cli.kill_orphaned_services
@ -84,7 +84,7 @@ RSpec.describe Services::Cli do
describe "#run" do
it "checks empty targets cause no error" do
expect(Services::System).not_to receive(:root?)
expect(Homebrew::Services::System).not_to receive(:root?)
services_cli.run([])
end
@ -100,14 +100,14 @@ RSpec.describe Services::Cli do
describe "#start" do
it "checks missing file causes error" do
expect(Services::System).not_to receive(:root?)
expect(Homebrew::Services::System).not_to receive(:root?)
expect do
services_cli.start(["service_name"], "/hfdkjshksdjhfkjsdhf/fdsjghsdkjhb")
end.to raise_error(UsageError, "Invalid usage: Provided service file does not exist")
end
it "checks empty targets cause no error" do
expect(Services::System).not_to receive(:root?)
expect(Homebrew::Services::System).not_to receive(:root?)
services_cli.start([])
end
@ -123,14 +123,14 @@ RSpec.describe Services::Cli do
describe "#stop" do
it "checks empty targets cause no error" do
expect(Services::System).not_to receive(:root?)
expect(Homebrew::Services::System).not_to receive(:root?)
services_cli.stop([])
end
end
describe "#kill" do
it "checks empty targets cause no error" do
expect(Services::System).not_to receive(:root?)
expect(Homebrew::Services::System).not_to receive(:root?)
services_cli.kill([])
end
@ -153,7 +153,7 @@ RSpec.describe Services::Cli do
describe "#install_service_file" do
it "checks service is installed" do
service = instance_double(Services::FormulaWrapper, name: "name", installed?: false)
service = instance_double(Homebrew::Services::FormulaWrapper, name: "name", installed?: false)
expect do
services_cli.install_service_file(service, nil)
end.to raise_error(UsageError, "Invalid usage: Formula `name` is not installed")
@ -161,7 +161,7 @@ RSpec.describe Services::Cli do
it "checks service file exists" do
service = instance_double(
Services::FormulaWrapper,
Homebrew::Services::FormulaWrapper,
name: "name",
installed?: true,
service_file: instance_double(Pathname, exist?: false),
@ -177,17 +177,17 @@ RSpec.describe Services::Cli do
describe "#systemd_load", :needs_linux do
it "checks non-enabling run" do
expect(Services::System::Systemctl).to receive(:run).once.and_return(true)
expect(Homebrew::Services::System::Systemctl).to receive(:run).once.and_return(true)
services_cli.systemd_load(
instance_double(Services::FormulaWrapper, service_name: "name"),
instance_double(Homebrew::Services::FormulaWrapper, service_name: "name"),
enable: false,
)
end
it "checks enabling run" do
expect(Services::System::Systemctl).to receive(:run).twice.and_return(true)
expect(Homebrew::Services::System::Systemctl).to receive(:run).twice.and_return(true)
services_cli.systemd_load(
instance_double(Services::FormulaWrapper, service_name: "name"),
instance_double(Homebrew::Services::FormulaWrapper, service_name: "name"),
enable: true,
)
end
@ -195,57 +195,67 @@ RSpec.describe Services::Cli do
describe "#launchctl_load", :needs_macos do
it "checks non-enabling run" do
allow(Services::System).to receive(:launchctl).and_return(Pathname.new("/bin/launchctl"))
expect(Services::System).to receive(:domain_target).once.and_return("target")
allow(Homebrew::Services::System).to receive(:launchctl).and_return(Pathname.new("/bin/launchctl"))
expect(Homebrew::Services::System).to receive(:domain_target).once.and_return("target")
expect(described_class).to receive(:safe_system).once.and_return(true)
services_cli.launchctl_load(instance_double(Services::FormulaWrapper), file: "a", enable: false)
services_cli.launchctl_load(instance_double(Homebrew::Services::FormulaWrapper), file: "a", enable: false)
end
it "checks enabling run" do
allow(Services::System).to receive(:launchctl).and_return(Pathname.new("/bin/launchctl"))
expect(Services::System).to receive(:domain_target).twice.and_return("target")
allow(Homebrew::Services::System).to receive(:launchctl).and_return(Pathname.new("/bin/launchctl"))
expect(Homebrew::Services::System).to receive(:domain_target).twice.and_return("target")
expect(described_class).to receive(:safe_system).twice.and_return(true)
services_cli.launchctl_load(instance_double(Services::FormulaWrapper, service_name: "name"), file: "a",
enable: true)
services_cli.launchctl_load(instance_double(Homebrew::Services::FormulaWrapper, service_name: "name"),
file: "a",
enable: true)
end
end
describe "#service_load" do
it "checks non-root for login" do
expect(Services::System).to receive(:launchctl?).once.and_return(false)
expect(Services::System).to receive(:systemctl?).once.and_return(false)
expect(Services::System).to receive(:root?).once.and_return(true)
expect(Homebrew::Services::System).to receive(:launchctl?).once.and_return(false)
expect(Homebrew::Services::System).to receive(:systemctl?).once.and_return(false)
expect(Homebrew::Services::System).to receive(:root?).once.and_return(true)
expect do
services_cli.service_load(
instance_double(Services::FormulaWrapper, name: "name", service_name: "service.name",
service_startup?: false), enable: false
instance_double(
Homebrew::Services::FormulaWrapper,
name: "name",
service_name: "service.name",
service_startup?: false,
),
enable: false,
)
end.to output("==> Successfully ran `name` (label: service.name)\n").to_stdout
end
it "checks root for startup" do
expect(Services::System).to receive(:launchctl?).once.and_return(false)
expect(Services::System).to receive(:systemctl?).once.and_return(false)
expect(Services::System).to receive(:root?).twice.and_return(false)
expect(Homebrew::Services::System).to receive(:launchctl?).once.and_return(false)
expect(Homebrew::Services::System).to receive(:systemctl?).once.and_return(false)
expect(Homebrew::Services::System).to receive(:root?).twice.and_return(false)
expect do
services_cli.service_load(
instance_double(Services::FormulaWrapper, name: "name", service_name: "service.name",
service_startup?: true),
instance_double(
Homebrew::Services::FormulaWrapper,
name: "name",
service_name: "service.name",
service_startup?: true,
),
enable: false,
)
end.to output("==> Successfully ran `name` (label: service.name)\n").to_stdout
end
it "triggers launchctl" do
expect(Services::System).to receive(:launchctl?).once.and_return(true)
expect(Services::System).not_to receive(:systemctl?)
expect(Services::System).to receive(:root?).twice.and_return(false)
expect(Homebrew::Services::System).to receive(:launchctl?).once.and_return(true)
expect(Homebrew::Services::System).not_to receive(:systemctl?)
expect(Homebrew::Services::System).to receive(:root?).twice.and_return(false)
expect(described_class).to receive(:launchctl_load).once.and_return(true)
expect do
services_cli.service_load(
instance_double(
Services::FormulaWrapper,
Homebrew::Services::FormulaWrapper,
name: "name",
service_name: "service.name",
service_startup?: false,
@ -257,14 +267,14 @@ service_startup?: true),
end
it "triggers systemctl" do
expect(Services::System).to receive(:launchctl?).once.and_return(false)
expect(Services::System).to receive(:systemctl?).once.and_return(true)
expect(Services::System).to receive(:root?).twice.and_return(false)
expect(Services::System::Systemctl).to receive(:run).once.and_return(true)
expect(Homebrew::Services::System).to receive(:launchctl?).once.and_return(false)
expect(Homebrew::Services::System).to receive(:systemctl?).once.and_return(true)
expect(Homebrew::Services::System).to receive(:root?).twice.and_return(false)
expect(Homebrew::Services::System::Systemctl).to receive(:run).once.and_return(true)
expect do
services_cli.service_load(
instance_double(
Services::FormulaWrapper,
Homebrew::Services::FormulaWrapper,
name: "name",
service_name: "service.name",
service_startup?: false,
@ -276,14 +286,14 @@ service_startup?: true),
end
it "represents correct action" do
expect(Services::System).to receive(:launchctl?).once.and_return(false)
expect(Services::System).to receive(:systemctl?).once.and_return(true)
expect(Services::System).to receive(:root?).twice.and_return(false)
expect(Services::System::Systemctl).to receive(:run).twice.and_return(true)
expect(Homebrew::Services::System).to receive(:launchctl?).once.and_return(false)
expect(Homebrew::Services::System).to receive(:systemctl?).once.and_return(true)
expect(Homebrew::Services::System).to receive(:root?).twice.and_return(false)
expect(Homebrew::Services::System::Systemctl).to receive(:run).twice.and_return(true)
expect do
services_cli.service_load(
instance_double(
Services::FormulaWrapper,
Homebrew::Services::FormulaWrapper,
name: "name",
service_name: "service.name",
service_startup?: false,

View File

@ -4,7 +4,7 @@ require "services/commands/cleanup"
require "services/system"
require "services/cli"
RSpec.describe Services::Commands::Cleanup do
RSpec.describe Homebrew::Services::Commands::Cleanup do
describe "#TRIGGERS" do
it "contains all restart triggers" do
expect(described_class::TRIGGERS).to eq(%w[cleanup clean cl rm])
@ -13,9 +13,9 @@ RSpec.describe Services::Commands::Cleanup do
describe "#run" do
it "root - prints on empty cleanup" do
expect(Services::System).to receive(:root?).once.and_return(true)
expect(Services::Cli).to receive(:kill_orphaned_services).once.and_return([])
expect(Services::Cli).to receive(:remove_unused_service_files).once.and_return([])
expect(Homebrew::Services::System).to receive(:root?).once.and_return(true)
expect(Homebrew::Services::Cli).to receive(:kill_orphaned_services).once.and_return([])
expect(Homebrew::Services::Cli).to receive(:remove_unused_service_files).once.and_return([])
expect do
described_class.run
@ -23,9 +23,9 @@ RSpec.describe Services::Commands::Cleanup do
end
it "user - prints on empty cleanup" do
expect(Services::System).to receive(:root?).once.and_return(false)
expect(Services::Cli).to receive(:kill_orphaned_services).once.and_return([])
expect(Services::Cli).to receive(:remove_unused_service_files).once.and_return([])
expect(Homebrew::Services::System).to receive(:root?).once.and_return(false)
expect(Homebrew::Services::Cli).to receive(:kill_orphaned_services).once.and_return([])
expect(Homebrew::Services::Cli).to receive(:remove_unused_service_files).once.and_return([])
expect do
described_class.run
@ -33,9 +33,9 @@ RSpec.describe Services::Commands::Cleanup do
end
it "prints nothing on cleanup" do
expect(Services::System).not_to receive(:root?)
expect(Services::Cli).to receive(:kill_orphaned_services).once.and_return(["a"])
expect(Services::Cli).to receive(:remove_unused_service_files).once.and_return(["b"])
expect(Homebrew::Services::System).not_to receive(:root?)
expect(Homebrew::Services::Cli).to receive(:kill_orphaned_services).once.and_return(["a"])
expect(Homebrew::Services::Cli).to receive(:remove_unused_service_files).once.and_return(["b"])
expect do
described_class.run

View File

@ -2,7 +2,7 @@
require "services/commands/info"
RSpec.describe Services::Commands::Info do
RSpec.describe Homebrew::Services::Commands::Info do
before do
allow_any_instance_of(IO).to receive(:tty?).and_return(false)
end

View File

@ -2,7 +2,7 @@
require "services/commands/list"
RSpec.describe Services::Commands::List do
RSpec.describe Homebrew::Services::Commands::List do
describe "#TRIGGERS" do
it "contains all restart triggers" do
expect(described_class::TRIGGERS).to eq([nil, "list", "ls"])
@ -11,7 +11,7 @@ RSpec.describe Services::Commands::List do
describe "#run" do
it "fails with empty list" do
expect(Services::Formulae).to receive(:services_list).and_return([])
expect(Homebrew::Services::Formulae).to receive(:services_list).and_return([])
expect do
allow($stderr).to receive(:tty?).and_return(true)
described_class.run
@ -27,7 +27,7 @@ RSpec.describe Services::Commands::List do
file: "/dev/null",
loaded: true,
}
expect(Services::Formulae).to receive(:services_list).and_return([formula])
expect(Homebrew::Services::Formulae).to receive(:services_list).and_return([formula])
expect do
described_class.run
end.to output(out).to_stdout
@ -47,7 +47,7 @@ RSpec.describe Services::Commands::List do
filtered_formula = formula.slice(*described_class::JSON_FIELDS)
expected_output = "#{JSON.pretty_generate([filtered_formula])}\n"
expect(Services::Formulae).to receive(:services_list).and_return([formula])
expect(Homebrew::Services::Formulae).to receive(:services_list).and_return([formula])
expect do
described_class.run(json: true)
end.to output(expected_output).to_stdout

View File

@ -1,9 +1,8 @@
# frozen_string_literal: true
require "services/commands/restart"
require "services/cli"
require "services/formula_wrapper"
RSpec.describe Services::Commands::Restart do
RSpec.describe Homebrew::Services::Commands::Restart do
describe "#TRIGGERS" do
it "contains all restart triggers" do
expect(described_class::TRIGGERS).to eq(%w[restart relaunch reload r])
@ -18,27 +17,27 @@ RSpec.describe Services::Commands::Restart do
end
it "starts if services are not loaded" do
expect(Services::Cli).not_to receive(:run)
expect(Services::Cli).not_to receive(:stop)
expect(Services::Cli).to receive(:start).once
service = instance_double(Services::FormulaWrapper, service_name: "name", loaded?: false)
expect(Homebrew::Services::Cli).not_to receive(:run)
expect(Homebrew::Services::Cli).not_to receive(:stop)
expect(Homebrew::Services::Cli).to receive(:start).once
service = instance_double(Homebrew::Services::FormulaWrapper, service_name: "name", loaded?: false)
expect(described_class.run([service], verbose: false)).to be_nil
end
it "starts if services are loaded with file" do
expect(Services::Cli).not_to receive(:run)
expect(Services::Cli).to receive(:start).once
expect(Services::Cli).to receive(:stop).once
service = instance_double(Services::FormulaWrapper, service_name: "name", loaded?: true,
expect(Homebrew::Services::Cli).not_to receive(:run)
expect(Homebrew::Services::Cli).to receive(:start).once
expect(Homebrew::Services::Cli).to receive(:stop).once
service = instance_double(Homebrew::Services::FormulaWrapper, service_name: "name", loaded?: true,
service_file_present?: true)
expect(described_class.run([service], verbose: false)).to be_nil
end
it "runs if services are loaded without file" do
expect(Services::Cli).not_to receive(:start)
expect(Services::Cli).to receive(:run).once
expect(Services::Cli).to receive(:stop).once
service = instance_double(Services::FormulaWrapper, service_name: "name", loaded?: true,
expect(Homebrew::Services::Cli).not_to receive(:start)
expect(Homebrew::Services::Cli).to receive(:run).once
expect(Homebrew::Services::Cli).to receive(:stop).once
service = instance_double(Homebrew::Services::FormulaWrapper, service_name: "name", loaded?: true,
service_file_present?: false)
expect(described_class.run([service], verbose: false)).to be_nil
end

View File

@ -2,7 +2,7 @@
require "services/formulae"
RSpec.describe Services::Formulae do
RSpec.describe Homebrew::Services::Formulae do
describe "#services_list" do
it "empty list without available formulae" do
allow(described_class).to receive(:available_services).and_return({})
@ -10,7 +10,7 @@ RSpec.describe Services::Formulae do
end
it "list with available formulae" do
formula = instance_double(Services::FormulaWrapper)
formula = instance_double(Homebrew::Services::FormulaWrapper)
expected = [
{
file: Pathname.new("/Library/LaunchDaemons/file.plist"),

View File

@ -4,7 +4,7 @@ require "services/system"
require "services/formula_wrapper"
require "tempfile"
RSpec.describe Services::FormulaWrapper do
RSpec.describe Homebrew::Services::FormulaWrapper do
subject(:service) { described_class.new(formula) }
let(:formula) do
@ -41,17 +41,17 @@ RSpec.describe Services::FormulaWrapper do
describe "#service_file" do
it "macOS - outputs the full service file path" do
allow(Services::System).to receive(:launchctl?).and_return(true)
allow(Homebrew::Services::System).to receive(:launchctl?).and_return(true)
expect(service.service_file.to_s).to eq("/usr/local/opt/mysql/homebrew.mysql.plist")
end
it "systemD - outputs the full service file path" do
allow(Services::System).to receive_messages(launchctl?: false, systemctl?: true)
allow(Homebrew::Services::System).to receive_messages(launchctl?: false, systemctl?: true)
expect(service.service_file.to_s).to eq("/usr/local/opt/mysql/homebrew.mysql.service")
end
it "Other - outputs no service file" do
allow(Services::System).to receive_messages(launchctl?: false, systemctl?: false)
allow(Homebrew::Services::System).to receive_messages(launchctl?: false, systemctl?: false)
expect(service.service_file).to be_nil
end
end
@ -64,45 +64,45 @@ RSpec.describe Services::FormulaWrapper do
describe "#service_name" do
it "macOS - outputs the service name" do
allow(Services::System).to receive(:launchctl?).and_return(true)
allow(Homebrew::Services::System).to receive(:launchctl?).and_return(true)
expect(service.service_name).to eq("plist-mysql-test")
end
it "systemD - outputs the service name" do
allow(Services::System).to receive_messages(launchctl?: false, systemctl?: true)
allow(Homebrew::Services::System).to receive_messages(launchctl?: false, systemctl?: true)
expect(service.service_name).to eq("plist-mysql-test")
end
it "Other - outputs no service name" do
allow(Services::System).to receive_messages(launchctl?: false, systemctl?: false)
allow(Homebrew::Services::System).to receive_messages(launchctl?: false, systemctl?: false)
expect(service.service_name).to be_nil
end
end
describe "#dest_dir" do
before do
allow(Services::System).to receive_messages(launchctl?: false, systemctl?: false)
allow(Homebrew::Services::System).to receive_messages(launchctl?: false, systemctl?: false)
end
it "macOS - user - outputs the destination directory for the service file" do
ENV["HOME"] = "/tmp_home"
allow(Services::System).to receive_messages(root?: false, launchctl?: true)
allow(Homebrew::Services::System).to receive_messages(root?: false, launchctl?: true)
expect(service.dest_dir.to_s).to eq("/tmp_home/Library/LaunchAgents")
end
it "macOS - root - outputs the destination directory for the service file" do
allow(Services::System).to receive_messages(launchctl?: true, root?: true)
allow(Homebrew::Services::System).to receive_messages(launchctl?: true, root?: true)
expect(service.dest_dir.to_s).to eq("/Library/LaunchDaemons")
end
it "systemD - user - outputs the destination directory for the service file" do
ENV["HOME"] = "/tmp_home"
allow(Services::System).to receive_messages(root?: false, launchctl?: false, systemctl?: true)
allow(Homebrew::Services::System).to receive_messages(root?: false, launchctl?: false, systemctl?: true)
expect(service.dest_dir.to_s).to eq("/tmp_home/.config/systemd/user")
end
it "systemD - root - outputs the destination directory for the service file" do
allow(Services::System).to receive_messages(root?: true, launchctl?: false, systemctl?: true)
allow(Homebrew::Services::System).to receive_messages(root?: true, launchctl?: false, systemctl?: true)
expect(service.dest_dir.to_s).to eq("/usr/lib/systemd/system")
end
end
@ -110,16 +110,16 @@ RSpec.describe Services::FormulaWrapper do
describe "#dest" do
before do
ENV["HOME"] = "/tmp_home"
allow(Services::System).to receive_messages(launchctl?: false, systemctl?: false)
allow(Homebrew::Services::System).to receive_messages(launchctl?: false, systemctl?: false)
end
it "macOS - outputs the destination for the service file" do
allow(Services::System).to receive(:launchctl?).and_return(true)
allow(Homebrew::Services::System).to receive(:launchctl?).and_return(true)
expect(service.dest.to_s).to eq("/tmp_home/Library/LaunchAgents/homebrew.mysql.plist")
end
it "systemD - outputs the destination for the service file" do
allow(Services::System).to receive(:systemctl?).and_return(true)
allow(Homebrew::Services::System).to receive(:systemctl?).and_return(true)
expect(service.dest.to_s).to eq("/tmp_home/.config/systemd/user/homebrew.mysql.service")
end
end
@ -132,20 +132,20 @@ RSpec.describe Services::FormulaWrapper do
describe "#loaded?" do
it "macOS - outputs if the service is loaded" do
allow(Services::System).to receive_messages(launchctl?: true, systemctl?: false)
allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false)
allow(Utils).to receive(:safe_popen_read)
expect(service.loaded?).to be(false)
end
it "systemD - outputs if the service is loaded" do
allow(Services::System).to receive_messages(launchctl?: false, systemctl?: true)
allow(Services::System::Systemctl).to receive(:quiet_run).and_return(false)
allow(Homebrew::Services::System).to receive_messages(launchctl?: false, systemctl?: true)
allow(Homebrew::Services::System::Systemctl).to receive(:quiet_run).and_return(false)
allow(Utils).to receive(:safe_popen_read)
expect(service.loaded?).to be(false)
end
it "Other - outputs no status" do
allow(Services::System).to receive_messages(launchctl?: false, systemctl?: false)
allow(Homebrew::Services::System).to receive_messages(launchctl?: false, systemctl?: false)
expect(service.loaded?).to be_nil
end
end
@ -182,7 +182,7 @@ RSpec.describe Services::FormulaWrapper do
it "user if file present" do
allow(service).to receive_messages(boot_path_service_file_present?: false,
user_path_service_file_present?: true)
allow(Services::System).to receive(:user).and_return("user")
allow(Homebrew::Services::System).to receive(:user).and_return("user")
expect(service.owner).to eq("user")
end
@ -195,24 +195,24 @@ RSpec.describe Services::FormulaWrapper do
describe "#service_file_present?" do
it "macOS - outputs if the service file is present" do
allow(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?).to be(false)
end
it "macOS - outputs if the service file is present for root" do
allow(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)
end
it "macOS - outputs if the service file is present for user" do
allow(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)
end
end
describe "#owner?" do
it "macOS - outputs the service file owner" do
allow(Services::System).to receive_messages(launchctl?: true, systemctl?: false)
allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false)
expect(service.owner).to be_nil
end
end
@ -321,7 +321,7 @@ RSpec.describe Services::FormulaWrapper do
describe "#to_hash" do
it "represents non-service values" do
allow(Services::System).to receive_messages(launchctl?: true, systemctl?: false)
allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false)
allow_any_instance_of(described_class).to receive_messages(service?: false, service_file_present?: false)
expected = {
exit_code: nil,
@ -340,7 +340,7 @@ RSpec.describe Services::FormulaWrapper do
it "represents running non-service values" do
ENV["HOME"] = "/tmp_home"
allow(Services::System).to receive_messages(launchctl?: true, systemctl?: false)
allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false)
expect(service).to receive(:service?).twice.and_return(false)
expect(service).to receive(:service_file_present?).and_return(true)
expected = {
@ -360,7 +360,7 @@ RSpec.describe Services::FormulaWrapper do
it "represents service values" do
ENV["HOME"] = "/tmp_home"
allow(Services::System).to receive_messages(launchctl?: true, systemctl?: false)
allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false)
expect(service).to receive(:service?).twice.and_return(true)
expect(service).to receive(:service_file_present?).and_return(true)
expect(service).to receive(:load_service).twice.and_return(service_object)

View File

@ -3,15 +3,15 @@
require "services/system"
require "services/system/systemctl"
RSpec.describe Services::System::Systemctl do
RSpec.describe Homebrew::Services::System::Systemctl do
describe ".scope" do
it "outputs systemctl scope for user" do
allow(Services::System).to receive(:root?).and_return(false)
allow(Homebrew::Services::System).to receive(:root?).and_return(false)
expect(described_class.scope).to eq("--user")
end
it "outputs systemctl scope for root" do
allow(Services::System).to receive(:root?).and_return(true)
allow(Homebrew::Services::System).to receive(:root?).and_return(true)
expect(described_class.scope).to eq("--system")
end
end

View File

@ -2,7 +2,7 @@
require "services/system"
RSpec.describe Services::System do
RSpec.describe Homebrew::Services::System do
describe "#launchctl" do
it "macOS - outputs launchctl command location", :needs_macos do
expect(described_class.launchctl).to eq(Pathname.new("/bin/launchctl"))