Skip to content

Commit

Permalink
service: add sockets and keepalive variants
Browse files Browse the repository at this point in the history
  • Loading branch information
SMillerDev committed Apr 5, 2022
1 parent f6ab300 commit 210eab3
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 5 deletions.
61 changes: 56 additions & 5 deletions Library/Homebrew/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class Service
PROCESS_TYPE_INTERACTIVE = :interactive
PROCESS_TYPE_ADAPTIVE = :adaptive

KEEP_ALIVE_KEYS = [:always, :succesful_exit, :crashed, :path].freeze

# sig { params(formula: Formula).void }
def initialize(formula, &block)
@formula = formula
Expand Down Expand Up @@ -100,15 +102,40 @@ def error_log_path(path = nil)
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)
case T.unsafe(value)
when nil
@keep_alive
when true, false
@keep_alive = { always: value }
when Hash
if T.cast(value, Hash).keys - KEEP_ALIVE_KEYS != []
raise TypeError, "Service#keep_alive allows only #{KEEP_ALIVE_KEYS}"
end

@keep_alive = value
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

Expand All @@ -117,7 +144,7 @@ def keep_alive(value = nil)
sig { returns(T::Boolean) }
def keep_alive?
instance_eval(&@service_block)
@keep_alive == true
!@keep_alive.nil? && @keep_alive[:always] != false
end

sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) }
Expand Down Expand Up @@ -310,7 +337,6 @@ def to_plist
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[:LegacyTimers] = @macos_legacy_timers if @macos_legacy_timers == true
base[:TimeOut] = @restart_delay if @restart_delay.present?
Expand All @@ -323,10 +349,34 @@ def to_plist
base[:StandardErrorPath] = @error_log_path if @error_log_path.present?
base[:EnvironmentVariables] = @environment_variables unless @environment_variables.empty?

if keep_alive?
if @keep_alive[:always].present?
base[:KeepAlive] = @keep_alive[:always]
elsif @keep_alive.key? :succesful_exit
base[:KeepAlive] = { SuccessfulExit: @keep_alive[:succesful_exit] }
elsif @keep_alive.key? :crashed
base[:KeepAlive] = { Crashed: @keep_alive[:crashed] }
elsif @keep_alive.key? :path
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
base[:StartCalendarInterval] = @cron.reject { |_, value| value == "*" }
end

odebug base

base.to_plist
end

Expand All @@ -350,7 +400,8 @@ def to_systemd_unit
options = []
options << "Type=#{@launch_only_once == true ? "oneshot" : "simple"}"
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 << "WorkingDirectory=#{@working_dir}" if @working_dir.present?
options << "RootDirectory=#{@root_dir}" if @root_dir.present?
Expand Down
165 changes: 165 additions & 0 deletions Library/Homebrew/test/service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@
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, :succesful_exit, :crashed, :path]"
end
end

describe "#run_type" do
it "throws for unexpected type" do
f.class.service do
Expand All @@ -58,6 +71,41 @@
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
it "returns valid manual_command" do
f.class.service do
Expand Down Expand Up @@ -107,6 +155,7 @@
restart_delay 30
interval 5
macos_legacy_timers true
sockets "tcp://127.0.0.1:80"
end

plist = f.service.to_plist
Expand Down Expand Up @@ -143,6 +192,20 @@
\t<string>#{HOMEBREW_PREFIX}/var</string>
\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>
\t<key>StandardErrorPath</key>
\t<string>#{HOMEBREW_PREFIX}/var/log/beanstalkd.error.log</string>
\t<key>StandardInPath</key>
Expand Down Expand Up @@ -247,6 +310,99 @@
EOS
expect(plist).to eq(plist_expect)
end

it "returns valid keepalive-exit plist" do
f.class.service do
run opt_bin/"beanstalkd"
keep_alive succesful_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

describe "#to_systemd_unit" do
Expand Down Expand Up @@ -426,6 +582,15 @@
end

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
f.class.service do
run [opt_bin/"beanstalkd", "test"]
Expand Down
50 changes: 50 additions & 0 deletions docs/Formula-Cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| `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 |
| `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:
```ruby
Expand Down Expand Up @@ -836,6 +837,55 @@ This method will set the path to `#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin
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

Homebrew has multiple levels of environment variable filtering which affects variables available to formulae.
Expand Down

0 comments on commit 210eab3

Please sign in to comment.