Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add brew bundle services helper #19552

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Library/Homebrew/bundle/commands/services.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# typed: strict
# frozen_string_literal: true

require "bundle/brewfile"
require "bundle/services"

Check warning on line 5 in Library/Homebrew/bundle/commands/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/commands/services.rb#L4-L5

Added lines #L4 - L5 were not covered by tests

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

Check warning on line 13 in Library/Homebrew/bundle/commands/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/commands/services.rb#L7-L13

Added lines #L7 - L13 were not covered by tests

parsed_entries = Brewfile.read(global:, file:).entries

Check warning on line 15 in Library/Homebrew/bundle/commands/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/commands/services.rb#L15

Added line #L15 was not covered by tests

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

Check warning on line 30 in Library/Homebrew/bundle/commands/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/commands/services.rb#L17-L30

Added lines #L17 - L30 were not covered by tests
112 changes: 112 additions & 0 deletions Library/Homebrew/bundle/services.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# typed: strict
# frozen_string_literal: true

require "bundle/dsl"
require "formula"
require "services/system"

Check warning on line 6 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L4-L6

Added lines #L4 - L6 were not covered by tests

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

Check warning on line 22 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L8-L22

Added lines #L8 - L22 were not covered by tests

entries_formulae = entries.filter_map do |entry|
next if entry.type != :brew

Check warning on line 25 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L24-L25

Added lines #L24 - L25 were not covered by tests

formula = Formula[entry.name]
next unless formula.any_version_installed?

Check warning on line 28 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L27-L28

Added lines #L27 - L28 were not covered by tests

[entry, formula]
end.to_h

Check warning on line 31 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L30-L31

Added lines #L30 - L31 were not covered by tests

# 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

Check warning on line 41 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L34-L41

Added lines #L34 - L41 were not covered by tests

# 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),
)

Check warning on line 47 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L45-L47

Added lines #L45 - L47 were not covered by tests

entries_formulae.filter_map do |entry, formula|
version = formula_versions[entry.name.downcase]
prefix = formula.rack/version if version

Check warning on line 51 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L49-L51

Added lines #L49 - L51 were not covered by tests

service_file = if prefix&.directory?
if Homebrew::Services::System.launchctl?
prefix/"#{formula.plist_name}.plist"
else
prefix/"#{formula.service_name}.service"
end
end

Check warning on line 59 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L53-L59

Added lines #L53 - L59 were not covered by tests

unless service_file&.file?
prefix = formula.any_installed_prefix
next if prefix.nil?

Check warning on line 63 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L61-L63

Added lines #L61 - L63 were not covered by tests

service_file = if Homebrew::Services::System.launchctl?
prefix/"#{formula.plist_name}.plist"
else
prefix/"#{formula.service_name}.service"
end
end

Check warning on line 70 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L65-L70

Added lines #L65 - L70 were not covered by tests

next unless service_file.file?

Check warning on line 72 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L72

Added line #L72 was not covered by tests

info = services_info.find { |candidate| candidate["name"] == formula.name }
conflicting_services = services_info.select do |candidate|
next unless candidate["running"]

Check warning on line 76 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L74-L76

Added lines #L74 - L76 were not covered by tests

formula.versioned_formulae_names.include?(candidate["name"])
end

Check warning on line 79 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L78-L79

Added lines #L78 - L79 were not covered by tests

raise "Failed to get service info for #{entry.name}" if info.nil?

Check warning on line 81 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L81

Added line #L81 was not covered by tests

yield info, service_file, conflicting_services
end
end

Check warning on line 85 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L83-L85

Added lines #L83 - L85 were not covered by tests

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

Check warning on line 93 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L87-L93

Added lines #L87 - L93 were not covered by tests

safe_system HOMEBREW_BREW_FILE, "services", "run", "--file=#{service_file}", info["name"]
end
end

Check warning on line 97 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L95-L97

Added lines #L95 - L97 were not covered by tests

sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry]).void }
def self.stop(entries)
map_entries(entries) do |info, _, _|
next unless info["loaded"]

Check warning on line 102 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L99-L102

Added lines #L99 - L102 were not covered by tests

# Try avoid services not started by `brew bundle services`
next if Homebrew::Services::System.launchctl? && info["registered"]

