service: add custom service name DSL

The main thing is that this DSL allows us to provide an
interface that can be serialized to the JSON API.

Changes:
- Homebrew::Service
  - Adds `#service_name` and `#plist_name` methods
    - Each is now included in the `#serialize` method as well
  - Eval block on instantiation
    - Before we lazy evaluated this but the cost is not significant
      and it complicated the code a bunch. This only gets called
      during install, when evaluating caveats and in the `brew service`
      command. It skips this evaluation if the service block isn't there.
  - Add `#command?` helper to avoid `#command.blank?` and `#command.present?`
- Formula
  - `#service` now returns a service whenever it's called. This call is
    hidden behind a call to `#service?` most of the time anyway so this
    should be fine.
  - `#plist_name` and `#service_name` now call the methods of the same name
    on the service class. This should have already been in the service object
    to begin with and keeping these methods here helps preserve backwards
    compatibility with people who were overwriting these methods before.
- Caveats
  - Prefer `service#command?`
  - Add helpers for checking on service commands
    - This duplicates some of the work in `brew services`. Maybe we should
      merge that repo in at some point.
  - Check for installed service at `#plist_name` or `#service_name`. I think
    this should be used instead of `Keg#plist_installed?` which checked for any plist file.
    We should think about deprecating `#plist_installed?` in the future.
  - Stop using `ps aux | grep #{formula.plist_name}` to check for service files
    because it was inaccurate (it always returns true on my machine) because the grep
    process is started before the ps process.
  - Note: The behavior is the same as it was before. This means that caveats
    only show up for custom service files on install or if they're already installed.
    Otherwise it won't show up in `brew info`. This is because it has to check
    first if the service file has been installed.
- Utils::Service
  - Add utils for evaluating if a service is installed and running. This duplicates
    some of the work already found in `brew services`. We should seriously consider
    merging `brew services` with the main brew repo in the future since it's already
    tightly coupled to the code in the main repo.
- Formulary.load_formula_from_api
  - Be more explicit about which types can be deserialized into run params since
    it is now possible for run params to be nil.
- Update and add tests
This commit is contained in:
apainintheneck 2023-04-13 23:33:31 -07:00
parent b0dc84b117
commit 1111706378
11 changed files with 209 additions and 77 deletions

View File

@ -2,6 +2,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require "language/python" require "language/python"
require "utils/service"
# A formula's caveats. # A formula's caveats.
# #
@ -153,33 +154,32 @@ class Caveats
end end
def service_caveats def service_caveats
return if !formula.plist && !formula.service? && !keg&.plist_installed? return if !formula.plist && !formula.service? && !Utils::Service.installed?(formula) && !keg&.plist_installed?
return if formula.service? && formula.service.command.blank? return if formula.service? && !formula.service.command? && !Utils::Service.installed?(formula)
s = [] s = []
command = if formula.service? command = if formula.service.command?
formula.service.manual_command formula.service.manual_command
else else
formula.plist_manual formula.plist_manual
end end
return <<~EOS if !which("launchctl") && formula.plist return <<~EOS if !Utils::Service.launchctl? && formula.plist
#{Formatter.warning("Warning:")} #{formula.name} provides a launchd plist which can only be used on macOS! #{Formatter.warning("Warning:")} #{formula.name} provides a launchd plist which can only be used on macOS!
You can manually execute the service instead with: You can manually execute the service instead with:
#{command} #{command}
EOS EOS
# Brew services only works with these two tools # Brew services only works with these two tools
return <<~EOS if !which("systemctl") && !which("launchctl") && formula.service? return <<~EOS if !Utils::Service.systemctl? && !Utils::Service.launchctl? && formula.service.command?
#{Formatter.warning("Warning:")} #{formula.name} provides a service which can only be used on macOS or systemd! #{Formatter.warning("Warning:")} #{formula.name} provides a service which can only be used on macOS or systemd!
You can manually execute the service instead with: You can manually execute the service instead with:
#{command} #{command}
EOS EOS
is_running_service = formula.service? && quiet_system("ps aux | grep #{formula.service.command&.first}") startup = formula.service.requires_root? || formula.plist_startup
startup = formula.service&.requires_root? || formula.plist_startup if Utils::Service.running?(formula)
if is_running_service || (formula.plist && quiet_system("/bin/launchctl list #{formula.plist_name} &>/dev/null"))
s << "To restart #{formula.full_name} after an upgrade:" s << "To restart #{formula.full_name} after an upgrade:"
s << " #{startup ? "sudo " : ""}brew services restart #{formula.full_name}" s << " #{startup ? "sudo " : ""}brew services restart #{formula.full_name}"
elsif startup elsif startup
@ -190,7 +190,7 @@ class Caveats
s << " brew services start #{formula.full_name}" s << " brew services start #{formula.full_name}"
end end
if formula.plist_manual || formula.service? if formula.plist_manual || formula.service.command?
s << "Or, if you don't want/need a background service you can just run:" s << "Or, if you don't want/need a background service you can just run:"
s << " #{command}" s << " #{command}"
end end

