mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00
services: migrate command to main repo (WIP)
This commit is contained in:
parent
5c11787465
commit
3ef52e4844
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -116,7 +116,6 @@ jobs:
|
||||
brew tap homebrew/bundle
|
||||
brew tap homebrew/command-not-found
|
||||
brew tap homebrew/portable-ruby
|
||||
brew tap homebrew/services
|
||||
|
||||
# brew style doesn't like world writable directories
|
||||
sudo chmod -R g-w,o-w "$(brew --repo)/Library/Taps"
|
||||
@ -124,7 +123,6 @@ jobs:
|
||||
- name: Run brew style on official taps
|
||||
run: |
|
||||
brew style homebrew/bundle \
|
||||
homebrew/services \
|
||||
homebrew/test-bot
|
||||
|
||||
brew style homebrew/command-not-found \
|
||||
|
152
Library/Homebrew/cmd/services.rb
Normal file
152
Library/Homebrew/cmd/services.rb
Normal file
@ -0,0 +1,152 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "abstract_command"
|
||||
require "services/service"
|
||||
|
||||
module Homebrew
|
||||
module Cmd
|
||||
class Services < AbstractCommand
|
||||
cmd_args do
|
||||
usage_banner <<~EOS
|
||||
`services` [<subcommand>]
|
||||
|
||||
Manage background services with macOS' `launchctl`(1) daemon manager or
|
||||
Linux's `systemctl`(1) service manager.
|
||||
|
||||
If `sudo` is passed, operate on `/Library/LaunchDaemons` or `/usr/lib/systemd/system` (started at boot).
|
||||
Otherwise, operate on `~/Library/LaunchAgents` or `~/.config/systemd/user` (started at login).
|
||||
|
||||
[`sudo`] `brew services` [`list`] (`--json`) (`--debug`):
|
||||
List information about all managed services for the current user (or root).
|
||||
Provides more output from Homebrew and `launchctl`(1) or `systemctl`(1) if run with `--debug`.
|
||||
|
||||
[`sudo`] `brew services info` (<formula>|`--all`|`--json`):
|
||||
List all managed services for the current user (or root).
|
||||
|
||||
[`sudo`] `brew services run` (<formula>|`--all`):
|
||||
Run the service <formula> without registering to launch at login (or boot).
|
||||
|
||||
[`sudo`] `brew services start` (<formula>|`--all`|`--file=`):
|
||||
Start the service <formula> immediately and register it to launch at login (or boot).
|
||||
|
||||
[`sudo`] `brew services stop` (<formula>|`--all`):
|
||||
Stop the service <formula> immediately and unregister it from launching at login (or boot).
|
||||
|
||||
[`sudo`] `brew services kill` (<formula>|`--all`):
|
||||
Stop the service <formula> immediately but keep it registered to launch at login (or boot).
|
||||
|
||||
[`sudo`] `brew services restart` (<formula>|`--all`):
|
||||
Stop (if necessary) and start the service <formula> immediately and register it to launch at login (or boot).
|
||||
|
||||
[`sudo`] `brew services cleanup`:
|
||||
Remove all unused services.
|
||||
EOS
|
||||
flag "--file=", description: "Use the service file from this location to `start` the service."
|
||||
flag "--sudo-service-user=", description: "When run as root on macOS, run the service(s) as this user."
|
||||
flag "--max-wait=", description: "Wait at most this many seconds for `stop` to finish stopping a service. " \
|
||||
"Omit this flag or set this to zero (0) seconds to wait indefinitely."
|
||||
switch "--all", description: "Run <subcommand> on all services."
|
||||
switch "--json", description: "Output as JSON."
|
||||
switch "--no-wait", description: "Don't wait for `stop` to finish stopping the service."
|
||||
conflicts "--max-wait=", "--no-wait"
|
||||
named_args max: 2
|
||||
end
|
||||
|
||||
sig { override.void }
|
||||
def run
|
||||
# pbpaste's exit status is a proxy for detecting the use of reattach-to-user-namespace
|
||||
if ENV["HOMEBREW_TMUX"] && (File.exist?("/usr/bin/pbpaste") && !quiet_system("/usr/bin/pbpaste"))
|
||||
raise UsageError,
|
||||
"`brew services` cannot run under tmux!"
|
||||
end
|
||||
|
||||
# Keep this after the .parse to keep --help fast.
|
||||
require "utils"
|
||||
|
||||
if !::Service::System.launchctl? && !::Service::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 ::Service::System.root?
|
||||
raise UsageError,
|
||||
"`brew services` is supported only when running as root!"
|
||||
end
|
||||
|
||||
unless ::Service::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
|
||||
|
||||
::Service::ServicesCli.sudo_service_user = sudo_service_user
|
||||
end
|
||||
|
||||
# Parse arguments.
|
||||
subcommand, formula, = args.named
|
||||
|
||||
if [*::Service::Commands::List::TRIGGERS, *::Service::Commands::Cleanup::TRIGGERS].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 ::Service::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!"
|
||||
end
|
||||
end
|
||||
|
||||
opoo "The --all argument overrides provided formula argument!" if formula.present? && args.all?
|
||||
|
||||
targets = if args.all?
|
||||
if subcommand == "start"
|
||||
::Service::Formulae.available_services(loaded: false, skip_root: !::Service::System.root?)
|
||||
elsif subcommand == "stop"
|
||||
::Service::Formulae.available_services(loaded: true, skip_root: !::Service::System.root?)
|
||||
else
|
||||
::Service::Formulae.available_services
|
||||
end
|
||||
elsif formula
|
||||
[::Service::FormulaWrapper.new(Formulary.factory(formula))]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
# Exit successfully if --all was used but there is nothing to do
|
||||
return if args.all? && targets.empty?
|
||||
|
||||
if ::Service::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 *::Service::Commands::List::TRIGGERS
|
||||
::Service::Commands::List.run(json: args.json?)
|
||||
when *::Service::Commands::Cleanup::TRIGGERS
|
||||
::Service::Commands::Cleanup.run
|
||||
when *::Service::Commands::Info::TRIGGERS
|
||||
::Service::Commands::Info.run(targets, verbose: args.verbose?, json: args.json?)
|
||||
when *::Service::Commands::Restart::TRIGGERS
|
||||
::Service::Commands::Restart.run(targets, verbose: args.verbose?)
|
||||
when *::Service::Commands::Run::TRIGGERS
|
||||
::Service::Commands::Run.run(targets, verbose: args.verbose?)
|
||||
when *::Service::Commands::Start::TRIGGERS
|
||||
::Service::Commands::Start.run(targets, args.file, verbose: args.verbose?)
|
||||
when *::Service::Commands::Stop::TRIGGERS
|
||||
max_wait = args.max_wait.to_f
|
||||
::Service::Commands::Stop.run(targets, verbose: args.verbose?, no_wait: args.no_wait?, max_wait:)
|
||||
when *::Service::Commands::Kill::TRIGGERS
|
||||
::Service::Commands::Kill.run(targets, verbose: args.verbose?)
|
||||
else
|
||||
raise UsageError, "unknown subcommand: `#{subcommand}`"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -9,7 +9,6 @@ OFFICIAL_CMD_TAPS = T.let({
|
||||
"homebrew/bundle" => ["bundle"],
|
||||
"homebrew/command-not-found" => ["command-not-found-init", "which-formula", "which-update"],
|
||||
"homebrew/test-bot" => ["test-bot"],
|
||||
"homebrew/services" => ["services"],
|
||||
}.freeze, T::Hash[String, T::Array[String]])
|
||||
|
||||
DEPRECATED_OFFICIAL_TAPS = %w[
|
||||
@ -33,6 +32,7 @@ DEPRECATED_OFFICIAL_TAPS = %w[
|
||||
php
|
||||
python
|
||||
science
|
||||
services
|
||||
tex
|
||||
versions
|
||||
x11
|
||||
|
17
Library/Homebrew/services/service.rb
Normal file
17
Library/Homebrew/services/service.rb
Normal file
@ -0,0 +1,17 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
# fix loadppath
|
||||
$LOAD_PATH.unshift(File.expand_path(__dir__))
|
||||
|
||||
require "service/formula_wrapper"
|
||||
require "service/services_cli"
|
||||
require "service/system"
|
||||
require "service/commands/cleanup"
|
||||
require "service/commands/info"
|
||||
require "service/commands/list"
|
||||
require "service/commands/restart"
|
||||
require "service/commands/run"
|
||||
require "service/commands/start"
|
||||
require "service/commands/stop"
|
||||
require "service/commands/kill"
|
20
Library/Homebrew/services/service/commands/cleanup.rb
Normal file
20
Library/Homebrew/services/service/commands/cleanup.rb
Normal file
@ -0,0 +1,20 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Service
|
||||
module Commands
|
||||
module Cleanup
|
||||
TRIGGERS = %w[cleanup clean cl rm].freeze
|
||||
|
||||
sig { void }
|
||||
def self.run
|
||||
cleaned = []
|
||||
|
||||
cleaned += Service::ServicesCli.kill_orphaned_services
|
||||
cleaned += Service::ServicesCli.remove_unused_service_files
|
||||
|
||||
puts "All #{System.root? ? "root" : "user-space"} services OK, nothing cleaned..." if cleaned.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
62
Library/Homebrew/services/service/commands/info.rb
Normal file
62
Library/Homebrew/services/service/commands/info.rb
Normal file
@ -0,0 +1,62 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Service
|
||||
module Commands
|
||||
module Info
|
||||
TRIGGERS = %w[info i].freeze
|
||||
|
||||
sig {
|
||||
params(targets: T::Array[Service::FormulaWrapper], verbose: T.nilable(T::Boolean),
|
||||
json: T.nilable(T::Boolean)).void
|
||||
}
|
||||
def self.run(targets, verbose:, json:)
|
||||
Service::ServicesCli.check(targets)
|
||||
|
||||
output = targets.map(&:to_hash)
|
||||
|
||||
if json
|
||||
puts JSON.pretty_generate(output)
|
||||
return
|
||||
end
|
||||
|
||||
output.each do |hash|
|
||||
puts output(hash, verbose:)
|
||||
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?
|
||||
|
||||
if bool
|
||||
"#{Tty.bold}#{Formatter.success("✔")}#{Tty.reset}"
|
||||
else
|
||||
"#{Tty.bold}#{Formatter.error("✘")}#{Tty.reset}"
|
||||
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
|
16
Library/Homebrew/services/service/commands/kill.rb
Normal file
16
Library/Homebrew/services/service/commands/kill.rb
Normal file
@ -0,0 +1,16 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Service
|
||||
module Commands
|
||||
module Kill
|
||||
TRIGGERS = %w[kill k].freeze
|
||||
|
||||
sig { params(targets: T::Array[Service::FormulaWrapper], verbose: T.nilable(T::Boolean)).void }
|
||||
def self.run(targets, verbose:)
|
||||
Service::ServicesCli.check(targets)
|
||||
Service::ServicesCli.kill(targets, verbose:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
84
Library/Homebrew/services/service/commands/list.rb
Normal file
84
Library/Homebrew/services/service/commands/list.rb
Normal file
@ -0,0 +1,84 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "service/formulae"
|
||||
|
||||
module Service
|
||||
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 `#{Service::ServicesCli.bin}`" if $stderr.tty?
|
||||
return
|
||||
end
|
||||
|
||||
if json
|
||||
print_json(formulae)
|
||||
else
|
||||
print_table(formulae)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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]
|
||||
|
||||
{ name: formula[:name], status:, user: formula[:user], file: }
|
||||
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}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
36
Library/Homebrew/services/service/commands/restart.rb
Normal file
36
Library/Homebrew/services/service/commands/restart.rb
Normal file
@ -0,0 +1,36 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Service
|
||||
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`.
|
||||
|
||||
TRIGGERS = %w[restart relaunch reload r].freeze
|
||||
|
||||
sig { params(targets: T::Array[Service::FormulaWrapper], verbose: T.nilable(T::Boolean)).returns(NilClass) }
|
||||
def self.run(targets, verbose:)
|
||||
Service::ServicesCli.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
|
||||
Service::ServicesCli.stop([service], verbose:) if service.loaded?
|
||||
end
|
||||
|
||||
Service::ServicesCli.run(targets, verbose:) if ran.present?
|
||||
Service::ServicesCli.start(started, verbose:) if started.present?
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
16
Library/Homebrew/services/service/commands/run.rb
Normal file
16
Library/Homebrew/services/service/commands/run.rb
Normal file
@ -0,0 +1,16 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Service
|
||||
module Commands
|
||||
module Run
|
||||
TRIGGERS = ["run"].freeze
|
||||
|
||||
sig { params(targets: T::Array[Service::FormulaWrapper], verbose: T.nilable(T::Boolean)).void }
|
||||
def self.run(targets, verbose:)
|
||||
Service::ServicesCli.check(targets)
|
||||
Service::ServicesCli.run(targets, verbose:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
19
Library/Homebrew/services/service/commands/start.rb
Normal file
19
Library/Homebrew/services/service/commands/start.rb
Normal file
@ -0,0 +1,19 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Service
|
||||
module Commands
|
||||
module Start
|
||||
TRIGGERS = %w[start launch load s l].freeze
|
||||
|
||||
sig {
|
||||
params(targets: T::Array[Service::FormulaWrapper], custom_plist: T.nilable(String),
|
||||
verbose: T.nilable(T::Boolean)).void
|
||||
}
|
||||
def self.run(targets, custom_plist, verbose:)
|
||||
Service::ServicesCli.check(targets)
|
||||
Service::ServicesCli.start(targets, custom_plist, verbose:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
21
Library/Homebrew/services/service/commands/stop.rb
Normal file
21
Library/Homebrew/services/service/commands/stop.rb
Normal file
@ -0,0 +1,21 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Service
|
||||
module Commands
|
||||
module Stop
|
||||
TRIGGERS = %w[stop unload terminate term t u].freeze
|
||||
|
||||
sig {
|
||||
params(targets: T::Array[Service::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:)
|
||||
Service::ServicesCli.check(targets)
|
||||
Service::ServicesCli.stop(targets, verbose:, no_wait:, max_wait:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
328
Library/Homebrew/services/service/formula_wrapper.rb
Normal file
328
Library/Homebrew/services/service/formula_wrapper.rb
Normal file
@ -0,0 +1,328 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Wrapper for a formula to handle service-related stuff like parsing and
|
||||
# generating the service/plist files.
|
||||
module Service
|
||||
class FormulaWrapper
|
||||
# Access the `Formula` instance.
|
||||
sig { returns(Formula) }
|
||||
attr_reader :formula
|
||||
|
||||
# Create a new `Service` instance from either a path or label.
|
||||
sig { params(path_or_label: T.any(Pathname, String)).returns(T.nilable(FormulaWrapper)) }
|
||||
def self.from(path_or_label)
|
||||
return unless path_or_label =~ path_or_label_regex
|
||||
|
||||
begin
|
||||
new(Formulary.factory(T.must(Regexp.last_match(1))))
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
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)
|
||||
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
|
||||
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
|
||||
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 [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(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
|
||||
: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 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 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/
|
||||
end
|
||||
end
|
||||
end
|
29
Library/Homebrew/services/service/formulae.rb
Normal file
29
Library/Homebrew/services/service/formulae.rb
Normal file
@ -0,0 +1,29 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Service
|
||||
module Formulae
|
||||
# All available services, with optional filters applied
|
||||
# @private
|
||||
sig { params(loaded: T.nilable(T::Boolean), skip_root: T::Boolean).returns(T::Array[Service::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 = formulae.select { |formula| formula.loaded? == loaded } unless loaded.nil?
|
||||
formulae = formulae.reject { |formula| formula.owner == "root" } if skip_root
|
||||
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
368
Library/Homebrew/services/service/services_cli.rb
Normal file
368
Library/Homebrew/services/service/services_cli.rb
Normal file
@ -0,0 +1,368 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Service
|
||||
module ServicesCli
|
||||
extend FileUtils
|
||||
|
||||
sig { returns(T.nilable(String)) }
|
||||
def self.sudo_service_user
|
||||
@sudo_service_user
|
||||
end
|
||||
|
||||
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+-.@]+)/
|
||||
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?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# Kill services that don't have a service file
|
||||
sig { returns(T::Array[Service::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 #{label} not managed by `#{bin}` => skipping"
|
||||
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, ""))
|
||||
|
||||
puts "Removing unused service file #{file}"
|
||||
rm file
|
||||
cleaned << file
|
||||
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[Service::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[Service::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[Service::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[Service::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", T.must(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: Service::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: Service::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: Service::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: Service::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 && Service::System.launchctl?
|
||||
# set the username in the new plist file
|
||||
ohai "Setting username in #{service.service_name} to #{Service::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
|
||||
|
||||
Service::System::Systemctl.run("daemon-reload") if System.systemctl?
|
||||
end
|
||||
end
|
||||
end
|
102
Library/Homebrew/services/service/system.rb
Normal file
102
Library/Homebrew/services/service/system.rb
Normal file
@ -0,0 +1,102 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "system/systemctl"
|
||||
|
||||
module Service
|
||||
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
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
49
Library/Homebrew/services/service/system/systemctl.rb
Normal file
49
Library/Homebrew/services/service/system/systemctl.rb
Normal file
@ -0,0 +1,49 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Service
|
||||
module System
|
||||
module Systemctl
|
||||
sig { returns(T.nilable(Pathname)) }
|
||||
def self.executable
|
||||
@executable ||= T.let(which("systemctl"), T.nilable(Pathname))
|
||||
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))).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.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
|
||||
end
|
29
Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/services.rbi
Normal file
29
Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/services.rbi
Normal file
@ -0,0 +1,29 @@
|
||||
# typed: strict
|
||||
|
||||
class Homebrew::Cmd::Services
|
||||
sig { returns(Homebrew::Cmd::Services::Args) }
|
||||
def args; end
|
||||
end
|
||||
|
||||
class Homebrew::Cmd::Services::Args < Homebrew::CLI::Args
|
||||
sig { returns(T::Boolean) }
|
||||
def all?; end
|
||||
|
||||
sig { returns(T.nilable(String)) }
|
||||
def file; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def json?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def non_bundler_gems?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def no_wait?; end
|
||||
|
||||
sig { returns(T.nilable(String)) }
|
||||
def sudo_service_user; end
|
||||
|
||||
sig { returns(T.nilable(String)) }
|
||||
def max_wait; end
|
||||
end
|
@ -1,13 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "cmd/services"
|
||||
require "cmd/shared_examples/args_parse"
|
||||
|
||||
RSpec.describe "Homebrew::Cmd::Services", :integration_test, :needs_network do
|
||||
before { setup_remote_tap "homebrew/services" }
|
||||
RSpec.describe Homebrew::Cmd::Services do
|
||||
it_behaves_like "parseable arguments"
|
||||
|
||||
it_behaves_like "parseable arguments", command_name: "services"
|
||||
|
||||
it "allows controlling services" do
|
||||
it "allows controlling services", :integration_test do
|
||||
expect { brew "services", "list" }
|
||||
.to not_to_output.to_stderr
|
||||
.and not_to_output.to_stdout
|
||||
|
43
Library/Homebrew/test/services/commands/cleanup_spec.rb
Normal file
43
Library/Homebrew/test/services/commands/cleanup_spec.rb
Normal file
@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "services/service"
|
||||
|
||||
RSpec.describe Service::Commands::Cleanup do
|
||||
describe "#TRIGGERS" do
|
||||
it "contains all restart triggers" do
|
||||
expect(described_class::TRIGGERS).to eq(%w[cleanup clean cl rm])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#run" do
|
||||
it "root - prints on empty cleanup" do
|
||||
expect(Service::System).to receive(:root?).once.and_return(true)
|
||||
expect(Service::ServicesCli).to receive(:kill_orphaned_services).once.and_return([])
|
||||
expect(Service::ServicesCli).to receive(:remove_unused_service_files).once.and_return([])
|
||||
|
||||
expect do
|
||||
described_class.run
|
||||
end.to output("All root services OK, nothing cleaned...\n").to_stdout
|
||||
end
|
||||
|
||||
it "user - prints on empty cleanup" do
|
||||
expect(Service::System).to receive(:root?).once.and_return(false)
|
||||
expect(Service::ServicesCli).to receive(:kill_orphaned_services).once.and_return([])
|
||||
expect(Service::ServicesCli).to receive(:remove_unused_service_files).once.and_return([])
|
||||
|
||||
expect do
|
||||
described_class.run
|
||||
end.to output("All user-space services OK, nothing cleaned...\n").to_stdout
|
||||
end
|
||||
|
||||
it "prints nothing on cleanup" do
|
||||
expect(Service::System).not_to receive(:root?)
|
||||
expect(Service::ServicesCli).to receive(:kill_orphaned_services).once.and_return(["a"])
|
||||
expect(Service::ServicesCli).to receive(:remove_unused_service_files).once.and_return(["b"])
|
||||
|
||||
expect do
|
||||
described_class.run
|
||||
end.not_to output("All user-space services OK, nothing cleaned...\n").to_stdout
|
||||
end
|
||||
end
|
||||
end
|
143
Library/Homebrew/test/services/commands/info_spec.rb
Normal file
143
Library/Homebrew/test/services/commands/info_spec.rb
Normal file
@ -0,0 +1,143 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "services/service"
|
||||
|
||||
# needs for tty color tests
|
||||
module Tty
|
||||
def self.green
|
||||
"<GREEN>"
|
||||
end
|
||||
|
||||
def self.yellow
|
||||
"<YELLOW>"
|
||||
end
|
||||
|
||||
def self.red
|
||||
"<RED>"
|
||||
end
|
||||
|
||||
def self.default
|
||||
"<DEFAULT>"
|
||||
end
|
||||
|
||||
def self.bold
|
||||
"<BOLD>"
|
||||
end
|
||||
|
||||
def self.reset
|
||||
"<RESET>"
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe Service::Commands::Info do
|
||||
before do
|
||||
allow_any_instance_of(IO).to receive(:tty?).and_return(true)
|
||||
end
|
||||
|
||||
describe "#TRIGGERS" do
|
||||
it "contains all restart triggers" do
|
||||
expect(described_class::TRIGGERS).to eq(%w[info i])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#run" do
|
||||
it "fails with empty list" do
|
||||
expect do
|
||||
described_class.run([], verbose: false, json: false)
|
||||
end.to raise_error UsageError,
|
||||
a_string_including("Formula(e) missing, please provide a formula name or use --all")
|
||||
end
|
||||
|
||||
it "succeeds with items" do
|
||||
out = "<BOLD>service<RESET> ()\nRunning: true\nLoaded: true\nSchedulable: false\n"
|
||||
formula = {
|
||||
name: "service",
|
||||
user: "user",
|
||||
status: :started,
|
||||
file: "/dev/null",
|
||||
running: true,
|
||||
loaded: true,
|
||||
schedulable: false,
|
||||
}
|
||||
expect do
|
||||
described_class.run([formula], verbose: false, json: false)
|
||||
end.to output(out).to_stdout
|
||||
end
|
||||
|
||||
it "succeeds with items - JSON" do
|
||||
formula = {
|
||||
name: "service",
|
||||
user: "user",
|
||||
status: :started,
|
||||
file: "/dev/null",
|
||||
running: true,
|
||||
loaded: true,
|
||||
schedulable: false,
|
||||
}
|
||||
out = "#{JSON.pretty_generate([formula])}\n"
|
||||
expect do
|
||||
described_class.run([formula], verbose: false, json: true)
|
||||
end.to output(out).to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
describe "#output" do
|
||||
it "returns minimal output" do
|
||||
out = "<BOLD>service<RESET> ()\nRunning: <BOLD><GREEN>✔<RESET><RESET>\n"
|
||||
out += "Loaded: <BOLD><GREEN>✔<RESET><RESET>\nSchedulable: <BOLD><RED>✘<RESET><RESET>\n"
|
||||
formula = {
|
||||
name: "service",
|
||||
user: "user",
|
||||
status: :started,
|
||||
file: "/dev/null",
|
||||
running: true,
|
||||
loaded: true,
|
||||
schedulable: false,
|
||||
}
|
||||
expect(described_class.output(formula, verbose: false)).to eq(out)
|
||||
end
|
||||
|
||||
it "returns normal output" do
|
||||
out = "<BOLD>service<RESET> ()\nRunning: <BOLD><GREEN>✔<RESET><RESET>\n"
|
||||
out += "Loaded: <BOLD><GREEN>✔<RESET><RESET>\nSchedulable: <BOLD><RED>✘<RESET><RESET>\n"
|
||||
out += "User: user\nPID: 42\n"
|
||||
formula = {
|
||||
name: "service",
|
||||
user: "user",
|
||||
status: :started,
|
||||
file: "/dev/null",
|
||||
running: true,
|
||||
loaded: true,
|
||||
schedulable: false,
|
||||
pid: 42,
|
||||
}
|
||||
expect(described_class.output(formula, verbose: false)).to eq(out)
|
||||
end
|
||||
|
||||
it "returns verbose output" do
|
||||
out = "<BOLD>service<RESET> ()\nRunning: <BOLD><GREEN>✔<RESET><RESET>\n"
|
||||
out += "Loaded: <BOLD><GREEN>✔<RESET><RESET>\nSchedulable: <BOLD><RED>✘<RESET><RESET>\n"
|
||||
out += "User: user\nPID: 42\nFile: /dev/null <BOLD><GREEN>✔<RESET><RESET>\nCommand: /bin/command\n"
|
||||
out += "Working directory: /working/dir\nRoot directory: /root/dir\nLog: /log/dir\nError log: /log/dir/error\n"
|
||||
out += "Interval: 3600s\nCron: 5 * * * *\n"
|
||||
formula = {
|
||||
name: "service",
|
||||
user: "user",
|
||||
status: :started,
|
||||
file: "/dev/null",
|
||||
running: true,
|
||||
loaded: true,
|
||||
schedulable: false,
|
||||
pid: 42,
|
||||
command: "/bin/command",
|
||||
working_dir: "/working/dir",
|
||||
root_dir: "/root/dir",
|
||||
log_path: "/log/dir",
|
||||
error_log_path: "/log/dir/error",
|
||||
interval: 3600,
|
||||
cron: "5 * * * *",
|
||||
}
|
||||
expect(described_class.output(formula, verbose: true)).to eq(out)
|
||||
end
|
||||
end
|
||||
end
|
169
Library/Homebrew/test/services/commands/list_spec.rb
Normal file
169
Library/Homebrew/test/services/commands/list_spec.rb
Normal file
@ -0,0 +1,169 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "services/service"
|
||||
|
||||
# needs for tty color tests
|
||||
module Tty
|
||||
def self.green
|
||||
"<GREEN>"
|
||||
end
|
||||
|
||||
def self.yellow
|
||||
"<YELLOW>"
|
||||
end
|
||||
|
||||
def self.red
|
||||
"<RED>"
|
||||
end
|
||||
|
||||
def self.default
|
||||
"<DEFAULT>"
|
||||
end
|
||||
|
||||
def self.bold
|
||||
"<BOLD>"
|
||||
end
|
||||
|
||||
def self.reset
|
||||
"<RESET>"
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe Service::Commands::List do
|
||||
describe "#TRIGGERS" do
|
||||
it "contains all restart triggers" do
|
||||
expect(described_class::TRIGGERS).to eq([nil, "list", "ls"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#run" do
|
||||
it "fails with empty list" do
|
||||
allow_any_instance_of(IO).to receive(:tty?).and_return(true)
|
||||
expect(Service::Formulae).to receive(:services_list).and_return([])
|
||||
expect do
|
||||
described_class.run
|
||||
end.to output(a_string_including("No services available to control with `brew services`")).to_stderr
|
||||
end
|
||||
|
||||
it "succeeds with list" do
|
||||
out = "<BOLD>Name Status User File<RESET>\nservice <GREEN>started<RESET> user /dev/null\n"
|
||||
formula = instance_double(
|
||||
Service::FormulaWrapper,
|
||||
name: "service",
|
||||
owner: "user",
|
||||
status_symbol: :started,
|
||||
service_file: +File::NULL,
|
||||
loaded?: true,
|
||||
)
|
||||
expect(Service::Formulae).to receive(:services_list).and_return([formula])
|
||||
expect do
|
||||
described_class.run
|
||||
end.to output(out).to_stdout
|
||||
end
|
||||
|
||||
it "succeeds with list - JSON" do
|
||||
formula = {
|
||||
name: "service",
|
||||
user: "user",
|
||||
status: :started,
|
||||
file: "/dev/null",
|
||||
running: true,
|
||||
loaded: true,
|
||||
schedulable: false,
|
||||
}
|
||||
|
||||
filtered_formula = formula.slice(*described_class::JSON_FIELDS)
|
||||
expected_output = "#{JSON.pretty_generate([filtered_formula])}\n"
|
||||
|
||||
expect(Service::Formulae).to receive(:services_list).and_return([formula])
|
||||
expect do
|
||||
described_class.run(json: true)
|
||||
end.to output(expected_output).to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
describe "#print_table" do
|
||||
it "prints all standard values" do
|
||||
formula = { name: "a", user: "u", file: Pathname.new("/tmp/file.file"), status: :stopped }
|
||||
expect do
|
||||
described_class.print_table([formula])
|
||||
end.to output("<BOLD>Name Status User File<RESET>\na <DEFAULT>stopped<RESET> u \n").to_stdout
|
||||
end
|
||||
|
||||
it "prints without user or file data" do
|
||||
formula = { name: "a", user: nil, file: nil, status: :started, loaded: true }
|
||||
expect do
|
||||
described_class.print_table([formula])
|
||||
end.to output("<BOLD>Name Status User File<RESET>\na <GREEN>started<RESET> \n").to_stdout
|
||||
end
|
||||
|
||||
it "prints shortened home directory" do
|
||||
ENV["HOME"] = "/tmp"
|
||||
formula = { name: "a", user: "u", file: Pathname.new("/tmp/file.file"), status: :started, loaded: true }
|
||||
expected_output = "<BOLD>Name Status User File<RESET>\na <GREEN>started<RESET> u ~/file.file\n"
|
||||
expect do
|
||||
described_class.print_table([formula])
|
||||
end.to output(expected_output).to_stdout
|
||||
end
|
||||
|
||||
it "prints an error code" do
|
||||
file = Pathname.new("/tmp/file.file")
|
||||
formula = { name: "a", user: "u", file:, status: :error, exit_code: 256, loaded: true }
|
||||
expected_output = "<BOLD>Name Status User File<RESET>\na <RED>error <RESET>256 u /tmp/file.file\n"
|
||||
expect do
|
||||
described_class.print_table([formula])
|
||||
end.to output(expected_output).to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
describe "#print_json" do
|
||||
it "prints all standard values" do
|
||||
formula = { name: "a", status: :stopped, user: "u", file: Pathname.new("/tmp/file.file") }
|
||||
expected_output = "#{JSON.pretty_generate([formula])}\n"
|
||||
expect do
|
||||
described_class.print_json([formula])
|
||||
end.to output(expected_output).to_stdout
|
||||
end
|
||||
|
||||
it "prints without user or file data" do
|
||||
formula = { name: "a", user: nil, file: nil, status: :started, loaded: true }
|
||||
filtered_formula = formula.slice(*described_class::JSON_FIELDS)
|
||||
expected_output = "#{JSON.pretty_generate([filtered_formula])}\n"
|
||||
expect do
|
||||
described_class.print_json([formula])
|
||||
end.to output(expected_output).to_stdout
|
||||
end
|
||||
|
||||
it "includes an exit code" do
|
||||
file = Pathname.new("/tmp/file.file")
|
||||
formula = { name: "a", user: "u", file:, status: :error, exit_code: 256, loaded: true }
|
||||
filtered_formula = formula.slice(*described_class::JSON_FIELDS)
|
||||
expected_output = "#{JSON.pretty_generate([filtered_formula])}\n"
|
||||
expect do
|
||||
described_class.print_json([formula])
|
||||
end.to output(expected_output).to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
describe "#get_status_string" do
|
||||
it "returns started" do
|
||||
expect(described_class.get_status_string(:started)).to eq("<GREEN>started<RESET>")
|
||||
end
|
||||
|
||||
it "returns stopped" do
|
||||
expect(described_class.get_status_string(:stopped)).to eq("<DEFAULT>stopped<RESET>")
|
||||
end
|
||||
|
||||
it "returns error" do
|
||||
expect(described_class.get_status_string(:error)).to eq("<RED>error <RESET>")
|
||||
end
|
||||
|
||||
it "returns unknown" do
|
||||
expect(described_class.get_status_string(:unknown)).to eq("<YELLOW>unknown<RESET>")
|
||||
end
|
||||
|
||||
it "returns other" do
|
||||
expect(described_class.get_status_string(:other)).to eq("<YELLOW>other<RESET>")
|
||||
end
|
||||
end
|
||||
end
|
46
Library/Homebrew/test/services/commands/restart_spec.rb
Normal file
46
Library/Homebrew/test/services/commands/restart_spec.rb
Normal file
@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "services/service"
|
||||
|
||||
RSpec.describe Service::Commands::Restart do
|
||||
describe "#TRIGGERS" do
|
||||
it "contains all restart triggers" do
|
||||
expect(described_class::TRIGGERS).to eq(%w[restart relaunch reload r])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#run" do
|
||||
it "fails with empty list" do
|
||||
expect do
|
||||
described_class.run([], verbose: false)
|
||||
end.to raise_error UsageError,
|
||||
a_string_including("Formula(e) missing, please provide a formula name or use --all")
|
||||
end
|
||||
|
||||
it "starts if services are not loaded" do
|
||||
expect(Service::ServicesCli).not_to receive(:run)
|
||||
expect(Service::ServicesCli).not_to receive(:stop)
|
||||
expect(Service::ServicesCli).to receive(:start).once
|
||||
service = instance_double(Service::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(Service::ServicesCli).not_to receive(:run)
|
||||
expect(Service::ServicesCli).to receive(:start).once
|
||||
expect(Service::ServicesCli).to receive(:stop).once
|
||||
service = instance_double(Service::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(Service::ServicesCli).not_to receive(:start)
|
||||
expect(Service::ServicesCli).to receive(:run).once
|
||||
expect(Service::ServicesCli).to receive(:stop).once
|
||||
service = instance_double(Service::FormulaWrapper, service_name: "name", loaded?: true,
|
||||
service_file_present?: false)
|
||||
expect(described_class.run([service], verbose: false)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
28
Library/Homebrew/test/services/formulae_spec.rb
Normal file
28
Library/Homebrew/test/services/formulae_spec.rb
Normal file
@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "services/service"
|
||||
|
||||
RSpec.describe Service::Formulae do
|
||||
describe "#services_list" do
|
||||
it "empty list without available formulae" do
|
||||
allow(described_class).to receive(:available_services).and_return({})
|
||||
expect(described_class.services_list).to eq([])
|
||||
end
|
||||
|
||||
it "list with available formulae" do
|
||||
formula = instance_double(Service::FormulaWrapper)
|
||||
expected = [
|
||||
{
|
||||
file: Pathname.new("/Library/LaunchDaemons/file.plist"),
|
||||
name: "formula",
|
||||
status: :known,
|
||||
user: "root",
|
||||
},
|
||||
]
|
||||
|
||||
expect(formula).to receive(:to_hash).and_return(expected[0])
|
||||
allow(described_class).to receive(:available_services).and_return([formula])
|
||||
expect(described_class.services_list).to eq(expected)
|
||||
end
|
||||
end
|
||||
end
|
388
Library/Homebrew/test/services/formulae_wrapper_spec.rb
Normal file
388
Library/Homebrew/test/services/formulae_wrapper_spec.rb
Normal file
@ -0,0 +1,388 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "services/service"
|
||||
require "tempfile"
|
||||
|
||||
RSpec.describe Service::FormulaWrapper do
|
||||
subject(:service) { described_class.new(formula) }
|
||||
|
||||
let(:formula) do
|
||||
instance_double(Formula,
|
||||
name: "mysql",
|
||||
plist_name: "plist-mysql-test",
|
||||
service_name: "plist-mysql-test",
|
||||
launchd_service_path: Pathname.new("/usr/local/opt/mysql/homebrew.mysql.plist"),
|
||||
systemd_service_path: Pathname.new("/usr/local/opt/mysql/homebrew.mysql.service"),
|
||||
opt_prefix: Pathname.new("/usr/local/opt/mysql"),
|
||||
any_version_installed?: true,
|
||||
service?: false)
|
||||
end
|
||||
|
||||
let(:service_object) do
|
||||
instance_double(Homebrew::Service,
|
||||
requires_root?: false,
|
||||
timed?: false,
|
||||
keep_alive?: false,
|
||||
command: "/bin/cmd",
|
||||
manual_command: "/bin/cmd",
|
||||
working_dir: nil,
|
||||
root_dir: nil,
|
||||
log_path: nil,
|
||||
error_log_path: nil,
|
||||
interval: nil,
|
||||
cron: nil)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(formula).to receive(:service).and_return(service_object)
|
||||
ENV["HOME"] = "/tmp_home"
|
||||
end
|
||||
|
||||
describe "#service_file" do
|
||||
it "macOS - outputs the full service file path" do
|
||||
allow(Service::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(Service::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(Service::System).to receive_messages(launchctl?: false, systemctl?: false)
|
||||
expect(service.service_file).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#name" do
|
||||
it "outputs formula name" do
|
||||
expect(service.name).to eq("mysql")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#service_name" do
|
||||
it "macOS - outputs the service name" do
|
||||
allow(Service::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(Service::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(Service::System).to receive_messages(launchctl?: false, systemctl?: false)
|
||||
expect(service.service_name).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#dest_dir" do
|
||||
before do
|
||||
allow(Service::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(Service::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(Service::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(Service::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(Service::System).to receive_messages(root?: true, launchctl?: false, systemctl?: true)
|
||||
expect(service.dest_dir.to_s).to eq("/usr/lib/systemd/system")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#dest" do
|
||||
before do
|
||||
ENV["HOME"] = "/tmp_home"
|
||||
allow(Service::System).to receive_messages(launchctl?: false, systemctl?: false)
|
||||
end
|
||||
|
||||
it "macOS - outputs the destination for the service file" do
|
||||
allow(Service::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(Service::System).to receive(:systemctl?).and_return(true)
|
||||
expect(service.dest.to_s).to eq("/tmp_home/.config/systemd/user/homebrew.mysql.service")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#installed?" do
|
||||
it "outputs if the service formula is installed" do
|
||||
expect(service.installed?).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#loaded?" do
|
||||
it "macOS - outputs if the service is loaded" do
|
||||
allow(Service::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(Service::System).to receive_messages(launchctl?: false, systemctl?: true)
|
||||
allow(Service::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(Service::System).to receive_messages(launchctl?: false, systemctl?: false)
|
||||
expect(service.loaded?).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#plist?" do
|
||||
it "false if not installed" do
|
||||
allow(service).to receive(:installed?).and_return(false)
|
||||
expect(service.plist?).to be(false)
|
||||
end
|
||||
|
||||
it "true if installed and file" do
|
||||
tempfile = File.new("/tmp/foo", File::CREAT)
|
||||
allow(service).to receive_messages(installed?: true, service_file: Pathname.new(tempfile))
|
||||
expect(service.plist?).to be(true)
|
||||
File.delete(tempfile)
|
||||
end
|
||||
|
||||
it "false if opt_prefix missing" do
|
||||
allow(service).to receive_messages(installed?: true,
|
||||
service_file: Pathname.new(File::NULL),
|
||||
formula: instance_double(Formula,
|
||||
plist: nil,
|
||||
opt_prefix: Pathname.new("/dfslkfhjdsolshlk")))
|
||||
expect(service.plist?).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#owner" do
|
||||
it "root if file present" do
|
||||
allow(service).to receive(:boot_path_service_file_present?).and_return(true)
|
||||
expect(service.owner).to eq("root")
|
||||
end
|
||||
|
||||
it "user if file present" do
|
||||
allow(service).to receive_messages(boot_path_service_file_present?: false,
|
||||
user_path_service_file_present?: true)
|
||||
allow(Service::System).to receive(:user).and_return("user")
|
||||
expect(service.owner).to eq("user")
|
||||
end
|
||||
|
||||
it "nil if no file present" do
|
||||
allow(service).to receive_messages(boot_path_service_file_present?: false,
|
||||
user_path_service_file_present?: false)
|
||||
expect(service.owner).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#service_file_present?" do
|
||||
it "macOS - outputs if the service file is present" do
|
||||
allow(Service::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(Service::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(Service::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(Service::System).to receive_messages(launchctl?: true, systemctl?: false)
|
||||
expect(service.owner).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#pid?" do
|
||||
it "outputs false because there is not pid" do
|
||||
allow(service).to receive(:pid).and_return(nil)
|
||||
expect(service.pid?).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#pid" do
|
||||
it "outputs nil because there is not pid" do
|
||||
expect(service.pid).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#error?" do
|
||||
it "outputs false because there a no PID" do
|
||||
allow(service).to receive(:pid).and_return(nil)
|
||||
expect(service.error?).to be(false)
|
||||
end
|
||||
|
||||
it "outputs false because there is a PID but no exit" do
|
||||
allow(service).to receive_messages(pid: 12, exit_code: nil)
|
||||
expect(service.error?).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#exit_code" do
|
||||
it "outputs nil because there is no exit code" do
|
||||
expect(service.exit_code).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#unknown_status?" do
|
||||
it "outputs true because there is no PID" do
|
||||
expect(service.unknown_status?).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#timed?" do
|
||||
it "returns true if timed service" do
|
||||
service_stub = instance_double(Homebrew::Service, timed?: true)
|
||||
allow(service).to receive_messages(service?: true, load_service: service_stub)
|
||||
allow(service_stub).to receive(:timed?).and_return(true)
|
||||
|
||||
expect(service.timed?).to be(true)
|
||||
end
|
||||
|
||||
it "returns false if no timed service" do
|
||||
service_stub = instance_double(Homebrew::Service, timed?: false)
|
||||
|
||||
allow(service).to receive(:service?).once.and_return(true)
|
||||
allow(service).to receive(:load_service).once.and_return(service_stub)
|
||||
allow(service_stub).to receive(:timed?).and_return(false)
|
||||
|
||||
expect(service.timed?).to be(false)
|
||||
end
|
||||
|
||||
it "returns nil if no service" do
|
||||
allow(service).to receive(:service?).once.and_return(false)
|
||||
|
||||
expect(service.timed?).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#keep_alive?" do
|
||||
it "returns true if service needs to stay alive" do
|
||||
service_stub = instance_double(Homebrew::Service, keep_alive?: true)
|
||||
|
||||
allow(service).to receive(:service?).once.and_return(true)
|
||||
allow(service).to receive(:load_service).once.and_return(service_stub)
|
||||
|
||||
expect(service.keep_alive?).to be(true)
|
||||
end
|
||||
|
||||
it "returns false if service does not need to stay alive" do
|
||||
service_stub = instance_double(Homebrew::Service, keep_alive?: false)
|
||||
|
||||
allow(service).to receive(:service?).once.and_return(true)
|
||||
allow(service).to receive(:load_service).once.and_return(service_stub)
|
||||
|
||||
expect(service.keep_alive?).to be(false)
|
||||
end
|
||||
|
||||
it "returns nil if no service" do
|
||||
allow(service).to receive(:service?).once.and_return(false)
|
||||
|
||||
expect(service.keep_alive?).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#service_startup?" do
|
||||
it "outputs false since there is no startup" do
|
||||
expect(service.service_startup?).to be(false)
|
||||
end
|
||||
|
||||
it "outputs true since there is a startup service" do
|
||||
allow(service).to receive(:service?).once.and_return(true)
|
||||
allow(service).to receive(:load_service).and_return(instance_double(Homebrew::Service, requires_root?: true))
|
||||
|
||||
expect(service.service_startup?).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#to_hash" do
|
||||
it "represents non-service values" do
|
||||
allow(Service::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,
|
||||
file: Pathname.new("/usr/local/opt/mysql/homebrew.mysql.plist"),
|
||||
loaded: false,
|
||||
name: "mysql",
|
||||
pid: nil,
|
||||
running: false,
|
||||
schedulable: nil,
|
||||
service_name: "plist-mysql-test",
|
||||
status: :none,
|
||||
user: nil,
|
||||
}
|
||||
expect(service.to_hash).to eq(expected)
|
||||
end
|
||||
|
||||
it "represents running non-service values" do
|
||||
ENV["HOME"] = "/tmp_home"
|
||||
allow(Service::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 = {
|
||||
exit_code: nil,
|
||||
file: Pathname.new("/tmp_home/Library/LaunchAgents/homebrew.mysql.plist"),
|
||||
loaded: false,
|
||||
name: "mysql",
|
||||
pid: nil,
|
||||
running: false,
|
||||
schedulable: nil,
|
||||
service_name: "plist-mysql-test",
|
||||
status: :none,
|
||||
user: nil,
|
||||
}
|
||||
expect(service.to_hash).to eq(expected)
|
||||
end
|
||||
|
||||
it "represents service values" do
|
||||
ENV["HOME"] = "/tmp_home"
|
||||
allow(Service::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)
|
||||
expected = {
|
||||
command: "/bin/cmd",
|
||||
cron: nil,
|
||||
error_log_path: nil,
|
||||
exit_code: nil,
|
||||
file: Pathname.new("/tmp_home/Library/LaunchAgents/homebrew.mysql.plist"),
|
||||
interval: nil,
|
||||
loaded: false,
|
||||
log_path: nil,
|
||||
name: "mysql",
|
||||
pid: nil,
|
||||
root_dir: nil,
|
||||
running: false,
|
||||
schedulable: false,
|
||||
service_name: "plist-mysql-test",
|
||||
status: :none,
|
||||
user: nil,
|
||||
working_dir: nil,
|
||||
}
|
||||
expect(service.to_hash).to eq(expected)
|
||||
end
|
||||
end
|
||||
end
|
296
Library/Homebrew/test/services/services_cli_spec.rb
Normal file
296
Library/Homebrew/test/services/services_cli_spec.rb
Normal file
@ -0,0 +1,296 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "services/service"
|
||||
|
||||
RSpec.describe Service::ServicesCli do
|
||||
subject(:services_cli) { described_class }
|
||||
|
||||
let(:service_string) { "service" }
|
||||
|
||||
describe "#bin" do
|
||||
it "outputs command name" do
|
||||
expect(services_cli.bin).to eq("brew services")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#running" do
|
||||
it "macOS - returns the currently running services" do
|
||||
allow(Service::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
|
||||
1234 34 homebrew.mxcl.postgresql@14
|
||||
EOS
|
||||
expect(services_cli.running).to eq([
|
||||
"homebrew.mxcl.php",
|
||||
"homebrew.mxcl.node_exporter",
|
||||
"homebrew.mxcl.postgresql@14",
|
||||
])
|
||||
end
|
||||
|
||||
it "systemD - returns the currently running services" do
|
||||
allow(Service::System).to receive(:launchctl?).and_return(false)
|
||||
allow(Service::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
|
||||
user@1000.service loaded active running User Manager for UID 1000
|
||||
EOS
|
||||
expect(services_cli.running).to eq(["homebrew.php.service"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#check" do
|
||||
it "checks the input does not exist" do
|
||||
expect do
|
||||
services_cli.check([])
|
||||
end.to raise_error(UsageError,
|
||||
a_string_including("Formula(e) missing, please provide a formula name or use --all"))
|
||||
end
|
||||
|
||||
it "checks the input exists" do
|
||||
expect do
|
||||
services_cli.check("hello")
|
||||
end.not_to raise_error(UsageError,
|
||||
a_string_including("Formula(e) missing, please provide a formula name or use --all"))
|
||||
end
|
||||
end
|
||||
|
||||
describe "#kill_orphaned_services" do
|
||||
it "skips unmanaged services" do
|
||||
service = instance_double(service_string, name: "example_service")
|
||||
allow(services_cli).to receive(:running).and_return(["example_service"])
|
||||
allow(Service::FormulaWrapper).to receive(:from).and_return(service)
|
||||
expect do
|
||||
services_cli.kill_orphaned_services
|
||||
end.to output("Service example_service not managed by `brew services` => skipping\n").to_stdout
|
||||
end
|
||||
|
||||
it "tries but is unable to kill a non existing service" do
|
||||
service = instance_double(
|
||||
service_string,
|
||||
name: "example_service",
|
||||
pid?: true,
|
||||
dest: Pathname("this_path_does_not_exist"),
|
||||
keep_alive?: false,
|
||||
)
|
||||
allow(service).to receive(:service_name)
|
||||
allow(Service::FormulaWrapper).to receive(:from).and_return(service)
|
||||
allow(services_cli).to receive(:running).and_return(["example_service"])
|
||||
expect do
|
||||
services_cli.kill_orphaned_services
|
||||
end.to output(a_string_including("Killing `example_service`... (might take a while)")).to_stdout.and
|
||||
output(a_string_including("Unable to kill `example_service` (label: )")).to_stderr
|
||||
end
|
||||
end
|
||||
|
||||
describe "#run" do
|
||||
it "checks empty targets cause no error" do
|
||||
expect(Service::System).not_to receive(:root?)
|
||||
services_cli.run([])
|
||||
end
|
||||
|
||||
it "checks if target service is already running and suggests restart instead" do
|
||||
expected_output = "Service `example_service` already running, " \
|
||||
"use `brew services restart example_service` to restart.\n"
|
||||
service = instance_double(service_string, name: "example_service", pid?: true)
|
||||
expect do
|
||||
services_cli.run([service])
|
||||
end.to output(expected_output).to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
describe "#start" do
|
||||
it "checks missing file causes error" do
|
||||
expect(Service::System).not_to receive(:root?)
|
||||
expect do
|
||||
services_cli.start(["service_name"], "/hfdkjshksdjhfkjsdhf/fdsjghsdkjhb")
|
||||
end.to raise_error(UsageError, a_string_including("Provided service file does not exist"))
|
||||
end
|
||||
|
||||
it "checks empty targets cause no error" do
|
||||
expect(Service::System).not_to receive(:root?)
|
||||
services_cli.start([])
|
||||
end
|
||||
|
||||
it "checks if target service has already been started and suggests restart instead" do
|
||||
expected_output = "Service `example_service` already started, " \
|
||||
"use `brew services restart example_service` to restart.\n"
|
||||
service = instance_double(service_string, name: "example_service", pid?: true)
|
||||
expect do
|
||||
services_cli.start([service])
|
||||
end.to output(expected_output).to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
describe "#stop" do
|
||||
it "checks empty targets cause no error" do
|
||||
expect(Service::System).not_to receive(:root?)
|
||||
services_cli.stop([])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#kill" do
|
||||
it "checks empty targets cause no error" do
|
||||
expect(Service::System).not_to receive(:root?)
|
||||
services_cli.kill([])
|
||||
end
|
||||
|
||||
it "prints a message if service is not running" do
|
||||
expected_output = "Service `example_service` is not started.\n"
|
||||
service = instance_double(service_string, name: "example_service", pid?: false)
|
||||
expect do
|
||||
services_cli.kill([service])
|
||||
end.to output(expected_output).to_stdout
|
||||
end
|
||||
|
||||
it "prints a message if service is set to keep alive" do
|
||||
expected_output = "Service `example_service` is set to automatically restart and can't be killed.\n"
|
||||
service = instance_double(service_string, name: "example_service", pid?: true, keep_alive?: true)
|
||||
expect do
|
||||
services_cli.kill([service])
|
||||
end.to output(expected_output).to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
describe "#install_service_file" do
|
||||
it "checks service is installed" do
|
||||
service = instance_double(Service::FormulaWrapper, name: "name", installed?: false)
|
||||
expect do
|
||||
services_cli.install_service_file(service, nil)
|
||||
end.to raise_error(UsageError, a_string_including("Formula `name` is not installed"))
|
||||
end
|
||||
|
||||
it "checks service file exists" do
|
||||
service = instance_double(
|
||||
Service::FormulaWrapper,
|
||||
name: "name",
|
||||
installed?: true,
|
||||
service_file: instance_double(Pathname, exist?: false),
|
||||
)
|
||||
expect do
|
||||
services_cli.install_service_file(service, nil)
|
||||
end.to raise_error(
|
||||
UsageError,
|
||||
a_string_including(
|
||||
"Formula `name` has not implemented #plist, #service or installed a locatable service file",
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#systemd_load", :needs_linux do
|
||||
it "checks non-enabling run" do
|
||||
expect(Service::System::Systemctl).to receive(:executable).once.and_return("/bin/systemctl")
|
||||
expect(Service::System::Systemctl).to receive(:scope).once.and_return("--user")
|
||||
services_cli.systemd_load(
|
||||
instance_double(Service::FormulaWrapper, service_name: "name"),
|
||||
enable: false,
|
||||
)
|
||||
end
|
||||
|
||||
it "checks enabling run" do
|
||||
expect(Service::System::Systemctl).to receive(:executable).twice.and_return("/bin/systemctl")
|
||||
expect(Service::System::Systemctl).to receive(:scope).twice.and_return("--user")
|
||||
services_cli.systemd_load(
|
||||
instance_double(Service::FormulaWrapper, service_name: "name"),
|
||||
enable: true,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#launchctl_load", :needs_macos do
|
||||
it "checks non-enabling run" do
|
||||
expect(Service::System).to receive(:domain_target).once.and_return("target")
|
||||
expect(Service::System).to receive(:launchctl).once.and_return("/bin/launchctl")
|
||||
services_cli.launchctl_load(instance_double(Service::FormulaWrapper), file: "a", enable: false)
|
||||
end
|
||||
|
||||
it "checks enabling run" do
|
||||
expect(Service::System).to receive(:domain_target).twice.and_return("target")
|
||||
expect(Service::System).to receive(:launchctl).twice.and_return("/bin/launchctl")
|
||||
services_cli.launchctl_load(instance_double(Service::FormulaWrapper, service_name: "name"), file: "a",
|
||||
enable: true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#service_load" do
|
||||
it "checks non-root for login" do
|
||||
expect(Service::System).to receive(:launchctl?).once.and_return(false)
|
||||
expect(Service::System).to receive(:systemctl?).once.and_return(false)
|
||||
expect(Service::System).to receive(:root?).once.and_return(true)
|
||||
|
||||
expect do
|
||||
services_cli.service_load(
|
||||
instance_double(Service::FormulaWrapper, name: "name", service_name: "service.name",
|
||||
service_startup?: false), enable: false
|
||||
)
|
||||
end.to output(a_string_including("Successfully ran `name` (label: service.name)")).to_stdout.and
|
||||
output(a_string_including("name must be run as non-root to start at user login!")).to_stderr
|
||||
end
|
||||
|
||||
it "checks root for startup" do
|
||||
expect(Service::System).to receive(:launchctl?).once.and_return(false)
|
||||
expect(Service::System).to receive(:systemctl?).once.and_return(false)
|
||||
expect(Service::System).to receive(:root?).twice.and_return(false)
|
||||
expect do
|
||||
services_cli.service_load(
|
||||
instance_double(Service::FormulaWrapper, name: "name", service_name: "service.name",
|
||||
service_startup?: true),
|
||||
enable: false,
|
||||
)
|
||||
end.to output(a_string_including("Successfully ran `name` (label: service.name)")).to_stdout.and
|
||||
output(a_string_including("name must be run as root to start at system startup!")).to_stderr
|
||||
end
|
||||
|
||||
it "triggers launchctl" do
|
||||
expect(Service::System).to receive(:domain_target).once.and_return("target")
|
||||
expect(Service::System).to receive(:launchctl?).once.and_return(true)
|
||||
expect(Service::System).to receive(:launchctl).once
|
||||
expect(Service::System).not_to receive(:systemctl?)
|
||||
expect(Service::System).to receive(:root?).twice.and_return(false)
|
||||
expect do
|
||||
services_cli.service_load(
|
||||
instance_double(Service::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 "triggers systemctl" do
|
||||
expect(Service::System).to receive(:launchctl?).once.and_return(false)
|
||||
expect(Service::System).to receive(:systemctl?).once.and_return(true)
|
||||
expect(Service::System).to receive(:root?).thrice.and_return(false)
|
||||
expect do
|
||||
services_cli.service_load(
|
||||
instance_double(
|
||||
Service::FormulaWrapper,
|
||||
name: "name",
|
||||
service_name: "service.name",
|
||||
service_startup?: false,
|
||||
dest: instance_double(Pathname, exist?: true),
|
||||
),
|
||||
enable: false,
|
||||
)
|
||||
end.to output("Successfully ran `name` (label: service.name)\n").to_stdout
|
||||
end
|
||||
|
||||
it "represents correct action" do
|
||||
expect(Service::System).to receive(:launchctl?).once.and_return(false)
|
||||
expect(Service::System).to receive(:systemctl?).once.and_return(true)
|
||||
expect(Service::System).to receive(:root?).exactly(4).times.and_return(false)
|
||||
expect do
|
||||
services_cli.service_load(
|
||||
instance_double(
|
||||
Service::FormulaWrapper,
|
||||
name: "name",
|
||||
service_name: "service.name",
|
||||
service_startup?: false,
|
||||
dest: instance_double(Pathname, exist?: true),
|
||||
),
|
||||
enable: true,
|
||||
)
|
||||
end.to output("Successfully started `name` (label: service.name)\n").to_stdout
|
||||
end
|
||||
end
|
||||
end
|
23
Library/Homebrew/test/services/system/systemctl_spec.rb
Normal file
23
Library/Homebrew/test/services/system/systemctl_spec.rb
Normal file
@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "services/service"
|
||||
|
||||
RSpec.describe Service::System::Systemctl do
|
||||
describe ".scope" do
|
||||
it "outputs systemctl scope for user" do
|
||||
allow(Service::System).to receive(:root?).and_return(false)
|
||||
expect(described_class.scope).to eq("--user")
|
||||
end
|
||||
|
||||
it "outputs systemctl scope for root" do
|
||||
allow(Service::System).to receive(:root?).and_return(true)
|
||||
expect(described_class.scope).to eq("--system")
|
||||
end
|
||||
end
|
||||
|
||||
describe ".executable" do
|
||||
it "outputs systemctl command location", :needs_linux do
|
||||
expect(described_class.executable).to eq("/bin/systemctl")
|
||||
end
|
||||
end
|
||||
end
|
143
Library/Homebrew/test/services/system_spec.rb
Normal file
143
Library/Homebrew/test/services/system_spec.rb
Normal file
@ -0,0 +1,143 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "services/service"
|
||||
|
||||
RSpec.describe Service::System do
|
||||
describe "#launchctl" do
|
||||
it "macOS - outputs launchctl command location", :needs_macos do
|
||||
expect(described_class.launchctl).to eq(Pathname.new("/bin/launchctl"))
|
||||
end
|
||||
|
||||
it "Other - outputs launchctl command location", :needs_linux do
|
||||
expect(described_class.launchctl).to eq_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#launchctl?" do
|
||||
it "macOS - outputs launchctl presence", :needs_macos do
|
||||
expect(described_class.launchctl?).to be(true)
|
||||
end
|
||||
|
||||
it "Other - outputs launchctl presence", :needs_linux do
|
||||
expect(described_class.launchctl?).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#systemctl?" do
|
||||
it "Linux - outputs systemctl presence", :needs_linux do
|
||||
expect(described_class.systemctl?).to be(true)
|
||||
end
|
||||
|
||||
it "Other - outputs systemctl presence", :needs_macos do
|
||||
expect(described_class.systemctl?).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#root?" do
|
||||
it "checks if the command is ran as root" do
|
||||
expect(described_class.root?).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#user" do
|
||||
it "returns the current username" do
|
||||
expect(described_class.user).to eq(ENV.fetch("USER"))
|
||||
end
|
||||
end
|
||||
|
||||
describe "#user_of_process" do
|
||||
it "returns the username for empty PID" do
|
||||
expect(described_class.user_of_process(nil)).to eq(ENV.fetch("USER"))
|
||||
end
|
||||
|
||||
it "returns the PID username" do
|
||||
allow(Utils).to receive(:safe_popen_read).and_return <<~EOS
|
||||
USER
|
||||
user
|
||||
EOS
|
||||
expect(described_class.user_of_process(50)).to eq("user")
|
||||
end
|
||||
|
||||
it "returns nil if unavailable" do
|
||||
allow(Utils).to receive(:safe_popen_read).and_return <<~EOS
|
||||
USER
|
||||
EOS
|
||||
expect(described_class.user_of_process(50)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#domain_target" do
|
||||
it "returns the current domain target" do
|
||||
allow(described_class).to receive(:root?).and_return(false)
|
||||
expect(described_class.domain_target).to match(%r{gui/(\d+)})
|
||||
end
|
||||
|
||||
it "returns the root domain target" do
|
||||
allow(described_class).to receive(:root?).and_return(true)
|
||||
expect(described_class.domain_target).to match("system")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#boot_path" do
|
||||
it "macOS - returns the boot path" do
|
||||
allow(described_class).to receive(:launchctl?).and_return(true)
|
||||
expect(described_class.boot_path.to_s).to eq("/Library/LaunchDaemons")
|
||||
end
|
||||
|
||||
it "SystemD - returns the boot path" do
|
||||
allow(described_class).to receive_messages(launchctl?: false, systemctl?: true)
|
||||
expect(described_class.boot_path.to_s).to eq("/usr/lib/systemd/system")
|
||||
end
|
||||
|
||||
it "Unknown - returns no boot path" do
|
||||
allow(described_class).to receive_messages(launchctl?: false, systemctl?: false)
|
||||
expect(described_class.boot_path.to_s).to eq("")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#user_path" do
|
||||
it "macOS - returns the user path" do
|
||||
ENV["HOME"] = "/tmp_home"
|
||||
allow(described_class).to receive_messages(launchctl?: true, systemctl?: false)
|
||||
expect(described_class.user_path.to_s).to eq("/tmp_home/Library/LaunchAgents")
|
||||
end
|
||||
|
||||
it "systemD - returns the user path" do
|
||||
ENV["HOME"] = "/tmp_home"
|
||||
allow(described_class).to receive_messages(launchctl?: false, systemctl?: true)
|
||||
expect(described_class.user_path.to_s).to eq("/tmp_home/.config/systemd/user")
|
||||
end
|
||||
|
||||
it "Unknown - returns no user path" do
|
||||
ENV["HOME"] = "/tmp_home"
|
||||
allow(described_class).to receive_messages(launchctl?: false, systemctl?: false)
|
||||
expect(described_class.user_path.to_s).to eq("")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#path" do
|
||||
it "macOS - user - returns the current relevant path" do
|
||||
ENV["HOME"] = "/tmp_home"
|
||||
allow(described_class).to receive_messages(root?: false, launchctl?: true, systemctl?: false)
|
||||
expect(described_class.path.to_s).to eq("/tmp_home/Library/LaunchAgents")
|
||||
end
|
||||
|
||||
it "macOS - root- returns the current relevant path" do
|
||||
ENV["HOME"] = "/tmp_home"
|
||||
allow(described_class).to receive_messages(root?: true, launchctl?: true, systemctl?: false)
|
||||
expect(described_class.path.to_s).to eq("/Library/LaunchDaemons")
|
||||
end
|
||||
|
||||
it "systemD - user - returns the current relevant path" do
|
||||
ENV["HOME"] = "/tmp_home"
|
||||
allow(described_class).to receive_messages(root?: false, launchctl?: false, systemctl?: true)
|
||||
expect(described_class.path.to_s).to eq("/tmp_home/.config/systemd/user")
|
||||
end
|
||||
|
||||
it "systemD - root- returns the current relevant path" do
|
||||
ENV["HOME"] = "/tmp_home"
|
||||
allow(described_class).to receive_messages(root?: true, launchctl?: false, systemctl?: true)
|
||||
expect(described_class.path.to_s).to eq("/usr/lib/systemd/system")
|
||||
end
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user