subserver 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ed0057cefa344204a08c579efd92a9f0df33aae3
4
+ data.tar.gz: 1d76ebad4bcf2f65e7dda873a21a39c13cebcd6c
5
+ SHA512:
6
+ metadata.gz: 07745b6c86ef5001384bf36fa6195f01ffa8c73676c4012936ae3d33a174eedad014336a3b94b2873b931eafec3b2fa56af4659234266f11a943a57799a72a77
7
+ data.tar.gz: 8ca91c9f88a06a5ab50ba7290da4754bf6f98fbfdb7ce6798054f31e359f431673dc1006d5c07222c642907f88f2172ad32158d466d6c97444f59faaa88cc7c8
@@ -0,0 +1,2 @@
1
+ # 1.0.0
2
+ - Initial Public Release
@@ -0,0 +1,54 @@
1
+ # Contribution Guide
2
+
3
+ Subserver is one of Life.Church's Open Source projects as part of our [opendigerati](https://www.opendigerati.com/) initiative.
4
+ Because of this we are continuously trying to improve the process for contributing to our projects.
5
+ This document outlines our current process, but please check back here every time you wish to contribute.
6
+
7
+ ## Open Development
8
+ All work on Subserver happens directly on GitHub. Both Life.Church team members and external contributors send pull requests which go through the same review process.
9
+
10
+ ## Branch Organization
11
+ We will do our best to keep the master branch in good shape, with tests passing at all times. But in order to move fast, we will make API changes that your application might not be compatible with. We recommend that you use the latest stable version of Subserver in your application.
12
+ If you send a pull request, please do it against the master branch. We maintain stable branches for major versions separately but we don’t accept pull requests to them directly. Instead, we cherry-pick non-breaking changes from master to the latest stable major version.
13
+
14
+ ## Semantic Versioning
15
+ Subserver follows [semantic versioning](http://semver.org/). We release patch versions for bugfixes, minor versions for new features, and major versions for any breaking changes. When we make breaking changes, we also introduce deprecation warnings in a minor version so that our users learn about the upcoming changes and migrate their code in advance.
16
+
17
+ We will add a version milestone to every approved pull request marking whether the change should go in the next patch, minor, or a major version.
18
+
19
+ Every significant change is documented in the [changelog](./CHANGELOG.md) file.
20
+
21
+ ## Bugs
22
+ ### Where to Find Known Issues
23
+ We are using GitHub Issues for our public bugs. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn’t already exist.
24
+
25
+ ### Reporting New Issues
26
+ When reporting new issues, please be as detailed as possible and provide the following:
27
+ * Version of Subserver
28
+ * Version of Ruby
29
+ * Version of Rails (If using)
30
+ * Steps to Reproduce the Bug
31
+ * Any steps you have tried to resolve the issue
32
+
33
+ ### Security Bugs
34
+ For bugs dealing with security vulnerabilities please **do not** post a public issue. Please email the code maintainers [@wintheday](https://github.com/wintheday).
35
+
36
+ ### Proposing a Change
37
+ If you intend to change the API, or make any non-trivial changes to the implementation, we recommend filing an issue. This lets us reach an agreement on your proposal before you put significant effort into it.
38
+ If you’re only fixing a bug, it’s fine to submit a pull request right away but we still recommend to file an issue detailing what you’re fixing. This is helpful in case we don’t accept that specific fix but want to keep track of the issue.
39
+
40
+ ### Your First Pull Request
41
+ If this is your first ever Pull Request we are honored you have chosen to join Open Source through our community. You can learn the basics of Github and how Pull Requests work from this free video series:
42
+ [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github)
43
+
44
+ ### Sending a Pull Request
45
+ The code maintainers are monitoring for pull requests. We will review your pull request and either merge it, request changes to it, or close it with an explanation. For breaking changes we may need to fix our internal uses of Validstate, which could cause some delay. We’ll do our best to provide updates and feedback throughout the process.
46
+
47
+ #### Before submitting a pull request, please make sure the following is done:
48
+ 1. Fork the repository and create your branch from master.
49
+ 2. Run `bundle install` from the repository root.
50
+ 3. If you’ve fixed a bug or added code that should be tested, add tests!
51
+ 4. Ensure the test suite passes `rspec`.
52
+
53
+ ### Making changes to the Documentation
54
+ Please ensure that any change made to Subserver that introduce new functionality or a breaking change to the API are well documented via the wiki.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Life.Church
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ Subserver
2
+ ==============
3
+
4
+ <!-- [![Gem Version](https://badge.fury.io/rb/sidekiq.svg)](https://rubygems.org/gems/sidekiq) -->
5
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md)
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $TESTING = false
4
+
5
+ require_relative '../lib/subserver/cli'
6
+
7
+ begin
8
+ cli = Subserver::CLI.instance
9
+ cli.parse
10
+ cli.run
11
+ rescue => e
12
+ raise e if $DEBUG
13
+ STDERR.puts e.message
14
+ STDERR.puts e.backtrace.join("\n")
15
+ exit 1
16
+ end
@@ -0,0 +1,185 @@
1
+ require 'json'
2
+
3
+ require 'subserver/version'
4
+ fail "Subserver #{Subserver::VERSION} does not support Ruby versions below 2.3.1." if RUBY_PLATFORM != 'java' && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3.1')
5
+
6
+ require 'subserver/logging'
7
+ require 'subserver/pubsub'
8
+ require 'subserver/health'
9
+ require 'subserver/subscriber'
10
+ require 'subserver/middleware/chain'
11
+
12
+ module Subserver
13
+ NAME = 'Subserver'
14
+ LICENSE = 'Subserver is licensed under MIT.'
15
+
16
+ DEFAULTS = {
17
+ project_id: nil,
18
+ credentials: nil,
19
+ queues: [],
20
+ labels: [],
21
+ require: '.',
22
+ subscriber_dir: './subscribers',
23
+ environment: nil,
24
+ timeout: 35,
25
+ error_handlers: [],
26
+ death_handlers: [],
27
+ lifecycle_events: {
28
+ startup: [],
29
+ quiet: [],
30
+ shutdown: [],
31
+ heartbeat: [],
32
+ },
33
+ reloader: proc { |&block| block.call },
34
+ }
35
+
36
+ DEFAULT_SUBSCRIBER_OPTIONS = {
37
+ subscription: nil,
38
+ deadline: 60,
39
+ streams: 2,
40
+ threads: {
41
+ callback: 4,
42
+ push: 2
43
+ },
44
+ inventory: 1000,
45
+ queue: 'default'
46
+ }
47
+
48
+
49
+ def self.options
50
+ @options ||= load_config
51
+ end
52
+
53
+ def self.options=(opts)
54
+ @options = opts
55
+ end
56
+
57
+ def self.configure
58
+ yield self
59
+ end
60
+
61
+ def self.load_config(file=nil)
62
+ opts = DEFAULTS.dup
63
+ file = Dir["config/subserver.yml*"].first if file.nil?
64
+ return opts unless file && File.exists?(file)
65
+ opts.merge(parse_config(file))
66
+ end
67
+
68
+ def self.load_json(string)
69
+ JSON.parse(string)
70
+ end
71
+
72
+ def self.dump_json(object)
73
+ JSON.generate(object)
74
+ end
75
+
76
+ def self.logger
77
+ Subserver::Logging.logger
78
+ end
79
+
80
+ def self.logger=(log)
81
+ Subserver::Logging.logger = log
82
+ end
83
+
84
+ def self.health_server
85
+ @health_server ||= Subserver::Health.new
86
+ end
87
+
88
+ def self.pubsub_client
89
+ Subserver::Pubsub.client
90
+ end
91
+
92
+ def self.middleware
93
+ @chain ||= default_middleware
94
+ yield @chain if block_given?
95
+ @chain
96
+ end
97
+
98
+ def self.default_middleware
99
+ Middleware::Chain.new
100
+ end
101
+
102
+ def self.default_subscriber_options=(hash)
103
+ @default_subscriber_options = default_subscriber_options.merge(Hash[hash.map{|k, v| [k.to_s, v]}])
104
+ end
105
+
106
+ def self.default_subscriber_options
107
+ defined?(@default_subscriber_options) ? @default_subscriber_options : DEFAULT_SUBSCRIBER_OPTIONS
108
+ end
109
+
110
+ # Death handlers are called when all retries for a job have been exhausted and
111
+ # the job dies. It's the notification to your application
112
+ # that this job will not succeed without manual intervention.
113
+ #
114
+ # Subserver.configure do |config|
115
+ # config.death_handlers << ->(job, ex) do
116
+ # end
117
+ # end
118
+ def self.death_handlers
119
+ options[:death_handlers]
120
+ end
121
+
122
+ # Register a proc to handle any error which occurs within the Subserver process.
123
+ #
124
+ # Subserver.configure do |config|
125
+ # config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
126
+ # end
127
+ #
128
+ # The default error handler logs errors to Subserver.logger.
129
+ def self.error_handlers
130
+ self.options[:error_handlers]
131
+ end
132
+
133
+ # Register a block to run at a point in the Subserver lifecycle.
134
+ # :startup, :quiet or :shutdown are valid events.
135
+ #
136
+ # Subserver.configure do |config|
137
+ # config.on(:shutdown) do
138
+ # puts "Goodbye cruel world!"
139
+ # end
140
+ # end
141
+ def self.on(event, &block)
142
+ raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
143
+ raise ArgumentError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)
144
+ options[:lifecycle_events][event] << block
145
+ end
146
+
147
+ # We are shutting down Subserver but what about workers that
148
+ # are working on some long job? This error is
149
+ # raised in workers that have not finished within the hard
150
+ # timeout limit. This is needed to rollback db transactions,
151
+ # otherwise Ruby's Thread#kill will commit.
152
+ # DO NOT RESCUE THIS ERROR IN YOUR SUBSCRIBERS
153
+ class Shutdown < Interrupt; end
154
+
155
+ private
156
+
157
+ def self.parse_config(cfile)
158
+ opts = {}
159
+ if File.exist?(cfile)
160
+ opts = YAML.load(ERB.new(IO.read(cfile)).result) || opts
161
+
162
+ if opts.respond_to? :deep_symbolize_keys!
163
+ opts.deep_symbolize_keys!
164
+ else
165
+ symbolize_keys_deep!(opts)
166
+ end
167
+
168
+ else
169
+ # allow a non-existent config file so Subserver
170
+ # can be deployed by cap with just the defaults.
171
+ end
172
+ opts
173
+ end
174
+
175
+ def self.symbolize_keys_deep!(hash)
176
+ hash.keys.each do |k|
177
+ symkey = k.respond_to?(:to_sym) ? k.to_sym : k
178
+ hash[symkey] = hash.delete k
179
+ symbolize_keys_deep! hash[symkey] if hash[symkey].kind_of? Hash
180
+ end
181
+ end
182
+
183
+ end
184
+
185
+ require 'subserver/rails' if defined?(::Rails::Engine)
@@ -0,0 +1,353 @@
1
+ # frozen_string_literal: true
2
+ $stdout.sync = true
3
+
4
+ require 'yaml'
5
+ require 'singleton'
6
+ require 'optparse'
7
+ require 'erb'
8
+ require 'fileutils'
9
+
10
+ require 'subserver'
11
+ require 'subserver/util'
12
+
13
+ module Subserver
14
+ class CLI
15
+ include Util
16
+ include Singleton unless $TESTING
17
+
18
+ attr_accessor :code
19
+ attr_accessor :launcher
20
+ attr_accessor :environment
21
+
22
+ def initialize
23
+ @code = nil
24
+ end
25
+
26
+ def parse(args=ARGV)
27
+ @code = nil
28
+
29
+ setup_options(args)
30
+ initialize_logger
31
+ validate!
32
+ daemonize
33
+ write_pid
34
+ end
35
+
36
+ def jruby?
37
+ defined?(::JRUBY_VERSION)
38
+ end
39
+
40
+ def run
41
+ boot_system
42
+ print_banner
43
+
44
+ self_read, self_write = IO.pipe
45
+ sigs = %w(INT TERM TTIN TSTP)
46
+ # USR1 and USR2 don't work on the JVM
47
+ if !jruby?
48
+ sigs << 'USR1'
49
+ sigs << 'USR2'
50
+ end
51
+
52
+ sigs.each do |sig|
53
+ begin
54
+ trap sig do
55
+ self_write.write("#{sig}\n")
56
+ end
57
+ rescue ArgumentError
58
+ puts "Signal #{sig} not supported"
59
+ end
60
+ end
61
+
62
+ logger.info "Running in #{RUBY_DESCRIPTION}"
63
+ logger.info Subserver::LICENSE
64
+
65
+ # cache process identity
66
+ Subserver.options[:identity] = identity
67
+
68
+ # Touch middleware so it isn't lazy loaded by multiple threads.
69
+ Subserver.middleware
70
+
71
+ # Before this point, the process is initializing with just the main thread.
72
+ # Starting here the process will now have multiple threads running.
73
+ fire_event(:startup, reverse: false, reraise: true)
74
+
75
+ logger.debug { "Middleware: #{Subserver.middleware.map(&:klass).join(', ')}" }
76
+
77
+ if !options[:daemon]
78
+ logger.info 'Starting processing, hit Ctrl-C to stop'
79
+ end
80
+
81
+ # Start Health Server
82
+ @health_thread = safe_thread("health_server") do
83
+ Subserver.health_server.start
84
+ end
85
+
86
+ require 'subserver/launcher'
87
+ @launcher = Subserver::Launcher.new(options)
88
+
89
+ begin
90
+ launcher.run
91
+
92
+ while readable_io = IO.select([self_read])
93
+ signal = readable_io.first[0].gets.strip
94
+ handle_signal(signal)
95
+ end
96
+ rescue Interrupt
97
+ logger.info 'Shutting down'
98
+ launcher.stop
99
+ exit(0)
100
+ end
101
+ end
102
+
103
+ def self.banner
104
+ %q{
105
+ ================================
106
+ Subserver
107
+ ================================
108
+ }
109
+ end
110
+
111
+ SIGNAL_HANDLERS = {
112
+ # Ctrl-C in terminal
113
+ 'INT' => ->(cli) { raise Interrupt },
114
+ # TERM is the signal that Subserver must exit.
115
+ # Heroku sends TERM and then waits 30 seconds for process to exit.
116
+ 'TERM' => ->(cli) { raise Interrupt },
117
+ 'USR1' => ->(cli) {
118
+ Subserver.logger.info "Received USR1, no longer accepting new work"
119
+ cli.launcher.quiet
120
+ },
121
+ 'TSTP' => ->(cli) {
122
+ Subserver.logger.info "Received TSTP, no longer accepting new work"
123
+ cli.launcher.quiet
124
+ },
125
+ 'USR2' => ->(cli) {
126
+ if Subserver.options[:logfile]
127
+ Subserver.logger.info "Received USR2, reopening log file"
128
+ Subserver::Logging.reopen_logs
129
+ end
130
+ },
131
+ 'TTIN' => ->(cli) {
132
+ Thread.list.each do |thread|
133
+ Subserver.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread['subserver_label']}"
134
+ if thread.backtrace
135
+ Subserver.logger.warn thread.backtrace.join("\n")
136
+ else
137
+ Subserver.logger.warn "<no backtrace available>"
138
+ end
139
+ end
140
+ },
141
+ }
142
+
143
+ def handle_signal(sig)
144
+ Subserver.logger.debug "Got #{sig} signal"
145
+ handy = SIGNAL_HANDLERS[sig]
146
+ if handy
147
+ handy.call(self)
148
+ else
149
+ Subserver.logger.info { "No signal handler for #{sig}" }
150
+ end
151
+ end
152
+
153
+ private unless $TESTING
154
+
155
+ def print_banner
156
+ # Print logo and banner for development
157
+ if environment == 'development' && $stdout.tty?
158
+ puts Subserver::CLI.banner
159
+ end
160
+ end
161
+
162
+ def daemonize
163
+ return unless options[:daemon]
164
+
165
+ raise ArgumentError, "You really should set a logfile if you're going to daemonize" unless options[:logfile]
166
+ files_to_reopen = []
167
+ ObjectSpace.each_object(File) do |file|
168
+ files_to_reopen << file unless file.closed?
169
+ end
170
+
171
+ ::Process.daemon(true, true)
172
+
173
+ files_to_reopen.each do |file|
174
+ begin
175
+ file.reopen file.path, "a+"
176
+ file.sync = true
177
+ rescue ::Exception
178
+ end
179
+ end
180
+
181
+ [$stdout, $stderr].each do |io|
182
+ File.open(options[:logfile], 'ab') do |f|
183
+ io.reopen(f)
184
+ end
185
+ io.sync = true
186
+ end
187
+ $stdin.reopen('/dev/null')
188
+
189
+ initialize_logger
190
+ end
191
+
192
+ def set_environment(cli_env)
193
+ @environment = cli_env || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
194
+ end
195
+
196
+ alias_method :die, :exit
197
+ alias_method :☠, :exit
198
+
199
+ def setup_options(args)
200
+ opts = parse_options(args)
201
+ set_environment opts[:environment]
202
+
203
+ cfile = opts[:config_file]
204
+ opts = Subserver.load_config(cfile).merge(opts)
205
+
206
+ Subserver.options = opts
207
+ end
208
+
209
+ def options
210
+ Subserver.options
211
+ end
212
+
213
+ def boot_system
214
+ ENV['RACK_ENV'] = ENV['RAILS_ENV'] = environment
215
+
216
+ raise ArgumentError, "#{options[:require]} does not exist" unless File.exist?(options[:require])
217
+
218
+ if File.directory?(options[:require])
219
+ require 'rails'
220
+ if ::Rails::VERSION::MAJOR < 4
221
+ raise "Subserver does not support this version of Rails."
222
+ elsif ::Rails::VERSION::MAJOR == 4
223
+ require File.expand_path("#{options[:require]}/config/application.rb")
224
+ ::Rails::Application.initializer "subserver.eager_load" do
225
+ ::Rails.application.config.eager_load = true
226
+ end
227
+ require 'subserver/rails'
228
+ require File.expand_path("#{options[:require]}/config/environment.rb")
229
+ else
230
+ require 'subserver/rails'
231
+ require File.expand_path("#{options[:require]}/config/environment.rb")
232
+ end
233
+ options[:tag] ||= default_tag
234
+ else
235
+ not_required_message = "#{options[:require]} was not required, you should use an explicit path: " +
236
+ "./#{options[:require]} or /path/to/#{options[:require]}"
237
+
238
+ require(options[:require]) || raise(ArgumentError, not_required_message)
239
+ end
240
+ end
241
+
242
+ def default_tag
243
+ dir = ::Rails.root
244
+ name = File.basename(dir)
245
+ if name.to_i != 0 && prevdir = File.dirname(dir) # Capistrano release directory?
246
+ if File.basename(prevdir) == 'releases'
247
+ return File.basename(File.dirname(prevdir))
248
+ end
249
+ end
250
+ name
251
+ end
252
+
253
+ def validate!
254
+ options[:queues] << 'default' if options[:queues].empty?
255
+
256
+ if !File.exist?(options[:require]) ||
257
+ (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb"))
258
+ logger.info "=================================================================="
259
+ logger.info " Please point subserver to a Rails 4/5 application or a Ruby file "
260
+ logger.info " to load your subscriber classes with -r [DIR|FILE]."
261
+ logger.info "=================================================================="
262
+ logger.info @parser
263
+ die(1)
264
+ end
265
+
266
+ raise ArgumentError, "#{timeout}: #{options[:timeout]} is not a valid value" if options.has_key?(:timeout) && options[:timeout].to_i <= 0
267
+ end
268
+
269
+ def parse_options(argv)
270
+ opts = {}
271
+
272
+ @parser = OptionParser.new do |o|
273
+ o.on "-c", "--credentials PATH", "Path to Google Cloud credentials JSON file." do |arg|
274
+ opts[:credentials] = arg
275
+ end
276
+
277
+ o.on '-d', '--daemon', "Daemonize process" do |arg|
278
+ opts[:daemon] = arg
279
+ end
280
+
281
+ o.on '-e', '--environment ENV', "Application environment" do |arg|
282
+ opts[:environment] = arg
283
+ end
284
+
285
+ o.on '-g', '--tag TAG', "Process tag for procline" do |arg|
286
+ opts[:tag] = arg
287
+ end
288
+
289
+ o.on '-p', '--project ID', "Google Cloud Project ID" do |arg|
290
+ opts[:project_id] = arg
291
+ end
292
+
293
+ o.on "-q", "--queue QUEUE", "Subscriber queues to process with this server" do |arg|
294
+ queue = arg
295
+ opts = (opts[:queues] ||= []) << queue
296
+ end
297
+
298
+ o.on '-r', '--require [PATH|DIR]', "Location of Rails application with subscribers or file to require" do |arg|
299
+ opts[:require] = arg
300
+ end
301
+
302
+ o.on '-t', '--timeout NUM', "Shutdown timeout" do |arg|
303
+ opts[:timeout] = Integer(arg)
304
+ end
305
+
306
+ o.on "-v", "--verbose", "Print more verbose output" do |arg|
307
+ opts[:verbose] = arg
308
+ end
309
+
310
+ o.on '-C', '--config PATH', "path to YAML config file" do |arg|
311
+ opts[:config_file] = arg
312
+ end
313
+
314
+ o.on '-L', '--logfile PATH', "path to writable logfile" do |arg|
315
+ opts[:logfile] = arg
316
+ end
317
+
318
+ o.on '-P', '--pidfile PATH', "path to pidfile" do |arg|
319
+ opts[:pidfile] = arg
320
+ end
321
+
322
+ o.on '-V', '--version', "Print version and exit" do |arg|
323
+ puts "Subserver #{Subserver::VERSION}"
324
+ die(0)
325
+ end
326
+ end
327
+
328
+ @parser.banner = "subserver [options]"
329
+ @parser.on_tail "-h", "--help", "Show help" do
330
+ logger.info @parser
331
+ die 1
332
+ end
333
+ @parser.parse!(argv)
334
+
335
+ opts
336
+ end
337
+
338
+ def initialize_logger
339
+ Subserver::Logging.initialize_logger(options[:logfile]) if options[:logfile]
340
+ Subserver.logger.level = ::Logger::DEBUG if options[:verbose]
341
+ end
342
+
343
+ def write_pid
344
+ if path = options[:pidfile]
345
+ pidfile = File.expand_path(path)
346
+ File.open(pidfile, 'w') do |f|
347
+ f.puts ::Process.pid
348
+ end
349
+ end
350
+ end
351
+
352
+ end
353
+ end