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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50b13dbd4649490c2673d9dde14aef89251279b2ea39c6cb816d110320078efa
4
- data.tar.gz: f380c5e9c07a27b55bfa20a5a4deddbb3e1978ecb9d63db8acc5888e9457f533
3
+ metadata.gz: 67a2b224bd6ed00dcfe15688c0ebaa8764649cc7a201174c20c6238bab45bc27
4
+ data.tar.gz: e1fececdb06d683f1879ac8baab15a26ba8bb6db2a15a3ea8e0dd247876435e9
5
5
  SHA512:
6
- metadata.gz: 878a3446024f4ef4ac5464acb7866ed5517d27dd4a113ed2e2f24c119d9a0416bc82eb5cf00462a0dd4e3a602a566bf15a9f1b900c8e28fba5734399ed5a3d48
7
- data.tar.gz: 0fe2162e70524a4bd85eba990d32b5d5397c4366bb8532d3f8a11a06824e7bce169f03b4dbe27e04600f0763a62a6f4a596a597f24ce4197a1bdb07e8ee8de07
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 managing external processes.
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
- [Coming soon.]
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
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Potluck
4
- VERSION = '0.0.1'
4
+ VERSION = '0.0.5'
5
5
  end
data/lib/potluck.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative('potluck/dish')
3
+ require_relative('potluck/service')
4
4
 
5
5
  module Potluck
6
6
  DIR = File.expand_path(File.join(ENV['HOME'], '.potluck')).freeze
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.1
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-03-27 00:00:00.000000000 Z
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/dish.rb
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.3
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