View File

@ -1015,13 +1015,13 @@ class Formula
# The generated launchd {.plist} service name. # The generated launchd {.plist} service name.
sig { returns(String) } sig { returns(String) }
def plist_name def plist_name
"homebrew.mxcl.#{name}" service.plist_name
end end
# The generated service name. # The generated service name.
sig { returns(String) } sig { returns(String) }
def service_name def service_name
"homebrew.#{name}" service.service_name
end end
# The generated launchd {.plist} file path. # The generated launchd {.plist} file path.
@ -1051,8 +1051,6 @@ class Formula
# The service specification of the software. # The service specification of the software.
def service def service
return unless service?
@service ||= Homebrew::Service.new(self, &self.class.service) @service ||= Homebrew::Service.new(self, &self.class.service)
end end
@ -2169,7 +2167,7 @@ class Formula
"disabled" => disabled?, "disabled" => disabled?,
"disable_date" => disable_date, "disable_date" => disable_date,
"disable_reason" => disable_reason, "disable_reason" => disable_reason,
"service" => service&.serialize, "service" => (service.serialize if service?),
"tap_git_head" => tap_git_head, "tap_git_head" => tap_git_head,
"ruby_source_path" => ruby_source_path, "ruby_source_path" => ruby_source_path,
"ruby_source_checksum" => {}, "ruby_source_checksum" => {},

View File

@ -288,7 +288,7 @@ module FormulaCellarChecks
def check_service_command(formula) def check_service_command(formula)
return unless formula.prefix.directory? return unless formula.prefix.directory?
return unless formula.service? return unless formula.service?
return if formula.service.command.blank? return unless formula.service.command?
"Service command does not exist" unless File.exist?(formula.service.command.first) "Service command does not exist" unless File.exist?(formula.service.command.first)
end end

View File

@ -1040,7 +1040,7 @@ on_request: installed_on_request?, options: options)
return return
end end
if formula.service? && formula.service.command.present? if formula.service? && formula.service.command?
service_path = formula.systemd_service_path service_path = formula.systemd_service_path
service_path.atomic_write(formula.service.to_systemd_unit) service_path.atomic_write(formula.service.to_systemd_unit)
service_path.chmod 0644 service_path.chmod 0644
@ -1052,7 +1052,7 @@ on_request: installed_on_request?, options: options)
end end
end end
service = if formula.service? && formula.service.command.present? service = if formula.service? && formula.service.command?
formula.service.to_plist formula.service.to_plist
elsif formula.plist elsif formula.plist
formula.plist formula.plist

View File

@ -261,9 +261,10 @@ module Formulary
run_params = service_hash.delete(:run) run_params = service_hash.delete(:run)
service do service do
T.bind(self, Homebrew::Service) T.bind(self, Homebrew::Service)
if run_params.is_a?(Hash) case run_params
when Hash
run(**run_params) run(**run_params)
else when Array, String
run run_params run run_params
end end
service_hash.each do |key, arg| service_hash.each do |key, arg|

View File

