potluck 0.0.1 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +17 -2
- data/VERSION +1 -1
- data/lib/potluck/service.rb +266 -0
- data/lib/potluck/version.rb +1 -1
- data/lib/potluck.rb +1 -1
- metadata +4 -4
- data/lib/potluck/dish.rb +0 -142
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67a2b224bd6ed00dcfe15688c0ebaa8764649cc7a201174c20c6238bab45bc27
|
4
|
+
data.tar.gz: e1fececdb06d683f1879ac8baab15a26ba8bb6db2a15a3ea8e0dd247876435e9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 43b41b2bade1a6b87ee0d266a38f0d59b7417e5a652804521952356d7aec8fb9f98c3e2727e903e5b79b91587ff621c557b7be90325e86db303ab66cf5c193d1
|
7
|
+
data.tar.gz: 6ffedd39746332d83f7c09b3cf3a7a4e40c60503eb88f3083e904347712a60a508199fcb44b8d8949250992e61de160e476654f5acb2f4a1b7ca867e26a13ba8
|
data/README.md
CHANGED
@@ -1,6 +1,19 @@
|
|
1
1
|
# Potluck
|
2
2
|
|
3
|
-
Potluck is an extensible Ruby framework for
|
3
|
+
Potluck is an extensible Ruby framework for configuring, controlling, and interacting with external
|
4
|
+
processes. It leverages `launchctl` for starting and stopping processes when the command is available (e.g.
|
5
|
+
when developing locally on macOS) while gracefully taking either a more passive or manual role with external
|
6
|
+
processes in other environments (e.g. production).
|
7
|
+
|
8
|
+
The core Potluck gem provides a simple interface which is used by service-specific extensions to the gem.
|
9
|
+
Currently there are two official extensions:
|
10
|
+
|
11
|
+
* [Potluck::Nginx](potluck-nginx/README.md) - Generates Nginx configuration files and (optionally) controls
|
12
|
+
the Nginx process with `launchctl` or manual commands. Allows for multiple Ruby apps as well as other
|
13
|
+
external processes to all seamlessly use Nginx simultaneously.
|
14
|
+
* [Potluck::Postgres](potluck-postgres/README.md) - Provides control of the Postgres process and basic
|
15
|
+
common functionality for connecting to and setting up a database. Uses the
|
16
|
+
[Sequel](https://github.com/jeremyevans/sequel) and [pg](https://github.com/ged/ruby-pg) gems.
|
4
17
|
|
5
18
|
## Installation
|
6
19
|
|
@@ -18,7 +31,9 @@ gem install potluck
|
|
18
31
|
|
19
32
|
## Usage
|
20
33
|
|
21
|
-
|
34
|
+
The core Potluck gem is not meant to be used directly. Rather its `Service` class defines a common interface
|
35
|
+
for external processes which can be inherited by service-specific child classes. See
|
36
|
+
[Potluck::Nginx](potluck-nginx/README.md) and [Potluck::Postgres](potluck-postgres/README.md) for examples.
|
22
37
|
|
23
38
|
## Contributing
|
24
39
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.5
|
@@ -0,0 +1,266 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require('fileutils')
|
4
|
+
|
5
|
+
module Potluck
|
6
|
+
##
|
7
|
+
# General error class used for errors encountered with a service.
|
8
|
+
#
|
9
|
+
class ServiceError < StandardError; end
|
10
|
+
|
11
|
+
##
|
12
|
+
# A Ruby interface for configuring, controlling, and interacting with external processes. Serves as a
|
13
|
+
# parent class for service-specific child classes.
|
14
|
+
#
|
15
|
+
class Service
|
16
|
+
SERVICE_PREFIX = 'potluck.npickens.'
|
17
|
+
LAUNCHCTL_ERROR_REGEX = /^-|\t[^0]\t/.freeze
|
18
|
+
|
19
|
+
##
|
20
|
+
# Creates a new instance.
|
21
|
+
#
|
22
|
+
# * +logger+ - +Logger+ instance to use for outputting info and error messages (optional). Output will
|
23
|
+
# be sent to stdout and stderr if none is supplied.
|
24
|
+
# * +manage+ - True if the service runs locally and should be managed by this process (default: true if
|
25
|
+
# launchctl is available and false otherwise).
|
26
|
+
#
|
27
|
+
def initialize(logger: nil, manage: launchctl?)
|
28
|
+
@logger = logger
|
29
|
+
@manage = !!manage
|
30
|
+
|
31
|
+
if manage.kind_of?(Hash)
|
32
|
+
@status_command = manage[:status]
|
33
|
+
@status_error_regex = manage[:status_error_regex]
|
34
|
+
@start_command = manage[:start]
|
35
|
+
@stop_command = manage[:stop]
|
36
|
+
elsif manage
|
37
|
+
ensure_launchctl!
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Returns true if the service is managed.
|
43
|
+
#
|
44
|
+
def manage?
|
45
|
+
@manage
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Returns true if launchctl is available.
|
50
|
+
#
|
51
|
+
def launchctl?
|
52
|
+
defined?(@@launchctl) ? @@launchctl : (@@launchctl = `which launchctl 2>&1` && $? == 0)
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Checks if launchctl is available and raises an error if not.
|
57
|
+
#
|
58
|
+
def ensure_launchctl!
|
59
|
+
launchctl? || raise(ServiceError.new("Cannot manage #{self.pretty_name}: launchctl not found"))
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Command to get the status of the service.
|
64
|
+
#
|
65
|
+
def status_command
|
66
|
+
@status_command || "launchctl list 2>&1 | grep #{SERVICE_PREFIX}#{self.class.service_name}"
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Regular expression to check the output of +#status_command+ against to determine if the service is in
|
71
|
+
# an error state.
|
72
|
+
#
|
73
|
+
def status_error_regex
|
74
|
+
@status_error_regex || LAUNCHCTL_ERROR_REGEX
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Command to start the service.
|
79
|
+
#
|
80
|
+
def start_command
|
81
|
+
@start_command || "launchctl bootstrap gui/#{Process.uid} #{self.class.plist_path}"
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Command to stop the service.
|
86
|
+
#
|
87
|
+
def stop_command
|
88
|
+
@stop_command || "launchctl bootout gui/#{Process.uid}/#{self.class.launchctl_name}"
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Writes the service's launchctl plist file to disk.
|
93
|
+
#
|
94
|
+
def ensure_plist
|
95
|
+
FileUtils.mkdir_p(File.dirname(self.class.plist_path))
|
96
|
+
File.write(self.class.plist_path, self.class.plist)
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# Returns the status of the service:
|
101
|
+
#
|
102
|
+
# * +:active+ if the service is managed and running.
|
103
|
+
# * +:inactive+ if the service is not managed or is not running.
|
104
|
+
# * +:error+ if the service is managed and is in an error state.
|
105
|
+
#
|
106
|
+
def status
|
107
|
+
return :inactive unless manage?
|
108
|
+
|
109
|
+
output = `#{status_command}`
|
110
|
+
|
111
|
+
if $? != 0
|
112
|
+
:inactive
|
113
|
+
elsif status_error_regex && output[status_error_regex]
|
114
|
+
:error
|
115
|
+
else
|
116
|
+
:active
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Starts the service if it's managed and is not active.
|
122
|
+
#
|
123
|
+
def start
|
124
|
+
return unless manage?
|
125
|
+
|
126
|
+
ensure_plist unless @start_command
|
127
|
+
|
128
|
+
case status
|
129
|
+
when :error then stop
|
130
|
+
when :active then return
|
131
|
+
end
|
132
|
+
|
133
|
+
run(start_command)
|
134
|
+
wait { status == :inactive }
|
135
|
+
|
136
|
+
raise(ServiceError.new("Could not start #{self.class.pretty_name}")) if status != :active
|
137
|
+
|
138
|
+
log("#{self.class.pretty_name} started")
|
139
|
+
end
|
140
|
+
|
141
|
+
##
|
142
|
+
# Stops the service if it's managed and is active or in an error state.
|
143
|
+
#
|
144
|
+
def stop
|
145
|
+
return unless manage? && status != :inactive
|
146
|
+
|
147
|
+
run(stop_command)
|
148
|
+
wait { status != :inactive }
|
149
|
+
|
150
|
+
raise(ServiceError.new("Could not stop #{self.class.pretty_name}")) if status != :inactive
|
151
|
+
|
152
|
+
log("#{self.class.pretty_name} stopped")
|
153
|
+
end
|
154
|
+
|
155
|
+
##
|
156
|
+
# Restarts the service if it's managed by calling stop and then start.
|
157
|
+
#
|
158
|
+
def restart
|
159
|
+
return unless manage?
|
160
|
+
|
161
|
+
stop
|
162
|
+
start
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# Runs a command with the default shell. Raises an error if the command exits with a non-zero status.
|
167
|
+
#
|
168
|
+
# * +command+ - Command to run.
|
169
|
+
# * +redirect_stderr+ - True if stderr should be redirected to stdout; otherwise stderr output will not
|
170
|
+
# be logged (default: true).
|
171
|
+
#
|
172
|
+
def run(command, redirect_stderr: true)
|
173
|
+
output = `#{command}#{' 2>&1' if redirect_stderr}`
|
174
|
+
status = $?
|
175
|
+
|
176
|
+
if status != 0
|
177
|
+
output.split("\n").each { |line| log(line, :error) }
|
178
|
+
raise(ServiceError.new("Command exited with status #{status.to_i}: #{command}"))
|
179
|
+
else
|
180
|
+
output
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
# Logs a message using the logger or stdout/stderr if no logger is configured.
|
186
|
+
#
|
187
|
+
# * +message+ - Message to log.
|
188
|
+
# * +error+ - True if the message is an error (default: false).
|
189
|
+
#
|
190
|
+
def log(message, error = false)
|
191
|
+
if @logger
|
192
|
+
error ? @logger.error(message) : @logger.info(message)
|
193
|
+
else
|
194
|
+
error ? $stderr.puts(message) : $stdout.puts(message)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
##
|
201
|
+
# Calls the supplied block repeatedly until it returns false. Checks frequently at first and gradually
|
202
|
+
# reduces down to one-second intervals.
|
203
|
+
#
|
204
|
+
# * +timeout+ - Maximum number of seconds to wait before timing out (default: 30).
|
205
|
+
# * +block+ - Block to call until it returns false.
|
206
|
+
#
|
207
|
+
def wait(timeout = 30, &block)
|
208
|
+
while block.call && timeout > 0
|
209
|
+
reduce = [[(30 - timeout.to_i) / 5.0, 0.1].max, 1].min
|
210
|
+
timeout -= reduce
|
211
|
+
|
212
|
+
sleep(reduce)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
##
|
217
|
+
# Human-friendly name of the service.
|
218
|
+
#
|
219
|
+
def self.pretty_name
|
220
|
+
@pretty_name ||= self.to_s.split('::').last
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# Computer-friendly name of the service.
|
225
|
+
#
|
226
|
+
def self.service_name
|
227
|
+
@service_name ||= pretty_name.downcase
|
228
|
+
end
|
229
|
+
|
230
|
+
##
|
231
|
+
# Name for the launchctl service.
|
232
|
+
#
|
233
|
+
def self.launchctl_name
|
234
|
+
"#{SERVICE_PREFIX}#{service_name}"
|
235
|
+
end
|
236
|
+
|
237
|
+
##
|
238
|
+
# Path to the launchctl plist file of the service.
|
239
|
+
#
|
240
|
+
def self.plist_path
|
241
|
+
File.join(DIR, "#{launchctl_name}.plist")
|
242
|
+
end
|
243
|
+
|
244
|
+
##
|
245
|
+
# Content of the launchctl plist file.
|
246
|
+
#
|
247
|
+
def self.plist(content)
|
248
|
+
<<~EOS
|
249
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
250
|
+
#{'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.'\
|
251
|
+
'0.dtd">'}
|
252
|
+
<plist version="1.0">
|
253
|
+
<dict>
|
254
|
+
<key>Label</key>
|
255
|
+
<string>#{launchctl_name}</string>
|
256
|
+
<key>RunAtLoad</key>
|
257
|
+
<true/>
|
258
|
+
<key>KeepAlive</key>
|
259
|
+
<false/>
|
260
|
+
#{content.gsub(/^/, ' ').strip}
|
261
|
+
</dict>
|
262
|
+
</plist>
|
263
|
+
EOS
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
data/lib/potluck/version.rb
CHANGED
data/lib/potluck.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: potluck
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nate Pickens
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-12-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -54,7 +54,7 @@ files:
|
|
54
54
|
- README.md
|
55
55
|
- VERSION
|
56
56
|
- lib/potluck.rb
|
57
|
-
- lib/potluck/
|
57
|
+
- lib/potluck/service.rb
|
58
58
|
- lib/potluck/version.rb
|
59
59
|
homepage: https://github.com/npickens/potluck
|
60
60
|
licenses:
|
@@ -78,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
78
|
- !ruby/object:Gem::Version
|
79
79
|
version: '0'
|
80
80
|
requirements: []
|
81
|
-
rubygems_version: 3.2.
|
81
|
+
rubygems_version: 3.2.32
|
82
82
|
signing_key:
|
83
83
|
specification_version: 4
|
84
84
|
summary: An extensible Ruby framework for managing external processes.
|
data/lib/potluck/dish.rb
DELETED
@@ -1,142 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Potluck
|
4
|
-
class Dish
|
5
|
-
SERVICE_PREFIX = 'potluck.npickens.'
|
6
|
-
|
7
|
-
PLIST_XML = '<?xml version="1.0" encoding="UTF-8"?>'
|
8
|
-
PLIST_DOCTYPE = '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/Prope'\
|
9
|
-
'rtyList-1.0.dtd">'
|
10
|
-
|
11
|
-
LAUNCHCTL_ERROR_REGEX = /^-|\t[^0]\t/.freeze
|
12
|
-
|
13
|
-
def initialize(logger: nil, is_local: nil)
|
14
|
-
@logger = logger
|
15
|
-
@is_local = is_local.nil? ? (IS_MACOS && ensure_launchctl! rescue false) : is_local
|
16
|
-
end
|
17
|
-
|
18
|
-
def ensure_launchctl!
|
19
|
-
@@launchctl = `which launchctl` && $? == 0 unless defined?(@@launchctl)
|
20
|
-
@@launchctl || raise("Cannot manage #{self.class.to_s.split('::').last}: launchctl not found")
|
21
|
-
end
|
22
|
-
|
23
|
-
def ensure_plist
|
24
|
-
File.write(self.class.plist_path, self.class.plist)
|
25
|
-
end
|
26
|
-
|
27
|
-
def status
|
28
|
-
return :inactive unless @is_local && ensure_launchctl!
|
29
|
-
|
30
|
-
output = `launchctl list 2>&1 | grep #{SERVICE_PREFIX}#{self.class.service_name}`
|
31
|
-
|
32
|
-
if $? != 0
|
33
|
-
:inactive
|
34
|
-
elsif output[LAUNCHCTL_ERROR_REGEX]
|
35
|
-
:error
|
36
|
-
else
|
37
|
-
:active
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def start
|
42
|
-
return unless @is_local && ensure_launchctl!
|
43
|
-
|
44
|
-
ensure_plist
|
45
|
-
|
46
|
-
case status
|
47
|
-
when :error then stop
|
48
|
-
when :active then return
|
49
|
-
end
|
50
|
-
|
51
|
-
run("launchctl bootstrap gui/#{Process.uid} #{self.class.plist_path}")
|
52
|
-
wait { status == :inactive }
|
53
|
-
|
54
|
-
raise("Could not start #{self.class.pretty_name}") if status != :active
|
55
|
-
|
56
|
-
log("#{self.class.pretty_name} started")
|
57
|
-
end
|
58
|
-
|
59
|
-
def stop
|
60
|
-
return unless @is_local && ensure_launchctl! && status != :inactive
|
61
|
-
|
62
|
-
run("launchctl bootout gui/#{Process.uid}/#{self.class.launchctl_name}")
|
63
|
-
wait { status != :inactive }
|
64
|
-
|
65
|
-
raise("Could not stop #{self.class.pretty_name}") if status != :inactive
|
66
|
-
|
67
|
-
log("#{self.class.pretty_name} stopped")
|
68
|
-
end
|
69
|
-
|
70
|
-
def restart
|
71
|
-
return unless @is_local && ensure_launchctl!
|
72
|
-
|
73
|
-
stop
|
74
|
-
start
|
75
|
-
end
|
76
|
-
|
77
|
-
def run(command, redirect_stderr: true)
|
78
|
-
output = `#{command}#{' 2>&1' if redirect_stderr}`
|
79
|
-
status = $?
|
80
|
-
|
81
|
-
if status != 0
|
82
|
-
output.split("\n").each { |line| log(line, :error) }
|
83
|
-
raise("Command exited with status #{status.to_i}: #{command}")
|
84
|
-
else
|
85
|
-
output
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
def log(message, error = false)
|
90
|
-
if @logger
|
91
|
-
error ? @logger.error(message) : @logger.info(message)
|
92
|
-
else
|
93
|
-
error ? $stderr.puts(message) : $stdout.puts(message)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
private
|
98
|
-
|
99
|
-
def wait(timeout = 30, &block)
|
100
|
-
while block.call && timeout > 0
|
101
|
-
reduce = [[(30 - timeout.to_i) / 5.0, 0.1].max, 1].min
|
102
|
-
timeout -= reduce
|
103
|
-
|
104
|
-
sleep(reduce)
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def self.pretty_name
|
109
|
-
@pretty_name ||= self.to_s.split('::').last
|
110
|
-
end
|
111
|
-
|
112
|
-
def self.service_name
|
113
|
-
@service_name ||= pretty_name.downcase
|
114
|
-
end
|
115
|
-
|
116
|
-
def self.launchctl_name
|
117
|
-
"#{SERVICE_PREFIX}#{service_name}"
|
118
|
-
end
|
119
|
-
|
120
|
-
def self.plist_path
|
121
|
-
File.join(DIR, "#{launchctl_name}.plist")
|
122
|
-
end
|
123
|
-
|
124
|
-
def self.plist(content)
|
125
|
-
<<~EOS
|
126
|
-
#{PLIST_XML}
|
127
|
-
#{PLIST_DOCTYPE}
|
128
|
-
<plist version="1.0">
|
129
|
-
<dict>
|
130
|
-
<key>Label</key>
|
131
|
-
<string>#{launchctl_name}</string>
|
132
|
-
<key>RunAtLoad</key>
|
133
|
-
<true/>
|
134
|
-
<key>KeepAlive</key>
|
135
|
-
<false/>
|
136
|
-
#{content.gsub(/^/, ' ').strip}
|
137
|
-
</dict>
|
138
|
-
</plist>
|
139
|
-
EOS
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|