Merge pull request #20324 from Homebrew/dug/low-shape-service

Reduce shape variations in Homebrew::Service
This commit is contained in:
Mike McQuaid 2025-07-29 07:52:15 +00:00 committed by GitHub
commit 3b61b44c8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 69 additions and 61 deletions

View File

@ -306,7 +306,7 @@ module FormulaCellarChecks
return unless formula.service? return unless formula.service?
return unless formula.service.command? return unless formula.service.command?
"Service command does not exist" unless File.exist?(T.must(formula.service.command).first) "Service command does not exist" unless File.exist?(formula.service.command.first)
end end
sig { params(formula: Formula).returns(T.nilable(String)) } sig { params(formula: Formula).returns(T.nilable(String)) }

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "ipaddr" require "ipaddr"
@ -23,13 +23,38 @@ module Homebrew
PROCESS_TYPE_ADAPTIVE = :adaptive PROCESS_TYPE_ADAPTIVE = :adaptive
KEEP_ALIVE_KEYS = [:always, :successful_exit, :crashed, :path].freeze KEEP_ALIVE_KEYS = [:always, :successful_exit, :crashed, :path].freeze
SOCKET_STRING_REGEX = %r{^([a-z]+)://(.+):([0-9]+)$}i
# sig { params(formula: Formula).void } RunParam = T.type_alias { T.nilable(T.any(T::Array[T.any(String, Pathname)], String, Pathname)) }
Sockets = T.type_alias { T::Hash[Symbol, { host: String, port: String, type: String }] }
sig { returns(String) }
attr_reader :plist_name, :service_name
sig { params(formula: Formula, block: T.nilable(T.proc.void)).void }
def initialize(formula, &block) def initialize(formula, &block)
@cron = T.let({}, T::Hash[Symbol, T.any(Integer, String)])
@environment_variables = T.let({}, T::Hash[Symbol, String])
@error_log_path = T.let(nil, T.nilable(String))
@formula = formula @formula = formula
@run_type = RUN_TYPE_IMMEDIATE @input_path = T.let(nil, T.nilable(String))
@run_at_load = true @interval = T.let(nil, T.nilable(Integer))
@environment_variables = {} @keep_alive = T.let({}, T::Hash[Symbol, T.untyped])
@launch_only_once = T.let(false, T::Boolean)
@log_path = T.let(nil, T.nilable(String))
@macos_legacy_timers = T.let(false, T::Boolean)
@plist_name = T.let(default_plist_name, String)
@process_type = T.let(nil, T.nilable(Symbol))
@require_root = T.let(false, T::Boolean)
@restart_delay = T.let(nil, T.nilable(Integer))
@root_dir = T.let(nil, T.nilable(String))
@run = T.let([], T::Array[String])
@run_at_load = T.let(true, T::Boolean)
@run_params = T.let(nil, T.any(RunParam, T::Hash[Symbol, RunParam]))
@run_type = T.let(RUN_TYPE_IMMEDIATE, Symbol)
@service_name = T.let(default_service_name, String)
@sockets = T.let({}, Sockets)
@working_dir = T.let(nil, T.nilable(String))
instance_eval(&block) if block instance_eval(&block) if block
end end
@ -43,21 +68,11 @@ module Homebrew
"homebrew.mxcl.#{@formula.name}" "homebrew.mxcl.#{@formula.name}"
end end
sig { returns(String) }
def plist_name
@plist_name ||= default_plist_name
end
sig { returns(String) } sig { returns(String) }
def default_service_name def default_service_name
"homebrew.#{@formula.name}" "homebrew.#{@formula.name}"
end end
sig { returns(String) }
def service_name
@service_name ||= default_service_name
end
sig { params(macos: T.nilable(String), linux: T.nilable(String)).void } sig { params(macos: T.nilable(String), linux: T.nilable(String)).void }
def name(macos: nil, linux: nil) def name(macos: nil, linux: nil)
raise TypeError, "Service#name expects at least one String" if [macos, linux].none?(String) raise TypeError, "Service#name expects at least one String" if [macos, linux].none?(String)
@ -68,9 +83,9 @@ module Homebrew
sig { sig {
params( params(
command: T.nilable(T.any(T::Array[T.any(String, Pathname)], String, Pathname)), command: T.nilable(RunParam),
macos: T.nilable(T.any(T::Array[T.any(String, Pathname)], String, Pathname)), macos: T.nilable(RunParam),
linux: T.nilable(T.any(T::Array[T.any(String, Pathname)], String, Pathname)), linux: T.nilable(RunParam),
).returns(T.nilable(T::Array[T.any(String, Pathname)])) ).returns(T.nilable(T::Array[T.any(String, Pathname)]))
} }
def run(command = nil, macos: nil, linux: nil) def run(command = nil, macos: nil, linux: nil)
@ -86,9 +101,9 @@ module Homebrew
when nil when nil
@run @run
when String, Pathname when String, Pathname
@run = [command] @run = [command.to_s]
when Array when Array
@run = command @run = command.map(&:to_s)
end end
end end
@ -161,18 +176,17 @@ module Homebrew
end end
end end
sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) } sig { params(value: T.nilable(T::Boolean)).returns(T::Boolean) }
def require_root(value = nil) def require_root(value = nil)
case value case value
when nil when nil
@require_root @require_root
when true, false when TrueClass, FalseClass
@require_root = value @require_root = value
end end
end end
# Returns a `Boolean` describing if a service requires root access. # Returns a `Boolean` describing if a service requires root access.
# @return [Boolean]
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def requires_root? def requires_root?
@require_root.present? && @require_root == true @require_root.present? && @require_root == true
@ -183,16 +197,14 @@ module Homebrew
case value case value
when nil when nil
@run_at_load @run_at_load
when true, false when TrueClass, FalseClass
@run_at_load = value @run_at_load = value
end end
end end
SOCKET_STRING_REGEX = %r{^([a-z]+)://(.+):([0-9]+)$}i
sig { sig {
params(value: T.nilable(T.any(String, T::Hash[Symbol, String]))) params(value: T.nilable(T.any(String, T::Hash[Symbol, String])))
.returns(T.nilable(T::Hash[Symbol, T::Hash[Symbol, String]])) .returns(T::Hash[Symbol, T::Hash[Symbol, String]])
} }
def sockets(value = nil) def sockets(value = nil)
return @sockets if value.nil? return @sockets if value.nil?
@ -204,9 +216,11 @@ module Homebrew
value value
end.transform_values do |socket_string| end.transform_values do |socket_string|
match = socket_string.match(SOCKET_STRING_REGEX) match = socket_string.match(SOCKET_STRING_REGEX)
raise TypeError, "Service#sockets a formatted socket definition as <type>://<host>:<port>" if match.blank? raise TypeError, "Service#sockets a formatted socket definition as <type>://<host>:<port>" unless match
type, host, port = match.captures type = T.must(match[1])
host = T.must(match[2])
port = T.must(match[3])
begin begin
IPAddr.new(host) IPAddr.new(host)
@ -219,18 +233,17 @@ module Homebrew
end end
# Returns a `Boolean` describing if a service is set to be kept alive. # Returns a `Boolean` describing if a service is set to be kept alive.
# @return [Boolean]
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def keep_alive? def keep_alive?
@keep_alive.present? && @keep_alive[:always] != false !@keep_alive.empty? && @keep_alive[:always] != false
end end
sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) } sig { params(value: T.nilable(T::Boolean)).returns(T::Boolean) }
def launch_only_once(value = nil) def launch_only_once(value = nil)
case value case value
when nil when nil
@launch_only_once @launch_only_once
when true, false when TrueClass, FalseClass
@launch_only_once = value @launch_only_once = value
end end
end end
@ -281,13 +294,13 @@ module Homebrew
end end
end end
sig { params(value: T.nilable(String)).returns(T.nilable(Hash)) } sig { params(value: T.nilable(String)).returns(T::Hash[Symbol, T.any(Integer, String)]) }
def cron(value = nil) def cron(value = nil)
case value case value
when nil when nil
@cron @cron
when String when String
@cron = parse_cron(T.must(value)) @cron = parse_cron(value)
end end
end end
@ -345,12 +358,12 @@ module Homebrew
end end
end end
sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) } sig { params(value: T.nilable(T::Boolean)).returns(T::Boolean) }
def macos_legacy_timers(value = nil) def macos_legacy_timers(value = nil)
case value case value
when nil when nil
@macos_legacy_timers @macos_legacy_timers
when true, false when TrueClass, FalseClass
@macos_legacy_timers = value @macos_legacy_timers = value
end end
end end
@ -362,36 +375,33 @@ module Homebrew
"#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin" "#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin"
end end
sig { returns(T.nilable(T::Array[String])) } sig { returns(T::Array[String]) }
def command def command
@run&.map(&:to_s)&.map { |arg| arg.start_with?("~") ? File.expand_path(arg) : arg } @run.map(&:to_s).map { |arg| arg.start_with?("~") ? File.expand_path(arg) : arg }
end end
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def command? def command?
@run.present? !@run.empty?
end 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]
sig { returns(String) } sig { returns(String) }
def manual_command def manual_command
vars = @environment_variables.except(:PATH) vars = @environment_variables.except(:PATH)
.map { |k, v| "#{k}=\"#{v}\"" } .map { |k, v| "#{k}=\"#{v}\"" }
out = vars + T.must(command).map { |arg| Utils::Shell.sh_quote(arg) } if command? vars.concat(command.map { |arg| Utils::Shell.sh_quote(arg) })
out.join(" ") vars.join(" ")
end end
# Returns a `Boolean` describing if a service is timed. # Returns a `Boolean` describing if a service is timed.
# @return [Boolean]
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def timed? def timed?
@run_type == RUN_TYPE_CRON || @run_type == RUN_TYPE_INTERVAL @run_type == RUN_TYPE_CRON || @run_type == RUN_TYPE_INTERVAL
end end
# Returns a `String` plist. # Returns a `String` plist.
# @return [String]
sig { returns(String) } sig { returns(String) }
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
@ -425,7 +435,7 @@ module Homebrew
end end
end end
if @sockets.present? unless @sockets.empty?
base[:Sockets] = {} base[:Sockets] = {}
@sockets.each do |name, info| @sockets.each do |name, info|
base[:Sockets][name] = { base[:Sockets][name] = {
@ -436,7 +446,7 @@ module Homebrew
end end
end end
if @cron.present? && @run_type == RUN_TYPE_CRON if !@cron.empty? && @run_type == RUN_TYPE_CRON
base[:StartCalendarInterval] = @cron.reject { |_, value| value == "*" } base[:StartCalendarInterval] = @cron.reject { |_, value| value == "*" }
end end
@ -452,12 +462,11 @@ module Homebrew
end end
# Returns a `String` systemd unit. # Returns a `String` systemd unit.
# @return [String]
sig { returns(String) } sig { returns(String) }
def to_systemd_unit def to_systemd_unit
# command needs to be first because it initializes all other values # command needs to be first because it initializes all other values
cmd = command&.map { |arg| Utils::Service.systemd_quote(arg) } cmd = command.map { |arg| Utils::Service.systemd_quote(arg) }
&.join(" ") .join(" ")
options = [] options = []
options << "Type=#{(@launch_only_once == true) ? "oneshot" : "simple"}" options << "Type=#{(@launch_only_once == true) ? "oneshot" : "simple"}"
@ -485,7 +494,6 @@ module Homebrew
end end
# Returns a `String` systemd unit timer. # Returns a `String` systemd unit timer.
# @return [String]
sig { returns(String) } sig { returns(String) }
def to_systemd_timer def to_systemd_timer
options = [] options = []
@ -512,7 +520,7 @@ module Homebrew
end 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(T::Hash[Symbol, T.untyped]) }
def to_hash def to_hash
name_params = { name_params = {
macos: (plist_name if plist_name != default_plist_name), macos: (plist_name if plist_name != default_plist_name),
@ -521,13 +529,13 @@ module Homebrew
return { name: name_params }.compact_blank if @run_params.blank? return { name: name_params }.compact_blank if @run_params.blank?
cron_string = if @cron.present? cron_string = unless @cron.empty?
[:Minute, :Hour, :Day, :Month, :Weekday] [:Minute, :Hour, :Day, :Month, :Weekday]
.map { |key| @cron[key].to_s } .map { |key| @cron[key].to_s }
.join(" ") .join(" ")
end end
sockets_var = if @sockets.present? sockets_var = unless @sockets.empty?
@sockets.transform_values { |info| "#{info[:type]}://#{info[:host]}:#{info[:port]}" } @sockets.transform_values { |info| "#{info[:type]}://#{info[:host]}:#{info[:port]}" }
.then do |sockets_hash| .then do |sockets_hash|
# TODO: Remove this code when all users are running on versions of Homebrew # TODO: Remove this code when all users are running on versions of Homebrew
@ -561,11 +569,11 @@ module Homebrew
process_type: @process_type, process_type: @process_type,
macos_legacy_timers: @macos_legacy_timers, macos_legacy_timers: @macos_legacy_timers,
sockets: sockets_var, sockets: sockets_var,
}.compact }.compact_blank
end end
# Turn the service API hash values back into what is expected by the formula DSL. # Turn the service API hash values back into what is expected by the formula DSL.
sig { params(api_hash: Hash).returns(Hash) } sig { params(api_hash: T::Hash[String, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def self.from_hash(api_hash) def self.from_hash(api_hash)
hash = {} hash = {}
hash[:name] = api_hash["name"].transform_keys(&:to_sym) if api_hash.key?("name") hash[:name] = api_hash["name"].transform_keys(&:to_sym) if api_hash.key?("name")

View File

@ -984,7 +984,7 @@ RSpec.describe Homebrew::Service do
expect(command).to eq(["#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd", "test"]) expect(command).to eq(["#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd", "test"])
end end
it "returns nil on Linux", :needs_linux do it "returns empty on Linux", :needs_linux do
f = stub_formula do f = stub_formula do
service do service do
run macos: [opt_bin/"beanstalkd", "test"] run macos: [opt_bin/"beanstalkd", "test"]
@ -993,7 +993,7 @@ RSpec.describe Homebrew::Service do
end end
command = f.service.command command = f.service.command
expect(command).to be_nil expect(command).to be_empty
end end
it "returns @run data on macOS", :needs_macos do it "returns @run data on macOS", :needs_macos do
@ -1008,7 +1008,7 @@ RSpec.describe Homebrew::Service do
expect(command).to eq(["#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd", "test"]) expect(command).to eq(["#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd", "test"])
end end
it "returns nil on macOS", :needs_macos do it "returns empty on macOS", :needs_macos do
f = stub_formula do f = stub_formula do
service do service do
run linux: [opt_bin/"beanstalkd", "test"] run linux: [opt_bin/"beanstalkd", "test"]
@ -1017,7 +1017,7 @@ RSpec.describe Homebrew::Service do
end end
command = f.service.command command = f.service.command
expect(command).to be_nil expect(command).to be_empty
end end
it "returns appropriate @run data on Linux", :needs_linux do it "returns appropriate @run data on Linux", :needs_linux do