service: add sockets and keepalive variants

This commit is contained in:
Sean Molenaar 2022-01-25 19:29:16 +01:00
parent f6ab300fc1
commit 3d5d12e8b9
No known key found for this signature in database
GPG Key ID: 6BF5D8DF0D34FAAE
3 changed files with 296 additions and 5 deletions

View File

@ -18,6 +18,8 @@ module Homebrew
PROCESS_TYPE_INTERACTIVE = :interactive PROCESS_TYPE_INTERACTIVE = :interactive
PROCESS_TYPE_ADAPTIVE = :adaptive PROCESS_TYPE_ADAPTIVE = :adaptive
KEEP_ALIVE_KEYS = [:always, :successful_exit, :crashed, :path].freeze
# sig { params(formula: Formula).void } # sig { params(formula: Formula).void }
def initialize(formula, &block) def initialize(formula, &block)
@formula = formula @formula = formula
@ -100,15 +102,41 @@ module Homebrew
end end
end end
sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) } sig {
params(value: T.nilable(T.any(T::Boolean, T::Hash[Symbol, T.untyped])))
.returns(T.nilable(T::Hash[Symbol, T.untyped]))
}
def keep_alive(value = nil) def keep_alive(value = nil)
case T.unsafe(value) case T.unsafe(value)
when nil when nil
@keep_alive @keep_alive
when true, false when true, false
@keep_alive = { always: value }
when Hash
hash = T.cast(value, Hash)
unless (hash.keys - KEEP_ALIVE_KEYS).empty?
raise TypeError, "Service#keep_alive allows only #{KEEP_ALIVE_KEYS}"
end
@keep_alive = value @keep_alive = value
else else
raise TypeError, "Service#keep_alive expects a Boolean" raise TypeError, "Service#keep_alive expects a Boolean or Hash"
end
end
sig { params(value: T.nilable(String)).returns(T.nilable(T::Hash[Symbol, String])) }
def sockets(value = nil)
case T.unsafe(value)
when nil
@sockets
when String
match = T.must(value).match(%r{([a-z]+)://([a-z0-9.]+):([0-9]+)}i)
raise TypeError, "Service#sockets a formatted socket definition as <type>://<host>:<port>" if match.blank?
type, host, port = match.captures
@sockets = { host: host, port: port, type: type }
else
raise TypeError, "Service#sockets expects a String"
end end
end end
@ -117,7 +145,7 @@ module Homebrew
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def keep_alive? def keep_alive?
instance_eval(&@service_block) instance_eval(&@service_block)
@keep_alive == true @keep_alive.present? && @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.nilable(T::Boolean)) }
@ -310,7 +338,6 @@ module Homebrew
RunAtLoad: @run_type == RUN_TYPE_IMMEDIATE, RunAtLoad: @run_type == RUN_TYPE_IMMEDIATE,
} }
base[:KeepAlive] = @keep_alive if @keep_alive == true
base[:LaunchOnlyOnce] = @launch_only_once if @launch_only_once == true base[:LaunchOnlyOnce] = @launch_only_once if @launch_only_once == true
base[:LegacyTimers] = @macos_legacy_timers if @macos_legacy_timers == true base[:LegacyTimers] = @macos_legacy_timers if @macos_legacy_timers == true
base[:TimeOut] = @restart_delay if @restart_delay.present? base[:TimeOut] = @restart_delay if @restart_delay.present?
@ -323,6 +350,28 @@ module Homebrew
base[:StandardErrorPath] = @error_log_path if @error_log_path.present? base[:StandardErrorPath] = @error_log_path if @error_log_path.present?
base[:EnvironmentVariables] = @environment_variables unless @environment_variables.empty? base[:EnvironmentVariables] = @environment_variables unless @environment_variables.empty?
if keep_alive?
if (always = @keep_alive[:always].presence)
base[:KeepAlive] = always
elsif @keep_alive.key?(:successful_exit)
base[:KeepAlive] = { SuccessfulExit: @keep_alive[:successful_exit] }
elsif @keep_alive.key?(:crashed)
base[:KeepAlive] = { Crashed: @keep_alive[:crashed] }
elsif @keep_alive.key?(:path) && @keep_alive[:path].present?
base[:KeepAlive] = { PathState: @keep_alive[:path].to_s }
end
end
if @sockets.present?
base[:Sockets] = {}
base[:Sockets][:Listeners] = {
SockNodeName: @sockets[:host],
SockServiceName: @sockets[:port],
SockProtocol: @sockets[:type].upcase,
SockFamily: "IPv4v6",
}
end
if @cron.present? && @run_type == RUN_TYPE_CRON if @cron.present? && @run_type == RUN_TYPE_CRON
base[:StartCalendarInterval] = @cron.reject { |_, value| value == "*" } base[:StartCalendarInterval] = @cron.reject { |_, value| value == "*" }
end end
@ -350,7 +399,8 @@ module Homebrew
options = [] options = []
options << "Type=#{@launch_only_once == true ? "oneshot" : "simple"}" options << "Type=#{@launch_only_once == true ? "oneshot" : "simple"}"
options << "ExecStart=#{cmd}" options << "ExecStart=#{cmd}"
options << "Restart=always" if @keep_alive == true
options << "Restart=always" if @keep_alive.present? && @keep_alive[:always].present?
options << "RestartSec=#{restart_delay}" if @restart_delay.present? options << "RestartSec=#{restart_delay}" if @restart_delay.present?
options << "WorkingDirectory=#{@working_dir}" if @working_dir.present? options << "WorkingDirectory=#{@working_dir}" if @working_dir.present?
options << "RootDirectory=#{@root_dir}" if @root_dir.present? options << "RootDirectory=#{@root_dir}" if @root_dir.present?

