agentdispatcher 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|