potluck 0.0.2 → 0.0.6

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: 92a30f07b5c14817cd0f61c62cf3ea56dce665498b022321348d9d9fe99605b7
4
- data.tar.gz: f7166d54bb8c382714c6503569a4d4416c9db432575de49ee763402f42520310
3
+ metadata.gz: 6881b14c35b5ae6be793e8390d8ab87d1b8220ea78a2803dac55f5ea8c2bc4cf
4
+ data.tar.gz: e0c7b0e3a472f03dded30e35e38fbad4f8f484a1d3cb44cd4ced99248cdd298c
5
5
  SHA512:
6
- metadata.gz: 76407292ddfa7135cfe05728a1b860237ee6914fed9ebc5477960cf607174515d81058d2b0f23e1d7bab525a977593430d8830394083f2326f2b30d41b04bb02
7
- data.tar.gz: ad0a00525b1146bf8587defa8fd96e2a1bd7066dd92961bd37d74a53283da22aaaab9bf434a848bf92d375756d2c204e96229e52c3c2d5fa4725c8eaf67b1b37
6
+ metadata.gz: 0ce2edbef44e0f8e27ffb0bb63e717d5f9781f29a27661b72de6bd77b0421f73757a230f5d3193f8e04bbaba8abd9e282d8021af9c4aded66e5e10efd78bb2c7
7
+ data.tar.gz: 5246bb757a883318ed514c8a5a5a94d5631732170c9622d1b20f92541a419fb36b9b559a3a6967ca700d2ba765ec58a323fed3b39fb464f222a9ca9dfc84e77b
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2021 Nate Pickens
1
+ Copyright 2021-2022 Nate Pickens
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
4
4
  documentation files (the "Software"), to deal in the Software without restriction, including without
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.2
1
+ 0.0.6
@@ -0,0 +1,275 @@
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: self.class.launchctl?)
28
+ @logger = logger
29
+ @manage = !!manage
30
+ @manage_with_launchctl = false
31
+
32
+ if manage.kind_of?(Hash)
33
+ @status_command = manage[:status]
34
+ @status_error_regex = manage[:status_error_regex]
35
+ @start_command = manage[:start]
36
+ @stop_command = manage[:stop]
37
+ elsif manage
38
+ @manage_with_launchctl = true
39
+ self.class.ensure_launchctl!
40
+ end
41
+ end
42
+
43
+ ##
44
+ # Returns true if the service is managed.
45
+ #
46
+ def manage?
47
+ @manage
48
+ end
49
+
50
+ ##
51
+ # Returns true if the service is managed via launchctl.
52
+ #
53
+ def manage_with_launchctl?
54
+ @manage_with_launchctl
55
+ end
56
+
57
+ ##
58
+ # Returns the status of the service:
59
+ #
60
+ # * +:active+ if the service is managed and running.
61
+ # * +:inactive+ if the service is not managed or is not running.
62
+ # * +:error+ if the service is managed and is in an error state.
63
+ #
64
+ def status
65
+ return :inactive unless manage?
66
+
67
+ output = `#{status_command}`
68
+
69
+ if $? != 0
70
+ :inactive
71
+ elsif status_error_regex && output[status_error_regex]
72
+ :error
73
+ else
74
+ :active
75
+ end
76
+ end
77
+
78
+ ##
79
+ # Starts the service if it's managed and is not active.
80
+ #
81
+ def start
82
+ return unless manage?
83
+
84
+ case status
85
+ when :error then stop
86
+ when :active then return
87
+ end
88
+
89
+ self.class.write_plist if manage_with_launchctl?
90
+ run(start_command)
91
+ wait { status == :inactive }
92
+
93
+ raise(ServiceError, "Could not start #{self.class.pretty_name}") if status != :active
94
+
95
+ log("#{self.class.pretty_name} started")
96
+ end
97
+
98
+ ##
99
+ # Stops the service if it's managed and is active or in an error state.
100
+ #
101
+ def stop
102
+ return unless manage? && status != :inactive
103
+
104
+ self.class.write_plist if manage_with_launchctl?
105
+ run(stop_command)
106
+ wait { status != :inactive }
107
+
108
+ raise(ServiceError, "Could not stop #{self.class.pretty_name}") if status != :inactive
109
+
110
+ log("#{self.class.pretty_name} stopped")
111
+ end
112
+
113
+ ##
114
+ # Restarts the service if it's managed by calling stop and then start.
115
+ #
116
+ def restart
117
+ return unless manage?
118
+
119
+ stop
120
+ start
121
+ end
122
+
123
+ ##
124
+ # Runs a command with the default shell. Raises an error if the command exits with a non-zero status.
125
+ #
126
+ # * +command+ - Command to run.
127
+ # * +capture_stderr+ - True if stderr should be redirected to stdout; otherwise stderr output will not
128
+ # be logged (default: true).
129
+ #
130
+ def run(command, capture_stderr: true)
131
+ output = `#{command}#{' 2>&1' if capture_stderr}`
132
+ status = $?
133
+
134
+ if status != 0
135
+ output.split("\n").each { |line| log(line, :error) }
136
+ raise(ServiceError, "Command exited with status #{status.exitstatus}: #{command}")
137
+ else
138
+ output
139
+ end
140
+ end
141
+
142
+ ##
143
+ # Logs a message using the logger or stdout/stderr if no logger is configured.
144
+ #
145
+ # * +message+ - Message to log.
146
+ # * +error+ - True if the message is an error (default: false).
147
+ #
148
+ def log(message, error = false)
149
+ if @logger
150
+ error ? @logger.error(message) : @logger.info(message)
151
+ else
152
+ error ? $stderr.puts(message) : $stdout.puts(message)
153
+ end
154
+ end
155
+
156
+ ##
157
+ # Human-friendly name of the service.
158
+ #
159
+ def self.pretty_name
160
+ @pretty_name ||= self.to_s.split('::').last
161
+ end
162
+
163
+ ##
164
+ # Computer-friendly name of the service.
165
+ #
166
+ def self.service_name
167
+ @service_name ||= pretty_name.downcase
168
+ end
169
+
170
+ ##
171
+ # Name for the launchctl service.
172
+ #
173
+ def self.launchctl_name
174
+ "#{SERVICE_PREFIX}#{service_name}"
175
+ end
176
+
177
+ ##
178
+ # Path to the launchctl plist file of the service.
179
+ #
180
+ def self.plist_path
181
+ File.join(DIR, "#{launchctl_name}.plist")
182
+ end
183
+
184
+ ##
185
+ # Content of the launchctl plist file.
186
+ #
187
+ def self.plist(content = '')
188
+ <<~EOS
189
+ <?xml version="1.0" encoding="UTF-8"?>
190
+ #{'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.'\
191
+ '0.dtd">'}
192
+ <plist version="1.0">
193
+ <dict>
194
+ <key>Label</key>
195
+ <string>#{launchctl_name}</string>
196
+ <key>RunAtLoad</key>
197
+ <true/>
198
+ <key>KeepAlive</key>
199
+ <false/>
200
+ #{content.gsub(/^/, ' ').strip}
201
+ </dict>
202
+ </plist>
203
+ EOS
204
+ end
205
+
206
+ ##
207
+ # Writes the service's launchctl plist file to disk.
208
+ #
209
+ def self.write_plist
210
+ FileUtils.mkdir_p(File.dirname(plist_path))
211
+ File.write(plist_path, plist)
212
+ end
213
+
214
+ ##
215
+ # Returns true if launchctl is available.
216
+ #
217
+ def self.launchctl?
218
+ defined?(@@launchctl) ? @@launchctl : (@@launchctl = `which launchctl 2>&1` && $? == 0)
219
+ end
220
+
221
+ ##
222
+ # Checks if launchctl is available and raises an error if not.
223
+ #
224
+ def self.ensure_launchctl!
225
+ launchctl? || raise(ServiceError, "Cannot manage #{pretty_name}: launchctl not found")
226
+ end
227
+
228
+ private
229
+
230
+ ##
231
+ # Command to get the status of the service.
232
+ #
233
+ def status_command
234
+ @status_command || "launchctl list 2>&1 | grep #{SERVICE_PREFIX}#{self.class.service_name}"
235
+ end
236
+
237
+ ##
238
+ # Regular expression to check the output of +#status_command+ against to determine if the service is in
239
+ # an error state.
240
+ #
241
+ def status_error_regex
242
+ @status_error_regex || LAUNCHCTL_ERROR_REGEX
243
+ end
244
+
245
+ ##
246
+ # Command to start the service.
247
+ #
248
+ def start_command
249
+ @start_command || "launchctl bootstrap gui/#{Process.uid} #{self.class.plist_path}"
250
+ end
251
+
252
+ ##
253
+ # Command to stop the service.
254
+ #
255
+ def stop_command
256
+ @stop_command || "launchctl bootout gui/#{Process.uid}/#{self.class.launchctl_name}"
257
+ end
258
+
259
+ ##
260
+ # Calls the supplied block repeatedly until it returns false. Checks frequently at first and gradually
261
+ # reduces down to one-second intervals.
262
+ #
263
+ # * +timeout+ - Maximum number of seconds to wait before timing out (default: 30).
264
+ # * +block+ - Block to call until it returns false.
265
+ #
266
+ def wait(timeout = 30, &block)
267
+ while block.call && timeout > 0
268
+ reduce = [[(30 - timeout.to_i) / 5.0, 0.1].max, 1].min
269
+ timeout -= reduce
270
+
271
+ sleep(reduce)
272
+ end
273
+ end
274
+ end
275
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Potluck
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.6'
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.2
4
+ version: 0.0.6
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-12-13 00:00:00.000000000 Z
11
+ date: 2022-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -44,7 +44,10 @@ dependencies:
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
46
  version: 6.0.0
47
- description: An extensible Ruby framework for managing external processes.
47
+ description: Potluck provides a simple interface for managing external processes in
48
+ a way that plays nice with others as well as smoothly handling both development
49
+ and production environments. Current official gem extensions provide Nginx and Postgres
50
+ management.
48
51
  email:
49
52
  executables: []
50
53
  extensions: []
@@ -54,7 +57,7 @@ files:
54
57
  - README.md
55
58
  - VERSION
56
59
  - lib/potluck.rb
57
- - lib/potluck/dish.rb
60
+ - lib/potluck/service.rb
58
61
  - lib/potluck/version.rb
59
62
  homepage: https://github.com/npickens/potluck
60
63
  licenses:
@@ -78,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
81
  - !ruby/object:Gem::Version
79
82
  version: '0'
80
83
  requirements: []
81
- rubygems_version: 3.2.3
84
+ rubygems_version: 3.2.32
82
85
  signing_key:
83
86
  specification_version: 4
84
87
  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