acts_as_service 0.0.2
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.
- data/LICENSE +20 -0
- data/README.rdoc +78 -0
- data/TODO +9 -0
- data/acts_as_service.gemspec +46 -0
- data/lib/acts_as_service.rb +306 -0
- metadata +67 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Copyright (c) 2010 Umamibud, Inc.
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
= acts_as_service
|
2
|
+
|
3
|
+
== Overview
|
4
|
+
|
5
|
+
A gem that gives you an easy way to make service-like code and spend all your
|
6
|
+
effort on actually writing the code to get your job done.
|
7
|
+
|
8
|
+
Limitations:
|
9
|
+
* one process at a time
|
10
|
+
* only been tested while running inside a Rails application
|
11
|
+
(e.g. script/runner MySrv.start). should work if you just specify a pidfile
|
12
|
+
explicitly, but needs a little testing
|
13
|
+
* smooth shutdown depends on your work method returning frequently (however
|
14
|
+
long you run without returning from perform_work_chunk is how long it might
|
15
|
+
take for the service to shut down
|
16
|
+
|
17
|
+
Allows you to:
|
18
|
+
* start, stop, restart your process
|
19
|
+
* ensures that only one of each process is running on a given machine (if this
|
20
|
+
is not desired, you can probably hack it or enhance to allow up to N
|
21
|
+
instances if you want
|
22
|
+
* implement a method you want called over and over, and when the service is
|
23
|
+
supposed to shut down, it stops calling the method and exits
|
24
|
+
* see what the PID is of the process by inspecting the pidfile
|
25
|
+
* explicitly shutdown the process from within the service class (but you'll
|
26
|
+
still have to return from perform_work_chunk).
|
27
|
+
* initiate shutdown of service externally by either calling MyService.stop or
|
28
|
+
adding 'shutdown' to the PID file.
|
29
|
+
* hooks to execute methods after start or before stop
|
30
|
+
|
31
|
+
== What to do
|
32
|
+
|
33
|
+
require 'acts_as_service' # or add it to your Rails env as a config.gem
|
34
|
+
|
35
|
+
class MyService
|
36
|
+
|
37
|
+
acts_as_service
|
38
|
+
|
39
|
+
def self.perform_work_chunk
|
40
|
+
# do stuff that returns in a short period (e.g. a few seconds or less)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
you can get fancier and specify your process identifier and how long to sleep
|
46
|
+
between calls to perform_work_chunk:
|
47
|
+
|
48
|
+
class MyService
|
49
|
+
|
50
|
+
acts_as_service
|
51
|
+
|
52
|
+
def self.service_name
|
53
|
+
"MyAwesomeService"
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.sleep_time
|
57
|
+
5 # seconds
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.perform_work_chunk
|
61
|
+
# do stuff that returns in a short period (e.g. a few seconds or less)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
== Maintaining acts_as_service
|
67
|
+
|
68
|
+
Make sure you have jeweler.
|
69
|
+
|
70
|
+
To build:
|
71
|
+
|
72
|
+
cd .../acts_as_service
|
73
|
+
rake gemspec
|
74
|
+
rake build
|
75
|
+
|
76
|
+
To test:
|
77
|
+
|
78
|
+
Sorry, little lazy here. Create a sample service class and letter rip!
|
data/TODO
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
TODOs
|
2
|
+
-----
|
3
|
+
|
4
|
+
put things in here that could be worked on
|
5
|
+
|
6
|
+
* extricate the gem from needing to run within Rails (maybe nothing to do here,
|
7
|
+
maybe just coming up with different pidfile default path if RAILS_ROOT
|
8
|
+
isn't defined. but including class can always just specify explicit path and
|
9
|
+
should be fine
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{acts_as_service}
|
8
|
+
s.version = "0.0.2"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Brian Percival"]
|
12
|
+
s.date = %q{2010-02-18}
|
13
|
+
s.description = %q{A gem with a mixin to let you turn a class into something that runs like a service,
|
14
|
+
which you can MyService.start, MyService.stop, and MyService.restart. It tracks
|
15
|
+
its own pid. For now, pretty sure it requires that the class is running inside
|
16
|
+
a rails context (e.g. run by script/runner MyService.start), but that could
|
17
|
+
probably be changed without too much difficulty.
|
18
|
+
}
|
19
|
+
s.email = %q{percivalatumamibuddotcom}
|
20
|
+
s.extra_rdoc_files = [
|
21
|
+
"LICENSE",
|
22
|
+
"README.rdoc",
|
23
|
+
"TODO"
|
24
|
+
]
|
25
|
+
s.files = [
|
26
|
+
"README.rdoc",
|
27
|
+
"acts_as_service.gemspec",
|
28
|
+
"lib/acts_as_service.rb"
|
29
|
+
]
|
30
|
+
s.homepage = %q{http://github.com/bmpercy/acts_as_service}
|
31
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
32
|
+
s.require_paths = ["lib"]
|
33
|
+
s.rubygems_version = %q{1.3.5}
|
34
|
+
s.summary = %q{Makes it very easy to create a service-like class that's easy to start and stop, taken from work at discovereads.com}
|
35
|
+
|
36
|
+
if s.respond_to? :specification_version then
|
37
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
38
|
+
s.specification_version = 3
|
39
|
+
|
40
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
41
|
+
else
|
42
|
+
end
|
43
|
+
else
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
@@ -0,0 +1,306 @@
|
|
1
|
+
# module that makes it easy to turn a class into a service, giving it methods
|
2
|
+
# like stop, start, restart for easy cronjobbage, etc.
|
3
|
+
#
|
4
|
+
# simply require 'acts_as_service' in your class' file and then call
|
5
|
+
# acts_as_service in your class definition, then implement a few required
|
6
|
+
# and option methods:
|
7
|
+
# - required:
|
8
|
+
# - self.perform_work_chunk: do a chunk of work that the service is intended
|
9
|
+
# to do. note that this method should return
|
10
|
+
# periodically to enable shutdown. alternatively,
|
11
|
+
# your method /could/ (but not recommended) check
|
12
|
+
# the status to see if it's set to SHUTTING_DOWN
|
13
|
+
# and return if so. otherwise, the shutdown
|
14
|
+
# by this module will not work.
|
15
|
+
#
|
16
|
+
# - optional:
|
17
|
+
# - self.service_name: return the string name of the service.
|
18
|
+
# default is the class name (without module prefixes)
|
19
|
+
# - self.service_pid_filename: path to the file that should contain the PID for
|
20
|
+
# the process while it's running.
|
21
|
+
# default is #{RAILS_ROOT}/tmp/pids/<underscore version of service name>
|
22
|
+
# - self.sleep_time: seconds to sleep between calls to peform_work_chunk
|
23
|
+
# default: no sleep till brooklyn!
|
24
|
+
# - self.after_start: a hook to run a method after the service is started
|
25
|
+
# but before first call to perform_work_chunk
|
26
|
+
# - self.before_stop: a hook to run a method before final shutdown (and
|
27
|
+
# after last run to perform_work_chunk)
|
28
|
+
#
|
29
|
+
# you can also call self.shutdown() from within your perform_work_chunk
|
30
|
+
# code to initiate a shutdown (don't just exit() because there's pidfile
|
31
|
+
# cleanup etc. to peform).
|
32
|
+
#
|
33
|
+
# # my_service.rb:
|
34
|
+
#
|
35
|
+
# require 'acts_as_service'
|
36
|
+
#
|
37
|
+
# class MyService
|
38
|
+
#
|
39
|
+
# acts_as_service
|
40
|
+
#
|
41
|
+
# # ... define methods
|
42
|
+
#
|
43
|
+
# end
|
44
|
+
#-------------------------------------------------------------------------------
|
45
|
+
module ActsAsService
|
46
|
+
|
47
|
+
ACTS_AS_SERVICE_RUNNING = 'running'
|
48
|
+
ACTS_AS_SERVICE_OTHER_RUNNING = 'other running'
|
49
|
+
ACTS_AS_SERVICE_STOPPED = 'stopped'
|
50
|
+
ACTS_AS_SERVICE_SHUTTING_DOWN = 'shutting down'
|
51
|
+
ACTS_AS_SERVICE_PID_NO_PROCESS = 'pid no process'
|
52
|
+
|
53
|
+
def self.included(base)
|
54
|
+
base.extend ClassMethods
|
55
|
+
end
|
56
|
+
|
57
|
+
module ClassMethods
|
58
|
+
# starts the process if it's not already running
|
59
|
+
#---------------------------------------------------------------------------
|
60
|
+
def start
|
61
|
+
begin
|
62
|
+
if _status != ACTS_AS_SERVICE_STOPPED &&
|
63
|
+
_status != ACTS_AS_SERVICE_PID_NO_PROCESS
|
64
|
+
|
65
|
+
puts "#{_display_name} (#{_pid_file_pid}) is already running. Ignoring."
|
66
|
+
else
|
67
|
+
# clean out a stale pid
|
68
|
+
if _status == ACTS_AS_SERVICE_PID_NO_PROCESS
|
69
|
+
puts 'Pid file exists but process is not running. Removing old pid file.'
|
70
|
+
File.delete(_pid_filename)
|
71
|
+
end
|
72
|
+
puts "Starting #{_display_name} (#{_pid})...."
|
73
|
+
puts "Run #{name}.stop to stop\n\n"
|
74
|
+
File.open(_pid_filename, 'w') {|f| f.write(_pid.to_s) }
|
75
|
+
if self.respond_to?(:after_start)
|
76
|
+
after_start
|
77
|
+
end
|
78
|
+
while (_status == ACTS_AS_SERVICE_RUNNING)
|
79
|
+
# only sleep if asked to
|
80
|
+
if self.respond_to?(:sleep_time)
|
81
|
+
sleep sleep_time
|
82
|
+
end
|
83
|
+
perform_work_chunk
|
84
|
+
end
|
85
|
+
puts "Shutting down #{_display_name} (#{_pid})"
|
86
|
+
File.delete(_pid_filename)
|
87
|
+
end
|
88
|
+
# if something happens, dump an error and clean up the pidfile if
|
89
|
+
# it's owned by this process
|
90
|
+
rescue Object => e
|
91
|
+
puts "ERROR: #{e}\n#{e.respond_to?(:backtrace) ? e.backtrace.join("\n ") : ''}"
|
92
|
+
puts "Exiting (#{_pid})\n"
|
93
|
+
if _process_running?
|
94
|
+
File.delete(_pid_filename)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
# stops the process if it's running
|
101
|
+
#---------------------------------------------------------------------------
|
102
|
+
def stop
|
103
|
+
if _status == ACTS_AS_SERVICE_STOPPED
|
104
|
+
puts "#{_display_name} is not running"
|
105
|
+
elsif _status == ACTS_AS_SERVICE_PID_NO_PROCESS
|
106
|
+
puts 'Pid file exists but process is not running. Removing old pid file.'
|
107
|
+
File.delete(_pid_filename)
|
108
|
+
else
|
109
|
+
pid_to_stop = _pid_file_pid
|
110
|
+
puts "Stopping #{_display_name} (#{pid_to_stop})...."
|
111
|
+
shutdown
|
112
|
+
while (_status != ACTS_AS_SERVICE_STOPPED)
|
113
|
+
sleep(1)
|
114
|
+
end
|
115
|
+
puts "#{_display_name} (#{pid_to_stop}) stopped\n"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
# stops the current service process and runs a new one in the current process
|
121
|
+
#-----------------------------------------------------------------------------
|
122
|
+
def restart
|
123
|
+
stop
|
124
|
+
start
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
# initiate shutdown. call this from within perform_work_chunk if the service's
|
129
|
+
# work is done and it should shut down (makes sense for cronjobs, say)
|
130
|
+
#-----------------------------------------------------------------------------
|
131
|
+
def shutdown
|
132
|
+
if self.respond_to?(:before_stop)
|
133
|
+
before_stop
|
134
|
+
end
|
135
|
+
# change the pid file so the original process sees this 'stop' signal
|
136
|
+
File.open(_pid_filename, 'a') do |f|
|
137
|
+
f.write("\n#{ACTS_AS_SERVICE_SHUTTING_DOWN}")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
# method for outside consumption of status info. hopefully not
|
143
|
+
# tempting method name for class-writers to conflict with...
|
144
|
+
#---------------------------------------------------------------------------
|
145
|
+
def service_running?
|
146
|
+
_status == ACTS_AS_SERVICE_RUNNING || _status == ACTS_AS_SERVICE_OTHER_RUNNING
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
# method for outside consumption of the pid value. hopefully not
|
151
|
+
# tempting method name for class-writers to conflict with...
|
152
|
+
#---------------------------------------------------------------------------
|
153
|
+
def service_pid
|
154
|
+
_pid_file_pid
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
#---------------------------------------------------------------------------
|
159
|
+
# helper methods, no real need to access these from client code
|
160
|
+
#---------------------------------------------------------------------------
|
161
|
+
|
162
|
+
|
163
|
+
# fetches the service's name, using class name as default
|
164
|
+
#---------------------------------------------------------------------------
|
165
|
+
def _display_name
|
166
|
+
@@_display_name ||= (respond_to?(:service_name) ?
|
167
|
+
service_name :
|
168
|
+
name.split("::").last)
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
# returns the pid filename
|
173
|
+
#---------------------------------------------------------------------------
|
174
|
+
def _pid_filename
|
175
|
+
if defined?(@@_pid_filename)
|
176
|
+
return @@_pid_filename
|
177
|
+
end
|
178
|
+
|
179
|
+
if respond_to?(:service_pid_filename)
|
180
|
+
@@_pid_filename = service_pid_filename
|
181
|
+
else
|
182
|
+
@@_pid_filename = File.join(RAILS_ROOT, 'tmp', 'pids',
|
183
|
+
"#{_display_name.underscore.gsub(/\s+/, '_')}.pid")
|
184
|
+
end
|
185
|
+
return @@_pid_filename
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
# the current status of the service. possible values:
|
190
|
+
# ACTS_AS_SERVICE_STOPPED : no service process is running
|
191
|
+
# ACTS_AS_SERVICE_SHUTTING_DOWN : process currently shutting down
|
192
|
+
# ACTS_AS_SERVICE_RUNNING : the current process is the service process
|
193
|
+
# ACTS_AS_SERVICE_OTHER_RUNNING : another pid is running the service
|
194
|
+
# ACTS_AS_SERVICE_PID_NO_PROCESS : pidfile exists, but no process running
|
195
|
+
#-----------------------------------------------------------------------------
|
196
|
+
def _status
|
197
|
+
_status = nil
|
198
|
+
|
199
|
+
# logic:
|
200
|
+
# if the pid file doesn't exist, it's stopped
|
201
|
+
# otherwise, if 'shutting down', it's shutting down
|
202
|
+
# or if the pidfile's pid is running, another process is running
|
203
|
+
# or if the pidfile's pid matches this process, it's running
|
204
|
+
# otherwise, the pidfile's there but no one's running
|
205
|
+
if !_pid_file_exists?
|
206
|
+
_status = ACTS_AS_SERVICE_STOPPED
|
207
|
+
elsif Regexp.new(ACTS_AS_SERVICE_SHUTTING_DOWN) =~ _pid_file_content
|
208
|
+
_status = ACTS_AS_SERVICE_SHUTTING_DOWN
|
209
|
+
elsif _process_running?
|
210
|
+
_status = ACTS_AS_SERVICE_RUNNING
|
211
|
+
elsif _pid_file_process_running?
|
212
|
+
_status = ACTS_AS_SERVICE_OTHER_RUNNING
|
213
|
+
else
|
214
|
+
_status = ACTS_AS_SERVICE_PID_NO_PROCESS
|
215
|
+
end
|
216
|
+
|
217
|
+
return _status
|
218
|
+
end
|
219
|
+
|
220
|
+
|
221
|
+
# indicates if the pid file exists for this service
|
222
|
+
#---------------------------------------------------------------------------
|
223
|
+
def _pid_file_exists?
|
224
|
+
File.exist?(_pid_filename)
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
# returns the entire contents of the pid file
|
229
|
+
#---------------------------------------------------------------------------
|
230
|
+
def _pid_file_content
|
231
|
+
begin
|
232
|
+
f = File.open(_pid_filename, 'r')
|
233
|
+
return f.blank? ? f : f.read
|
234
|
+
rescue Errno::ENOENT
|
235
|
+
return nil
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
# the pid found in the pidfile (if it exists, nil if it doesn't)
|
241
|
+
#---------------------------------------------------------------------------
|
242
|
+
def _pid_file_pid
|
243
|
+
/^(\d*)$/ =~ _pid_file_content
|
244
|
+
return $1.blank? ? nil : $1.to_i
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
# the current process' pid
|
249
|
+
#---------------------------------------------------------------------------
|
250
|
+
def _pid
|
251
|
+
@@_pid ||= Process.pid
|
252
|
+
end
|
253
|
+
|
254
|
+
|
255
|
+
# Checks to see if the process pointed to by the pid file is actually
|
256
|
+
# running Note that I couldn't find a good way to check for the status of a
|
257
|
+
# different process by its pid, so this checks to see if the proces has a
|
258
|
+
# process group id, and if the process doesn't exist, an exception is
|
259
|
+
# returned
|
260
|
+
#---------------------------------------------------------------------------
|
261
|
+
def _pid_file_process_running?
|
262
|
+
begin
|
263
|
+
Process.getpgid(_pid_file_pid)
|
264
|
+
return true
|
265
|
+
rescue
|
266
|
+
return false
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
|
271
|
+
# returns true if the current process is the pid in the pid file. false
|
272
|
+
# otherwise
|
273
|
+
#---------------------------------------------------------------------------
|
274
|
+
def _process_running?
|
275
|
+
return _pid == _pid_file_pid
|
276
|
+
end
|
277
|
+
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# gives us the handy acts_as_service method we can declare inside any class
|
282
|
+
#-------------------------------------------------------------------------------
|
283
|
+
class Object
|
284
|
+
def self.acts_as_service
|
285
|
+
self.send(:include, ActsAsService)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
|
290
|
+
# totally addicted to blank?
|
291
|
+
#-------------------------------------------------------------------------------
|
292
|
+
class File
|
293
|
+
def blank?
|
294
|
+
return self.nil?
|
295
|
+
end
|
296
|
+
end
|
297
|
+
class String
|
298
|
+
def blank?
|
299
|
+
return self.nil? || self == ''
|
300
|
+
end
|
301
|
+
end
|
302
|
+
class NilClass
|
303
|
+
def blank?
|
304
|
+
return self.nil?
|
305
|
+
end
|
306
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_service
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Percival
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-02-18 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: |
|
17
|
+
A gem with a mixin to let you turn a class into something that runs like a service,
|
18
|
+
which you can MyService.start, MyService.stop, and MyService.restart. It tracks
|
19
|
+
its own pid. For now, pretty sure it requires that the class is running inside
|
20
|
+
a rails context (e.g. run by script/runner MyService.start), but that could
|
21
|
+
probably be changed without too much difficulty.
|
22
|
+
|
23
|
+
email: percivalatumamibuddotcom
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- LICENSE
|
30
|
+
- README.rdoc
|
31
|
+
- TODO
|
32
|
+
files:
|
33
|
+
- README.rdoc
|
34
|
+
- acts_as_service.gemspec
|
35
|
+
- lib/acts_as_service.rb
|
36
|
+
- LICENSE
|
37
|
+
- TODO
|
38
|
+
has_rdoc: true
|
39
|
+
homepage: http://github.com/bmpercy/acts_as_service
|
40
|
+
licenses: []
|
41
|
+
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options:
|
44
|
+
- --charset=UTF-8
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
requirements: []
|
60
|
+
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 1.3.5
|
63
|
+
signing_key:
|
64
|
+
specification_version: 3
|
65
|
+
summary: Makes it very easy to create a service-like class that's easy to start and stop, taken from work at discovereads.com
|
66
|
+
test_files: []
|
67
|
+
|