View File

@ -45,6 +45,19 @@ describe Homebrew::Service do
end end
end end
describe "#keep_alive" do
it "throws for unexpected keys" do
f.class.service do
run opt_bin/"beanstalkd"
keep_alive test: "key"
end
expect {
f.service.manual_command
}.to raise_error TypeError, "Service#keep_alive allows only [:always, :successful_exit, :crashed, :path]"
end
end
describe "#run_type" do describe "#run_type" do
it "throws for unexpected type" do it "throws for unexpected type" do
f.class.service do f.class.service do
@ -58,6 +71,41 @@ describe Homebrew::Service do
end end
end end
describe "#sockets" do
it "throws for missing type" do
f.class.service do
run opt_bin/"beanstalkd"
sockets "127.0.0.1:80"
end
expect {
f.service.manual_command
}.to raise_error TypeError, "Service#sockets a formatted socket definition as <type>://<host>:<port>"
end
it "throws for missing host" do
f.class.service do
run opt_bin/"beanstalkd"
sockets "tcp://:80"
end
expect {
f.service.manual_command
}.to raise_error TypeError, "Service#sockets a formatted socket definition as <type>://<host>:<port>"
end
it "throws for missing port" do
f.class.service do
run opt_bin/"beanstalkd"
sockets "tcp://127.0.0.1"
end
expect {
f.service.manual_command
}.to raise_error TypeError, "Service#sockets a formatted socket definition as <type>://<host>:<port>"
end
end
describe "#manual_command" do describe "#manual_command" do
it "returns valid manual_command" do it "returns valid manual_command" do
f.class.service do f.class.service do
@ -159,6 +207,47 @@ describe Homebrew::Service do
expect(plist).to eq(plist_expect) expect(plist).to eq(plist_expect)
end end
it "returns valid plist with socket" do
f.class.service do
run [opt_bin/"beanstalkd", "test"]
sockets "tcp://127.0.0.1:80"
end
plist = f.service.to_plist
plist_expect = <<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
\t<key>Label</key>
\t<string>homebrew.mxcl.formula_name</string>
\t<key>ProgramArguments</key>
\t<array>
\t\t<string>#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd</string>
\t\t<string>test</string>
\t</array>
\t<key>RunAtLoad</key>
\t<true/>
\t<key>Sockets</key>
\t<dict>
\t\t<key>Listeners</key>
\t\t<dict>
\t\t\t<key>SockFamily</key>
\t\t\t<string>IPv4v6</string>
\t\t\t<key>SockNodeName</key>
\t\t\t<string>127.0.0.1</string>
\t\t\t<key>SockProtocol</key>
\t\t\t<string>TCP</string>
\t\t\t<key>SockServiceName</key>
\t\t\t<string>80</string>
\t\t</dict>
\t</dict>
</dict>
</plist>
EOS
expect(plist).to eq(plist_expect)
end
it "returns valid partial plist" do it "returns valid partial plist" do
f.class.service do f.class.service do
run opt_bin/"beanstalkd" run opt_bin/"beanstalkd"
@ -247,6 +336,99 @@ describe Homebrew::Service do
EOS EOS
expect(plist).to eq(plist_expect) expect(plist).to eq(plist_expect)
end end
it "returns valid keepalive-exit plist" do
f.class.service do
run opt_bin/"beanstalkd"
keep_alive successful_exit: false
end
plist = f.service.to_plist
plist_expect = <<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
\t<key>KeepAlive</key>
\t<dict>
\t\t<key>SuccessfulExit</key>
\t\t<false/>
\t</dict>
\t<key>Label</key>
\t<string>homebrew.mxcl.formula_name</string>
\t<key>ProgramArguments</key>
\t<array>
\t\t<string>#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd</string>
\t</array>
\t<key>RunAtLoad</key>
\t<true/>
</dict>
</plist>
EOS
expect(plist).to eq(plist_expect)
end
it "returns valid keepalive-crashed plist" do
f.class.service do
run opt_bin/"beanstalkd"
keep_alive crashed: true
end
plist = f.service.to_plist
plist_expect = <<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
\t<key>KeepAlive</key>
\t<dict>
\t\t<key>Crashed</key>
\t\t<true/>
\t</dict>
\t<key>Label</key>
\t<string>homebrew.mxcl.formula_name</string>
\t<key>ProgramArguments</key>
\t<array>
\t\t<string>#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd</string>
\t</array>
\t<key>RunAtLoad</key>
\t<true/>
</dict>
</plist>
EOS
expect(plist).to eq(plist_expect)
end
it "returns valid keepalive-path plist" do
f.class.service do
run opt_bin/"beanstalkd"
keep_alive path: opt_pkgshare/"test-path"
end
plist = f.service.to_plist
plist_expect = <<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
\t<key>KeepAlive</key>
\t<dict>
\t\t<key>PathState</key>
\t\t<string>#{HOMEBREW_PREFIX}/opt/formula_name/share/formula_name/test-path</string>
\t</dict>
\t<key>Label</key>
\t<string>homebrew.mxcl.formula_name</string>
\t<key>ProgramArguments</key>
\t<array>
\t\t<string>#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd</string>
\t</array>
\t<key>RunAtLoad</key>
\t<true/>
</dict>
</plist>
EOS
expect(plist).to eq(plist_expect)
end
end end
describe "#to_systemd_unit" do describe "#to_systemd_unit" do
@ -426,6 +608,15 @@ describe Homebrew::Service do
end end
describe "#keep_alive?" do describe "#keep_alive?" do
it "returns true when keep_alive set to hash" do
f.class.service do
run [opt_bin/"beanstalkd", "test"]
keep_alive crashed: true
end
expect(f.service.keep_alive?).to be(true)
end
it "returns true when keep_alive set to true" do it "returns true when keep_alive set to true" do
f.class.service do f.class.service do
run [opt_bin/"beanstalkd", "test"] run [opt_bin/"beanstalkd", "test"]

