-
Notifications
You must be signed in to change notification settings - Fork 1
/
basic-command.rb
448 lines (381 loc) · 13.4 KB
/
basic-command.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
module Pione
module Command
# BasicCommand provides PIONE command model. PIONE commands have 4 phases:
# "init", "setup", "action", "termination". Concrete commands implement some
# processings as each phases.
class BasicCommand
class << self
attr_reader :option_definition
attr_reader :phase_option
attr_reader :command_name_block
attr_reader :command_front_block
attr_reader :init_actions
attr_reader :setup_actions
attr_reader :execution_actions
attr_reader :termination_actions
attr_reader :exception_handler
attr_reader :subcommand
def inherited(subclass)
subclass.instance_eval do
@subcommand = {}
@phase_option = {:init => {}, :setup => {}, :execution => {}, :termination => {}}
@option_definition = OptionDefinition.new
@command_name = nil
@command_name_block = nil
@command_banner = nil
@command_front = nil
@command_front_block = nil
@init_actions = Array.new
@setup_actions = Array.new
@execution_actions = Array.new
@termination_actions = Array.new
@exception_handler = {:init => {}, :setup => {}, :execution => {}, :termination => {}}
@toplevel = false
# define init phase actions
init :process_name
init :signal_trap
init :option
init :front
init :process_additional_information
end
end
def option_parser_mode(mode)
@option_definition.parser_mode = mode
end
# Set toplvel command.
def toplevel(b)
@toplevel = b
end
def toplevel?
@toplevel
end
# Set progaram name or return the name.
def command_name(name=nil, &b)
if name
@command_name = name
@command_name_block = block_given? ? b : nil
else
@command_name
end
end
# Set program banner or return the banner.
def command_banner(banner=nil)
if banner
@command_banner = banner
else
@command_banner
end
end
# Set command front or return the front class.
def command_front(front_class=nil, &b)
if front_class
@command_front = front_class
@command_front_block = b
else
@command_front
end
end
def define_subcommand(name, subclass)
@subcommand[name] = subclass
end
forward :@option_definition, :use, :use_option
forward :@option_definition, :define, :define_option
forward :@option_definition, :item, :option_item
forward :@option_definition, :default, :option_default
forward :@option_definition, :validate, :validate_option
# Run the command with the arguments.
def run(argv)
self.new(argv).run
end
# Set setup phase options.
def init_phase(option)
set_phase_option(:init, option)
end
# Set setup phase options.
def setup_phase(option)
set_phase_option(:setup, option)
end
# Set execution phase options.
def execution_phase(option)
set_phase_option(:execution, option)
end
# Set termination phase options.
def termination_phase(option)
set_phase_option(:termination, option)
end
# Register the action to init phase.
def init(action, option={})
register_action(@init_actions, action, option)
end
# Register the action to setup phase.
def setup(action, option={})
register_action(@setup_actions, action, option)
end
# Register the action to execution phase.
def execute(action, option={})
register_action(@execution_actions, action, option)
end
# Register the action to termination phase.
def terminate(action, option={})
register_action(@termination_actions, action, option)
end
def handle_exception(phase_name, exceptions, &action)
exceptions.each {|e| @exception_handler[phase_name][e] = action}
end
def handle_setup_exception(*exceptions, &action)
handle_exception(:setup, exceptions, &action)
end
def handle_execution_exception(*exceptions, &action)
handle_exception(:execution, exceptions, &action)
end
def handle_termination_exception(*exceptions, &action)
handle_exception(:termination, exceptions, &action)
end
private
# Set phase option.
def set_phase_option(name, option)
@phase_option[name] = option
end
# Register the action to the phase.
def register_action(phase_actions, action, option={})
_action = action.is_a?(Hash) ? action : {[] => action}
_action.each do |key, val|
phase_actions << [key.is_a?(Array) ? key : [key], val, option]
end
end
end
attr_reader :option
attr_reader :running_thread
attr_accessor :action_type
forward! :class, :option_definition, :command_name, :command_name_block
forward! :class, :command_banner, :command_front, :command_front_block, :subcommand
def initialize(argv)
@argv = argv
@option = {}
@__exit_status__ = true
@__phase_name__ = nil
@__action_name__ = nil
@action_type = nil
# process has just one command object
Global.command = self
end
# Return current phase name.
#
# @return [Symbol]
# :init, :setup, :execution, or :termination
def current_phase
@__phase_name__
end
# Run 4 phase lifecycle of the command. This fires actions in each phase.
def run
# check subcommand
if subcommand.size > 0
if name = @argv.first
if subcommand.has_key?(name)
return subcommand[name].run(@argv.drop(1))
else
unless name[0] == "-"
abort("no such subcommand: %s" % name)
end
end
else
abort("require a subcommand name")
end
end
# run this command
@running_thread = Thread.current
enter_phase(:init)
enter_phase(:setup)
enter_phase(:execution)
terminate # => enter_phase(:termination) and exit
end
# Enter setup phase.
def enter_phase(phase_name)
# avoid double launch
return if @__phase_name__ == phase_name
# show debug message for entering phase
Log::Debug.system("%s enters phase \"%s\"" % [command_name, phase_name])
limit = self.class.phase_option[phase_name][:timeout]
phase_keyword, actions = find_phase_actions(phase_name)
timeout(limit) do
@__phase_name__ = phase_name
actions.each do |(targets, action_name, action_option)|
# check current mode is target or not
if not(targets.empty?) and not(targets.include?(@action_type))
next
end
# show debug message for firing action
Log::Debug.system("%s fires action \"%s\" in phase \"%s\"" % [command_name, action_name, phase_name])
# fire action
@__action_name__ = action_name
full_action_name = ("%s_%s" % [phase_keyword, action_name]).to_sym
if action_option[:module]
# call action in command action module
instance_eval(&action_option[:module].get(full_action_name))
else
# call action in self object
method(full_action_name).call
end
end
end
rescue *self.class.exception_handler[phase_name].keys => e
self.class.exception_handler[phase_name][e.class].call(self, e)
rescue Timeout::Error
args = [command_name, @__action_name__, @__phase_name__, limit]
abort("%s timeouted at action \"%s\" in phase \"%s\". (%i sec)" % args)
end
# Terminate the command. Note that this enters in termination phase first,
# and command exit.
def terminate
enter_phase(:termination)
exit # end with status code
end
# Return true if it is in init phase.
def init?
@__phase_name__ == :init
end
# Return true if it is in setup phase.
def setup?
@__phase_name__ == :setup
end
# Return true if it is in execution phase.
def execution?
@__phase_name__ == :execution
end
# Return true if it is in termination phase.
def termination?
@__phase_name__ == :termination
end
# Exit running command and return status.
def exit
Log::Debug.system("%s exits with status \"%s\"" % [command_name, @__exit_status__])
Global.system_logger.terminate
Kernel.exit(Global.exit_status)
end
# Exit running command and return failure status. Note that this method
# enters termination phase before it exits.
def abort(msg_or_exception, pos=caller(1).first)
# hide the message because some option errors are meaningless
invisible = msg_or_exception.is_a?(HideableOptionError)
# setup abortion message
msg = msg_or_exception.is_a?(Exception) ? msg_or_exception.message : msg_or_exception
# show the message
if invisible
Log::Debug.system(msg, pos)
else
Log::SystemLog.fatal(msg, pos)
end
# set exit status code
Global.exit_status = false
# go to termination phase
terminate
end
private
# Initialize process name.
def init_process_name
$PROGRAM_NAME = command_name
end
# Initialize signal trap actions.
def init_signal_trap
# explicit exit for signal INT (exit status code: failure)
Signal.trap(:INT) do
abort("%s is terminated by signal INT" % command_name)
end
# implicit abortion for signal TERM (exit status code: success)
Signal.trap(:TERM) do
Log::Debug.system("%s is terminated by signal TERM" % command_name)
terminate
end
end
# Initialize command options.
def init_option
@option = option_definition.parse(@argv, self)
rescue OptionParser::ParseError, OptionError => e
abort(e)
end
# Initialize front server of this process if it needs.
def init_front
if command_front
front_args = command_front_block ? command_front_block.call(self) : []
Global.front = command_front.new(*front_args)
end
end
# Modify process name to be with the additional informations.
def init_process_additional_information
if self.class.command_name_block
$PROGRAM_NAME = "%s (%s)" % [command_name, command_name_block.call(self)]
end
end
# Find phase actions by phase name.
def find_phase_actions(phase_name)
case phase_name
when :init; [:init, self.class.init_actions]
when :setup; [:setup, self.class.setup_actions]
when :execution; [:execute, self.class.execution_actions]
when :termination; [:terminate, self.class.termination_actions]
end
end
end
module CommandActionInterface
class << self
def extended(mod)
mod.instance_variable_set(:@action, Hash.new)
end
end
# Return the named action.
def get(name)
@action[name] || (raise ActionNotFound.new(self, name))
end
def define_action(name, &b)
@action[name] = b
end
end
module CommonCommandAction
extend CommandActionInterface
define_action(:terminate_child_process) do |cmd|
if Global.front
# send signal TERM to the child process
Global.front.child_pids.each do |pid|
Util.ignore_exception {Process.kill(:TERM, pid)}
end
# wait all children
children = Process.waitall.map{|(pid, _)| pid}
if not(children.empty?)
Log::Debug.system("%s killed #%s" % [cmd.command_name, children.join(", ")])
end
end
end
define_action(:setup_parent_process_connection) do |cmd|
if cmd.option[:parent_front]
begin
cmd.option[:parent_front].register_child(Process.pid, Global.front.uri)
ParentFrontWatchDog.new(self) # start to watch parent process
rescue Front::ChildRegistrationError
# terminate if the registration failed
Global.command.terminate
end
end
end
define_action(:terminate_parent_process_connection) do |cmd|
# maybe parent process is dead in this timing
Util.ignore_exception do
cmd.option[:parent_front].unregister_child(Process.pid)
end
end
end
class ParentFrontWatchDog
def initialize(command)
@command = command
Thread.new do
while true
# PPID 1 means the parent process is dead
if Process.ppid == 1 or Util.error?{command.option[:parent_front].ping}
break @command.terminate
end
sleep 1
end
end
end
end
end
end