Check warning on line 105 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L105

Added line #L105 was not covered by tests

safe_system HOMEBREW_BREW_FILE, "services", "stop", info["name"]
end
end
end
end
end

Check warning on line 112 in Library/Homebrew/bundle/services.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/bundle/services.rb#L107-L112

Added lines #L107 - L112 were not covered by tests
12 changes: 11 additions & 1 deletion Library/Homebrew/cmd/bundle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@

`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. " \
Expand Down Expand Up @@ -133,7 +139,7 @@
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

Expand Down Expand Up @@ -274,6 +280,10 @@
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:)

Check warning on line 286 in Library/Homebrew/cmd/bundle.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/cmd/bundle.rb#L285-L286

Added lines #L285 - L286 were not covered by tests
else
raise UsageError, "unknown subcommand: #{subcommand}"
end
Expand Down
23 changes: 19 additions & 4 deletions Library/Homebrew/cmd/services.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ class Services < AbstractCommand
[`sudo`] `brew services start` (<formula>|`--all`|`--file=`):
Start the service <formula> immediately and register it to launch at login (or boot).

[`sudo`] `brew services stop` (<formula>|`--all`):
Stop the service <formula> immediately and unregister it from launching at login (or boot).
[`sudo`] `brew services stop` (`--keep`) (`--no-wait`|`--max-wait=`) (<formula>|`--all`):
Stop the service <formula> immediately and unregister it from launching at login (or boot),
unless `--keep` is specified.

[`sudo`] `brew services kill` (<formula>|`--all`):
Stop the service <formula> immediately but keep it registered to launch at login (or boot).
Expand All @@ -57,6 +58,7 @@ class Services < AbstractCommand
switch "--all", description: "Run <subcommand> on all services."
switch "--json", description: "Output as JSON."
switch "--no-wait", description: "Don't wait for `stop` to finish stopping the service."
switch "--keep", description: "When stopped, don't unregister the service from launching at login (or boot)."
conflicts "--max-wait=", "--no-wait"
named_args
end
Expand Down Expand Up @@ -117,6 +119,14 @@ def run
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?
Expand Down Expand Up @@ -162,8 +172,13 @@ def run
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
Expand Down
19 changes: 13 additions & 6 deletions Library/Homebrew/services/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,13 @@ def self.start(targets, service_file = nil, verbose: false)
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:
Expand All @@ -188,7 +189,11 @@ def self.stop(targets, verbose: false, no_wait: false, max_wait: 0)
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
Expand All @@ -204,9 +209,11 @@ def self.stop(targets, verbose: false, no_wait: false, max_wait: 0)
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})"
Expand Down
1 change: 1 addition & 0 deletions Library/Homebrew/services/commands/info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def self.output(hash, verbose:)
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?
Expand Down
5 changes: 3 additions & 2 deletions Library/Homebrew/services/commands/stop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
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:)

Check warning on line 23 in Library/Homebrew/services/commands/stop.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/services/commands/stop.rb#L23

Added line #L23 was not covered by tests
end
end
end
Expand Down
1 change: 1 addition & 0 deletions Library/Homebrew/services/formula_wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ def to_hash
user: owner,
status: status_symbol,
file: service_file_present? ? dest : service_file,
registered: service_file_present?,
}

return hash unless service?
Expand Down
3 changes: 3 additions & 0 deletions Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/services.rbi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Library/Homebrew/test/services/commands/info_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,15 @@
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 = {
name: "service",
user: "user",
status: :started,
file: "/dev/null",
registered: true,
running: true,
loaded: true,
schedulable: false,
Expand Down
7 changes: 5 additions & 2 deletions Library/Homebrew/test/services/formula_wrapper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@
loaded: false,
name: "mysql",
pid: nil,
registered: false,
running: false,
schedulable: nil,
service_name: "plist-mysql-test",
Expand All @@ -384,13 +385,14 @@
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",
Expand All @@ -404,7 +406,7 @@
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",
Expand All @@ -417,6 +419,7 @@
log_path: nil,
name: "mysql",
pid: nil,
registered: true,
root_dir: nil,
running: false,
schedulable: false,
Expand Down
Loading