@ -28,7 +28,7 @@ module Homebrew
@run_type = RUN_TYPE_IMMEDIATE @run_type = RUN_TYPE_IMMEDIATE
@run_at_load = true @run_at_load = true
@environment_variables = {} @environment_variables = {}
@service_block = block instance_eval(&block) if block
end end
sig { returns(Formula) } sig { returns(Formula) }
@ -36,6 +36,40 @@ module Homebrew
@formula @formula
end end
sig { returns(String) }
def default_plist_name
"homebrew.mxcl.#{@formula.name}"
end
sig { params(name: T.nilable(String)).returns(String) }
def plist_name(name = nil)
case T.unsafe(name)
when nil
@plist_name ||= default_plist_name
when String
@plist_name = name
else
raise TypeError, "Service#plist_name expects a String"
end
end
sig { returns(String) }
def default_service_name
"homebrew.#{@formula.name}"
end
sig { params(name: T.nilable(String)).returns(String) }
def service_name(name = nil)
case T.unsafe(name)
when nil
@service_name ||= default_service_name
when String
@service_name = name
else
raise TypeError, "Service#service_name expects a String"
end
end
sig { sig {
params( params(
command: T.nilable(T.any(T::Array[String], String, Pathname)), command: T.nilable(T.any(T::Array[String], String, Pathname)),
@ -162,7 +196,6 @@ module Homebrew
# @return [Boolean] # @return [Boolean]
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def requires_root? def requires_root?
eval_service_block
@require_root.present? && @require_root == true @require_root.present? && @require_root == true
end end
@ -198,7 +231,6 @@ module Homebrew
# @return [Boolean] # @return [Boolean]
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def keep_alive? def keep_alive?
eval_service_block
@keep_alive.present? && @keep_alive[:always] != false @keep_alive.present? && @keep_alive[:always] != false
end end
@ -357,20 +389,22 @@ module Homebrew
sig { returns(T.nilable(T::Array[String])) } sig { returns(T.nilable(T::Array[String])) }
def command def command
eval_service_block
@run&.map(&:to_s) @run&.map(&:to_s)
end end
sig { returns(T::Boolean) }
def command?
@run.present?
end
# Returns the `String` command to run manually instead of the service. # Returns the `String` command to run manually instead of the service.
# @return [String] # @return [String]
sig { returns(String) } sig { returns(String) }
def manual_command def manual_command
eval_service_block
vars = @environment_variables.except(:PATH) vars = @environment_variables.except(:PATH)
.map { |k, v| "#{k}=\"#{v}\"" } .map { |k, v| "#{k}=\"#{v}\"" }
cmd = command out = vars + command if command?
out = vars + cmd if cmd.present?
out.join(" ") out.join(" ")
end end
@ -378,7 +412,6 @@ module Homebrew
# @return [Boolean] # @return [Boolean]
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def timed? def timed?
eval_service_block
@run_type == RUN_TYPE_CRON || @run_type == RUN_TYPE_INTERVAL @run_type == RUN_TYPE_CRON || @run_type == RUN_TYPE_INTERVAL
end end
@ -388,7 +421,7 @@ module Homebrew
def to_plist def to_plist
# command needs to be first because it initializes all other values # command needs to be first because it initializes all other values
base = { base = {
Label: @formula.plist_name, Label: plist_name,
ProgramArguments: command, ProgramArguments: command,
RunAtLoad: @run_at_load == true, RunAtLoad: @run_at_load == true,
} }
@ -487,10 +520,9 @@ module Homebrew
WantedBy=timers.target WantedBy=timers.target
[Timer] [Timer]
Unit=#{@formula.service_name} Unit=#{service_name}
EOS EOS
eval_service_block
options = [] options = []
options << "Persistent=true" if @run_type == RUN_TYPE_CRON options << "Persistent=true" if @run_type == RUN_TYPE_CRON
options << "OnUnitActiveSec=#{@interval}" if @run_type == RUN_TYPE_INTERVAL options << "OnUnitActiveSec=#{@interval}" if @run_type == RUN_TYPE_INTERVAL
@ -504,19 +536,18 @@ module Homebrew
timer + options.join("\n") timer + options.join("\n")
end end
# Only evaluate the service block once.
sig { void }
def eval_service_block
return if @eval_service_block
instance_eval(&@service_block)
@eval_service_block = true
end
# Prepare the service hash for inclusion in the formula API JSON. # Prepare the service hash for inclusion in the formula API JSON.
sig { returns(Hash) } sig { returns(Hash) }
def serialize def serialize
eval_service_block custom_plist_name = plist_name if plist_name != default_plist_name
custom_service_name = service_name if service_name != default_service_name
unless command?
return {
plist_name: custom_plist_name,
service_name: custom_service_name,
}.compact
end
cron_string = if @cron.present? cron_string = if @cron.present?
[:Minute, :Hour, :Day, :Month, :Weekday] [:Minute, :Hour, :Day, :Month, :Weekday]
@ -527,6 +558,8 @@ module Homebrew
sockets_string = "#{@sockets[:type]}://#{@sockets[:host]}:#{@sockets[:port]}" if @sockets.present? sockets_string = "#{@sockets[:type]}://#{@sockets[:host]}:#{@sockets[:port]}" if @sockets.present?
{ {
plist_name: custom_plist_name,
service_name: custom_service_name,
run: @run_params, run: @run_params,
run_type: @run_type, run_type: @run_type,
interval: @interval, interval: @interval,
@ -551,6 +584,17 @@ module Homebrew
sig { params(api_hash: Hash).returns(Hash) } sig { params(api_hash: Hash).returns(Hash) }
def self.deserialize(api_hash) def self.deserialize(api_hash)
hash = {} hash = {}
%w[plist_name service_name].each do |key|
next if (value = api_hash[key]).nil?
hash[key.to_sym] = value
end
# The service hash might not have a "run" command if it only documents
# an existing service file with "plist_name" or "service_name".
return hash unless api_hash.key?("run")
hash[:run] = hash[:run] =
case api_hash["run"] case api_hash["run"]
when String when String

View File

@ -32,6 +32,10 @@ describe Caveats do
describe "#caveats" do describe "#caveats" do
context "when f.plist is not nil", :needs_macos do context "when f.plist is not nil", :needs_macos do
before do
allow(Utils::Service).to receive(:launchctl?).and_return(true)
end
it "prints error when no launchd is present" do it "prints error when no launchd is present" do
f = formula do f = formula do
url "foo-1.0" url "foo-1.0"
@ -39,7 +43,7 @@ describe Caveats do
"plist_test.plist" "plist_test.plist"
end end
end end
allow_any_instance_of(Object).to receive(:which).with("launchctl").and_return(nil) expect(Utils::Service).to receive(:launchctl?).once.and_return(false)
expect(described_class.new(f).caveats).to include("provides a launchd plist which can only be used on macOS!") expect(described_class.new(f).caveats).to include("provides a launchd plist which can only be used on macOS!")
end end
@ -50,7 +54,7 @@ describe Caveats do
"plist_test.plist" "plist_test.plist"
end end
end end
expect(described_class.new(f).caveats).to include("login") expect(described_class.new(f).caveats).to include("restart at login")
end end
it "gives information about service" do it "gives information about service" do
@ -82,12 +86,25 @@ describe Caveats do
expect(caveats).to include("WARNING:") expect(caveats).to include("WARNING:")
expect(caveats).to include("tmux") expect(caveats).to include("tmux")
end end
# @todo This should get deprecated and the service block `plist_name` method should get used instead.
it "prints info when there are custom service files" do
f = formula do
url "foo-1.0"
def plist_name
"custom.mxcl.foo"
end
end
expect(Utils::Service).to receive(:installed?).with(f).once.and_return(true)
expect(Utils::Service).to receive(:running?).with(f).once.and_return(false)
expect(described_class.new(f).caveats).to include("restart at login")
end
end end
context "when f.service is not nil" do context "when service block is defined" do
before do before do
allow_any_instance_of(Object).to receive(:which).with("launchctl").and_return(true) allow(Utils::Service).to receive(:launchctl?).and_return(true)
allow_any_instance_of(Object).to receive(:which).with("systemctl").and_return(true) allow(Utils::Service).to receive(:systemctl?).and_return(true)
end end
it "prints warning when no service deamon is found" do it "prints warning when no service deamon is found" do
@ -97,9 +114,8 @@ describe Caveats do
run [bin/"cmd"] run [bin/"cmd"]
end end
end end
expect(Utils::Service).to receive(:launchctl?).twice.and_return(false)
allow_any_instance_of(Object).to receive(:which).with("launchctl").and_return(nil) expect(Utils::Service).to receive(:systemctl?).once.and_return(false)
allow_any_instance_of(Object).to receive(:which).with("systemctl").and_return(nil)
expect(described_class.new(f).caveats).to include("service which can only be used on macOS or systemd!") expect(described_class.new(f).caveats).to include("service which can only be used on macOS or systemd!")
end end
@ -111,9 +127,7 @@ describe Caveats do
require_root true require_root true
end end
end end
cmd = "#{HOMEBREW_CELLAR}/formula_name/1.0/bin/cmd" expect(Utils::Service).to receive(:running?).with(f).once.and_return(false)
allow(Homebrew).to receive(:_system).and_return(true)
allow(Homebrew).to receive(:_system).with("ps aux | grep #{cmd}").and_return(false)
expect(described_class.new(f).caveats).to include("startup") expect(described_class.new(f).caveats).to include("startup")
end end
@ -124,10 +138,8 @@ describe Caveats do
run [bin/"cmd"] run [bin/"cmd"]
end end
end end
cmd = "#{HOMEBREW_CELLAR}/formula_name/1.0/bin/cmd" expect(Utils::Service).to receive(:running?).with(f).once.and_return(false)
allow(Homebrew).to receive(:_system).and_return(true) expect(described_class.new(f).caveats).to include("restart at login")
expect(Homebrew).to receive(:_system).with("ps aux | grep #{cmd}").and_return(false)
expect(described_class.new(f).caveats).to include("login")
end end
it "gives information about require_root restarting services after upgrade" do it "gives information about require_root restarting services after upgrade" do
@ -138,10 +150,8 @@ describe Caveats do
require_root true require_root true
end end
end end
cmd = "#{HOMEBREW_CELLAR}/formula_name/1.0/bin/cmd"
f_obj = described_class.new(f) f_obj = described_class.new(f)
allow(Homebrew).to receive(:_system).and_return(true) expect(Utils::Service).to receive(:running?).with(f).once.and_return(true)
expect(Homebrew).to receive(:_system).with("ps aux | grep #{cmd}").and_return(true)
expect(f_obj.caveats).to include(" sudo brew services restart #{f.full_name}") expect(f_obj.caveats).to include(" sudo brew services restart #{f.full_name}")
end end
@ -152,10 +162,8 @@ describe Caveats do
run [bin/"cmd"] run [bin/"cmd"]
end end
end end
cmd = "#{HOMEBREW_CELLAR}/formula_name/1.0/bin/cmd"
f_obj = described_class.new(f) f_obj = described_class.new(f)
allow(Homebrew).to receive(:_system).and_return(true) expect(Utils::Service).to receive(:running?).with(f).once.and_return(true)
expect(Homebrew).to receive(:_system).with("ps aux | grep #{cmd}").and_return(true)
expect(f_obj.caveats).to include(" brew services restart #{f.full_name}") expect(f_obj.caveats).to include(" brew services restart #{f.full_name}")
end end
@ -167,10 +175,8 @@ describe Caveats do
require_root true require_root true
end end
end end
cmd = "#{HOMEBREW_CELLAR}/formula_name/1.0/bin/cmd"
f_obj = described_class.new(f) f_obj = described_class.new(f)
allow(Homebrew).to receive(:_system).and_return(true) expect(Utils::Service).to receive(:running?).with(f).once.and_return(false)
allow(Homebrew).to receive(:_system).with("ps aux | grep #{cmd}").and_return(false)
expect(f_obj.caveats).to include(" sudo brew services start #{f.full_name}") expect(f_obj.caveats).to include(" sudo brew services start #{f.full_name}")
end end
@ -181,10 +187,8 @@ describe Caveats do
run [bin/"cmd"] run [bin/"cmd"]
end end
end end
cmd = "#{HOMEBREW_CELLAR}/formula_name/1.0/bin/cmd"
f_obj = described_class.new(f) f_obj = described_class.new(f)
allow(Homebrew).to receive(:_system).and_return(true) expect(Utils::Service).to receive(:running?).with(f).once.and_return(false)
allow(Homebrew).to receive(:_system).with("ps aux | grep #{cmd}").and_return(false)
expect(f_obj.caveats).to include(" brew services start #{f.full_name}") expect(f_obj.caveats).to include(" brew services start #{f.full_name}")
end end
@ -202,6 +206,19 @@ describe Caveats do
expect(caveats).to include("if you don't want/need a background service") expect(caveats).to include("if you don't want/need a background service")
expect(caveats).to include("VAR=\"foo\" #{cmd} start") expect(caveats).to include("VAR=\"foo\" #{cmd} start")
end end
it "prints info when there are custom service files" do
f = formula do
url "foo-1.0"
service do
plist_name "custom.mxcl.foo"
service_name "custom.foo"
end
end
expect(Utils::Service).to receive(:installed?).with(f).once.and_return(true)
expect(Utils::Service).to receive(:running?).with(f).once.and_return(false)
expect(described_class.new(f).caveats).to include("restart at login")
end
end end
context "when f.keg_only is not nil" do context "when f.keg_only is not nil" do

View File

@ -218,21 +218,21 @@ describe FormulaInstaller do
it "works if service is set" do it "works if service is set" do
formula = Testball.new formula = Testball.new
service = Homebrew::Service.new(formula)
launchd_service_path = formula.launchd_service_path launchd_service_path = formula.launchd_service_path
service_path = formula.systemd_service_path service_path = formula.systemd_service_path
service = Homebrew::Service.new(formula)
formula.opt_prefix.mkpath formula.opt_prefix.mkpath
expect(formula).to receive(:plist).and_return(nil) expect(formula).to receive(:plist).and_return(nil)
expect(formula).to receive(:service?).exactly(3).and_return(true) expect(formula).to receive(:service?).exactly(3).and_return(true)
expect(formula).to receive(:service).exactly(5).and_return(service) expect(formula).to receive(:service).exactly(7).and_return(service)
expect(formula).to receive(:launchd_service_path).and_call_original expect(formula).to receive(:launchd_service_path).and_call_original
expect(formula).to receive(:systemd_service_path).and_call_original expect(formula).to receive(:systemd_service_path).and_call_original
expect(service).to receive(:timed?).and_return(false) expect(service).to receive(:timed?).and_return(false)
expect(service).to receive(:command?).exactly(2).and_return(true)
expect(service).to receive(:to_plist).and_return("plist") expect(service).to receive(:to_plist).and_return("plist")
expect(service).to receive(:to_systemd_unit).and_return("unit") expect(service).to receive(:to_systemd_unit).and_return("unit")
expect(service).to receive(:command).exactly(2).and_return("/bin/sh")
installer = described_class.new(formula) installer = described_class.new(formula)
expect do expect do
@ -245,24 +245,24 @@ describe FormulaInstaller do
it "works if timed service is set" do it "works if timed service is set" do
formula = Testball.new formula = Testball.new
service = Homebrew::Service.new(formula)
launchd_service_path = formula.launchd_service_path launchd_service_path = formula.launchd_service_path
service_path = formula.systemd_service_path service_path = formula.systemd_service_path
timer_path = formula.systemd_timer_path timer_path = formula.systemd_timer_path
service = Homebrew::Service.new(formula)
formula.opt_prefix.mkpath formula.opt_prefix.mkpath
expect(formula).to receive(:plist).and_return(nil) expect(formula).to receive(:plist).and_return(nil)
expect(formula).to receive(:service?).exactly(3).and_return(true) expect(formula).to receive(:service?).exactly(3).and_return(true)
expect(formula).to receive(:service).exactly(6).and_return(service) expect(formula).to receive(:service).exactly(9).and_return(service)
expect(formula).to receive(:launchd_service_path).and_call_original expect(formula).to receive(:launchd_service_path).and_call_original
expect(formula).to receive(:systemd_service_path).and_call_original expect(formula).to receive(:systemd_service_path).and_call_original
expect(formula).to receive(:systemd_timer_path).and_call_original expect(formula).to receive(:systemd_timer_path).and_call_original
expect(service).to receive(:to_plist).and_return("plist")
expect(service).to receive(:timed?).and_return(true) expect(service).to receive(:timed?).and_return(true)
expect(service).to receive(:command?).exactly(2).and_return(true)
expect(service).to receive(:to_plist).and_return("plist")
expect(service).to receive(:to_systemd_unit).and_return("unit") expect(service).to receive(:to_systemd_unit).and_return("unit")
expect(service).to receive(:to_systemd_timer).and_return("timer") expect(service).to receive(:to_systemd_timer).and_return("timer")
expect(service).to receive(:command).exactly(2).and_return("/bin/sh")
installer = described_class.new(formula) installer = described_class.new(formula)
expect do expect do

View File

@ -701,7 +701,7 @@ describe Formula do
url "https://brew.sh/test-1.0.tbz" url "https://brew.sh/test-1.0.tbz"
end end
expect(f.service).to be_nil expect(f.service.serialize).to eq({})
end end
specify "service complicated" do specify "service complicated" do
@ -717,7 +717,8 @@ describe Formula do
keep_alive true keep_alive true
end end
end end
expect(f.service).not_to be_nil expect(f.service.serialize.keys)
.to contain_exactly(:run, :run_type, :error_log_path, :log_path, :working_dir, :keep_alive)
end end
specify "service uses simple run" do specify "service uses simple run" do
@ -728,7 +729,21 @@ describe Formula do
end end
end end
expect(f.service).not_to be_nil expect(f.service.serialize.keys).to contain_exactly(:run, :run_type)
end
specify "service with only custom names" do
f = formula do
url "https://brew.sh/test-1.0.tbz"
service do
plist_name "custom.macos.beanstalkd"
service_name "custom.linux.beanstalkd"
end
end
expect(f.plist_name).to eq("custom.macos.beanstalkd")
expect(f.service_name).to eq("custom.linux.beanstalkd")
expect(f.service.serialize.keys).to contain_exactly(:plist_name, :service_name)
end end
specify "service helpers return data" do specify "service helpers return data" do

View File

@ -0,0 +1,50 @@
# typed: true
# frozen_string_literal: true
module Utils
# Helpers for `brew services` related code.
module Service
# Check if a service is running for a specified formula.
sig { params(formula: Formula).returns(T::Boolean) }
def self.running?(formula)
if launchctl?
quiet_system(launchctl, "list", formula.plist_name)
elsif systemctl?
quiet_system(systemctl, "is-active", "--quiet", formula.service_name)
end
end
# Check if a service file is installed in the expected location.
sig { params(formula: Formula).returns(T::Boolean) }
def self.installed?(formula)
(launchctl? && formula.launchd_service_path.exist?) ||
(systemctl? && formula.systemd_service_path.exist?)
end
# Path to launchctl binary.
sig { returns(T.nilable(Pathname)) }
def self.launchctl
return @launchctl if defined? @launchctl
@launchctl = which("launchctl")
end
# Path to systemctl binary.
sig { returns(T.nilable(Pathname)) }
def self.systemctl
return @systemctl if defined? @systemctl
@systemctl = which("systemctl")
end
sig { returns(T::Boolean) }
def self.launchctl?
!launchctl.nil?
end
sig { returns(T::Boolean) }
def self.systemctl?
!systemctl.nil?
end
end
end

View File

@ -0,0 +1,7 @@
# typed: strict
module Utils
module Service
include Kernel
end
end