mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00
service: add sockets and keepalive variants
This commit is contained in:
parent
f6ab300fc1
commit
3d5d12e8b9
@ -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?
|
||||||
|
@ -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"]
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user