senotrusov-ruby-process-controller 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,205 @@
1
+
2
+ Copyright 2006-2009 Stanislav Senotrusov <senotrusov@gmail.com>
3
+
4
+ Apache License
5
+ Version 2.0, January 2004
6
+ http://www.apache.org/licenses/
7
+
8
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
9
+
10
+ 1. Definitions.
11
+
12
+ "License" shall mean the terms and conditions for use, reproduction,
13
+ and distribution as defined by Sections 1 through 9 of this document.
14
+
15
+ "Licensor" shall mean the copyright owner or entity authorized by
16
+ the copyright owner that is granting the License.
17
+
18
+ "Legal Entity" shall mean the union of the acting entity and all
19
+ other entities that control, are controlled by, or are under common
20
+ control with that entity. For the purposes of this definition,
21
+ "control" means (i) the power, direct or indirect, to cause the
22
+ direction or management of such entity, whether by contract or
23
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
24
+ outstanding shares, or (iii) beneficial ownership of such entity.
25
+
26
+ "You" (or "Your") shall mean an individual or Legal Entity
27
+ exercising permissions granted by this License.
28
+
29
+ "Source" form shall mean the preferred form for making modifications,
30
+ including but not limited to software source code, documentation
31
+ source, and configuration files.
32
+
33
+ "Object" form shall mean any form resulting from mechanical
34
+ transformation or translation of a Source form, including but
35
+ not limited to compiled object code, generated documentation,
36
+ and conversions to other media types.
37
+
38
+ "Work" shall mean the work of authorship, whether in Source or
39
+ Object form, made available under the License, as indicated by a
40
+ copyright notice that is included in or attached to the work
41
+ (an example is provided in the Appendix below).
42
+
43
+ "Derivative Works" shall mean any work, whether in Source or Object
44
+ form, that is based on (or derived from) the Work and for which the
45
+ editorial revisions, annotations, elaborations, or other modifications
46
+ represent, as a whole, an original work of authorship. For the purposes
47
+ of this License, Derivative Works shall not include works that remain
48
+ separable from, or merely link (or bind by name) to the interfaces of,
49
+ the Work and Derivative Works thereof.
50
+
51
+ "Contribution" shall mean any work of authorship, including
52
+ the original version of the Work and any modifications or additions
53
+ to that Work or Derivative Works thereof, that is intentionally
54
+ submitted to Licensor for inclusion in the Work by the copyright owner
55
+ or by an individual or Legal Entity authorized to submit on behalf of
56
+ the copyright owner. For the purposes of this definition, "submitted"
57
+ means any form of electronic, verbal, or written communication sent
58
+ to the Licensor or its representatives, including but not limited to
59
+ communication on electronic mailing lists, source code control systems,
60
+ and issue tracking systems that are managed by, or on behalf of, the
61
+ Licensor for the purpose of discussing and improving the Work, but
62
+ excluding communication that is conspicuously marked or otherwise
63
+ designated in writing by the copyright owner as "Not a Contribution."
64
+
65
+ "Contributor" shall mean Licensor and any individual or Legal Entity
66
+ on behalf of whom a Contribution has been received by Licensor and
67
+ subsequently incorporated within the Work.
68
+
69
+ 2. Grant of Copyright License. Subject to the terms and conditions of
70
+ this License, each Contributor hereby grants to You a perpetual,
71
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
72
+ copyright license to reproduce, prepare Derivative Works of,
73
+ publicly display, publicly perform, sublicense, and distribute the
74
+ Work and such Derivative Works in Source or Object form.
75
+
76
+ 3. Grant of Patent License. Subject to the terms and conditions of
77
+ this License, each Contributor hereby grants to You a perpetual,
78
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
79
+ (except as stated in this section) patent license to make, have made,
80
+ use, offer to sell, sell, import, and otherwise transfer the Work,
81
+ where such license applies only to those patent claims licensable
82
+ by such Contributor that are necessarily infringed by their
83
+ Contribution(s) alone or by combination of their Contribution(s)
84
+ with the Work to which such Contribution(s) was submitted. If You
85
+ institute patent litigation against any entity (including a
86
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
87
+ or a Contribution incorporated within the Work constitutes direct
88
+ or contributory patent infringement, then any patent licenses
89
+ granted to You under this License for that Work shall terminate
90
+ as of the date such litigation is filed.
91
+
92
+ 4. Redistribution. You may reproduce and distribute copies of the
93
+ Work or Derivative Works thereof in any medium, with or without
94
+ modifications, and in Source or Object form, provided that You
95
+ meet the following conditions:
96
+
97
+ (a) You must give any other recipients of the Work or
98
+ Derivative Works a copy of this License; and
99
+
100
+ (b) You must cause any modified files to carry prominent notices
101
+ stating that You changed the files; and
102
+
103
+ (c) You must retain, in the Source form of any Derivative Works
104
+ that You distribute, all copyright, patent, trademark, and
105
+ attribution notices from the Source form of the Work,
106
+ excluding those notices that do not pertain to any part of
107
+ the Derivative Works; and
108
+
109
+ (d) If the Work includes a "NOTICE" text file as part of its
110
+ distribution, then any Derivative Works that You distribute must
111
+ include a readable copy of the attribution notices contained
112
+ within such NOTICE file, excluding those notices that do not
113
+ pertain to any part of the Derivative Works, in at least one
114
+ of the following places: within a NOTICE text file distributed
115
+ as part of the Derivative Works; within the Source form or
116
+ documentation, if provided along with the Derivative Works; or,
117
+ within a display generated by the Derivative Works, if and
118
+ wherever such third-party notices normally appear. The contents
119
+ of the NOTICE file are for informational purposes only and
120
+ do not modify the License. You may add Your own attribution
121
+ notices within Derivative Works that You distribute, alongside
122
+ or as an addendum to the NOTICE text from the Work, provided
123
+ that such additional attribution notices cannot be construed
124
+ as modifying the License.
125
+
126
+ You may add Your own copyright statement to Your modifications and
127
+ may provide additional or different license terms and conditions
128
+ for use, reproduction, or distribution of Your modifications, or
129
+ for any such Derivative Works as a whole, provided Your use,
130
+ reproduction, and distribution of the Work otherwise complies with
131
+ the conditions stated in this License.
132
+
133
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
134
+ any Contribution intentionally submitted for inclusion in the Work
135
+ by You to the Licensor shall be under the terms and conditions of
136
+ this License, without any additional terms or conditions.
137
+ Notwithstanding the above, nothing herein shall supersede or modify
138
+ the terms of any separate license agreement you may have executed
139
+ with Licensor regarding such Contributions.
140
+
141
+ 6. Trademarks. This License does not grant permission to use the trade
142
+ names, trademarks, service marks, or product names of the Licensor,
143
+ except as required for reasonable and customary use in describing the
144
+ origin of the Work and reproducing the content of the NOTICE file.
145
+
146
+ 7. Disclaimer of Warranty. Unless required by applicable law or
147
+ agreed to in writing, Licensor provides the Work (and each
148
+ Contributor provides its Contributions) on an "AS IS" BASIS,
149
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
150
+ implied, including, without limitation, any warranties or conditions
151
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
152
+ PARTICULAR PURPOSE. You are solely responsible for determining the
153
+ appropriateness of using or redistributing the Work and assume any
154
+ risks associated with Your exercise of permissions under this License.
155
+
156
+ 8. Limitation of Liability. In no event and under no legal theory,
157
+ whether in tort (including negligence), contract, or otherwise,
158
+ unless required by applicable law (such as deliberate and grossly
159
+ negligent acts) or agreed to in writing, shall any Contributor be
160
+ liable to You for damages, including any direct, indirect, special,
161
+ incidental, or consequential damages of any character arising as a
162
+ result of this License or out of the use or inability to use the
163
+ Work (including but not limited to damages for loss of goodwill,
164
+ work stoppage, computer failure or malfunction, or any and all
165
+ other commercial damages or losses), even if such Contributor
166
+ has been advised of the possibility of such damages.
167
+
168
+ 9. Accepting Warranty or Additional Liability. While redistributing
169
+ the Work or Derivative Works thereof, You may choose to offer,
170
+ and charge a fee for, acceptance of support, warranty, indemnity,
171
+ or other liability obligations and/or rights consistent with this
172
+ License. However, in accepting such obligations, You may act only
173
+ on Your own behalf and on Your sole responsibility, not on behalf
174
+ of any other Contributor, and only if You agree to indemnify,
175
+ defend, and hold each Contributor harmless for any liability
176
+ incurred by, or claims asserted against, such Contributor by reason
177
+ of your accepting any such warranty or additional liability.
178
+
179
+ END OF TERMS AND CONDITIONS
180
+
181
+ APPENDIX: How to apply the Apache License to your work.
182
+
183
+ To apply the Apache License to your work, attach the following
184
+ boilerplate notice, with the fields enclosed by brackets "[]"
185
+ replaced with your own identifying information. (Don't include
186
+ the brackets!) The text should be enclosed in the appropriate
187
+ comment syntax for the file format. We also recommend that a
188
+ file or class name and description of purpose be included on the
189
+ same "printed page" as the copyright notice for easier
190
+ identification within third-party archives.
191
+
192
+ Copyright [yyyy] [name of copyright owner]
193
+
194
+ Licensed under the Apache License, Version 2.0 (the "License");
195
+ you may not use this file except in compliance with the License.
196
+ You may obtain a copy of the License at
197
+
198
+ http://www.apache.org/licenses/LICENSE-2.0
199
+
200
+ Unless required by applicable law or agreed to in writing, software
201
+ distributed under the License is distributed on an "AS IS" BASIS,
202
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
203
+ See the License for the specific language governing permissions and
204
+ limitations under the License.
205
+
data/README ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,22 @@
1
+
2
+ # Copyright 2006-2009 Stanislav Senotrusov <senotrusov@gmail.com>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require 'etc'
17
+ require 'ruby-toolkit'
18
+ require 'ruby-threading-toolkit'
19
+
20
+ require 'ruby-process-controller/argv_parser.rb'
21
+ require 'ruby-process-controller/process_controller.rb'
22
+ require 'ruby-process-controller/test_daemon.rb'
@@ -0,0 +1,486 @@
1
+
2
+ # Copyright 2006-2008 Stanislav Senotrusov <senotrusov@gmail.com>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ # TODO: separator between options
18
+
19
+ class ArgvParser
20
+
21
+ class UnnamedOption
22
+ def initialize option_name, value_is_optional, value_may_be_array, comment, conditions, block
23
+ @option_name = option_name
24
+ @value_is_optional = value_is_optional
25
+ @value_may_be_array = value_may_be_array
26
+ @comment = comment
27
+ @conditions = conditions
28
+ @block = block
29
+ end
30
+
31
+ attr_accessor :option_name, :value_is_optional, :value_may_be_array, :comment, :conditions, :block
32
+ attr_accessor :short_option_name, :option_is_boolean, :value_name
33
+
34
+ def display
35
+ "#{"[" if value_is_optional}#{option_name}#{"]" if value_is_optional}#{"..." if value_may_be_array}"
36
+ end
37
+
38
+ def columnized_display
39
+ [display, comment]
40
+ end
41
+ end
42
+
43
+
44
+ class Option < UnnamedOption
45
+ def initialize short_option_name, option_name, option_is_boolean, value_is_optional, value_may_be_array, value_name, comment, conditions, block
46
+ super option_name, value_is_optional, value_may_be_array, comment, conditions, block
47
+
48
+ @short_option_name = short_option_name
49
+ @option_is_boolean = option_is_boolean
50
+ @value_name = value_name
51
+ end
52
+
53
+ def display_names
54
+ [display_short_name, display_name].compact * "/"
55
+ end
56
+
57
+ def display_name
58
+ "--#{"[no-]" if option_is_boolean}#{option_name}" if option_name
59
+ end
60
+
61
+ def display_short_name
62
+ "-#{short_option_name}" if short_option_name
63
+ end
64
+
65
+ def display_value
66
+ "#{"[" if value_is_optional}#{value_name}#{"]" if value_is_optional}#{"..." if value_may_be_array}"
67
+ end
68
+
69
+ def columnized_display
70
+ [display_short_name, display_name, display_value, comment]
71
+ end
72
+ end
73
+
74
+
75
+ class IteratableArray < Array
76
+ def initialize *args, &block
77
+ @position = 0
78
+ super(*args, &block)
79
+ end
80
+
81
+ attr_accessor :position
82
+
83
+ def each_with_iterator
84
+ while @position < length
85
+ result = yield(current)
86
+
87
+ if result.kind_of?(Array)
88
+ if result.first == :and_crop
89
+ slice!(@position, result.last)
90
+ elsif result.first == :after_slice_until!
91
+
92
+ else
93
+ step
94
+ end
95
+ else
96
+ step
97
+ end
98
+ end
99
+ end
100
+
101
+ def slice_until!
102
+ slice_from = @position
103
+ step
104
+
105
+ while @position < length
106
+ break if yield(current)
107
+ step
108
+ end
109
+
110
+ result = slice!(slice_from, @position - slice_from)
111
+
112
+ @position = slice_from
113
+
114
+ return result
115
+ end
116
+
117
+ def current
118
+ self[@position]
119
+ end
120
+
121
+ def look_ahead(offset = 1)
122
+ self[@position + offset]
123
+ end
124
+
125
+ def remains
126
+ slice(@position..-1)
127
+ end
128
+
129
+ def step
130
+ @position += 1
131
+ end
132
+
133
+ def end?
134
+ @position >= length
135
+ end
136
+ end
137
+
138
+
139
+ def initialize argv
140
+ # TODO ! Normalisation of "=" and ",". Remove "=", join comma separated arrays to arrays.
141
+
142
+ @argv = IteratableArray.new(argv)
143
+ @values = {}
144
+
145
+ @header = []
146
+ @footer = []
147
+
148
+ @errors = []
149
+
150
+ @heading_opts = IteratableArray.new
151
+ @tailing_opts = IteratableArray.new
152
+ @floating_opts = IteratableArray.new
153
+
154
+ @opts = {}
155
+ @opts_list = []
156
+
157
+ @tailing_start_position = 0
158
+
159
+ yield(self) if block_given?
160
+ end
161
+
162
+ def [] option
163
+ if @values.has_key?(option)
164
+ @values[option]
165
+
166
+ elsif @values.has_key?(key = option.to_s)
167
+ @values[key]
168
+
169
+ elsif @values.has_key?(key = option.to_s.gsub(/_/, '-'))
170
+ @values[key]
171
+
172
+ elsif @values.has_key?(key = option.to_s.gsub(/_/, '-').upcase)
173
+ @values[key]
174
+
175
+ else
176
+ nil
177
+ end
178
+ end
179
+
180
+ def []= option, value
181
+ @values[option] = value
182
+ end
183
+
184
+ def to_hash
185
+ @values
186
+ end
187
+
188
+ def merge hash
189
+ to_hash.merge hash
190
+ end
191
+
192
+ def inspect
193
+ @values.inspect
194
+ end
195
+
196
+ attr_reader :header, :footer, :errors
197
+
198
+ def errors?
199
+ !@errors.empty?
200
+ end
201
+
202
+ def complete?
203
+ (errors? ||
204
+ !@argv.empty? ||
205
+ unnamed_opts_incomplete?(@heading_opts) ||
206
+ unnamed_opts_incomplete?(@tailing_opts) ||
207
+ unnamed_opts_incomplete?(@floating_opts)) ? false : true
208
+ end
209
+
210
+ private
211
+
212
+ def unnamed_opts_incomplete? collection
213
+ !collection.remains.select{|item| !item.value_is_optional && !@values[item.option_name]}.empty?
214
+ end
215
+
216
+ def display_incomplete_required_unnamed_opts collection
217
+ collection.remains.select{|item| !item.value_is_optional}.collect{|item|item.display}
218
+ end
219
+
220
+ public
221
+
222
+ def show_options_and_errors_on_incomplete
223
+ unless complete?
224
+ show_options_and_errors
225
+ true
226
+ else
227
+ false
228
+ end
229
+ end
230
+
231
+ def show_options_and_errors
232
+ show_options
233
+ show_errors
234
+ end
235
+
236
+ def show_errors_and_options
237
+ show_errors
238
+ show_options
239
+ end
240
+
241
+ def show_errors output = STDERR
242
+
243
+ # TODO: Check MacOS compatibility
244
+
245
+ if ENV["LANG"].include?("UTF-8")
246
+ header_line = "⬇⬇⬇ "
247
+ footer_line = "⬆⬆⬆ "
248
+ else
249
+ header_line = "vvv "
250
+ footer_line = "^^^ "
251
+ end
252
+
253
+ output << (header_line * 20 + "\n\n")
254
+ output << (@errors * "\n" + "\n") unless @errors.empty?
255
+ output << ("Can't parse options: " + @argv * " " + "\n") unless @argv.empty?
256
+ output << ("Incomplete heading options: " + display_incomplete_required_unnamed_opts(@heading_opts) * " " + "\n") if unnamed_opts_incomplete?(@heading_opts)
257
+ output << ("Incomplete tailing options: " + display_incomplete_required_unnamed_opts(@tailing_opts) * " " + "\n") if unnamed_opts_incomplete?(@tailing_opts)
258
+ output << ("Incomplete floating options: " + display_incomplete_required_unnamed_opts(@floating_opts) * " " + "\n") if unnamed_opts_incomplete?(@floating_opts)
259
+ output << ("\n" + (footer_line * 20) + "\n\n")
260
+
261
+ output.flush if output.respond_to?(:flush)
262
+ end
263
+
264
+ def show_options output = STDOUT
265
+ output << (@header * "\n" + "\n\n") unless @header.empty?
266
+
267
+ overview = @heading_opts.collect{|option| option.display}
268
+ overview += [("[Options]..." unless @opts_list.empty?)]
269
+
270
+ unless @floating_opts.empty?
271
+ overview += @floating_opts.collect{|option| option.display}
272
+ overview += [("[Options]..." unless @opts_list.empty?)]
273
+ end
274
+
275
+ overview += @tailing_opts.collect{|option| option.display}
276
+
277
+ output << "Usage:\n " + ("#{$0} " + (overview * " ") + "\n\n")
278
+
279
+ options = @opts_list.collect {|option| option.columnized_display }
280
+ unnamed_opts = (@heading_opts + @tailing_opts + @floating_opts).collect{|option| option.columnized_display}
281
+
282
+ first_column = options.collect{|option| "#{option[0]},".length}.max
283
+ second_column = (unnamed_opts.collect{|option| "#{option[0]}".length} + options.collect{|option| option[1] ? "#{option[1]} #{option[2]}".length : ("#{option[2]}".length - 1)}).max
284
+
285
+ lines = unnamed_opts.collect do |option|
286
+ sprintf(" %#{first_column}s %#{0 - second_column}s %s", nil, option[0], option[1])
287
+ end
288
+
289
+ lines.push ""
290
+ lines.push "Options:"
291
+
292
+ lines += options.collect do |option|
293
+ if option[1]
294
+ sprintf(" %#{first_column}s %#{0 - second_column}s %s", ("#{option[0]}," if option[0]), "#{option[1]} #{option[2]}", option[3])
295
+ else
296
+ sprintf(" %#{first_column - 1}s %#{-1-second_column}s %s", option[0], option[2], option[3])
297
+ end
298
+ end
299
+
300
+ output << (lines * "\n" + "\n\n")
301
+
302
+ output << (@footer * "\n" + "\n\n")
303
+
304
+ output.flush if output.respond_to?(:flush)
305
+ end
306
+
307
+ private
308
+
309
+ VALUE_NAME = /[\w\d\-\/\\\:]+/
310
+
311
+ def parse_option(option, comment, conditions, &block)
312
+ unless (matchdata = option.match(/^\s*(-([\w\d\?]+)\s*((\[?)(#{VALUE_NAME})(\]?)(\.{0,3}))?\s*(,*)\s*)?(--(\[no-\])?([\w\d\-\?]+)\s*((\[?)(#{VALUE_NAME})(\]?)(\.{0,3}))?\s*(,*)\s*)?$/))
313
+ raise "Can not recognize option declaration '#{option}'"
314
+ end
315
+
316
+ return Option.new(
317
+ matchdata[2], # short_option_name
318
+ matchdata[11], # option_name
319
+ matchdata[10] == "[no-]" || (!matchdata[14] && !matchdata[5]) ? true : false, # option_is_boolean
320
+ matchdata[13] == "[" || matchdata[4] == "[" ? true : false, # value_is_optional
321
+ matchdata[16] == "..." || matchdata[7] == "..." ? true : false, # value_may_be_array
322
+ matchdata[14] || matchdata[5], # value_name
323
+ comment,
324
+ conditions,
325
+ block
326
+ )
327
+ end
328
+
329
+ def parse_unnamed_option(option, comment, conditions, &block)
330
+ unless (matchdata = option.match(/^\s*(\[?)(#{VALUE_NAME})(\]?)(\.{0,3}?)\s*$/))
331
+ raise "Can not recognize option declaration '#{option}'"
332
+ end
333
+
334
+ return UnnamedOption.new(
335
+ matchdata[2], # option_name
336
+ matchdata[1] == "[" ? true : false, # value_is_optional
337
+ matchdata[4] == "..." ? true : false, # value_may_be_array
338
+ comment,
339
+ conditions,
340
+ block
341
+ )
342
+ end
343
+
344
+ def push_unnamed_option(collection, option)
345
+ raise("There are no text left for #{option.option_name} after a greedy array '#{collection.last.option_name}'") if collection.last && collection.last.value_may_be_array
346
+ raise("Strictly required text #{option.option_name} can't be after a optional text '#{collection.last.option_name}'") if !option.value_is_optional && collection.last && collection.last.value_is_optional
347
+
348
+ collection.push option
349
+ end
350
+
351
+ public
352
+
353
+ # "--option-foo [VALUE]"
354
+ # "--option [VALUE]..." (zero or more comma separated values)
355
+ # "--option VALUE"
356
+ # "--option VALUE..." (one or more comma separated values)
357
+ # "--option"
358
+ # "--[no-]option"
359
+ #
360
+ # "-ooo [VALUE]"
361
+ # "-o [VALUE]..." (zero or more comma separated values)
362
+ # "-o VALUE"
363
+ # "-o VALUE..." (one or more comma separated values)
364
+ # "-o"
365
+ #
366
+ # "-o, --option [VALUE]"
367
+ # "-o, --option [VALUE]..." (zero or more comma separated values)
368
+ # "-o, --option VALUE"
369
+ # "-o, --option VALUE..." (one or more comma separated values)
370
+ # "-o, --option"
371
+ # "-o, --[no-]option"
372
+ def option(option, comment = nil, conditions = nil, &block)
373
+ option = parse_option(option, comment, conditions, &block)
374
+
375
+ @opts_list.push option
376
+ @opts[option.short_option_name] = option if option.short_option_name
377
+ @opts[option.option_name] = option if option.option_name
378
+ end
379
+
380
+ # "[VALUE]"
381
+ # "VALUE"
382
+ # "[VALUE]..." (zero or more space separated values)
383
+ # "VALUE..." (one or more space separated values)
384
+ def heading_option(option, comment = nil, conditions = nil, &block)
385
+ push_unnamed_option(@heading_opts, parse_unnamed_option(option, comment, conditions, &block))
386
+ end
387
+
388
+ # "[VALUE]"
389
+ # "VALUE"
390
+ # "[VALUE]..." (zero or more space separated values)
391
+ # "VALUE..." (one or more space separated values)
392
+ def tailing_option(option, comment = nil, conditions = nil, &block)
393
+ push_unnamed_option(@tailing_opts, parse_unnamed_option(option, comment, conditions, &block))
394
+ end
395
+
396
+ # "[VALUE]"
397
+ # "VALUE"
398
+ # "[VALUE]..." (zero or more space separated values)
399
+ # "VALUE..." (one or more space separated values)
400
+ def floating_option(option, comment = nil, conditions = nil, &block)
401
+ push_unnamed_option(@floating_opts, parse_unnamed_option(option, comment, conditions, &block))
402
+ end
403
+
404
+
405
+ private
406
+
407
+ def check_and_assign_value option, value
408
+ value = value.split(",") if option.value_may_be_array
409
+
410
+ if option.option_is_boolean
411
+ option.block.call if value && option.block
412
+ else
413
+ begin
414
+ value = option.block.call(value) if option.block
415
+ rescue ArgumentError => exception
416
+ @errors << "#{option.display_names}: #{exception.message}"
417
+
418
+ rescue Exception => exception
419
+ @errors << "#{option.display_names}: #{exception.inspect_with_backtrace}"
420
+
421
+ raise exception unless exception.kind_of?(StandardError) || exception.kind_of?(ScriptError)
422
+ end
423
+ end
424
+
425
+ # TODO: conditions
426
+
427
+ @values[option.option_name || option.short_option_name] = value
428
+ end
429
+
430
+ def parse_unnamed_opts collection
431
+ @argv.each_with_iterator do |item|
432
+ break if collection.end? || item =~ /^-/
433
+
434
+ if collection.current.value_may_be_array
435
+ check_and_assign_value(collection.current, @argv.slice_until!{|item| item =~ /^-/})
436
+ collection.step
437
+ break
438
+ else
439
+ check_and_assign_value(collection.current, item)
440
+ collection.step
441
+ next :and_crop, 1
442
+ end
443
+ end
444
+ end
445
+
446
+ public
447
+
448
+ # TODO: "tar -czf file.tgz dir", "tar czf file.tgz dir"
449
+
450
+ def parse!
451
+ @argv.position = 0
452
+
453
+ parse_unnamed_opts @heading_opts
454
+
455
+ @argv.each_with_iterator do |item|
456
+ next unless (matchdata = item.match(/^--?(no-)?([\w\d\-\?]+)$/))
457
+ next unless (option = @opts[matchdata[2]])
458
+
459
+ @tailing_start_position = @argv.position
460
+
461
+ if option.option_is_boolean
462
+ check_and_assign_value(option, (matchdata[1].nil? ? true : false))
463
+ next :and_crop, 1
464
+
465
+ elsif @argv.look_ahead =~ /^-/ || !@argv.look_ahead
466
+ if option.value_is_optional
467
+ check_and_assign_value(option, true)
468
+ else
469
+ errors << "#{option.display_names} must have a value"
470
+ end
471
+ next :and_crop, 1
472
+ else
473
+ check_and_assign_value(option, @argv.look_ahead)
474
+ next :and_crop, 2
475
+ end
476
+ end
477
+
478
+ unless @argv.detect{|item| item =~ /^-/}
479
+ @argv.position = @tailing_start_position
480
+ parse_unnamed_opts @tailing_opts
481
+
482
+ @argv.position = 0
483
+ parse_unnamed_opts @floating_opts
484
+ end
485
+ end
486
+ end
@@ -0,0 +1,484 @@
1
+
2
+ # Copyright 2006-2009 Stanislav Senotrusov <senotrusov@gmail.com>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+
18
+ # CALL SEQUENCE AND CONCURRENCY
19
+ # -----------------------------
20
+ #
21
+ # initialize
22
+ # start
23
+ # join
24
+ # stop
25
+ # before_exit
26
+ #
27
+ # initialize выполняется в main без каких-либо тредов
28
+ # start, stop, before_exit выполняются строго по порядку, неконкурентно
29
+ # join вызывается после start, при этом stop может быть как позже, так и раньше join
30
+
31
+ # EXCEPTION HANDLING
32
+ # ------------------
33
+ #
34
+ # on exception from initialize - пишется в лог и на экран, процесс завершится
35
+ # on exception from start - стартуется termination watchdog (15 секунд по умолчанию), вызывается join и stop в непредсказуемом порядке, после join - before_exit
36
+ # on exception from stop - процесс аварийно останавливается, before_exit не выполняется
37
+ # on exception from join - пишется в лог, переходит к before_exit
38
+ # on exception from before_exit - пишется в лог, процесс завершается
39
+
40
+ # SIGNAL HANDLING AND BEHAVIOR WITH UNIMPLEMENTED METHODS
41
+ # -------------------------------------------------------
42
+ #
43
+ # join, stop and before exit MAY BE implemented or not.
44
+ #
45
+ # В момент выполнения initialize, сигналы ещё не перехватываются и ruby вызывает SignalException в произвольном месте initialize.
46
+ # Если сигнал на остановку приходит после initialize, но до start, то start-join-stop-before_exit не выполняются вовсе.
47
+ #
48
+ # If join() and stop() IS implemented, join() will be after start() and stop() will be called in a separate thread ONLY ONCE AFTER FIRST SIGNAL
49
+ #
50
+ # If stop() is not implemented, SignalException raised (by ruby) for main thread running start() ON EACH received INT, TERM, ABRT or HUP
51
+ #
52
+ # If join() is not implemented, stop will be called in main thread, как бы его приостанавливая ON EACH received INT, TERM, ABRT or HUP
53
+ # Note, in that case, when stop() call is still working and the next signal is arrived, first stop() processing stops and seconds starts.
54
+ #
55
+ # Если определён только join, но не stop, то join вызывается после завершения start.
56
+
57
+
58
+ class ProcessController
59
+ COMMANDS = %w(start stop run)
60
+
61
+ FAILURE_EXIT_CODES = {
62
+ :process_controller_init => 1,
63
+ :termination_watchdog => 2,
64
+ :stop_thread => 3,
65
+ :dispatch_termination_signal => 4,
66
+ :stop_has_no_effect => 5
67
+ }
68
+
69
+ DEFAULT_NAME = "daemon"
70
+ LOGGER_CLASS = defined?(ActiveSupport::BufferedLogger) && ActiveSupport::BufferedLogger ||
71
+ defined?(Merb::Logger) && Merb::Logger ||
72
+ defined?(Logger) && Logger
73
+
74
+ DEFAULT_PID_DIR = "tmp/pids"
75
+ DEFAULT_LOG_DIR = "log"
76
+
77
+ DEFAULT_TERM_TIMEOUT = 15
78
+ MINIMUM_TERM_TIMEOUT = 1
79
+
80
+ attr_reader :logger, :env, :name, :argv
81
+
82
+
83
+ # ARGV OPTIONS
84
+ # TODO: extract class
85
+
86
+ def initialize_argv_options(argv)
87
+ @argv ||= ArgvParser.new(ARGV)
88
+ define_argv_options
89
+ @argv.parse!
90
+ apply_argv_options
91
+ end
92
+
93
+ def define_argv_options
94
+ @argv.heading_option "[COMMAND]", "ProcessController command (#{COMMANDS * "/"})"
95
+
96
+ @argv.option "-e, --environment NAME", "Run in environment (development/production/testing)"
97
+ @argv.option "-n, --name NAME", "Daemon's name"
98
+
99
+ @argv.option "--user USER", "Run as user"
100
+ @argv.option "--group GROUP", "Run as group"
101
+ @argv.option "--working-dir DIRECTORY", "Working directory, defaults to ."
102
+
103
+ @argv.option "--pid-dir DIRECTORY", "PID directory, relative to working-dir, defaults to '#{DEFAULT_PID_DIR}', fallbacks to '.', may be absolute path"
104
+ @argv.option "--pid-file FILE", "PID file, defaults to [name].pid, may be absolute path"
105
+
106
+ @argv.option "--log-level LEVEL", "Log level (#{LOGGER_CLASS::Severity.constants * "/"})"
107
+ @argv.option "--log-dir DIRECTORY", "Log directory, relative to working-dir, default to '#{DEFAULT_LOG_DIR}', fallbacks to '.', may be absolute path"
108
+ @argv.option "--log-file FILE", "Logfile, default to [name].log, may be absolute path"
109
+
110
+ @argv.option "--term-timeout SECONDS", "Termination timeout, default to 30 seconds"
111
+ @argv.option "-h, --help", "Show this help message"
112
+ end
113
+
114
+ def apply_argv_options
115
+ @user = @argv["user"]
116
+ @group = @argv["group"]
117
+
118
+ @working_dir = @argv["working-dir"]
119
+
120
+ @command = @argv["COMMAND"] || "run"
121
+
122
+ @env = @argv["environment"] || (@command == "start" ? "production" : "development")
123
+
124
+ @name = @argv["name"] || DEFAULT_NAME
125
+
126
+ @pid_dir = @argv['pid-dir'] || DEFAULT_PID_DIR
127
+ @pid_dir = '.' unless File.directory? @pid_dir
128
+
129
+ @pid_file = if @argv['pid-file']
130
+ (@argv['pid-file'] =~ /^\//) ? @argv['pid-file'] : "#{@pid_dir}/#{@argv['pid-file']}"
131
+ else
132
+ "#{@pid_dir}/#{@name}.pid"
133
+ end
134
+
135
+ @log_level = LOGGER_CLASS::Severity.const_get((@argv["log-level"] || (@env == "production" ? "warn" : "debug")).upcase.to_sym)
136
+
137
+ @log_dir = @argv["log-dir"] || DEFAULT_LOG_DIR
138
+ @log_dir = '.' unless File.directory?(@log_dir)
139
+
140
+ @log_file = if (@command == "run" || @command == "stop")
141
+ STDOUT
142
+ elsif @argv["log-file"]
143
+ (@argv["log-file"] =~ /^\//) ? @argv["log-file"] : "#{@log_dir}/#{@argv["log-file"]}"
144
+ else
145
+ "#{@log_dir}/#{@name}.log"
146
+ end
147
+
148
+ @term_timeout = (@argv['term-timeout'] || DEFAULT_TERM_TIMEOUT).to_i
149
+ @term_timeout = MINIMUM_TERM_TIMEOUT if @term_timeout < MINIMUM_TERM_TIMEOUT
150
+ end
151
+
152
+
153
+ # INITIALIZE
154
+
155
+ def initialize(argv = nil)
156
+ initialize_argv_options(argv)
157
+
158
+ change_process_privileges
159
+
160
+ Dir.chdir(@working_dir) if @working_dir
161
+
162
+ if @command != "stop"
163
+ @logger = LOGGER_CLASS.new(@log_file, @log_level)
164
+ @logger.auto_flushing = true
165
+
166
+ @stop_mutex = Mutex.new
167
+ @stop_was_called = false
168
+
169
+ @stop_thread_mutex = Mutex.new
170
+ @stop_thread_was_runned = false
171
+
172
+ @daemon_mutex = Mutex.new
173
+ @must_terminate = false
174
+ @daemon_started_to_some_extent = false
175
+ @detached = false
176
+
177
+ if (running_pid = getpid)
178
+ if is_running? running_pid
179
+ raise "#{@name}: already running with pid #{running_pid}"
180
+ else
181
+ log(:warn, "#{self.class}#initialize @name:`#{@name}' -- Found pidfile:`#{@pid_file}' with pid:`#{running_pid}' and is not running, it may be result of an unclean shutdown.")
182
+ end
183
+ end
184
+
185
+ @logger.info "#{self.class}#initialize @name:`#{@name}' -- Creating process..."
186
+
187
+ @daemon = yield(self)
188
+
189
+ @logger.info "#{self.class}#initialize @name:`#{@name}' -- Process initialized."
190
+ end
191
+
192
+ if @argv["help"]
193
+ @argv.show_options
194
+
195
+ elsif @argv.complete?
196
+ __send__("execute_#{@command}")
197
+
198
+ else
199
+ @argv.show_errors(@logger) if @logger
200
+ @argv.show_options_and_errors
201
+ end
202
+
203
+ @logger.flush if @logger && @logger.respond_to?(:flush)
204
+
205
+ rescue Exception => exception
206
+ begin
207
+ log(:fatal, "#{self.class}#initialize @name:`#{@name}' -- #{exception.inspect_with_backtrace}") if @log_file != STDOUT
208
+
209
+ unless @detached
210
+ @argv.errors << exception.inspect_with_backtrace
211
+ @argv.show_errors
212
+ end
213
+ rescue Exception => another_exception
214
+ STDERR.puts "\n#{exception.inspect_with_backtrace}"
215
+ STDERR.puts "\nWhile handling previous exception another error was occured:\n#{another_exception.inspect_with_backtrace}"
216
+ ensure
217
+ Process.exit!(FAILURE_EXIT_CODES[:process_controller_init])
218
+ end
219
+ end
220
+
221
+ def stop
222
+ @stop_mutex.synchronize do
223
+ unless @stop_was_called
224
+ @stop_was_called = true
225
+ dispatch_termination_signal
226
+ end
227
+ end
228
+ end
229
+
230
+ private
231
+
232
+
233
+ # START/RUN/STOP COMMANDS
234
+
235
+ def execute_start
236
+ detach
237
+ execute_run
238
+ end
239
+
240
+ def execute_run
241
+ @logger.info "#{self.class}#execute_run @name:`#{@name}' -- Starting..."
242
+
243
+ create_pid
244
+
245
+ @termination_watchdog = termination_watchdog
246
+ @stop_thread = stop_thread if @daemon.respond_to?(:stop) && @daemon.respond_to?(:join)
247
+
248
+ trap_signals if @daemon.respond_to?(:stop)
249
+
250
+ daemon_start_sequence
251
+
252
+ ensure
253
+ untrap_signals
254
+ delete_pid
255
+ @logger.info "#{self.class}#execute_run @name:`#{@name}' -- Stopped"
256
+ end
257
+
258
+ def daemon_start_sequence
259
+ start_daemon
260
+ join_daemon
261
+
262
+ ensure
263
+ before_exit_daemon_handler
264
+ end
265
+
266
+
267
+ def start_daemon
268
+ @daemon_mutex.synchronize do
269
+ return if @must_terminate
270
+
271
+ @daemon_started_to_some_extent = true
272
+
273
+ @daemon.start
274
+ end
275
+ rescue Exception => exception
276
+ log(:fatal, "#{self.class}#start_daemon @name:`#{@name}' -- #{exception.inspect_with_backtrace}")
277
+ stop_daemon
278
+ end
279
+
280
+
281
+ def join_daemon
282
+ @daemon.join if @daemon.respond_to?(:join) && @daemon_started_to_some_extent
283
+ rescue Exception => exception
284
+ log(:fatal, "#{self.class}#join_daemon @name:`#{@name}' -- #{exception.inspect_with_backtrace}")
285
+ end
286
+
287
+
288
+ def before_exit_daemon_handler
289
+ @daemon_mutex.synchronize do
290
+ @daemon.before_exit if @daemon.respond_to?(:before_exit) && @daemon_started_to_some_extent
291
+ end
292
+ rescue Exception => exception
293
+ log(:fatal, "#{self.class}#before_exit_daemon_handler @name:`#{@name}' -- #{exception.inspect_with_backtrace}")
294
+ end
295
+
296
+
297
+ def execute_stop
298
+ if (running_pid = getpid)
299
+ if is_running? running_pid
300
+ Process.kill("TERM", running_pid)
301
+
302
+ start_waiting = Time.now
303
+ user_is_given_to_know_whats_happening = false
304
+
305
+ while is_running? running_pid
306
+ sleep 0.1
307
+ now = Time.now
308
+
309
+ if !user_is_given_to_know_whats_happening && (now - start_waiting > @term_timeout * 0.2)
310
+ STDOUT.puts "#{self.class}#execute_stop @name:`#{@name}' -- Waiting for @term_timeout:`#{@term_timeout}' seconds..."
311
+ user_is_given_to_know_whats_happening = true
312
+ start_waiting = Time.now
313
+
314
+ elsif now - start_waiting > @term_timeout
315
+ STDOUT.puts "#{self.class}#execute_stop @name:`#{@name}' -- ERROR. Process DOES NOT stopped in @term_timeout:`#{@term_timeout}' seconds"
316
+ Process.exit!(FAILURE_EXIT_CODES[:stop_has_no_effect])
317
+
318
+ end
319
+ end
320
+ STDOUT.puts "#{self.class}#execute_stop @name:`#{@name}' -- Stopped"
321
+ else
322
+ raise "@name:`#{@name}' -- Process with pid:`#{running_pid}' is not running."
323
+ end
324
+ else
325
+ raise "@name:`#{@name}' -- Pidfile:`#{@pid_file}' not found"
326
+ end
327
+ end
328
+
329
+
330
+ # PROCESS PRIVILEGES AND TERMINAL DETACH
331
+
332
+ # TODO: REVIEW THIS
333
+
334
+ def change_process_privileges
335
+ uid = @user && Etc.getpwnam(@user).uid || Process.euid
336
+ gid = @group && Etc.getgrnam(@group).gid || Process.egid
337
+
338
+ # http://www.ruby-forum.com/topic/110492
339
+ Process.initgroups(@user, gid) if @user
340
+
341
+ Process::GID.change_privilege(uid)
342
+ Process::UID.change_privilege(gid)
343
+ end
344
+
345
+ # based on Reimer Behrends notes http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/87467
346
+ def detach
347
+ Process.exit!(0) if fork # Parent exits, child continues.
348
+ Process.setsid # Become session leader.
349
+ Process.exit!(0) if fork # Zap session leader. See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
350
+
351
+ File.umask 022 # Ensure sensible umask. Adjust as needed.
352
+
353
+ STDIN.reopen "/dev/null" # Free file descriptors and
354
+
355
+
356
+ # STDOUT.reopen(@log_file.gsub(/\.(log)$/, '.output'), "a") # point them somewhere sensible.
357
+ # STDERR.reopen STDOUT # STDOUT/ERR should better go to a logfile.
358
+
359
+ STDOUT.reopen(@logger.instance_variable_get("@log")) # point them somewhere sensible.
360
+ STDERR.reopen STDOUT # STDOUT/ERR should better go to a logfile.
361
+
362
+ @detached = true
363
+ end
364
+
365
+
366
+ # SIGNAL HANDLING
367
+
368
+
369
+ # This "Thread.stop" complexity is needed, кажется, for some cases, where ruby can not spawn new threads.
370
+ # И я совершенно забыл, при каких обстоятельствах я наблюдал такую картину.
371
+ # Кажется это какие-то случаи c IO для сокетов. Надо поглядеть в коде rubymq
372
+ # Ещё иногда удобно нажимать ctrl+c два раза и не ждать нормального завершения
373
+
374
+ def termination_watchdog
375
+ Thread.new_with_exception_handling(lambda { |exception| Process.exit!(FAILURE_EXIT_CODES[:termination_watchdog]) unless exception.kind_of?(ThreadTerminatedError) }, @logger, :fatal, "#{self.class}#termination_watchdog") do
376
+ Thread.stop
377
+ Thread.main.join_and_terminate(@term_timeout)
378
+ end
379
+ end
380
+
381
+ def stop_thread
382
+ Thread.new_with_exception_handling(lambda { Process.exit!(FAILURE_EXIT_CODES[:stop_thread]) }, @logger, :fatal, "#{self.class}#stop_thread") do
383
+ Thread.stop
384
+ @logger.debug("#{self.class}#stop_thread @name:`#{@name}' -- Stopping...")
385
+ @daemon_mutex.synchronize do
386
+ @must_terminate = true
387
+ @daemon.stop if @daemon_started_to_some_extent
388
+ end
389
+ end
390
+ end
391
+
392
+ def dispatch_termination_signal
393
+ stop_daemon
394
+ rescue Exception => exception
395
+ begin
396
+ log(:fatal, "#{self.class}#dispatch_termination_signal @name:`#{@name}' -- Error while stopping: #{exception.inspect_with_backtrace}")
397
+ ensure
398
+ Process.exit!(FAILURE_EXIT_CODES[:dispatch_termination_signal])
399
+ end
400
+ end
401
+
402
+ def stop_daemon
403
+ log(:info, "\n#{self.class}#stop_daemon @name:`#{@name}' -- Stopping...")
404
+
405
+ if @termination_watchdog
406
+ begin
407
+ @termination_watchdog.run
408
+ rescue ThreadError
409
+ end
410
+ end
411
+
412
+ if @stop_thread
413
+ begin
414
+ @stop_thread_mutex.synchronize do
415
+ unless @stop_thread_was_runned
416
+ @stop_thread_was_runned = true
417
+ @stop_thread.run
418
+ end
419
+ end
420
+ rescue ThreadError
421
+ end
422
+ else
423
+ @daemon.stop
424
+ end
425
+ end
426
+
427
+
428
+ # SIGNAL TRAPPING
429
+
430
+ def trap_signals
431
+ Signal.trap('INT') {dispatch_termination_signal} # Ctrl+C
432
+ Signal.trap('TERM') {dispatch_termination_signal} # kill
433
+ Signal.trap('ABRT') {dispatch_termination_signal} # Ctrl-\
434
+ Signal.trap('HUP') {dispatch_termination_signal} # terminal line hand-up
435
+ end
436
+
437
+ def untrap_signals
438
+ Signal.trap 'INT', 'DEFAULT'
439
+ Signal.trap 'TERM', 'DEFAULT'
440
+ Signal.trap 'ABRT', 'DEFAULT'
441
+ Signal.trap 'HUP', 'DEFAULT'
442
+ end
443
+
444
+
445
+ # PID FILE
446
+
447
+ def create_pid
448
+ File.write @pid_file, Process.pid
449
+ end
450
+
451
+ def delete_pid
452
+ File.delete @pid_file
453
+ end
454
+
455
+ def getpid
456
+ if File.exists?(@pid_file)
457
+ File.read(@pid_file).to_i
458
+ else
459
+ false
460
+ end
461
+ end
462
+
463
+
464
+ # DETECTION OF ALREADY RUNNING DAEMON
465
+
466
+ def is_running? pid
467
+ begin
468
+ Process.getpgid(pid) && true
469
+ rescue Errno::ESRCH
470
+ false
471
+ end
472
+ end
473
+
474
+
475
+ # LOGGING
476
+
477
+ def log severity, message
478
+ if defined?(@logger)
479
+ @logger.__send__(severity, message)
480
+ @logger.flush if @logger.respond_to?(:flush)
481
+ end
482
+ end
483
+ end
484
+
@@ -0,0 +1,38 @@
1
+
2
+ # Copyright 2006-2009 Stanislav Senotrusov <senotrusov@gmail.com>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ class ProcessController::TestDaemon
17
+ SLEEP_FOR = 100
18
+
19
+ def define_options(options)
20
+ options.header << "Test Daemon"
21
+ options.option "--foo FOO", "Foo option"
22
+ end
23
+
24
+ def apply_options(options)
25
+ puts options.inspect
26
+ end
27
+
28
+ def start
29
+ puts "#{self.class.inspect} STARTED"
30
+ puts "#{self.class.inspect} SLEEPING FOR #{SLEEP_FOR} seconds..."
31
+ sleep SLEEP_FOR
32
+ end
33
+
34
+ def stop
35
+ puts "#{self.class.inspect} STOP method"
36
+ end
37
+ end
38
+
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: senotrusov-ruby-process-controller
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Stanislav Senotrusov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-23 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: senotrusov-ruby-toolkit
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: senotrusov-ruby-threading-toolkit
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description:
36
+ email: senotrusov@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README
43
+ - LICENSE
44
+ files:
45
+ - README
46
+ - LICENSE
47
+ - lib/ruby-process-controller.rb
48
+ - lib/ruby-process-controller
49
+ - lib/ruby-process-controller/argv_parser.rb
50
+ - lib/ruby-process-controller/test_daemon.rb
51
+ - lib/ruby-process-controller/process_controller.rb
52
+ has_rdoc: false
53
+ homepage: http://github.com/senotrusov
54
+ post_install_message:
55
+ rdoc_options:
56
+ - --inline-source
57
+ - --charset=UTF-8
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.2.0
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: Staging for console ruby apps - ARGV parser, logger, pids, 2nd instance run prevention, privileges, signals, graceful shutdown for multithreaded app
79
+ test_files: []
80
+