agentdispatcher 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -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,2 @@
1
+ butcher: true
2
+ shared: 123
@@ -0,0 +1,2 @@
1
+ milkman: true
2
+ shared: 456
@@ -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
@@ -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