diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 8af5410613..1d4afac8b9 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -1005,6 +1005,12 @@ class Formula opt_prefix/"#{service_name}.service" end + # The generated systemd {.timer} file path. + sig { returns(Pathname) } + def systemd_timer_path + opt_prefix/"#{service_name}.timer" + end + # The service specification of the software. def service return unless service? diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 7a2f1b801f..0a51b1fbb7 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -1043,6 +1043,12 @@ class FormulaInstaller service_path = formula.systemd_service_path service_path.atomic_write(formula.service.to_systemd_unit) service_path.chmod 0644 + + if formula.service.timed? + timer_path = formula.systemd_timer_path + timer_path.atomic_write(formula.service.to_systemd_timer) + timer_path.chmod 0644 + end end service = if formula.service? diff --git a/Library/Homebrew/service.rb b/Library/Homebrew/service.rb index 9d5e708430..1750d3d9a2 100644 --- a/Library/Homebrew/service.rb +++ b/Library/Homebrew/service.rb @@ -203,6 +203,8 @@ module Homebrew @run.map(&:to_s) end + # Returns the `String` command to run manually instead of the service. + # @return [String] sig { returns(String) } def manual_command instance_eval(&@service_block) @@ -213,6 +215,14 @@ module Homebrew out.join(" ") end + # Returns a `Boolean` describing if a service is timed. + # @return [Boolean] + sig { returns(T::Boolean) } + def timed? + instance_eval(&@service_block) + @run_type == RUN_TYPE_CRON || @run_type == RUN_TYPE_INTERVAL + end + # Returns a `String` plist. # @return [String] sig { returns(String) } @@ -267,5 +277,28 @@ module Homebrew unit + options.join("\n") end + + # Returns a `String` systemd unit timer. + # @return [String] + sig { returns(String) } + def to_systemd_timer + timer = <<~EOS + [Unit] + Description=Homebrew generated timer for #{@formula.name} + + [Install] + WantedBy=timers.target + + [Timer] + Unit=#{@formula.service_name} + EOS + + instance_eval(&@service_block) + options = [] + options << "Persistent=true=" if @run_type == RUN_TYPE_CRON + options << "OnUnitActiveSec=#{@interval}" if @run_type == RUN_TYPE_INTERVAL + + timer + options.join("\n") + end end end diff --git a/Library/Homebrew/test/formula_installer_spec.rb b/Library/Homebrew/test/formula_installer_spec.rb index e3179214f7..a140d3441f 100644 --- a/Library/Homebrew/test/formula_installer_spec.rb +++ b/Library/Homebrew/test/formula_installer_spec.rb @@ -245,10 +245,11 @@ describe FormulaInstaller do expect(formula).to receive(:plist).and_return(nil) expect(formula).to receive(:service?).exactly(3).and_return(true) - expect(formula).to receive(:service).twice.and_return(service) + expect(formula).to receive(:service).exactly(3).and_return(service) expect(formula).to receive(:plist_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(:to_plist).and_return("plist") expect(service).to receive(:to_systemd_unit).and_return("unit") @@ -261,6 +262,36 @@ describe FormulaInstaller do expect(service_path).to exist end + it "works if timed service is set" do + formula = Testball.new + plist_path = formula.plist_path + service_path = formula.systemd_service_path + timer_path = formula.systemd_timer_path + service = Homebrew::Service.new(formula) + formula.opt_prefix.mkpath + + expect(formula).to receive(:plist).and_return(nil) + expect(formula).to receive(:service?).exactly(3).and_return(true) + expect(formula).to receive(:service).exactly(4).and_return(service) + expect(formula).to receive(:plist_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(service).to receive(:to_plist).and_return("plist") + expect(service).to receive(:timed?).and_return(true) + expect(service).to receive(:to_systemd_unit).and_return("unit") + expect(service).to receive(:to_systemd_timer).and_return("timer") + + installer = described_class.new(formula) + expect { + installer.install_service + }.not_to output(/Error: Failed to install service files/).to_stderr + + expect(plist_path).to exist + expect(service_path).to exist + expect(timer_path).to exist + end + it "returns without definition" do formula = Testball.new path = formula.plist_path diff --git a/Library/Homebrew/test/formula_spec.rb b/Library/Homebrew/test/formula_spec.rb index b02c3c41f0..8f31f76d62 100644 --- a/Library/Homebrew/test/formula_spec.rb +++ b/Library/Homebrew/test/formula_spec.rb @@ -758,6 +758,7 @@ describe Formula do expect(f.service_name).to eq("homebrew.formula_name") expect(f.plist_path).to eq(HOMEBREW_PREFIX/"opt/formula_name/homebrew.mxcl.formula_name.plist") expect(f.systemd_service_path).to eq(HOMEBREW_PREFIX/"opt/formula_name/homebrew.formula_name.service") + expect(f.systemd_timer_path).to eq(HOMEBREW_PREFIX/"opt/formula_name/homebrew.formula_name.timer") end end diff --git a/Library/Homebrew/test/service_spec.rb b/Library/Homebrew/test/service_spec.rb index bc13a120b9..542df63220 100644 --- a/Library/Homebrew/test/service_spec.rb +++ b/Library/Homebrew/test/service_spec.rb @@ -272,6 +272,70 @@ describe Homebrew::Service do end end + describe "#to_systemd_timer" do + it "returns valid timer" do + f.class.service do + run [opt_bin/"beanstalkd", "test"] + run_type :interval + interval 5 + end + + unit = f.service.to_systemd_timer + unit_expect = <<~EOS + [Unit] + Description=Homebrew generated timer for formula_name + + [Install] + WantedBy=timers.target + + [Timer] + Unit=homebrew.formula_name + OnUnitActiveSec=5 + EOS + expect(unit).to eq(unit_expect.strip) + end + + it "returns valid partial timer" do + f.class.service do + run opt_bin/"beanstalkd" + run_type :immediate + end + + unit = f.service.to_systemd_timer + unit_expect = <<~EOS + [Unit] + Description=Homebrew generated timer for formula_name + + [Install] + WantedBy=timers.target + + [Timer] + Unit=homebrew.formula_name + EOS + expect(unit).to eq(unit_expect) + end + end + + describe "#timed?" do + it "returns false for immediate" do + f.class.service do + run [opt_bin/"beanstalkd", "test"] + run_type :immediate + end + + expect(f.service.timed?).to eq(false) + end + + it "returns true for interval" do + f.class.service do + run [opt_bin/"beanstalkd", "test"] + run_type :interval + end + + expect(f.service.timed?).to eq(true) + end + end + describe "#command" do it "returns @run data" do f.class.service do