Merge pull request #15396 from apainintheneck/custom-service-name

Custom service name
This commit is contained in:
Kevin 2023-05-17 22:09:31 -07:00 committed by GitHub
commit 946478aed4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 288 additions and 88 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

@ -1022,13 +1022,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.
@ -1058,8 +1058,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
@ -2176,7 +2174,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

@ -260,14 +260,22 @@ module Formulary
if (service_hash = json_formula["service"]) if (service_hash = json_formula["service"])
service_hash = Homebrew::Service.deserialize(service_hash) service_hash = Homebrew::Service.deserialize(service_hash)
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)
if (run_params = service_hash.delete(:run))
case run_params
when Hash
run(**run_params) run(**run_params)
else when Array, String
run run_params run run_params
end end
end
if (name_params = service_hash.delete(:name))
name(**name_params)
end
service_hash.each do |key, arg| service_hash.each do |key, arg|
public_send(key, arg) public_send(key, arg)
end end

View File

@ -21,15 +21,35 @@ module RuboCop
share: :opt_share, share: :opt_share,
}.freeze }.freeze
# At least one of these methods must be defined in a service block.
REQUIRED_METHOD_CALLS = [:run, :name].freeze
def audit_formula(_node, _class_node, _parent_class_node, body_node) def audit_formula(_node, _class_node, _parent_class_node, body_node)
service_node = find_block(body_node, :service) service_node = find_block(body_node, :service)
return if service_node.blank? return if service_node.blank?
method_calls = service_node.each_descendant(:send).group_by(&:method_name)
method_calls.delete(:service)
# NOTE: Solving the first problem here might solve the second one too
# so we don't show both of them at the same time.
if (method_calls.keys & REQUIRED_METHOD_CALLS).empty?
offending_node(service_node)
problem "Service blocks require `run` or `name` to be defined."
elsif !method_calls.key?(:run)
other_method_calls = method_calls.keys - [:name]
if other_method_calls.any?
offending_node(service_node)
problem "`run` must be defined to use methods other than `name` like #{other_method_calls}."
end
end
# This check ensures that cellar paths like `bin` are not referenced # This check ensures that cellar paths like `bin` are not referenced
# because their `opt_` variants are more portable and work with the # because their `opt_` variants are more portable and work with the API.
# API.
CELLAR_PATH_AUDIT_CORRECTIONS.each do |path, opt_path| CELLAR_PATH_AUDIT_CORRECTIONS.each do |path, opt_path|
find_every_method_call_by_name(service_node, path).each do |node| next unless method_calls.key?(path)
method_calls.fetch(path).each do |node|
offending_node(node) offending_node(node)
problem "Use `#{opt_path}` instead of `#{path}` in service blocks." do |corrector| problem "Use `#{opt_path}` instead of `#{path}` in service blocks." do |corrector|
corrector.replace(node.source_range, opt_path) corrector.replace(node.source_range, opt_path)

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,34 @@ module Homebrew
@formula @formula
end end
sig { returns(String) }
def default_plist_name
"homebrew.mxcl.#{@formula.name}"
end
sig { returns(String) }
def plist_name
@plist_name ||= default_plist_name
end
sig { returns(String) }
def default_service_name
"homebrew.#{@formula.name}"
end
sig { returns(String) }
def service_name
@service_name ||= default_service_name
end
sig { params(macos: T.nilable(String), linux: T.nilable(String)).void }
def name(macos: nil, linux: nil)
raise TypeError, "Service#name expects at least one String" if [macos, linux].none?(String)
@plist_name = macos if macos
@service_name = linux if linux
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 +190,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 +225,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 +383,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 +406,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 +415,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 +514,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 +530,17 @@ 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 name_params = {
macos: (plist_name if plist_name != default_plist_name),
linux: (service_name if service_name != default_service_name),
}.compact
unless command?
return name_params.blank? ? {} : { name: name_params }
end
cron_string = if @cron.present? cron_string = if @cron.present?
[:Minute, :Hour, :Day, :Month, :Weekday] [:Minute, :Hour, :Day, :Month, :Weekday]
@ -527,6 +551,7 @@ module Homebrew
sockets_string = "#{@sockets[:type]}://#{@sockets[:host]}:#{@sockets[:port]}" if @sockets.present? sockets_string = "#{@sockets[:type]}://#{@sockets[:host]}:#{@sockets[:port]}" if @sockets.present?
{ {
name: name_params.presence,
run: @run_params, run: @run_params,
run_type: @run_type, run_type: @run_type,
interval: @interval, interval: @interval,
@ -551,6 +576,12 @@ 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 = {}
hash[:name] = api_hash["name"].transform_keys(&:to_sym) if api_hash.key?("name")
# The service hash might not have a "run" command if it only documents
# an existing service file with the "name" command.
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,18 @@ 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
name macos: "custom.mxcl.foo", linux: "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,20 @@ 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
name macos: "custom.macos.beanstalkd", linux: "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(:name)
end end
specify "service helpers return data" do specify "service helpers return data" do

View File

@ -272,6 +272,7 @@ describe Formulary do
"link_overwrite" => ["bin/abc"], "link_overwrite" => ["bin/abc"],
"caveats" => "example caveat string\n/$HOME\n$HOMEBREW_PREFIX", "caveats" => "example caveat string\n/$HOME\n$HOMEBREW_PREFIX",
"service" => { "service" => {
"name" => { macos: "custom.launchd.name", linux: "custom.systemd.name" },
"run" => ["$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "test"], "run" => ["$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "test"],
"run_type" => "immediate", "run_type" => "immediate",
"working_dir" => "/$HOME", "working_dir" => "/$HOME",
@ -362,6 +363,8 @@ describe Formulary do
expect(formula.service.command).to eq(["#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd", "test"]) expect(formula.service.command).to eq(["#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd", "test"])
expect(formula.service.run_type).to eq(:immediate) expect(formula.service.run_type).to eq(:immediate)
expect(formula.service.working_dir).to eq(Dir.home) expect(formula.service.working_dir).to eq(Dir.home)
expect(formula.plist_name).to eq("custom.launchd.name")
expect(formula.service_name).to eq("custom.systemd.name")
expect do expect do
formula.install formula.install

View File

@ -5,6 +5,46 @@ require "rubocops/service"
describe RuboCop::Cop::FormulaAudit::Service do describe RuboCop::Cop::FormulaAudit::Service do
subject(:cop) { described_class.new } subject(:cop) { described_class.new }
it "reports offenses when a service block is missing a required command" do
expect_offense(<<~RUBY)
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
service do
^^^^^^^^^^ FormulaAudit/Service: Service blocks require `run` or `name` to be defined.
run_type :cron
working_dir "/tmp/example"
end
end
RUBY
end
it "reports no offenses when a service block only includes custom names" do
expect_no_offenses(<<~RUBY)
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
service do
name macos: "custom.mcxl.foo", linux: "custom.foo"
end
end
RUBY
end
it "reports offenses when a service block includes more than custom names and no run command" do
expect_offense(<<~RUBY)
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
service do
^^^^^^^^^^ FormulaAudit/Service: `run` must be defined to use methods other than `name` like [:working_dir].
name macos: "custom.mcxl.foo", linux: "custom.foo"
working_dir "/tmp/example"
end
end
RUBY
end
it "reports offenses when a formula's service block uses cellar paths" do it "reports offenses when a formula's service block uses cellar paths" do
expect_offense(<<~RUBY) expect_offense(<<~RUBY)
class Foo < Formula class Foo < Formula
@ -31,7 +71,7 @@ describe RuboCop::Cop::FormulaAudit::Service do
RUBY RUBY
end end
it "reports no offenses when a formula's service block only uses opt paths" do it "reports no offenses when a service block only uses opt paths" do
expect_no_offenses(<<~RUBY) expect_no_offenses(<<~RUBY)
class Bin < Formula class Bin < Formula
url "https://brew.sh/foo-1.0.tgz" url "https://brew.sh/foo-1.0.tgz"

View File

@ -949,6 +949,10 @@ describe Homebrew::Service do
describe ".deserialize" do describe ".deserialize" do
let(:serialized_hash) do let(:serialized_hash) do
{ {
"name" => {
"linux" => "custom.systemd.name",
"macos" => "custom.launchd.name",
},
"environment_variables" => { "environment_variables" => {
"PATH" => "$HOMEBREW_PREFIX/bin:$HOMEBREW_PREFIX/sbin:/usr/bin:/bin:/usr/sbin:/sbin", "PATH" => "$HOMEBREW_PREFIX/bin:$HOMEBREW_PREFIX/sbin:/usr/bin:/bin:/usr/sbin:/sbin",
}, },
@ -961,6 +965,10 @@ describe Homebrew::Service do
let(:deserialized_hash) do let(:deserialized_hash) do
{ {
name: {
linux: "custom.systemd.name",
macos: "custom.launchd.name",
},
environment_variables: { environment_variables: {
PATH: "#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin", PATH: "#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin",
}, },

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

View File

@ -871,13 +871,17 @@ Another example would be configuration files that should not be overwritten on p
There are two ways to add `launchd` plists and `systemd` services to a formula, so that [`brew services`](https://github.com/Homebrew/homebrew-services) can pick them up: There are two ways to add `launchd` plists and `systemd` services to a formula, so that [`brew services`](https://github.com/Homebrew/homebrew-services) can pick them up:
1. If the package already provides a service file the formula can install it into the prefix: 1. If the package already provides a service file the formula can reference it by name:
```ruby ```ruby
prefix.install_symlink "file.plist" => "#{plist_name}.plist" service do
prefix.install_symlink "file.service" => "#{service_name}.service" name macos: "custom.launchd.name",
linux: "custom.systemd.name"
end
``` ```
To find the file we append `.plist` to the `launchd` service name and `.service` to the `systemd` service name internally.
2. If the formula does not provide a service file you can generate one using the following stanza: 2. If the formula does not provide a service file you can generate one using the following stanza:
```ruby ```ruby
@ -900,7 +904,7 @@ There are two ways to add `launchd` plists and `systemd` services to a formula,
#### Service block methods #### Service block methods
This table lists the options you can set within a `service` block. Only the `run` field is required which indicates what to run. This table lists the options you can set within a `service` block. The `run` or `name` field must be defined inside the service block. The `run` field indicates what command to run and is required before using fields other than `name`.
| method | default | macOS | Linux | description | | method | default | macOS | Linux | description |
| ----------------------- | ------------ | :---: | :---: | ----------- | | ----------------------- | ------------ | :---: | :---: | ----------- |
@ -921,6 +925,7 @@ This table lists the options you can set within a `service` block. Only the `run
| `process_type` | - | yes | no-op | type of process to manage: `:background`, `:standard`, `:interactive` or `:adaptive` | `process_type` | - | yes | no-op | type of process to manage: `:background`, `:standard`, `:interactive` or `:adaptive`
| `macos_legacy_timers` | - | yes | no-op | timers created by `launchd` jobs are coalesced unless this is set | `macos_legacy_timers` | - | yes | no-op | timers created by `launchd` jobs are coalesced unless this is set
| `sockets` | - | yes | no-op | socket that is created as an accesspoint to the service | `sockets` | - | yes | no-op | socket that is created as an accesspoint to the service
| `name` | - | yes | yes | a hash with the `launchd` service name on macOS and/or the `systemd` service name on Linux
For services that are kept alive after starting you can use the default `run_type`: For services that are kept alive after starting you can use the default `run_type`: