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 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