agentdispatcher 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +72 -0
- data/lib/agentdispatcher.rb +356 -0
- data/lib/opts.rb +86 -0
- data/lib/proctools.rb +110 -0
- data/lib/simpledispatcher.rb +110 -0
- data/test/runtime/config/config1.yml +2 -0
- data/test/runtime/config/config2.yml +2 -0
- data/test/test_agent_lowlevel.rb +30 -0
- data/test/test_agentdispatcher.rb +165 -0
- data/test/test_opts.rb +64 -0
- data/test/test_simpledispather.rb +74 -0
- metadata +72 -0
data/README
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
=AgentDispatcher (solid infrastructure fundamentals for script based agents)
|
2
|
+
v0.7.0
|
3
|
+
|
4
|
+
A non-intrusive application micro-framework that turns your plain ruby objects into configurable, reactive, and easily operable agents. They will react on commands; combine behaviors using dynamic configurations; multiply instances, play nice with with *nix infrastructure. In other words, object-oriented, shell-script-enabled approach towards agents modelling.
|
5
|
+
|
6
|
+
There are two types of dispatchers (mixins) available:
|
7
|
+
- SimpleDispatcher - reacts on commands, runs with pid, commandline invocation, concept of instance id. (obsolete)
|
8
|
+
- AgentDispatcher - additionally to SimpleDispather it adds configurations, command-line configuration, constructor/destructors, logging
|
9
|
+
|
10
|
+
The main mixin is AgentDispatcher. See datailed description and examples in docs of the each class.
|
11
|
+
|
12
|
+
==Features
|
13
|
+
- dispatching of commands with arguments. Handled by <tt>on_#{command}</tt> callback methods.
|
14
|
+
- static definition of acceptable commands (<tt>@AllowedCommands</tt>)
|
15
|
+
- PORO (Plain Old Ruby Objects) - the behavior can be applied (mixed-in) to any class not interfering
|
16
|
+
- own methods, own constructor
|
17
|
+
- chain of inheritable custom constructor and destructor methods (<tt>upMethod</tt>, <tt>downMethod</tt>)
|
18
|
+
- hierarchical (layered) configuration (code-provided defaults < config files < manual parameters on invocation)
|
19
|
+
- configurations by absolute path, or config file name
|
20
|
+
- class defined configuration defaults (<tt>@DefaultCfg</tt>)
|
21
|
+
- configuration overlays (something like multiple class inheritance) (<tt>@cfg</tt>)
|
22
|
+
- sensible default behaviors (id, command, config do not have to be provided)
|
23
|
+
- identity (agent instance id) (<tt>@id</tt>)
|
24
|
+
- pid file management
|
25
|
+
- customizable ROOT path (where resources are found, logs, pids written) (<tt>AGENTS_ROOT</tt>)
|
26
|
+
- logging per instance (can be overriden) (<tt>@log</tt>)
|
27
|
+
|
28
|
+
Options:
|
29
|
+
--config=A,B,C - cofiguration files
|
30
|
+
--log_path - custom log path
|
31
|
+
--pid_path - custom pid path
|
32
|
+
--nopid - dont mess with pid files, neither write, nor consider pid file
|
33
|
+
--forcepid - starts even if pid file exists
|
34
|
+
--quiet - no output
|
35
|
+
|
36
|
+
|
37
|
+
Instance variables:
|
38
|
+
@id, @log, @cfg
|
39
|
+
|
40
|
+
Run <tt>rake rdoc</tt> for documentation and examples.
|
41
|
+
See examples
|
42
|
+
|
43
|
+
==Instalation
|
44
|
+
World-wide installation:
|
45
|
+
- Available as gem: <tt>gem install agentdispatcher</tt>
|
46
|
+
- Project homepage http://rubyagent.rubyforge.org/
|
47
|
+
- Project development page, downloads, forum, svn: http://rubyforge.org/projects/rubyagent/
|
48
|
+
|
49
|
+
Local gem creation:
|
50
|
+
rake gem
|
51
|
+
rake test #to run all testcases
|
52
|
+
sudo gem install pkg/agentdispatcher-x.y.z.gem --local
|
53
|
+
|
54
|
+
==Author
|
55
|
+
Copyright (C) 2008 Viktor Zigo, http://alephzarro.com
|
56
|
+
|
57
|
+
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
58
|
+
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
59
|
+
You should have received a copy of the GNU General Public License along with this program (LICENSE file); if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
60
|
+
|
61
|
+
Sponsored by: http://7inf.com
|
62
|
+
|
63
|
+
==TODO:
|
64
|
+
HighPri:
|
65
|
+
- adjust to support agent caching (reuse)
|
66
|
+
|
67
|
+
LowPri:
|
68
|
+
- help (for simple dispatcher)
|
69
|
+
- not existing runtime - try to create it
|
70
|
+
- validate input/cfg (just by exposing a method, subclassing DynParser)
|
71
|
+
- adjust simpleDispatcher to compatible behavior with agentDispatcher (constructor)
|
72
|
+
----
|
@@ -0,0 +1,356 @@
|
|
1
|
+
# = Complex command dispatcher mixin / Application micro-framework
|
2
|
+
#
|
3
|
+
# Simplifies creation of configurable objects which can react on messages/events while using OO approach. They can be easily executed from command line.
|
4
|
+
# The micro-framework has lots of features. Therefore an example first:
|
5
|
+
# ==Example Usage
|
6
|
+
# agent.rb:
|
7
|
+
# class Agent
|
8
|
+
# include AgentDispatcher
|
9
|
+
# @AllowedCommands = %w(start stop)
|
10
|
+
# def on_run *args
|
11
|
+
# # do some job
|
12
|
+
# end
|
13
|
+
# def on_stop *args
|
14
|
+
# # do some job
|
15
|
+
# end
|
16
|
+
# ...
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# class AgentB < Agent
|
20
|
+
# @DefaultCfg = {:log_path=>'/tmp'}
|
21
|
+
# @AllowedCommands = %w(start run stop)
|
22
|
+
# upMethod :initBussines
|
23
|
+
# def initBussines
|
24
|
+
# puts "post creation placeholder"
|
25
|
+
# end
|
26
|
+
# downMethod :closeBussines
|
27
|
+
# def closeBussines
|
28
|
+
# puts "pre-shutdown placeholder"
|
29
|
+
# end
|
30
|
+
# def on_run *args
|
31
|
+
# # do some job
|
32
|
+
# end
|
33
|
+
# ...
|
34
|
+
# end
|
35
|
+
# if __FILE__ == $0
|
36
|
+
# AgentB::Dispatch *ARGV
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# There are several options how to dispatch events to the 'agent' (programatically)
|
40
|
+
# Agent::Dispatch *ARGV
|
41
|
+
# AgentB::Dispatch 'agent1', 'run', 'param1', 'param2'
|
42
|
+
# Agent::Dispatch 'start'
|
43
|
+
# Agent::DispatchString '--config cfgfile1,cfgfile2 agentID start'
|
44
|
+
# Agent.new(...).dispatch *ARGV
|
45
|
+
#
|
46
|
+
# The purpose of ARGV variants is of course to call the 'agent' right away from the command line, e.g.:
|
47
|
+
# agent.rb --config cfgfile1,cfgfile2 agentID start
|
48
|
+
# agent.rb --help
|
49
|
+
#
|
50
|
+
# = Feature Documentation
|
51
|
+
# - dispatching commands (+id, arguments, configuration)
|
52
|
+
# - defintion of accepted commands <tt>@AllowedCommands = %w(start stop)</tt>
|
53
|
+
# - command handler method in format <tt>on_#{command in lowercase}<tt> (e.g. <tt>run</tt> -> <tt>on_run<tt>)
|
54
|
+
# - dispatching from: command-line string, ARGV, Hash
|
55
|
+
# - the agents object can used conventional inheritance to override/add new behavior.
|
56
|
+
# - objects may have their own constructors (the class method Dispatch uses default constructor)
|
57
|
+
# - injection of business constructors/desctructors
|
58
|
+
# - <tt>upMethod <em>aMethodName</em></tt>
|
59
|
+
# - <tt>downMethod <em>aMethodName</em></tt>
|
60
|
+
# - autogeneration of command-line help
|
61
|
+
# - (TODO: in future support for any methods by injections)
|
62
|
+
# Configuration
|
63
|
+
# - hierarchical configuration (the final configuration is constructed by overlaying several sources)
|
64
|
+
# - default config can be defined in class variable <tt>@DefaultCfg = {:log_path=>'/tmp'}</tt>
|
65
|
+
# - loaded from file; specified in option :config either by path (ends with .yml) or by name (the file is loaded from <tt>#{AGENTS_ROOT}/config/#{name}.yml)</tt>
|
66
|
+
# - multiple configuration files can be passed <tt>:config=>'cfg1,cfg2'</tt>, precedence in the given order
|
67
|
+
# - the configuration can be given by arguments
|
68
|
+
# - precedence of configuration overlaying defaut < files < arguments
|
69
|
+
# Logging
|
70
|
+
# - <tt>@log</tt> instance variable is created, can be overriden in object constructor
|
71
|
+
# - default path is <tt>#{AGENTS_ROOT}/tmp/log/#{@id}.log</tt>
|
72
|
+
# - the path can be modified by setting <tt>:log_path</tt> option
|
73
|
+
# Process Management
|
74
|
+
# - a PID file is generated while the script is running
|
75
|
+
# - generated in <tt>#{AGENTS_ROOT}/var/run/#{@id}.pid</tt>
|
76
|
+
# - the path can be modified by setting <tt>:pid_path</tt> option
|
77
|
+
# - passing <tt>:nopid</tt> option supresses genereation of pid file
|
78
|
+
# - process does not start if a pid file already exists. Force execution by passing <tt>:forcepid</tt> option
|
79
|
+
# Comandline format
|
80
|
+
# Usage: #{@agentClass} [CONFIG] [ID] [COMMAND] [ARGS]
|
81
|
+
# CONFIG Any application specific configuration flags in the format --NAME VALUE[,VALUE]*
|
82
|
+
# Special flags: {help, id, config=[A,B,...],log_path, nopid, pid_path, quiet, cmd}
|
83
|
+
# ID Instance id of the agent (default: 'id-fied classname')
|
84
|
+
# COMMAND Command to execute [aplication soec. commands] (default: 'run')
|
85
|
+
# ARGS Space separated arguments (default:none)
|
86
|
+
# - Passing <tt>:help</tt> option makes the script to generate help intructions to the STDOUT
|
87
|
+
#
|
88
|
+
# ==Author
|
89
|
+
# Viktor Zigo, http://alephzarro.com, All rights reserved. Distributed under GNU GPL License (see LICENSE).
|
90
|
+
#
|
91
|
+
# sponsored by http://7inf.com
|
92
|
+
module AgentDispatcher
|
93
|
+
@VERSION='0.7.0'
|
94
|
+
PID_PATH = 'var/run'
|
95
|
+
LOG_PATH = 'tmp/log'
|
96
|
+
|
97
|
+
$:.unshift File.join(File.dirname(__FILE__))
|
98
|
+
require 'logger'
|
99
|
+
require 'opts.rb'
|
100
|
+
require 'proctools.rb'
|
101
|
+
|
102
|
+
AGENTS_ROOT = (ENV['AGENTS_ROOT'] || '.' )
|
103
|
+
## @DefaultCfg = {}
|
104
|
+
## @AllowedCommands = []
|
105
|
+
|
106
|
+
def dispatchString aString
|
107
|
+
dispatch *aString.split
|
108
|
+
end
|
109
|
+
|
110
|
+
def dispatch *argv
|
111
|
+
## puts "AGENTS_ROOT: #{AGENTS_ROOT}, agentClass: #{self.class} allowedCmds: #{self.class.AllowedCommands}"
|
112
|
+
#make validation customizable
|
113
|
+
cfg = AgentUtils::ConfFactory.new(self.class).createFromArgv *argv
|
114
|
+
id = cfg[:id]
|
115
|
+
#TODO: forcepid
|
116
|
+
|
117
|
+
#generate pid
|
118
|
+
cfg[:pid_path] = File.join( AGENTS_ROOT, PID_PATH, "#{id}.pid") unless cfg[:pid_path]
|
119
|
+
ProcTools::withPidFile(cfg[:pid_path], cfg) do
|
120
|
+
initializeAgent cfg
|
121
|
+
cmd = cfg[:cmd]
|
122
|
+
if self.class.AllowedCommands().include? cmd
|
123
|
+
executeInjections :postInit
|
124
|
+
begin
|
125
|
+
self.send "on_#{cmd.downcase}", *cfg[:args]
|
126
|
+
ensure
|
127
|
+
executeInjections :preExit
|
128
|
+
end
|
129
|
+
else
|
130
|
+
raise "Unknown command '#{cmd}', allowed commands are {#{self.class.AllowedCommands.join(',')}}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def executeInjections aType
|
136
|
+
typedInjections = self.class.__injections__.select { |m, d| d[0]==aType}
|
137
|
+
typedInjections = typedInjections.map {|m,d| [m, d[1]] }
|
138
|
+
typedInjections.sort! {|a,b| -1*(a[1]<=>b[1])}
|
139
|
+
#@log.debug "excuting #{aType} injections: #{typedInjections.join(',')}"
|
140
|
+
@log.debug("executing #{aType} injections: #{typedInjections.join(',')}") if @log
|
141
|
+
typedInjections.each { |m,d| self.send m}
|
142
|
+
end
|
143
|
+
|
144
|
+
def initializeAgent aConf
|
145
|
+
@cfg = aConf
|
146
|
+
@id = @cfg[:id] unless @id #do not override if already exists
|
147
|
+
#config
|
148
|
+
fileCfg = AgentUtils::ConfFactory.new(self.class).loadConfigs @cfg[:config], self.class.DefaultCfg
|
149
|
+
@cfg = fileCfg.merge @cfg #cmdLine config overwrites fileCfg
|
150
|
+
#logging
|
151
|
+
unless @log # don't do anything with logging as long as it's already defined
|
152
|
+
@cfg[:log_path] = File.join( AGENTS_ROOT, LOG_PATH, "#{@id}.log" ) unless @cfg[:log_path]
|
153
|
+
if ProcTools::existsPath?(@cfg[:log_path])
|
154
|
+
@log = Logger.new(@cfg[:log_path], shift_age = 7, shift_size = 1048576)
|
155
|
+
puts "See logs in: #{@cfg[:log_path]}" unless @cfg[:quiet]
|
156
|
+
else
|
157
|
+
@log = Logger.new(STDERR)
|
158
|
+
@log.error "log_path #{@cfg[:log_path]} does not exit. Logging to STDERR"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
@log.debug @cfg.inspect
|
162
|
+
end
|
163
|
+
private :executeInjections, :initializeAgent
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
|
168
|
+
class << AgentDispatcher
|
169
|
+
module AgentDispatcherClassMethods
|
170
|
+
#inherit by merging
|
171
|
+
def __injections__
|
172
|
+
injs = @__injections__ || {}
|
173
|
+
injs = (superclass.__injections__.merge injs) if self.superclass.respond_to? :__injections__
|
174
|
+
injs
|
175
|
+
end
|
176
|
+
|
177
|
+
#inherit by merging
|
178
|
+
def DefaultCfg
|
179
|
+
hash = @DefaultCfg || {}
|
180
|
+
hash = (superclass.DefaultCfg.merge hash) if self.superclass.respond_to? :DefaultCfg
|
181
|
+
hash
|
182
|
+
end
|
183
|
+
|
184
|
+
#inherit by replacing
|
185
|
+
def AllowedCommands
|
186
|
+
@AllowedCommands || (superclass.AllowedCommands() if self.superclass.respond_to? :AllowedCommands) || []
|
187
|
+
end
|
188
|
+
|
189
|
+
def upMethod aMethod, aPriority = 0
|
190
|
+
@__injections__ = {} unless @__injections__
|
191
|
+
@__injections__[aMethod.to_sym]=[:postInit, aPriority]
|
192
|
+
end
|
193
|
+
def downMethod aMethod, aPriority = 0
|
194
|
+
@__injections__[aMethod.to_sym]=[:preExit, aPriority]
|
195
|
+
end
|
196
|
+
|
197
|
+
def Dispatch *argv
|
198
|
+
instance = self.new
|
199
|
+
instance.dispatch *argv
|
200
|
+
instance
|
201
|
+
end
|
202
|
+
|
203
|
+
def DispatchString aString
|
204
|
+
Dispatch *aString.split
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
# add class methods
|
210
|
+
def included(klass)
|
211
|
+
super
|
212
|
+
klass.extend AgentDispatcherClassMethods
|
213
|
+
end
|
214
|
+
|
215
|
+
def append_features(mod)
|
216
|
+
# help out people counting on transitive mixins
|
217
|
+
unless mod.instance_of?(Class)
|
218
|
+
raise TypeError, "Inclusion of the AgentDispatcher module in module #{mod}"
|
219
|
+
end
|
220
|
+
super
|
221
|
+
end
|
222
|
+
|
223
|
+
#extending an object with AgentDispatcher is a bad idea
|
224
|
+
undef_method :extend_object
|
225
|
+
end
|
226
|
+
|
227
|
+
module AgentUtils
|
228
|
+
|
229
|
+
# A simple example agent
|
230
|
+
class AgentBase
|
231
|
+
include AgentDispatcher
|
232
|
+
@AllowedCommands = %w(run)
|
233
|
+
|
234
|
+
upMethod :initBussines
|
235
|
+
def initBussines
|
236
|
+
end
|
237
|
+
|
238
|
+
downMethod :closeBussines
|
239
|
+
def closeBussines
|
240
|
+
end
|
241
|
+
|
242
|
+
def onRun *args
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Handles args parsing, configuration management
|
247
|
+
class ConfFactory
|
248
|
+
RESERVED_OPTS = [:id, :cmd, :args, :config]
|
249
|
+
AGENTS_ROOT = (ENV['AGENTS_ROOT'] || '.' )
|
250
|
+
|
251
|
+
def initialize anAgentClass = nil
|
252
|
+
@agentClass = anAgentClass || AgentBase
|
253
|
+
@allowed_cmds = @agentClass.AllowedCommands
|
254
|
+
end
|
255
|
+
|
256
|
+
# genarates a help message
|
257
|
+
def dumpHelp
|
258
|
+
puts %Q{Usage: #{@agentClass} [CONFIG] [ID] [COMMAND] [ARGUMENTS]
|
259
|
+
CONFIG\tAny application specific configuration flags in the format --NAME VALUE[,VALUE]*
|
260
|
+
\tSpecial flags: {help, id, config=[A,B,...],log_path, nopid, forcepid, pid_path, quiet, cmd}
|
261
|
+
ID \tInstance id of the agent (default: '#{class_to_id(@agentClass)}')
|
262
|
+
COMMAND\tCommand to execute [#{@allowed_cmds.join(',')}] (default: 'run')
|
263
|
+
ARGUMENTS\tSpace separated arguments (default:none)
|
264
|
+
}
|
265
|
+
end
|
266
|
+
|
267
|
+
# creates the config from a string (like what you type on command-line)
|
268
|
+
def createFromLine aString
|
269
|
+
createFromArgv *fromLine(aString)
|
270
|
+
end
|
271
|
+
|
272
|
+
# create confign from parsed arguments
|
273
|
+
# [CONFIG] [agentID command] [arguments]
|
274
|
+
# [CONFG] [command] [arguments] - > agentID=class name
|
275
|
+
# [CONFIG] - > agentID=class name, command='run', no args
|
276
|
+
def createFromArgv *argv
|
277
|
+
createFromMethod *parseArgv( *argv)
|
278
|
+
end
|
279
|
+
|
280
|
+
# create config by explicitely giving it the required componnents
|
281
|
+
def createFromMethod anAgentID = nil, aCmd = nil, anArgs = [], aConf = {}
|
282
|
+
createFromOpts semantize(anAgentID, aCmd, anArgs, aConf)
|
283
|
+
end
|
284
|
+
|
285
|
+
#create config from options (Hash)
|
286
|
+
def createFromOpts aConf
|
287
|
+
#puts "Class: #{@agentClass.name}, File: #{__FILE__}, execFile: #{$0}"
|
288
|
+
raise "agentID, command, and args need to be defined" unless [:id, :cmd,:args].all? {|c| aConf.include? c}
|
289
|
+
#puts "Execution --- conf:#{aConf.inspect}"
|
290
|
+
@conf = aConf
|
291
|
+
end
|
292
|
+
|
293
|
+
attr_reader :conf
|
294
|
+
|
295
|
+
def [] aSelector
|
296
|
+
@conf[aSelector.to_sym]
|
297
|
+
end
|
298
|
+
|
299
|
+
# Load a yaml config file and merge it with defaults
|
300
|
+
# The aPathOrName can be a path (if it ends with .yml) or just a configuration file name (to be looked up in 'runtime' path, see AGENTS_ROOT)
|
301
|
+
# return the merged hash - all keys are symbolized
|
302
|
+
def loadConfig aPathOrName, aDefaults={}
|
303
|
+
return aDefaults unless aPathOrName
|
304
|
+
require 'yaml'
|
305
|
+
aPathOrName = File.join(AGENTS_ROOT , "config", "#{aPathOrName}.yml") unless aPathOrName=~/\.yml$/
|
306
|
+
cfg = YAML.load_file aPathOrName
|
307
|
+
cfg = ProcTools.Intern cfg
|
308
|
+
return cfg ? (aDefaults.merge cfg) : aDefaults
|
309
|
+
end
|
310
|
+
|
311
|
+
# loads chain of configuration files
|
312
|
+
def loadConfigs aPathsOrNames, aDefaults={}
|
313
|
+
aPathsOrNames = [aPathsOrNames] unless aPathsOrNames.kind_of? Array
|
314
|
+
aPathsOrNames.each {|pathOrName|
|
315
|
+
aDefaults = loadConfig pathOrName, aDefaults
|
316
|
+
}
|
317
|
+
aDefaults
|
318
|
+
end
|
319
|
+
|
320
|
+
private
|
321
|
+
def fromLine aString
|
322
|
+
aString.split
|
323
|
+
end
|
324
|
+
|
325
|
+
def parseArgv *argv
|
326
|
+
opts = DynamicCmdParse.new.parse! argv
|
327
|
+
dumpHelp if opts[:help]
|
328
|
+
id, cmd = nil, nil
|
329
|
+
if argv.length==0
|
330
|
+
elsif @allowed_cmds.include? argv.first
|
331
|
+
# without instance id
|
332
|
+
cmd = argv.shift
|
333
|
+
else
|
334
|
+
# with instance id
|
335
|
+
id = argv.shift
|
336
|
+
cmd = argv.shift if argv.length > 0
|
337
|
+
end
|
338
|
+
[id, cmd, argv, opts]
|
339
|
+
end
|
340
|
+
|
341
|
+
# verifies which options are set, how
|
342
|
+
def semantize anAgentID = nil, aCmd = nil, anArgs = [], aConf = {}
|
343
|
+
# real values or default values?
|
344
|
+
conf = aConf.clone
|
345
|
+
conf[:id] = (anAgentID ? anAgentID.to_s : class_to_id(@agentClass) )
|
346
|
+
conf[:cmd] = (aCmd ? aCmd.to_s : 'run')
|
347
|
+
conf[:args] = anArgs
|
348
|
+
conf
|
349
|
+
end
|
350
|
+
|
351
|
+
# used by pid files
|
352
|
+
def class_to_id aClass
|
353
|
+
aClass.name.gsub('::',"-").downcase
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
data/lib/opts.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
|
2
|
+
class StaticCmdParser
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
# destructively parses the ARGV for statically declared config options by addOpts method and valided by validateOpts method
|
6
|
+
# return opts {symbol=>string}, the ARGV contains the remaining content
|
7
|
+
def parse! aDefaults ={}, allowedCommands = ["run"]
|
8
|
+
cmds = allowedCommands.join('|')
|
9
|
+
cmds = "<#{cmds}>" if allowedCommands.length>1
|
10
|
+
opts = aDefaults
|
11
|
+
parser = OptionParser.new do |p|
|
12
|
+
p.banner = "Usage: #{$0} [options] #{cmds}"
|
13
|
+
p.separator ""
|
14
|
+
p.separator "Options:"
|
15
|
+
addOpts p, opts
|
16
|
+
end
|
17
|
+
## opts = parser.parse!
|
18
|
+
parser.parse!
|
19
|
+
#opts[:cmd] = *ARGV[-1]
|
20
|
+
#raise OptionParser::MissingArgument, "lang" if opts[:lang].nil? and opts[:config_file].nil?
|
21
|
+
validateOpts opts, ARGV
|
22
|
+
opts
|
23
|
+
end
|
24
|
+
|
25
|
+
#for adding options in subclass
|
26
|
+
def addOpts aParser, opts
|
27
|
+
#p.on("-c", "--config YMLFILE", "Configuration file, can be overriden by cmdline opts") { |file| opts[:config] = file }
|
28
|
+
#p.on( "--forcepid", "XXX") { |v| opts[:pid]= v }
|
29
|
+
opts
|
30
|
+
end
|
31
|
+
#for validating parsed options
|
32
|
+
def validateOpts opts, argv
|
33
|
+
#raise OptionParser::MissingArgument, "lang" if opts[:lang].nil?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
class DynamicCmdParse
|
39
|
+
|
40
|
+
# destructively parses the ARGV for any dash-starting options without any or with one argument. The options can be validated by validateOpts method.
|
41
|
+
# return opts {symbol=>string}, the ARGV contains the remaining (and unparsed) content
|
42
|
+
def parse! argv = nil, aDefaults = {}
|
43
|
+
argv = ARGV unless argv
|
44
|
+
key = nil
|
45
|
+
rest = []
|
46
|
+
opts = aDefaults
|
47
|
+
while (argv.length>0 or key) do
|
48
|
+
tmp = argv.shift
|
49
|
+
new_key = tmp ? tmp.scan(/-+([\w\d\-\_]+)/).flatten.first : nil
|
50
|
+
if new_key
|
51
|
+
opts[key.intern] = true if key
|
52
|
+
key=new_key
|
53
|
+
else
|
54
|
+
if key
|
55
|
+
values = true
|
56
|
+
#are there any values?
|
57
|
+
if tmp
|
58
|
+
#is it a list?
|
59
|
+
values = tmp.split ','
|
60
|
+
values = (values.length>1 ? values : values.first )
|
61
|
+
end
|
62
|
+
opts[key.intern] = values
|
63
|
+
key = nil
|
64
|
+
else
|
65
|
+
rest << tmp
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
argv.replace rest
|
70
|
+
validateOpts opts, argv
|
71
|
+
opts
|
72
|
+
end
|
73
|
+
|
74
|
+
#for adding options in subclass
|
75
|
+
def validateOpts opts, argv
|
76
|
+
#raise OptionParser::MissingArgument, "lang" if opts[:lang].nil?
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
if __FILE__ == $0
|
82
|
+
##ARGV.replace ["--forcepid", "--config", "aaa", "-c", "Big test", "anID", "start"]
|
83
|
+
p "Inpput: #{ARGV}"
|
84
|
+
opts = DynamicCmdParse.new.parse!
|
85
|
+
p "Output: #{opts.inspect}, ARGV = #{ARGV.inspect}"
|
86
|
+
end
|
data/lib/proctools.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# =ProcTools - Utilities for process management
|
2
|
+
#
|
3
|
+
# == Usage
|
4
|
+
# ProcTools::withPidFile('test.pid') do
|
5
|
+
# exitcode = ProcTools::execSync 'echo 123 l sleep 5', 10
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
# ==Author
|
9
|
+
# Viktor Zigo, http://alephzarro.com, All rights reserved. Distributed under GNU GPL License (see LICENSE).
|
10
|
+
#
|
11
|
+
# sponsored by http://7inf.com
|
12
|
+
# ==History
|
13
|
+
# version: 0.2
|
14
|
+
# * 0.2 - added Intern; p->puts
|
15
|
+
#
|
16
|
+
# ==TODO tests
|
17
|
+
# * process cannot be started
|
18
|
+
# * process runs a finishes in time
|
19
|
+
# * process timesout
|
20
|
+
# *error codes returned
|
21
|
+
|
22
|
+
|
23
|
+
module ProcTools
|
24
|
+
require 'timeout'
|
25
|
+
|
26
|
+
#TODO: tests
|
27
|
+
# process cannot be started
|
28
|
+
# process runs a finishes in time
|
29
|
+
# process timesout
|
30
|
+
# error codes returned
|
31
|
+
|
32
|
+
# Executes an external command
|
33
|
+
def self.execSync aCmd, aTimeout = 0, doKill = true
|
34
|
+
puts "Spawning process #{aCmd}"
|
35
|
+
pid = fork {
|
36
|
+
exec aCmd
|
37
|
+
}
|
38
|
+
puts "Waiting on process pid=#{pid} timeout = #{aTimeout}..."
|
39
|
+
exitcode = nil
|
40
|
+
if aTimeout>0
|
41
|
+
begin
|
42
|
+
timeout(aTimeout){
|
43
|
+
return_pid, exitcode = Process.wait2
|
44
|
+
puts "Process #{pid} finished exitcode: #{exitcode}, cmd: #{aCmd}"
|
45
|
+
}
|
46
|
+
rescue Timeout::Error
|
47
|
+
puts "Process #{pid} timeuot"
|
48
|
+
if doKill
|
49
|
+
puts "Killing-9 the process #{pid} - #{aCmd}"
|
50
|
+
Process.kill 9, pid
|
51
|
+
end
|
52
|
+
end
|
53
|
+
else
|
54
|
+
return_pid, exitcode = Process.wait2
|
55
|
+
puts "Process #{pid} finished exitcode: #{exitcode}, cmd: #{aCmd}"
|
56
|
+
STDOUT.flush
|
57
|
+
end
|
58
|
+
exitcode
|
59
|
+
end
|
60
|
+
|
61
|
+
# Creates a pid file while running a block
|
62
|
+
# Pid file name is teken from ENV['PID_FILE'] or aFilename
|
63
|
+
def self.withPidFile aFilename = nil, opts={}, &code
|
64
|
+
# should we use pid file at all?
|
65
|
+
if opts[:nopid]
|
66
|
+
yield
|
67
|
+
else
|
68
|
+
pid_file = ENV['PID_FILE'] || aFilename
|
69
|
+
raise "Process may already run PID-file exists (#{pid_file}). Use :forcepid to run anyway" if File.exist?(pid_file) and not opts[:forcepid]
|
70
|
+
File.open(pid_file, 'w') {|f| f.puts $$}
|
71
|
+
begin
|
72
|
+
yield
|
73
|
+
rescue StandardError=>e
|
74
|
+
#rethrow
|
75
|
+
raise e
|
76
|
+
#puts e, e.backtrace
|
77
|
+
ensure
|
78
|
+
File.delete(pid_file) if pid_file
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.existsPath? aPath
|
84
|
+
File.exist? File.dirname(aPath)
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.Intern(aHash)
|
88
|
+
return {} unless aHash
|
89
|
+
|
90
|
+
hash={}
|
91
|
+
aHash.each_pair do
|
92
|
+
|n,v|
|
93
|
+
v=Intern(v) if v.kind_of? Hash
|
94
|
+
hash[n.to_sym]=v
|
95
|
+
end
|
96
|
+
hash
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
if __FILE__ == $0
|
102
|
+
ProcTools::withPidFile('proc.pid') do
|
103
|
+
puts 'starting'
|
104
|
+
exitcode = ProcTools::execSync './test1.sh'
|
105
|
+
puts "returned, exit code #{exitcode}"
|
106
|
+
puts "waiting to give the child chance to to st"
|
107
|
+
#sleep 10
|
108
|
+
puts 'done'
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# =Command dispatcher mixin
|
2
|
+
#
|
3
|
+
# Simplifies creation of objects which can react on messages/events while using OO approach.
|
4
|
+
# For more complex behavior see AgentDispatcher module.
|
5
|
+
#
|
6
|
+
# ==Usage
|
7
|
+
# class Agent
|
8
|
+
# include Dispatcher
|
9
|
+
# @AllowedCommands = %w(start stop run)
|
10
|
+
# def initialize anId = nil; @id = anId ; end
|
11
|
+
# def on_run *args
|
12
|
+
# # do some job
|
13
|
+
# end
|
14
|
+
# def on_start *args
|
15
|
+
# # do some job
|
16
|
+
# end
|
17
|
+
# ...
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# Several options how events to the 'agent' can be dispatched
|
21
|
+
# Agent::Dispatch *ARGV
|
22
|
+
# Agent::Dispatch 'agent1', 'run', 'param1', 'param2'
|
23
|
+
# Agent::Dispatch 'start'
|
24
|
+
# Agent.new('agent0').dispatch *ARGV
|
25
|
+
# Agent.DispatchOpts :cmd=>'start', :whatever=>'echo 42'
|
26
|
+
#
|
27
|
+
# The ARGV variants of dispatching allows to call the 'agent' right away from command line, e.g.:
|
28
|
+
# ./agent.rb agent1 run
|
29
|
+
#
|
30
|
+
# Another types of agents can inherit from <tt>Agent</tt> and override/add only new behavior.
|
31
|
+
#
|
32
|
+
# ==Author
|
33
|
+
# Viktor Zigo, http://7inf.com, All rights reserved. Distributed under GNU GPL License (see LICENSE).
|
34
|
+
# Sponsored by: 7inf.com
|
35
|
+
# ==History
|
36
|
+
# version: 0.5
|
37
|
+
module SimpleDispatcher
|
38
|
+
def dispatch *args
|
39
|
+
cmd = args.shift
|
40
|
+
# do not allow no commands - leads to user mistakes
|
41
|
+
# cmd = self.class.AllowedCommands().first unless cmd
|
42
|
+
if self.class.AllowedCommands().include? cmd
|
43
|
+
self.send "on_#{cmd.downcase}", args
|
44
|
+
else
|
45
|
+
raise "Unknown command '#{cmd}', allowed commands are {#{self.class.AllowedCommands.join(',')}}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def dispatchOpts opts
|
51
|
+
cmd = opts[:cmd]
|
52
|
+
if self.class.AllowedCommands().include? cmd
|
53
|
+
self.send "on_#{cmd.downcase}", opts
|
54
|
+
else
|
55
|
+
raise "Unknown command '#{cmd}', allowed commands are {#{self.class.AllowedCommands.join(',')}}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
alias :dispatch2 :dispatchOpts
|
59
|
+
end
|
60
|
+
|
61
|
+
class << SimpleDispatcher
|
62
|
+
module SimpleDispatcherClassMethods
|
63
|
+
#override
|
64
|
+
#@AllowedCommands = []
|
65
|
+
#inherit by replacing
|
66
|
+
def AllowedCommands
|
67
|
+
@AllowedCommands || (superclass.AllowedCommands() if self.superclass.respond_to? :AllowedCommands) || []
|
68
|
+
end
|
69
|
+
|
70
|
+
def Dispatch *args
|
71
|
+
if self.AllowedCommands.include? args.first
|
72
|
+
# without instance id (use lowcase classname)
|
73
|
+
inst = self.new self.name.gsub('::',"-").downcase
|
74
|
+
else
|
75
|
+
# with instance id
|
76
|
+
inst = self.new args.shift
|
77
|
+
end
|
78
|
+
inst.dispatch *args
|
79
|
+
inst
|
80
|
+
end
|
81
|
+
|
82
|
+
# opts :id, :cmd
|
83
|
+
def DispatchOpts opts = {}
|
84
|
+
id = opts[:id]
|
85
|
+
id = self.name.gsub('::',"-").downcase unless id and id.length>0
|
86
|
+
inst = self.new id
|
87
|
+
inst.dispatchOpts opts
|
88
|
+
inst
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
# add class methods
|
95
|
+
def included(klass)
|
96
|
+
super
|
97
|
+
klass.extend SimpleDispatcherClassMethods
|
98
|
+
end
|
99
|
+
|
100
|
+
def append_features(mod)
|
101
|
+
# help out people counting on transitive mixins
|
102
|
+
unless mod.instance_of?(Class)
|
103
|
+
raise TypeError, "Inclusion of the Dispatcher module in module #{mod}"
|
104
|
+
end
|
105
|
+
super
|
106
|
+
end
|
107
|
+
|
108
|
+
#extending an object with Dispatcher is a bad idea
|
109
|
+
undef_method :extend_object
|
110
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#Viktor Zigo, http://alephzarro.com, All rights reserved.
|
2
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
ENV['AGENTS_ROOT']=File.join(File.dirname(__FILE__), 'runtime') unless ENV['AGENTS_ROOT']
|
6
|
+
require 'agentdispatcher.rb'
|
7
|
+
|
8
|
+
class AgentDispatcherLowTest< Test::Unit::TestCase
|
9
|
+
def test_confFactory1
|
10
|
+
cmdLine = "--config qwe.yml agent1 start 12:00"
|
11
|
+
cfg = AgentUtils::ConfFactory.new.createFromLine cmdLine
|
12
|
+
assert_equal( {:config=>'qwe.yml', :id=>'agent1', :cmd=>'start',:args=>['12:00']}, cfg )
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_confFactory2
|
16
|
+
ARGV.replace ["--forcepid", "--config", "file.yml", "-c", "Big test", "agent1", "start"]
|
17
|
+
cfg = AgentUtils::ConfFactory.new.createFromArgv *ARGV
|
18
|
+
assert_equal( {:config=>'file.yml', :c=>'Big test', :id=>'agent1', :cmd=>'start',:args=>[], :forcepid=>true}, cfg )
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_confFactoryEmptyLine
|
22
|
+
cfg = AgentUtils::ConfFactory.new.createFromLine ""
|
23
|
+
assert_equal( { :id=>'agentutils-agentbase', :cmd=>'run',:args=>[]}, cfg )
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_confFactoryOnlyCommand
|
27
|
+
cfg = AgentUtils::ConfFactory.new.createFromLine "run arg1 arg2"
|
28
|
+
assert_equal( { :id=>'agentutils-agentbase', :cmd=>'run',:args=>['arg1', 'arg2']}, cfg )
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
#Viktor Zigo, http://alephzarro.com, All rights reserved.
|
2
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
ENV['AGENTS_ROOT']=File.join(File.dirname(__FILE__), 'runtime') unless ENV['AGENTS_ROOT']
|
6
|
+
require 'agentdispatcher.rb'
|
7
|
+
|
8
|
+
#* *************** Test agents **************
|
9
|
+
|
10
|
+
class Agent
|
11
|
+
|
12
|
+
include AgentDispatcher
|
13
|
+
@DefaultCfg = {:shared=>0}
|
14
|
+
@AllowedCommands = %w(run)
|
15
|
+
|
16
|
+
attr_reader :cfg, :id
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@trace = ["myinit"]
|
20
|
+
end
|
21
|
+
|
22
|
+
def trace
|
23
|
+
@trace.join(',')
|
24
|
+
end
|
25
|
+
|
26
|
+
upMethod :initBussines
|
27
|
+
def initBussines
|
28
|
+
@trace<<'initbussines'
|
29
|
+
end
|
30
|
+
|
31
|
+
downMethod :closeBussines
|
32
|
+
def closeBussines
|
33
|
+
@trace<<'closebussines'
|
34
|
+
end
|
35
|
+
|
36
|
+
def on_run *args
|
37
|
+
@trace<<"#{@id}.run(#{args})"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
class AgentChild < Agent
|
43
|
+
@AllowedCommands = %w(start stop run)
|
44
|
+
|
45
|
+
upMethod :specialInit
|
46
|
+
def specialInit
|
47
|
+
@trace<<"specialInit"
|
48
|
+
end
|
49
|
+
|
50
|
+
def on_start *args
|
51
|
+
@trace<<"#{@id}.start(#{args})"
|
52
|
+
end
|
53
|
+
|
54
|
+
def on_stop *args
|
55
|
+
@trace<<"#{@id}.stop(#{args})"
|
56
|
+
end
|
57
|
+
|
58
|
+
def on_run *args
|
59
|
+
@trace<<"#{@id}.run2(#{args})"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class AgentChildB < Agent
|
64
|
+
def initialize myOwnConstructor
|
65
|
+
super()
|
66
|
+
@trace<<"ownConstructor:#{myOwnConstructor}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class AgentChildForced < Agent
|
71
|
+
def initialize
|
72
|
+
super()
|
73
|
+
@id = "myOwn"
|
74
|
+
@log = Logger.new(STDOUT)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
#* *************** Test-cases *********************
|
79
|
+
|
80
|
+
class AgentDispatcherTest< Test::Unit::TestCase
|
81
|
+
def test_basic
|
82
|
+
d = Agent::DispatchString "agent0 run arg1 arg2"
|
83
|
+
assert_equal 'myinit,initbussines,agent0.run(arg1arg2),closebussines', d.trace
|
84
|
+
|
85
|
+
d = Agent::Dispatch "agent0", "run", "args"
|
86
|
+
assert_equal 'myinit,initbussines,agent0.run(args),closebussines', d.trace
|
87
|
+
|
88
|
+
#no id
|
89
|
+
d=Agent::DispatchString "run"
|
90
|
+
assert_equal 'myinit,initbussines,agent.run(),closebussines', d.trace
|
91
|
+
|
92
|
+
#no command
|
93
|
+
d = Agent::Dispatch()
|
94
|
+
assert_equal 'myinit,initbussines,agent.run(),closebussines', d.trace
|
95
|
+
|
96
|
+
|
97
|
+
# Agent::DispatchOpts() TODO
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_childA
|
101
|
+
d = AgentChild::DispatchString "agent0 run arg1 arg2"
|
102
|
+
assert_equal 'myinit,initbussines,specialInit,agent0.run2(arg1arg2),closebussines', d.trace
|
103
|
+
|
104
|
+
d = AgentChild::DispatchString "agent0 start"
|
105
|
+
assert_equal 'myinit,initbussines,specialInit,agent0.start(),closebussines', d.trace
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_childB
|
109
|
+
d = AgentChildB.new 'xxx'
|
110
|
+
d.dispatchString "agent1 run args"
|
111
|
+
assert_equal 'myinit,ownConstructor:xxx,initbussines,agent1.run(args),closebussines', d.trace
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_badcommand
|
115
|
+
assert_raises (RuntimeError) {
|
116
|
+
d = Agent::Dispatch 'agent3', 'nocmd', 'xxx'
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
def test_config
|
121
|
+
#default config - root
|
122
|
+
d = Agent::DispatchString "agent1 run"
|
123
|
+
assert_equal( {:shared=>0}, hashStrip(d.cfg), 'config')
|
124
|
+
|
125
|
+
#default config - inherited
|
126
|
+
d = AgentChild::DispatchString "agent0 run"
|
127
|
+
assert_equal( {:shared=>0}, hashStrip(d.cfg), 'config')
|
128
|
+
|
129
|
+
root = ENV['AGENTS_ROOT']
|
130
|
+
#config with path and with filename
|
131
|
+
d = AgentChild::DispatchString "--config #{File.join(root,'config/config1.yml')} agent0 run"
|
132
|
+
assert_equal( {:shared=>123, :butcher=>true}, hashStrip(d.cfg), 'config')
|
133
|
+
|
134
|
+
#config with config name
|
135
|
+
d = AgentChild::DispatchString "--config config2 agent0 run"
|
136
|
+
assert_equal( {:shared=>456, :milkman=>true}, hashStrip(d.cfg), 'config')
|
137
|
+
|
138
|
+
#mutliple configs
|
139
|
+
d = AgentChild::DispatchString "--config config1,config2 agent0 run"
|
140
|
+
assert_equal( {:shared=>456, :butcher=>true, :milkman=>true}, hashStrip(d.cfg), 'config')
|
141
|
+
|
142
|
+
# with command line overwriting
|
143
|
+
d = AgentChild::DispatchString "--config config1,config2 --shared 999 agent0 run"
|
144
|
+
assert_equal( {:shared=>'999', :butcher=>true, :milkman=>true}, hashStrip(d.cfg), 'config')
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
#test with overriding @log and @id in constructor
|
149
|
+
def test_forcedProperties
|
150
|
+
d = AgentChildForced::DispatchString "agentForce run"
|
151
|
+
assert_equal 'myOwn', d.id, 'custom id'
|
152
|
+
end
|
153
|
+
|
154
|
+
def test_nopid
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_noruntime
|
158
|
+
end
|
159
|
+
private
|
160
|
+
def hashStrip sup
|
161
|
+
ex = [:log_path, :pid_path, :id, :cmd, :args, :config]
|
162
|
+
sup.reject {|k,v| ex.include? k}
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
data/test/test_opts.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
#Viktor Zigo, http://alephzarro.com, All rights reserved.
|
2
|
+
require 'test/unit'
|
3
|
+
thispath=File.dirname(__FILE__)
|
4
|
+
require File.join(thispath, '../lib', 'opts.rb')
|
5
|
+
|
6
|
+
class OptsTest< Test::Unit::TestCase
|
7
|
+
def test_dynamic1
|
8
|
+
ARGV.replace ["--forcepid", "--config", "file.cfg", "-c", "Big test", "anID", "start"]
|
9
|
+
p "Input: #{ARGV.inspect}"
|
10
|
+
opts = DynamicCmdParse .new.parse!
|
11
|
+
p "Output: #{opts.inspect}, ARGV = #{ARGV.inspect}"
|
12
|
+
assert_equal( {:forcepid=>true, :config=>'file.cfg', :c=>'Big test'}, opts, "parsed opts")
|
13
|
+
assert_equal( ['anID', 'start'], ARGV, "rest of argv")
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_dynamic2
|
17
|
+
ARGV.replace ["tooearly", "--c-1", "val1", "-c_2", "val2", "toolate"]
|
18
|
+
p "Input: #{ARGV.inspect}"
|
19
|
+
opts = DynamicCmdParse.new.parse!
|
20
|
+
p "Output: #{opts.inspect}, ARGV = #{ARGV.inspect}"
|
21
|
+
assert_equal( {:'c-1'=>'val1', :c_2=>'val2'}, opts, "parsed opts")
|
22
|
+
assert_equal( ['tooearly', 'toolate'], ARGV, "rest of argv")
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_dynamic3
|
26
|
+
#only one flag
|
27
|
+
ARGV.replace ["--help"]
|
28
|
+
p "Input: #{ARGV.inspect}"
|
29
|
+
opts = DynamicCmdParse .new.parse!
|
30
|
+
p "Output: #{opts.inspect}, ARGV = #{ARGV.inspect}"
|
31
|
+
assert_equal( {:help=>true}, opts, "parsed opts")
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_dynamicArray
|
35
|
+
ARGV.replace ["-array","1,a_3,fero"]
|
36
|
+
p "Input: #{ARGV.inspect}"
|
37
|
+
opts = DynamicCmdParse.new.parse!
|
38
|
+
p "Output: #{opts.inspect}, ARGV = #{ARGV.inspect}"
|
39
|
+
assert_equal( {:array=>['1','a_3','fero']}, opts, "parsed opts")
|
40
|
+
assert_equal( [], ARGV, "rest of argv")
|
41
|
+
end
|
42
|
+
|
43
|
+
class TestCmdParse < StaticCmdParser
|
44
|
+
def addOpts aParser, opts
|
45
|
+
aParser.on("-c", "--config YMLFILE", "Configuration file, can be overriden by cmdline opts") { |file| opts[:config] = file }
|
46
|
+
aParser.on( "-f", "--forcepid", "Forces execution without pid") { |v| opts[:pid]= v }
|
47
|
+
opts
|
48
|
+
end
|
49
|
+
def validateOpts opts, argv
|
50
|
+
#puts "XXXXXXXXX: #{opts.inspect}"
|
51
|
+
raise OptionParser::MissingArgument, "config" unless opts[:config]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_static1
|
56
|
+
ARGV.replace ["-c", "file.yml", "--forcepid", "rest1", "rest2"]
|
57
|
+
p "Input: #{ARGV.inspect}"
|
58
|
+
opts = TestCmdParse.new.parse!
|
59
|
+
p "Output: #{opts.inspect}, ARGV = #{ARGV.inspect}"
|
60
|
+
assert_equal( {:config=>"file.yml", :pid=>true}, opts, "opts" )
|
61
|
+
assert_equal( ["rest1", "rest2"], ARGV, "rest" )
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
#Viktor Zigo, http://alephzarro.com, All rights reserved. Distributed under GNU GPL License
|
2
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
require 'test/unit'
|
4
|
+
require 'simpledispatcher.rb'
|
5
|
+
|
6
|
+
class SimpleAgent
|
7
|
+
include SimpleDispatcher
|
8
|
+
@AllowedCommands = %w(start stop)
|
9
|
+
|
10
|
+
def initialize id = nil
|
11
|
+
puts "initializing #{self.class}, id=#{id}"
|
12
|
+
@trace = ["init:#{id}"]
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_start *args
|
16
|
+
@trace<< "start:#{args}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_stop *args
|
20
|
+
@trace<< "stop:#{args}"
|
21
|
+
end
|
22
|
+
attr_reader :trace
|
23
|
+
end
|
24
|
+
|
25
|
+
class SimpleAgentChild < SimpleAgent
|
26
|
+
@AllowedCommands = %w(start stop run)
|
27
|
+
|
28
|
+
def on_run *args
|
29
|
+
@trace<< "run2:#{args}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class SimpleDispatcherTest< Test::Unit::TestCase
|
34
|
+
|
35
|
+
def test_basic
|
36
|
+
d = SimpleAgent::Dispatch 'agent0', 'start', 'echo 42'
|
37
|
+
assert_equal "init:agent0,start:echo 42", d.trace.join(',')
|
38
|
+
|
39
|
+
#instance
|
40
|
+
d=SimpleAgent.new('agent0')
|
41
|
+
d.dispatch 'start', 'echo 42'
|
42
|
+
assert_equal "init:agent0,start:echo 42", d.trace.join(',')
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_badcommand
|
46
|
+
assert_raises (RuntimeError) {
|
47
|
+
d = SimpleAgent::Dispatch 'agent3', 'run', 'xxx'
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_lessParams
|
52
|
+
#no id
|
53
|
+
d = SimpleAgent::Dispatch 'start', 'echo 42'
|
54
|
+
assert_equal "init:simpleagent,start:echo 42", d.trace.join(',')
|
55
|
+
#nothing
|
56
|
+
d = SimpleAgent.Dispatch 'start'
|
57
|
+
assert_equal "init:simpleagent,start:", d.trace.join(',')
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_basicOpts
|
61
|
+
d = SimpleAgent::DispatchOpts :id=>'agent0', :cmd=>'start', :whatever=>'echo 42'
|
62
|
+
#assert_equal "init:agent0,start:cmdstartidagent0whateverecho 42", d.trace.join(',')
|
63
|
+
|
64
|
+
#instance
|
65
|
+
d = SimpleAgent::DispatchOpts :cmd=>'start', :whatever=>'echo 42'
|
66
|
+
#assert_equal "init:simpleagent,start:cmdstartwhateverecho 42", d.trace.join(',')
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def test_basicchild
|
71
|
+
d = SimpleAgentChild::Dispatch 'agent1', 'run', 'echo 42'
|
72
|
+
assert_equal "init:agent1,run2:echo 42", d.trace.join(',')
|
73
|
+
end
|
74
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: agentdispatcher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.7.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Viktor Zigo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-04-05 00:00:00 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: viz@alephzarro.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
files:
|
25
|
+
- lib/agentdispatcher.rb
|
26
|
+
- lib/opts.rb
|
27
|
+
- lib/proctools.rb
|
28
|
+
- lib/simpledispatcher.rb
|
29
|
+
- test/runtime
|
30
|
+
- test/runtime/var
|
31
|
+
- test/runtime/var/run
|
32
|
+
- test/runtime/config
|
33
|
+
- test/runtime/config/config1.yml
|
34
|
+
- test/runtime/config/config2.yml
|
35
|
+
- test/runtime/tmp
|
36
|
+
- test/runtime/tmp/log
|
37
|
+
- test/test_opts.rb
|
38
|
+
- test/test_agentdispatcher.rb
|
39
|
+
- test/test_simpledispather.rb
|
40
|
+
- test/test_agent_lowlevel.rb
|
41
|
+
- README
|
42
|
+
has_rdoc: true
|
43
|
+
homepage: http://rubyagent.rubyforge.org/
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project: rubyagent
|
64
|
+
rubygems_version: 1.0.1
|
65
|
+
signing_key:
|
66
|
+
specification_version: 2
|
67
|
+
summary: Solid infrastructure roots for script based agents
|
68
|
+
test_files:
|
69
|
+
- test/test_opts.rb
|
70
|
+
- test/test_agentdispatcher.rb
|
71
|
+
- test/test_simpledispather.rb
|
72
|
+
- test/test_agent_lowlevel.rb
|