View File

@ -799,6 +799,7 @@ The only required field in a `service` block is the `run` field to indicate what
| `restart_delay` | - | yes | yes | The delay before restarting a process | | `restart_delay` | - | yes | yes | The delay before restarting a process |
| `process_type` | - | yes | no-op | The type of process to manage, `:background`, `:standard`, `:interactive` or `:adaptive` | | `process_type` | - | yes | no-op | The type of process to manage, `:background`, `:standard`, `:interactive` or `:adaptive` |
| `macos_legacy_timers` | - | yes | no-op | Timers created by launchd jobs are coalesced unless this is set | | `macos_legacy_timers` | - | yes | no-op | Timers created by launchd jobs are coalesced unless this is set |
| `sockets` | - | yes | no-op | A socket that is created as an accesspoint to the service |
For services that start and keep running alive you can use the default `run_type :` like so: For services that start and keep running alive you can use the default `run_type :` like so:
```ruby ```ruby
@ -836,6 +837,55 @@ This method will set the path to `#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin
end end
``` ```
#### KeepAlive options
The standard options, keep alive regardless of any status or circomstances
```rb
service do
run [opt_bin/"beanstalkd", "test"]
keep_alive true # or false
end
```
Same as above in hash form
```rb
service do
run [opt_bin/"beanstalkd", "test"]
keep_alive { always: true }
end
```
Keep alive until the job exits with a non-zero return code
```rb
service do
run [opt_bin/"beanstalkd", "test"]
keep_alive { succesful_exit: true }
end
```
Keep alive only if the job crashed
```rb
service do
run [opt_bin/"beanstalkd", "test"]
keep_alive { crashed: true }
end
```
Keep alive as long as a file exists
```rb
service do
run [opt_bin/"beanstalkd", "test"]
keep_alive { path: "/some/path" }
end
```
#### Socket format
The sockets method accepts a formatted socket definition as `<type>://<host>:<port>`.
- `type`: `udp` or `tcp`
- `host`: The host to run the socket on. For example `0.0.0.0`
- `port`: The port the socket should listen on.
Please note that sockets will be accessible on IPv4 and IPv6 addresses by default.
### Using environment variables ### Using environment variables
Homebrew has multiple levels of environment variable filtering which affects variables available to formulae. Homebrew has multiple levels of environment variable filtering which affects variables available to formulae.