From 0b58e8fd375289c7d9fbe0cd0cb014d49b8b3821 Mon Sep 17 00:00:00 2001 From: Bo Anderson Date: Tue, 25 Mar 2025 03:43:54 +0000 Subject: [PATCH 1/7] services: add `--keep` flag --- Library/Homebrew/cmd/services.rb | 23 +++++++++++++++---- Library/Homebrew/services/cli.rb | 19 ++++++++++----- Library/Homebrew/services/commands/stop.rb | 5 ++-- .../sorbet/rbi/dsl/homebrew/cmd/services.rbi | 3 +++ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/Library/Homebrew/cmd/services.rb b/Library/Homebrew/cmd/services.rb index f072813778..ac0f3872c3 100644 --- a/Library/Homebrew/cmd/services.rb +++ b/Library/Homebrew/cmd/services.rb @@ -38,8 +38,9 @@ module Homebrew [`sudo`] `brew services start` (|`--all`|`--file=`): Start the service immediately and register it to launch at login (or boot). - [`sudo`] `brew services stop` (|`--all`): - Stop the service immediately and unregister it from launching at login (or boot). + [`sudo`] `brew services stop` (`--keep`) (`--no-wait`|`--max-wait=`) (|`--all`): + Stop the service immediately and unregister it from launching at login (or boot), + unless `--keep` is specified. [`sudo`] `brew services kill` (|`--all`): Stop the service immediately but keep it registered to launch at login (or boot). @@ -57,6 +58,7 @@ module Homebrew switch "--all", description: "Run on all services." switch "--json", description: "Output as JSON." switch "--no-wait", description: "Don't wait for `stop` to finish stopping the service." + switch "--keep", description: "When stopped, don't unregister the service from launching at login (or boot)." conflicts "--max-wait=", "--no-wait" named_args end @@ -117,6 +119,14 @@ module Homebrew end end + unless Homebrew::Services::Commands::Stop::TRIGGERS.include?(subcommand) + raise UsageError, "The `#{subcommand}` subcommand does not accept the --keep argument!" if args.keep? + raise UsageError, "The `#{subcommand}` subcommand does not accept the --no-wait argument!" if args.no_wait? + if args.max_wait + raise UsageError, "The `#{subcommand}` subcommand does not accept the --max-wait= argument!" + end + end + opoo "The --all argument overrides provided formula argument!" if formulae.present? && args.all? targets = if args.all? @@ -162,8 +172,13 @@ module Homebrew when *Homebrew::Services::Commands::Start::TRIGGERS Homebrew::Services::Commands::Start.run(targets, args.file, verbose: args.verbose?) when *Homebrew::Services::Commands::Stop::TRIGGERS - max_wait = args.max_wait.to_f - Homebrew::Services::Commands::Stop.run(targets, verbose: args.verbose?, no_wait: args.no_wait?, max_wait:) + Homebrew::Services::Commands::Stop.run( + targets, + verbose: args.verbose?, + no_wait: args.no_wait?, + max_wait: args.max_wait.to_f, + keep: args.keep?, + ) when *Homebrew::Services::Commands::Kill::TRIGGERS Homebrew::Services::Commands::Kill.run(targets, verbose: args.verbose?) else diff --git a/Library/Homebrew/services/cli.rb b/Library/Homebrew/services/cli.rb index 8e7e009526..0694b05bf0 100644 --- a/Library/Homebrew/services/cli.rb +++ b/Library/Homebrew/services/cli.rb @@ -159,12 +159,13 @@ module Homebrew verbose: T::Boolean, no_wait: T::Boolean, max_wait: T.nilable(T.any(Integer, Float)), + keep: T::Boolean, ).void } - def self.stop(targets, verbose: false, no_wait: false, max_wait: 0) + def self.stop(targets, verbose: false, no_wait: false, max_wait: 0, keep: false) targets.each do |service| unless service.loaded? - rm service.dest if service.dest.exist? # get rid of installed service file anyway, dude + rm service.dest if !keep && 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: @@ -188,7 +189,11 @@ module Homebrew end if System.systemctl? - System::Systemctl.quiet_run(*systemctl_args, "disable", "--now", service.service_name) + if keep + System::Systemctl.quiet_run(*systemctl_args, "stop", service.service_name) + else + System::Systemctl.quiet_run(*systemctl_args, "disable", "--now", service.service_name) + end elsif System.launchctl? quiet_system System.launchctl, "bootout", "#{System.domain_target}/#{service.service_name}" unless no_wait @@ -204,9 +209,11 @@ module Homebrew 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? + unless keep + 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? + end if service.pid? || service.loaded? opoo "Unable to stop `#{service.name}` (label: #{service.service_name})" diff --git a/Library/Homebrew/services/commands/stop.rb b/Library/Homebrew/services/commands/stop.rb index 2b9136da10..bdf1882e06 100644 --- a/Library/Homebrew/services/commands/stop.rb +++ b/Library/Homebrew/services/commands/stop.rb @@ -15,11 +15,12 @@ module Homebrew verbose: T::Boolean, no_wait: T::Boolean, max_wait: T.nilable(Float), + keep: T::Boolean, ).void } - def self.run(targets, verbose:, no_wait:, max_wait:) + def self.run(targets, verbose:, no_wait:, max_wait:, keep:) Services::Cli.check(targets) - Services::Cli.stop(targets, verbose:, no_wait:, max_wait:) + Services::Cli.stop(targets, verbose:, no_wait:, max_wait:, keep:) end end end diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/services.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/services.rbi index 704adb3658..7660126179 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/services.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/services.rbi @@ -20,6 +20,9 @@ class Homebrew::Cmd::Services::Args < Homebrew::CLI::Args sig { returns(T::Boolean) } def json?; end + sig { returns(T::Boolean) } + def keep?; end + sig { returns(T.nilable(String)) } def max_wait; end From 615fb764a1a138dd944bc11f781f5572ef6dc190 Mon Sep 17 00:00:00 2001 From: Bo Anderson Date: Thu, 20 Mar 2025 07:16:02 +0000 Subject: [PATCH 2/7] Add `brew bundle services` helper --- Library/Homebrew/bundle/commands/services.rb | 30 +++++ Library/Homebrew/bundle/services.rb | 112 ++++++++++++++++++ Library/Homebrew/cmd/bundle.rb | 12 +- Library/Homebrew/services/commands/info.rb | 1 + Library/Homebrew/services/formula_wrapper.rb | 1 + .../test/services/commands/info_spec.rb | 3 +- .../test/services/formula_wrapper_spec.rb | 7 +- 7 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 Library/Homebrew/bundle/commands/services.rb create mode 100644 Library/Homebrew/bundle/services.rb diff --git a/Library/Homebrew/bundle/commands/services.rb b/Library/Homebrew/bundle/commands/services.rb new file mode 100644 index 0000000000..4c8eb5cf4b --- /dev/null +++ b/Library/Homebrew/bundle/commands/services.rb @@ -0,0 +1,30 @@ +# typed: strict +# frozen_string_literal: true + +require "bundle/brewfile" +require "bundle/services" + +module Homebrew + module Bundle + module Commands + module Services + sig { params(args: String, global: T::Boolean, file: T.nilable(String)).void } + def self.run(*args, global:, file:) + raise UsageError, "invalid `brew bundle services` arguments" if args.length != 1 + + parsed_entries = Brewfile.read(global:, file:).entries + + subcommand = args.first + case subcommand + when "run" + Homebrew::Bundle::Services.run(parsed_entries) + when "stop" + Homebrew::Bundle::Services.stop(parsed_entries) + else + raise UsageError, "unknown bundle services subcommand: #{subcommand}" + end + end + end + end + end +end diff --git a/Library/Homebrew/bundle/services.rb b/Library/Homebrew/bundle/services.rb new file mode 100644 index 0000000000..32e31c859b --- /dev/null +++ b/Library/Homebrew/bundle/services.rb @@ -0,0 +1,112 @@ +# typed: strict +# frozen_string_literal: true + +require "bundle/dsl" +require "formula" +require "services/system" + +module Homebrew + module Bundle + module Services + sig { + params( + entries: T::Array[Homebrew::Bundle::Dsl::Entry], + _block: T.proc.params( + info: T::Hash[String, T.anything], + service_file: Pathname, + conflicting_services: T::Array[T::Hash[String, T.anything]], + ).void, + ).void + } + private_class_method def self.map_entries(entries, &_block) + formula_versions = Bundle.formula_versions_from_env + + entries_formulae = entries.filter_map do |entry| + next if entry.type != :brew + + formula = Formula[entry.name] + next unless formula.any_version_installed? + + [entry, formula] + end.to_h + + # The formula + everything that could possible conflict with the service + names_to_query = entries_formulae.flat_map do |entry, formula| + [ + formula.name, + *formula.versioned_formulae_names, + *formula.conflicts.map(&:name), + *entry.options[:conflicts_with], + ] + end + + # We parse from a command invocation so that brew wrappers can invoke special actions + # for the elevated nature of `brew services` + services_info = JSON.parse( + Utils.safe_popen_read(HOMEBREW_BREW_FILE, "services", "info", "--json", *names_to_query), + ) + + entries_formulae.filter_map do |entry, formula| + version = formula_versions[entry.name.downcase] + prefix = formula.rack/version if version + + service_file = if prefix&.directory? + if Homebrew::Services::System.launchctl? + prefix/"#{formula.plist_name}.plist" + else + prefix/"#{formula.service_name}.service" + end + end + + unless service_file&.file? + prefix = formula.any_installed_prefix + next if prefix.nil? + + service_file = if Homebrew::Services::System.launchctl? + prefix/"#{formula.plist_name}.plist" + else + prefix/"#{formula.service_name}.service" + end + end + + next unless service_file.file? + + info = services_info.find { |candidate| candidate["name"] == formula.name } + conflicting_services = services_info.select do |candidate| + next unless candidate["running"] + + formula.versioned_formulae_names.include?(candidate["name"]) + end + + raise "Failed to get service info for #{entry.name}" if info.nil? + + yield info, service_file, conflicting_services + end + end + + sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry]).void } + def self.run(entries) + map_entries(entries) do |info, service_file, conflicting_services| + safe_system HOMEBREW_BREW_FILE, "services", "stop", "--keep", info["name"] if info["running"] + conflicting_services.each do |conflicting_service| + safe_system HOMEBREW_BREW_FILE, "services", "stop", "--keep", conflicting_service["name"] + end + + safe_system HOMEBREW_BREW_FILE, "services", "run", "--file=#{service_file}", info["name"] + end + end + + sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry]).void } + def self.stop(entries) + map_entries(entries) do |info, _, _| + next unless info["loaded"] + + # Try avoid services not started by `brew bundle services` + next if Homebrew::Services::System.launchctl? && info["registered"] + + safe_system HOMEBREW_BREW_FILE, "services", "stop", info["name"] + end + end + end + end +end diff --git a/Library/Homebrew/cmd/bundle.rb b/Library/Homebrew/cmd/bundle.rb index 6d199428b8..df26f6cc9f 100755 --- a/Library/Homebrew/cmd/bundle.rb +++ b/Library/Homebrew/cmd/bundle.rb @@ -61,6 +61,12 @@ module Homebrew `brew bundle env`: Print the environment variables that would be set in a `brew bundle exec` environment. + + `brew bundle services run`: + Start services for formulae specified in the `Brewfile`. + + `brew bundle services stop`: + Stop services for formulae specified in the `Brewfile`. EOS flag "--file=", description: "Read from or write to the `Brewfile` from this location. " \ @@ -133,7 +139,7 @@ module Homebrew require "bundle" subcommand = args.named.first.presence - if ["exec", "add", "remove"].exclude?(subcommand) && args.named.size > 1 + if %w[exec add remove services].exclude?(subcommand) && args.named.size > 1 raise UsageError, "This command does not take more than 1 subcommand argument." end @@ -273,6 +279,10 @@ module Homebrew require "bundle/commands/remove" Homebrew::Bundle::Commands::Remove.run(*named_args, type: selected_types.first, global:, file:) end + when "services" + _, *named_args = args.named + require "bundle/commands/services" + Homebrew::Bundle::Commands::Services.run(*named_args, global:, file:) else raise UsageError, "unknown subcommand: #{subcommand}" end diff --git a/Library/Homebrew/services/commands/info.rb b/Library/Homebrew/services/commands/info.rb index c1edc2e0f1..ed2bc32434 100644 --- a/Library/Homebrew/services/commands/info.rb +++ b/Library/Homebrew/services/commands/info.rb @@ -53,6 +53,7 @@ module Homebrew return out unless verbose out += "File: #{hash[:file]} #{pretty_bool(hash[:file].present?)}\n" + out += "Registered at login: #{pretty_bool(hash[:registered])}\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? diff --git a/Library/Homebrew/services/formula_wrapper.rb b/Library/Homebrew/services/formula_wrapper.rb index 86694f5d8d..ddd6dce65d 100644 --- a/Library/Homebrew/services/formula_wrapper.rb +++ b/Library/Homebrew/services/formula_wrapper.rb @@ -201,6 +201,7 @@ module Homebrew user: owner, status: status_symbol, file: service_file_present? ? dest : service_file, + registered: service_file_present?, } return hash unless service? diff --git a/Library/Homebrew/test/services/commands/info_spec.rb b/Library/Homebrew/test/services/commands/info_spec.rb index 62f03099eb..f8f3fc21fe 100644 --- a/Library/Homebrew/test/services/commands/info_spec.rb +++ b/Library/Homebrew/test/services/commands/info_spec.rb @@ -89,7 +89,7 @@ RSpec.describe Homebrew::Services::Commands::Info do it "returns verbose output" do out = "service ()\nRunning: true\n" out += "Loaded: true\nSchedulable: false\n" - out += "User: user\nPID: 42\nFile: /dev/null true\nCommand: /bin/command\n" + out += "User: user\nPID: 42\nFile: /dev/null true\nRegistered at login: true\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 = { @@ -97,6 +97,7 @@ RSpec.describe Homebrew::Services::Commands::Info do user: "user", status: :started, file: "/dev/null", + registered: true, running: true, loaded: true, schedulable: false, diff --git a/Library/Homebrew/test/services/formula_wrapper_spec.rb b/Library/Homebrew/test/services/formula_wrapper_spec.rb index 09f3b1415b..da7ee70acd 100644 --- a/Library/Homebrew/test/services/formula_wrapper_spec.rb +++ b/Library/Homebrew/test/services/formula_wrapper_spec.rb @@ -371,6 +371,7 @@ RSpec.describe Homebrew::Services::FormulaWrapper do loaded: false, name: "mysql", pid: nil, + registered: false, running: false, schedulable: nil, service_name: "plist-mysql-test", @@ -384,13 +385,14 @@ RSpec.describe Homebrew::Services::FormulaWrapper do ENV["HOME"] = "/tmp_home" allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false) expect(service).to receive(:service?).twice.and_return(false) - expect(service).to receive(:service_file_present?).and_return(true) + expect(service).to receive(:service_file_present?).twice.and_return(true) expected = { exit_code: nil, file: Pathname.new("/tmp_home/Library/LaunchAgents/homebrew.mysql.plist"), loaded: false, name: "mysql", pid: nil, + registered: true, running: false, schedulable: nil, service_name: "plist-mysql-test", @@ -404,7 +406,7 @@ RSpec.describe Homebrew::Services::FormulaWrapper do ENV["HOME"] = "/tmp_home" allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false) expect(service).to receive(:service?).twice.and_return(true) - expect(service).to receive(:service_file_present?).and_return(true) + expect(service).to receive(:service_file_present?).twice.and_return(true) expect(service).to receive(:load_service).twice.and_return(service_object) expected = { command: "/bin/cmd", @@ -417,6 +419,7 @@ RSpec.describe Homebrew::Services::FormulaWrapper do log_path: nil, name: "mysql", pid: nil, + registered: true, root_dir: nil, running: false, schedulable: false, From 2b906e4fe36f58425e38447185d9fd98ad144740 Mon Sep 17 00:00:00 2001 From: Bo Anderson Date: Thu, 27 Mar 2025 06:10:24 +0000 Subject: [PATCH 3/7] services: support `--file=` in restart command --- Library/Homebrew/cmd/services.rb | 5 +++-- Library/Homebrew/services/commands/restart.rb | 14 ++++++++++---- .../test/services/commands/restart_spec.rb | 8 ++++---- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Library/Homebrew/cmd/services.rb b/Library/Homebrew/cmd/services.rb index ac0f3872c3..2dddd589ff 100644 --- a/Library/Homebrew/cmd/services.rb +++ b/Library/Homebrew/cmd/services.rb @@ -45,7 +45,7 @@ module Homebrew [`sudo`] `brew services kill` (|`--all`): Stop the service immediately but keep it registered to launch at login (or boot). - [`sudo`] `brew services restart` (|`--all`): + [`sudo`] `brew services restart` (|`--all`|`--file=`): Stop (if necessary) and start the service immediately and register it to launch at login (or boot). [`sudo`] `brew services cleanup`: @@ -110,6 +110,7 @@ module Homebrew file_commands = [ *Homebrew::Services::Commands::Start::TRIGGERS, *Homebrew::Services::Commands::Run::TRIGGERS, + *Homebrew::Services::Commands::Restart::TRIGGERS, ] if file_commands.exclude?(subcommand) raise UsageError, "The `#{subcommand}` subcommand does not accept the --file= argument!" @@ -166,7 +167,7 @@ module Homebrew when *Homebrew::Services::Commands::Info::TRIGGERS Homebrew::Services::Commands::Info.run(targets, verbose: args.verbose?, json: args.json?) when *Homebrew::Services::Commands::Restart::TRIGGERS - Homebrew::Services::Commands::Restart.run(targets, verbose: args.verbose?) + Homebrew::Services::Commands::Restart.run(targets, args.file, verbose: args.verbose?) when *Homebrew::Services::Commands::Run::TRIGGERS Homebrew::Services::Commands::Run.run(targets, args.file, verbose: args.verbose?) when *Homebrew::Services::Commands::Start::TRIGGERS diff --git a/Library/Homebrew/services/commands/restart.rb b/Library/Homebrew/services/commands/restart.rb index 94ae240c0c..4a357c6837 100644 --- a/Library/Homebrew/services/commands/restart.rb +++ b/Library/Homebrew/services/commands/restart.rb @@ -14,8 +14,14 @@ module Homebrew TRIGGERS = %w[restart relaunch reload r].freeze - sig { params(targets: T::Array[Services::FormulaWrapper], verbose: T::Boolean).void } - def self.run(targets, verbose:) + sig { + params( + targets: T::Array[Services::FormulaWrapper], + custom_plist: T.nilable(String), + verbose: T::Boolean, + ).void + } + def self.run(targets, custom_plist, verbose:) Services::Cli.check(targets) ran = [] @@ -30,8 +36,8 @@ module Homebrew Services::Cli.stop([service], verbose:) if service.loaded? end - Services::Cli.run(targets, verbose:) if ran.present? - Services::Cli.start(started, verbose:) if started.present? + Services::Cli.run(targets, custom_plist, verbose:) if ran.present? + Services::Cli.start(started, custom_plist, verbose:) if started.present? end end end diff --git a/Library/Homebrew/test/services/commands/restart_spec.rb b/Library/Homebrew/test/services/commands/restart_spec.rb index eb26b0c10c..c35a543dd5 100644 --- a/Library/Homebrew/test/services/commands/restart_spec.rb +++ b/Library/Homebrew/test/services/commands/restart_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Homebrew::Services::Commands::Restart do describe "#run" do it "fails with empty list" do expect do - described_class.run([], verbose: false) + described_class.run([], nil, verbose: false) end.to raise_error UsageError, "Invalid usage: Formula(e) missing, please provide a formula name or use --all" end @@ -21,7 +21,7 @@ RSpec.describe Homebrew::Services::Commands::Restart do expect(Homebrew::Services::Cli).not_to receive(:stop) expect(Homebrew::Services::Cli).to receive(:start).once service = instance_double(Homebrew::Services::FormulaWrapper, service_name: "name", loaded?: false) - expect { described_class.run([service], verbose: false) }.not_to raise_error + expect { described_class.run([service], nil, verbose: false) }.not_to raise_error end it "starts if services are loaded with file" do @@ -30,7 +30,7 @@ RSpec.describe Homebrew::Services::Commands::Restart do expect(Homebrew::Services::Cli).to receive(:stop).once service = instance_double(Homebrew::Services::FormulaWrapper, service_name: "name", loaded?: true, service_file_present?: true) - expect { described_class.run([service], verbose: false) }.not_to raise_error + expect { described_class.run([service], nil, verbose: false) }.not_to raise_error end it "runs if services are loaded without file" do @@ -39,7 +39,7 @@ service_file_present?: true) expect(Homebrew::Services::Cli).to receive(:stop).once service = instance_double(Homebrew::Services::FormulaWrapper, service_name: "name", loaded?: true, service_file_present?: false) - expect { described_class.run([service], verbose: false) }.not_to raise_error + expect { described_class.run([service], nil, verbose: false) }.not_to raise_error end end end From 786ad348a5ce6cfd1e2b31e5f77fea089b4bcfe6 Mon Sep 17 00:00:00 2001 From: Bo Anderson Date: Thu, 27 Mar 2025 06:11:48 +0000 Subject: [PATCH 4/7] bundle: use version env for installer service handling --- Library/Homebrew/bundle/brew_installer.rb | 7 +- Library/Homebrew/bundle/brew_services.rb | 54 +++++++-- Library/Homebrew/bundle/commands/services.rb | 113 +++++++++++++++++- Library/Homebrew/bundle/services.rb | 112 ----------------- .../test/bundle/brew_installer_spec.rb | 14 ++- 5 files changed, 166 insertions(+), 134 deletions(-) delete mode 100644 Library/Homebrew/bundle/services.rb diff --git a/Library/Homebrew/bundle/brew_installer.rb b/Library/Homebrew/bundle/brew_installer.rb index 579d98e4ba..83338ef85e 100644 --- a/Library/Homebrew/bundle/brew_installer.rb +++ b/Library/Homebrew/bundle/brew_installer.rb @@ -113,12 +113,15 @@ module Homebrew def service_change_state!(verbose:) require "bundle/brew_services" + + file = Bundle::BrewServices.versioned_service_file(@name) + if restart_service_needed? puts "Restarting #{@name} service." if verbose - BrewServices.restart(@full_name, verbose:) + BrewServices.restart(@full_name, file:, verbose:) elsif start_service_needed? puts "Starting #{@name} service." if verbose - BrewServices.start(@full_name, verbose:) + BrewServices.start(@full_name, file:, verbose:) else true end diff --git a/Library/Homebrew/bundle/brew_services.rb b/Library/Homebrew/bundle/brew_services.rb index 79ab2ad2d1..a32a4831a2 100644 --- a/Library/Homebrew/bundle/brew_services.rb +++ b/Library/Homebrew/bundle/brew_services.rb @@ -1,43 +1,58 @@ # typed: true # rubocop:todo Sorbet/StrictSigil # frozen_string_literal: true +require "services/system" + module Homebrew module Bundle module BrewServices - module_function - - def reset! + def self.reset! @started_services = nil end - def stop(name, verbose: false) + def self.stop(name, keep: false, verbose: false) return true unless started?(name) - return unless Bundle.brew("services", "stop", name, verbose:) + args = ["services", "stop", name] + args << "--keep" if keep + return unless Bundle.brew(*args, verbose:) started_services.delete(name) true end - def start(name, verbose: false) - return unless Bundle.brew("services", "start", name, verbose:) + def self.start(name, file: nil, verbose: false) + args = ["services", "start", name] + args << "--file=#{file}" if file + return unless Bundle.brew(*args, verbose:) started_services << name true end - def restart(name, verbose: false) - return unless Bundle.brew("services", "restart", name, verbose:) + def self.run(name, file: nil, verbose: false) + args = ["services", "run", name] + args << "--file=#{file}" if file + return unless Bundle.brew(*args, verbose:) started_services << name true end - def started?(name) + def self.restart(name, file: nil, verbose: false) + args = ["services", "restart", name] + args << "--file=#{file}" if file + return unless Bundle.brew(*args, verbose:) + + started_services << name + true + end + + def self.started?(name) started_services.include? name end - def started_services + def self.started_services @started_services ||= begin states_to_skip = %w[stopped none] Utils.safe_popen_read(HOMEBREW_BREW_FILE, "services", "list").lines.filter_map do |line| @@ -48,6 +63,23 @@ module Homebrew end end end + + def self.versioned_service_file(name) + env_version = Bundle.formula_versions_from_env[name] + return if env_version.nil? + + formula = Formula[name] + prefix = formula.rack/env_version + return unless prefix.directory? + + service_file = if Homebrew::Services::System.launchctl? + prefix/"#{formula.plist_name}.plist" + else + prefix/"#{formula.service_name}.service" + end + + service_file if service_file.file? + end end end end diff --git a/Library/Homebrew/bundle/commands/services.rb b/Library/Homebrew/bundle/commands/services.rb index 4c8eb5cf4b..e3c6d3dd5f 100644 --- a/Library/Homebrew/bundle/commands/services.rb +++ b/Library/Homebrew/bundle/commands/services.rb @@ -2,7 +2,8 @@ # frozen_string_literal: true require "bundle/brewfile" -require "bundle/services" +require "bundle/brew_services" +require "formula" module Homebrew module Bundle @@ -17,13 +18,119 @@ module Homebrew subcommand = args.first case subcommand when "run" - Homebrew::Bundle::Services.run(parsed_entries) + run_services(parsed_entries) when "stop" - Homebrew::Bundle::Services.stop(parsed_entries) + stop_services(parsed_entries) else raise UsageError, "unknown bundle services subcommand: #{subcommand}" end end + + sig { + params( + entries: T::Array[Homebrew::Bundle::Dsl::Entry], + _block: T.proc.params( + info: T::Hash[String, T.anything], + service_file: Pathname, + conflicting_services: T::Array[T::Hash[String, T.anything]], + ).void, + ).void + } + private_class_method def self.map_entries(entries, &_block) + entries_formulae = entries.filter_map do |entry| + next if entry.type != :brew + + formula = Formula[entry.name] + next unless formula.any_version_installed? + + [entry, formula] + end.to_h + + # The formula + everything that could possible conflict with the service + names_to_query = entries_formulae.flat_map do |entry, formula| + [ + formula.name, + *formula.versioned_formulae_names, + *formula.conflicts.map(&:name), + *entry.options[:conflicts_with], + ] + end + + # We parse from a command invocation so that brew wrappers can invoke special actions + # for the elevated nature of `brew services` + services_info = JSON.parse( + Utils.safe_popen_read(HOMEBREW_BREW_FILE, "services", "info", "--json", *names_to_query), + ) + + entries_formulae.filter_map do |entry, formula| + service_file = Bundle::BrewServices.versioned_service_file(entry.name) + + unless service_file&.file? + prefix = formula.any_installed_prefix + next if prefix.nil? + + service_file = if Homebrew::Services::System.launchctl? + prefix/"#{formula.plist_name}.plist" + else + prefix/"#{formula.service_name}.service" + end + end + + next unless service_file.file? + + info = services_info.find { |candidate| candidate["name"] == formula.name } + conflicting_services = services_info.select do |candidate| + next unless candidate["running"] + + formula.versioned_formulae_names.include?(candidate["name"]) + end + + raise "Failed to get service info for #{entry.name}" if info.nil? + + yield info, service_file, conflicting_services + end + end + + sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry], _block: T.nilable(T.proc.void)).void } + def self.run_services(entries, &_block) + map_entries(entries) do |info, service_file, conflicting_services| + if info["running"] && !Bundle::BrewServices.stop(info["name"], keep: true) + opoo "Failed to stop #{info["name"]} service" + end + + conflicting_services.each do |conflict| + if conflict["running"] && !Bundle::BrewServices.stop(conflict["name"], keep: true) + opoo "Failed to stop #{conflict["name"]} service" + end + end + + unless Bundle::BrewServices.run(info["name"], file: service_file) + opoo "Failed to start #{info["name"]} service" + end + + return unless block_given? + + begin + yield + ensure + stop_services(entries) + end + end + end + + sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry]).void } + def self.stop_services(entries) + map_entries(entries) do |info, _, _| + next unless info["loaded"] + + # Try avoid services not started by `brew bundle services` + next if Homebrew::Services::System.launchctl? && info["registered"] + + if info["running"] && !Bundle::BrewServices.stop(info["name"], keep: true) + opoo "Failed to stop #{info["name"]} service" + end + end + end end end end diff --git a/Library/Homebrew/bundle/services.rb b/Library/Homebrew/bundle/services.rb deleted file mode 100644 index 32e31c859b..0000000000 --- a/Library/Homebrew/bundle/services.rb +++ /dev/null @@ -1,112 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "bundle/dsl" -require "formula" -require "services/system" - -module Homebrew - module Bundle - module Services - sig { - params( - entries: T::Array[Homebrew::Bundle::Dsl::Entry], - _block: T.proc.params( - info: T::Hash[String, T.anything], - service_file: Pathname, - conflicting_services: T::Array[T::Hash[String, T.anything]], - ).void, - ).void - } - private_class_method def self.map_entries(entries, &_block) - formula_versions = Bundle.formula_versions_from_env - - entries_formulae = entries.filter_map do |entry| - next if entry.type != :brew - - formula = Formula[entry.name] - next unless formula.any_version_installed? - - [entry, formula] - end.to_h - - # The formula + everything that could possible conflict with the service - names_to_query = entries_formulae.flat_map do |entry, formula| - [ - formula.name, - *formula.versioned_formulae_names, - *formula.conflicts.map(&:name), - *entry.options[:conflicts_with], - ] - end - - # We parse from a command invocation so that brew wrappers can invoke special actions - # for the elevated nature of `brew services` - services_info = JSON.parse( - Utils.safe_popen_read(HOMEBREW_BREW_FILE, "services", "info", "--json", *names_to_query), - ) - - entries_formulae.filter_map do |entry, formula| - version = formula_versions[entry.name.downcase] - prefix = formula.rack/version if version - - service_file = if prefix&.directory? - if Homebrew::Services::System.launchctl? - prefix/"#{formula.plist_name}.plist" - else - prefix/"#{formula.service_name}.service" - end - end - - unless service_file&.file? - prefix = formula.any_installed_prefix - next if prefix.nil? - - service_file = if Homebrew::Services::System.launchctl? - prefix/"#{formula.plist_name}.plist" - else - prefix/"#{formula.service_name}.service" - end - end - - next unless service_file.file? - - info = services_info.find { |candidate| candidate["name"] == formula.name } - conflicting_services = services_info.select do |candidate| - next unless candidate["running"] - - formula.versioned_formulae_names.include?(candidate["name"]) - end - - raise "Failed to get service info for #{entry.name}" if info.nil? - - yield info, service_file, conflicting_services - end - end - - sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry]).void } - def self.run(entries) - map_entries(entries) do |info, service_file, conflicting_services| - safe_system HOMEBREW_BREW_FILE, "services", "stop", "--keep", info["name"] if info["running"] - conflicting_services.each do |conflicting_service| - safe_system HOMEBREW_BREW_FILE, "services", "stop", "--keep", conflicting_service["name"] - end - - safe_system HOMEBREW_BREW_FILE, "services", "run", "--file=#{service_file}", info["name"] - end - end - - sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry]).void } - def self.stop(entries) - map_entries(entries) do |info, _, _| - next unless info["loaded"] - - # Try avoid services not started by `brew bundle services` - next if Homebrew::Services::System.launchctl? && info["registered"] - - safe_system HOMEBREW_BREW_FILE, "services", "stop", info["name"] - end - end - end - end -end diff --git a/Library/Homebrew/test/bundle/brew_installer_spec.rb b/Library/Homebrew/test/bundle/brew_installer_spec.rb index e6507f64d4..40edf2c12d 100644 --- a/Library/Homebrew/test/bundle/brew_installer_spec.rb +++ b/Library/Homebrew/test/bundle/brew_installer_spec.rb @@ -58,7 +58,7 @@ RSpec.describe Homebrew::Bundle::BrewInstaller do context "with a successful installation" do it "start service" do expect(Homebrew::Bundle::BrewServices).to \ - receive(:start).with(formula_name, verbose: false).and_return(true) + receive(:start).with(formula_name, file: nil, verbose: false).and_return(true) described_class.preinstall(formula_name, start_service: true) described_class.install(formula_name, start_service: true) end @@ -67,7 +67,7 @@ RSpec.describe Homebrew::Bundle::BrewInstaller do context "with a skipped installation" do it "start service" do expect(Homebrew::Bundle::BrewServices).to \ - receive(:start).with(formula_name, verbose: false).and_return(true) + receive(:start).with(formula_name, file: nil, verbose: false).and_return(true) described_class.install(formula_name, preinstall: false, start_service: true) end end @@ -83,7 +83,7 @@ RSpec.describe Homebrew::Bundle::BrewInstaller do context "with a successful installation" do it "restart service" do expect(Homebrew::Bundle::BrewServices).to \ - receive(:restart).with(formula_name, verbose: false).and_return(true) + receive(:restart).with(formula_name, file: nil, verbose: false).and_return(true) described_class.preinstall(formula_name, restart_service: :always) described_class.install(formula_name, restart_service: :always) end @@ -92,7 +92,7 @@ RSpec.describe Homebrew::Bundle::BrewInstaller do context "with a skipped installation" do it "restart service" do expect(Homebrew::Bundle::BrewServices).to \ - receive(:restart).with(formula_name, verbose: false).and_return(true) + receive(:restart).with(formula_name, file: nil, verbose: false).and_return(true) described_class.install(formula_name, preinstall: false, restart_service: :always) end end @@ -201,7 +201,8 @@ RSpec.describe Homebrew::Bundle::BrewInstaller do verbose:).and_return(true) expect(Homebrew::Bundle::BrewServices).to receive(:stop).with("mysql55", verbose:).and_return(true) expect(Homebrew::Bundle::BrewServices).to receive(:stop).with("mysql56", verbose:).and_return(true) - expect(Homebrew::Bundle::BrewServices).to receive(:restart).with(formula_name, verbose:).and_return(true) + expect(Homebrew::Bundle::BrewServices).to receive(:restart).with(formula_name, file: nil, + verbose:).and_return(true) described_class.preinstall(formula_name, restart_service: :always, conflicts_with: ["mysql56"]) described_class.install(formula_name, restart_service: :always, conflicts_with: ["mysql56"]) end @@ -216,7 +217,8 @@ RSpec.describe Homebrew::Bundle::BrewInstaller do verbose:).and_return(true) expect(Homebrew::Bundle::BrewServices).to receive(:stop).with("mysql55", verbose:).and_return(true) expect(Homebrew::Bundle::BrewServices).to receive(:stop).with("mysql56", verbose:).and_return(true) - expect(Homebrew::Bundle::BrewServices).to receive(:restart).with(formula_name, verbose:).and_return(true) + expect(Homebrew::Bundle::BrewServices).to receive(:restart).with(formula_name, file: nil, + verbose:).and_return(true) described_class.preinstall(formula_name, restart_service: :always, conflicts_with: ["mysql56"], verbose: true) described_class.install(formula_name, restart_service: :always, conflicts_with: ["mysql56"], verbose: true) end From c7e8b66da32aa725c2fa23058263dc165475f150 Mon Sep 17 00:00:00 2001 From: Bo Anderson Date: Thu, 27 Mar 2025 06:12:10 +0000 Subject: [PATCH 5/7] bundle: add `--services` flag for sh and exec --- Library/Homebrew/bundle/commands/exec.rb | 15 +++++++++++++-- Library/Homebrew/bundle/commands/services.rb | 6 ++++++ Library/Homebrew/cmd/bundle.rb | 4 +++- .../sorbet/rbi/dsl/homebrew/cmd/bundle.rbi | 3 +++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Library/Homebrew/bundle/commands/exec.rb b/Library/Homebrew/bundle/commands/exec.rb index 15968bbba0..a51b627141 100644 --- a/Library/Homebrew/bundle/commands/exec.rb +++ b/Library/Homebrew/bundle/commands/exec.rb @@ -47,7 +47,7 @@ module Homebrew PATH_LIKE_ENV_REGEX = /.+#{File::PATH_SEPARATOR}/ - def self.run(*args, global: false, file: nil, subcommand: "") + def self.run(*args, global: false, file: nil, subcommand: "", services: false) # Cleanup Homebrew's global environment HOMEBREW_ENV_CLEANUP.each { |key| ENV.delete(key) } @@ -157,7 +157,18 @@ module Homebrew return end - exec(*args) + if services + require "bundle/commands/services" + + exit_code = 0 + Services.run_services(@dsl.entries) do + Kernel.system(*args) + exit_code = $CHILD_STATUS.exitstatus + end + exit!(exit_code) + else + exec(*args) + end end end end diff --git a/Library/Homebrew/bundle/commands/services.rb b/Library/Homebrew/bundle/commands/services.rb index e3c6d3dd5f..74518ca141 100644 --- a/Library/Homebrew/bundle/commands/services.rb +++ b/Library/Homebrew/bundle/commands/services.rb @@ -114,6 +114,12 @@ module Homebrew yield ensure stop_services(entries) + + conflicting_services.each do |conflict| + if conflict["running"] && conflict["registered"] && !Bundle::BrewServices.run(conflict["name"]) + opoo "Failed to restart #{conflict["name"]} service" + end + end end end end diff --git a/Library/Homebrew/cmd/bundle.rb b/Library/Homebrew/cmd/bundle.rb index df26f6cc9f..e014799fda 100755 --- a/Library/Homebrew/cmd/bundle.rb +++ b/Library/Homebrew/cmd/bundle.rb @@ -89,6 +89,8 @@ module Homebrew "even if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set. " switch "--install", description: "Run `install` before continuing to other operations e.g. `exec`." + switch "--services", + description: "Temporarily start services while running the `exec` or `sh` command." switch "-f", "--force", description: "`install` runs with `--force`/`--overwrite`. " \ "`dump` overwrites an existing `Brewfile`. " \ @@ -238,7 +240,7 @@ module Homebrew ["env"] end require "bundle/commands/exec" - Homebrew::Bundle::Commands::Exec.run(*named_args, global:, file:, subcommand:) + Homebrew::Bundle::Commands::Exec.run(*named_args, global:, file:, subcommand:, services: args.services?) when "list" require "bundle/commands/list" Homebrew::Bundle::Commands::List.run( diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/bundle.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/bundle.rbi index 99b7673e46..43c5e5fe61 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/bundle.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/bundle.rbi @@ -59,6 +59,9 @@ class Homebrew::Cmd::Bundle::Args < Homebrew::CLI::Args sig { returns(T::Boolean) } def no_vscode?; end + sig { returns(T::Boolean) } + def services?; end + sig { returns(T::Boolean) } def tap?; end From 650f62bcba8ba10dcb4481b19c86eacec64f1d19 Mon Sep 17 00:00:00 2001 From: Bo Anderson Date: Fri, 28 Mar 2025 05:39:46 +0000 Subject: [PATCH 6/7] Fold `brew bundle services` into `exec` --- Library/Homebrew/bundle/commands/exec.rb | 121 +++++++++++++++- Library/Homebrew/bundle/commands/services.rb | 143 ------------------- Library/Homebrew/cmd/bundle.rb | 12 +- 3 files changed, 120 insertions(+), 156 deletions(-) delete mode 100644 Library/Homebrew/bundle/commands/services.rb diff --git a/Library/Homebrew/bundle/commands/exec.rb b/Library/Homebrew/bundle/commands/exec.rb index a51b627141..b2a4f52400 100644 --- a/Library/Homebrew/bundle/commands/exec.rb +++ b/Library/Homebrew/bundle/commands/exec.rb @@ -158,10 +158,10 @@ module Homebrew end if services - require "bundle/commands/services" + require "bundle/brew_services" exit_code = 0 - Services.run_services(@dsl.entries) do + run_services(@dsl.entries) do Kernel.system(*args) exit_code = $CHILD_STATUS.exitstatus end @@ -170,6 +170,123 @@ module Homebrew exec(*args) end end + + sig { + params( + entries: T::Array[Homebrew::Bundle::Dsl::Entry], + _block: T.proc.params( + info: T::Hash[String, T.anything], + service_file: Pathname, + conflicting_services: T::Array[T::Hash[String, T.anything]], + ).void, + ).void + } + private_class_method def self.map_service_info(entries, &_block) + entries_formulae = entries.filter_map do |entry| + next if entry.type != :brew + + formula = Formula[entry.name] + next unless formula.any_version_installed? + + [entry, formula] + end.to_h + + # The formula + everything that could possible conflict with the service + names_to_query = entries_formulae.flat_map do |entry, formula| + [ + formula.name, + *formula.versioned_formulae_names, + *formula.conflicts.map(&:name), + *entry.options[:conflicts_with], + ] + end + + # We parse from a command invocation so that brew wrappers can invoke special actions + # for the elevated nature of `brew services` + services_info = JSON.parse( + Utils.safe_popen_read(HOMEBREW_BREW_FILE, "services", "info", "--json", *names_to_query), + ) + + entries_formulae.filter_map do |entry, formula| + service_file = Bundle::BrewServices.versioned_service_file(entry.name) + + unless service_file&.file? + prefix = formula.any_installed_prefix + next if prefix.nil? + + service_file = if Homebrew::Services::System.launchctl? + prefix/"#{formula.plist_name}.plist" + else + prefix/"#{formula.service_name}.service" + end + end + + next unless service_file.file? + + info = services_info.find { |candidate| candidate["name"] == formula.name } + conflicting_services = services_info.select do |candidate| + next unless candidate["running"] + + formula.versioned_formulae_names.include?(candidate["name"]) + end + + raise "Failed to get service info for #{entry.name}" if info.nil? + + yield info, service_file, conflicting_services + end + end + + sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry], _block: T.nilable(T.proc.void)).void } + private_class_method def self.run_services(entries, &_block) + services_to_restart = [] + + map_service_info(entries) do |info, service_file, conflicting_services| + if info["running"] && !Bundle::BrewServices.stop(info["name"], keep: true) + opoo "Failed to stop #{info["name"]} service" + end + + conflicting_services.each do |conflict| + if Bundle::BrewServices.stop(conflict["name"], keep: true) + services_to_restart << conflict["name"] if conflict["registered"] + else + opoo "Failed to stop #{conflict["name"]} service" + end + end + + unless Bundle::BrewServices.run(info["name"], file: service_file) + opoo "Failed to start #{info["name"]} service" + end + end + + return unless block_given? + + begin + yield + ensure + # Do a full re-evaluation of services instead state has changed + stop_services(entries) + + services_to_restart.each do |service| + next if Bundle::BrewServices.run(service) + + opoo "Failed to restart #{service} service" + end + end + end + + sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry]).void } + private_class_method def self.stop_services(entries) + map_service_info(entries) do |info, _, _| + next unless info["loaded"] + + # Try avoid services not started by `brew bundle services` + next if Homebrew::Services::System.launchctl? && info["registered"] + + if info["running"] && !Bundle::BrewServices.stop(info["name"], keep: true) + opoo "Failed to stop #{info["name"]} service" + end + end + end end end end diff --git a/Library/Homebrew/bundle/commands/services.rb b/Library/Homebrew/bundle/commands/services.rb deleted file mode 100644 index 74518ca141..0000000000 --- a/Library/Homebrew/bundle/commands/services.rb +++ /dev/null @@ -1,143 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "bundle/brewfile" -require "bundle/brew_services" -require "formula" - -module Homebrew - module Bundle - module Commands - module Services - sig { params(args: String, global: T::Boolean, file: T.nilable(String)).void } - def self.run(*args, global:, file:) - raise UsageError, "invalid `brew bundle services` arguments" if args.length != 1 - - parsed_entries = Brewfile.read(global:, file:).entries - - subcommand = args.first - case subcommand - when "run" - run_services(parsed_entries) - when "stop" - stop_services(parsed_entries) - else - raise UsageError, "unknown bundle services subcommand: #{subcommand}" - end - end - - sig { - params( - entries: T::Array[Homebrew::Bundle::Dsl::Entry], - _block: T.proc.params( - info: T::Hash[String, T.anything], - service_file: Pathname, - conflicting_services: T::Array[T::Hash[String, T.anything]], - ).void, - ).void - } - private_class_method def self.map_entries(entries, &_block) - entries_formulae = entries.filter_map do |entry| - next if entry.type != :brew - - formula = Formula[entry.name] - next unless formula.any_version_installed? - - [entry, formula] - end.to_h - - # The formula + everything that could possible conflict with the service - names_to_query = entries_formulae.flat_map do |entry, formula| - [ - formula.name, - *formula.versioned_formulae_names, - *formula.conflicts.map(&:name), - *entry.options[:conflicts_with], - ] - end - - # We parse from a command invocation so that brew wrappers can invoke special actions - # for the elevated nature of `brew services` - services_info = JSON.parse( - Utils.safe_popen_read(HOMEBREW_BREW_FILE, "services", "info", "--json", *names_to_query), - ) - - entries_formulae.filter_map do |entry, formula| - service_file = Bundle::BrewServices.versioned_service_file(entry.name) - - unless service_file&.file? - prefix = formula.any_installed_prefix - next if prefix.nil? - - service_file = if Homebrew::Services::System.launchctl? - prefix/"#{formula.plist_name}.plist" - else - prefix/"#{formula.service_name}.service" - end - end - - next unless service_file.file? - - info = services_info.find { |candidate| candidate["name"] == formula.name } - conflicting_services = services_info.select do |candidate| - next unless candidate["running"] - - formula.versioned_formulae_names.include?(candidate["name"]) - end - - raise "Failed to get service info for #{entry.name}" if info.nil? - - yield info, service_file, conflicting_services - end - end - - sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry], _block: T.nilable(T.proc.void)).void } - def self.run_services(entries, &_block) - map_entries(entries) do |info, service_file, conflicting_services| - if info["running"] && !Bundle::BrewServices.stop(info["name"], keep: true) - opoo "Failed to stop #{info["name"]} service" - end - - conflicting_services.each do |conflict| - if conflict["running"] && !Bundle::BrewServices.stop(conflict["name"], keep: true) - opoo "Failed to stop #{conflict["name"]} service" - end - end - - unless Bundle::BrewServices.run(info["name"], file: service_file) - opoo "Failed to start #{info["name"]} service" - end - - return unless block_given? - - begin - yield - ensure - stop_services(entries) - - conflicting_services.each do |conflict| - if conflict["running"] && conflict["registered"] && !Bundle::BrewServices.run(conflict["name"]) - opoo "Failed to restart #{conflict["name"]} service" - end - end - end - end - end - - sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry]).void } - def self.stop_services(entries) - map_entries(entries) do |info, _, _| - next unless info["loaded"] - - # Try avoid services not started by `brew bundle services` - next if Homebrew::Services::System.launchctl? && info["registered"] - - if info["running"] && !Bundle::BrewServices.stop(info["name"], keep: true) - opoo "Failed to stop #{info["name"]} service" - end - end - end - end - end - end -end diff --git a/Library/Homebrew/cmd/bundle.rb b/Library/Homebrew/cmd/bundle.rb index e014799fda..71df929e75 100755 --- a/Library/Homebrew/cmd/bundle.rb +++ b/Library/Homebrew/cmd/bundle.rb @@ -61,12 +61,6 @@ module Homebrew `brew bundle env`: Print the environment variables that would be set in a `brew bundle exec` environment. - - `brew bundle services run`: - Start services for formulae specified in the `Brewfile`. - - `brew bundle services stop`: - Stop services for formulae specified in the `Brewfile`. EOS flag "--file=", description: "Read from or write to the `Brewfile` from this location. " \ @@ -141,7 +135,7 @@ module Homebrew require "bundle" subcommand = args.named.first.presence - if %w[exec add remove services].exclude?(subcommand) && args.named.size > 1 + if %w[exec add remove].exclude?(subcommand) && args.named.size > 1 raise UsageError, "This command does not take more than 1 subcommand argument." end @@ -281,10 +275,6 @@ module Homebrew require "bundle/commands/remove" Homebrew::Bundle::Commands::Remove.run(*named_args, type: selected_types.first, global:, file:) end - when "services" - _, *named_args = args.named - require "bundle/commands/services" - Homebrew::Bundle::Commands::Services.run(*named_args, global:, file:) else raise UsageError, "unknown subcommand: #{subcommand}" end From c273d8b467e7d0bb2d1695d6a3a0973dffbaae1e Mon Sep 17 00:00:00 2001 From: Bo Anderson Date: Fri, 28 Mar 2025 05:54:36 +0000 Subject: [PATCH 7/7] test/bundle: improve coverage --- .../test/bundle/brew_services_spec.rb | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Library/Homebrew/test/bundle/brew_services_spec.rb b/Library/Homebrew/test/bundle/brew_services_spec.rb index cdc2897414..27a088edc1 100644 --- a/Library/Homebrew/test/bundle/brew_services_spec.rb +++ b/Library/Homebrew/test/bundle/brew_services_spec.rb @@ -46,6 +46,14 @@ RSpec.describe Homebrew::Bundle::BrewServices do expect(described_class.started_services).to include("nginx") end + it "runs the service" do + allow(described_class).to receive(:started_services).and_return([]) + expect(Homebrew::Bundle).to receive(:system).with(HOMEBREW_BREW_FILE, "services", "run", "nginx", + verbose: false).and_return(true) + expect(described_class.run("nginx")).to be(true) + expect(described_class.started_services).to include("nginx") + end + it "restarts the service" do allow(described_class).to receive(:started_services).and_return([]) expect(Homebrew::Bundle).to receive(:system).with(HOMEBREW_BREW_FILE, "services", "restart", "nginx", @@ -54,4 +62,32 @@ RSpec.describe Homebrew::Bundle::BrewServices do expect(described_class.started_services).to include("nginx") end end + + describe ".versioned_service_file" do + let(:foo) do + instance_double( + Formula, + name: "fooformula", + version: "1.0", + rack: HOMEBREW_CELLAR/"fooformula", + plist_name: "homebrew.mxcl.fooformula", + ) + end + + it "returns the versioned service file" do + expect(Formula).to receive(:[]).with(foo.name).and_return(foo) + expect(Homebrew::Bundle).to receive(:formula_versions_from_env).and_return(foo.name => foo.version) + + prefix = foo.rack/"1.0" + allow(FileTest).to receive(:directory?).and_call_original + expect(FileTest).to receive(:directory?).with(prefix.to_s).and_return(true) + + service_file = prefix/"#{foo.plist_name}.plist" + expect(Homebrew::Services::System).to receive(:launchctl?).and_return(true) + allow(FileTest).to receive(:file?).and_call_original + expect(FileTest).to receive(:file?).with(service_file.to_s).and_return(true) + + expect(described_class.versioned_service_file(foo.name)).to eq(service_file) + end + end end