pampa_workers 1.1.3

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.
@@ -0,0 +1,9 @@
1
+ module BlackStack
2
+
3
+ # es un proceso sin conexion a base de datos, que itera infinitamente.
4
+ # en cada iteracion saluda a la central (hello), obtiene parametros (get)
5
+ class MyChildProcess < BlackStack::MyProcess
6
+
7
+ end # MyChildProcess
8
+
9
+ end # module BlackStack
@@ -0,0 +1,49 @@
1
+ module BlackStack
2
+
3
+ # process class
4
+ class MyCrawlProcess < BlackStack::MyLocalProcess
5
+
6
+ attr_accessor :nErrors, :nSuccesses, :browser, :proxy, :bot
7
+
8
+ # constructor
9
+ def initialize(
10
+ the_worker_name,
11
+ the_division_name,
12
+ the_minimum_enlapsed_seconds=MyProcess::DEFAULT_MINIMUM_ENLAPSED_SECONDS,
13
+ the_verify_configuration=true,
14
+ the_email=nil,
15
+ the_password=nil
16
+ )
17
+ super(the_worker_name, the_division_name, the_minimum_enlapsed_seconds, the_verify_configuration, the_email, the_password)
18
+ self.nErrors = 0
19
+ self.nSuccesses = 0
20
+ self.browser = nil
21
+ self.proxy = nil
22
+ self.bot = nil
23
+ end
24
+
25
+ def canRun?()
26
+ super &&
27
+ nErrors < Params.getValue("crawl.company.discretion.max_errors") &&
28
+ nSuccesses < Params.getValue("crawl.company.discretion.max_successes")
29
+ #(Params.getValue("crawl.company.use_proxy")==false || Company.availableProxiesWithDiscretionForCrawl() > 0)
30
+ end
31
+
32
+ def whyCantRun()
33
+ ret = super
34
+ if (ret.to_s.size == 0)
35
+ if (self.nErrors >= Params.getValue("crawl.company.discretion.max_errors"))
36
+ return "Reached the max number of errors (#{self.nErrors.to_s})"
37
+ end
38
+
39
+ if (self.nSuccesses >= Params.getValue("crawl.company.discretion.max_successes"))
40
+ return "Reached the max number of successes (#{self.nSuccesses.to_s})"
41
+ end
42
+ end
43
+ return ret
44
+ end
45
+
46
+ end # class MyCrawlProcess
47
+
48
+
49
+ end # module BlackStack
@@ -0,0 +1,170 @@
1
+ module BlackStack
2
+
3
+ # Process located in the same LAN than the Database Server
4
+ class MyLocalProcess < BlackStack::MyChildProcess
5
+
6
+ # constructor
7
+ def initialize(
8
+ the_worker_name,
9
+ the_division_name,
10
+ the_minimum_enlapsed_seconds=BlackStack::MyProcess::DEFAULT_MINIMUM_ENLAPSED_SECONDS,
11
+ the_verify_configuration=true,
12
+ the_email=nil,
13
+ the_password=nil
14
+ )
15
+ super(the_worker_name, the_division_name, the_minimum_enlapsed_seconds, the_verify_configuration, the_email, the_password)
16
+ end
17
+
18
+ def division()
19
+ if (self.division_name != "local")
20
+ d = BlackStack::Division.where(:name=>self.division_name).first
21
+ if (d!=nil)
22
+ return BlackStack::Division.where(:db_name=>d.db_name, :home=>true).first
23
+ else
24
+ return nil
25
+ end
26
+ else
27
+ return BlackStack::Division.where(:central=>true).first
28
+ end
29
+ end
30
+
31
+ def worker()
32
+ BlackStack::Worker.where(:name=>self.fullWorkerName).first
33
+ end
34
+
35
+
36
+ # update worker configuration in the division
37
+ def updateWorker()
38
+ w = BlackStack::Worker.where(:name=>self.fullWorkerName).first
39
+ if (w==nil)
40
+ w = BlackStack::Worker.new
41
+ w.id = guid()
42
+ w.process = ''
43
+ w.last_ping_time = now()
44
+ w.name = self.fullWorkerName
45
+ w.assigned_process = self.assigned_process
46
+ w.id_client = self.id_client
47
+ w.division_name = self.division_name
48
+ w.save
49
+ end
50
+ if (w!=nil)
51
+ w.assigned_process = self.assigned_process
52
+ w.id_client = self.id_client
53
+ w.division_name = self.division_name
54
+ w.id_division = self.id_division
55
+ w.save
56
+ end
57
+ end
58
+
59
+ def run()
60
+ super
61
+
62
+ # creo el objeto logger
63
+ self.logger = RemoteLogger.new(
64
+ "#{self.fullWorkerName}.log",
65
+ BlackStack::Pampa::api_protocol,
66
+ BlackStack::Pampa::api_domain,
67
+ BlackStack::Pampa::api_port,
68
+ BlackStack::Pampa::api_key,
69
+ self.id_client # ID of the client that has this thread assigned
70
+ )
71
+
72
+ # announcing my in the log
73
+ logger.log "Child process is alive!"
74
+
75
+ # obtengo los parametros del worker
76
+ logger.logs "Update from central (1-local)... "
77
+ self.get
78
+ logger.done
79
+
80
+ # actualizo los datos de este worker (parent process)
81
+ logger.logs "Update worker (1-local)... "
82
+ self.updateWorker
83
+ logger.done
84
+
85
+ # actualizo los datos de este worker (parent process)
86
+ logger.logs "Switch logger id_client (log folder may change)... "
87
+ self.logger.id_client = self.id_client
88
+ logger.done
89
+
90
+ while (self.canRun?)
91
+ # reseteo en contador nested del logger
92
+ self.logger.reset()
93
+
94
+ # announcing my in the log
95
+ logger.log "Going to Run Local"
96
+ logger.log "Process: #{self.assigned_process.to_s}."
97
+ logger.log "Client: #{(self.id_client.to_s.size==0)? 'n/a' : self.id_client.to_s}."
98
+
99
+ # obtengo la hora de inicio
100
+ start_time = Time.now
101
+
102
+ begin
103
+ # libero recursos
104
+ logger.logs "Release resources... "
105
+ GC.start
106
+ DB.disconnect
107
+ logger.done
108
+
109
+ # cargo el objeto worker
110
+ logger.logs "Load the worker... "
111
+ the_worker = self.worker
112
+ logger.done
113
+
114
+ # actualizo el valor del proceso que corre actualmente para este worker
115
+ logger.logs "Update current process... "
116
+ the_worker.process=self.assigned_process
117
+ the_worker.active = true
118
+ the_worker.save()
119
+ logger.done
120
+
121
+ logger.logs "Ping... "
122
+ the_worker.ping()
123
+ logger.done
124
+
125
+ # corro el procesamiento
126
+ self.process(ARGV)
127
+
128
+ rescue => e
129
+ puts ""
130
+ logger.log "Local Process Error: " + e.to_s + "\r\n" + e.backtrace.join("\r\n").to_s
131
+ end
132
+
133
+ # obtengo los parametros del worker
134
+ logger.logs "Update from central (2)... "
135
+ self.get
136
+ logger.done
137
+
138
+ # actualizo los datos de este worker (parent process)
139
+ logger.logs "Update worker (2)... "
140
+ self.updateWorker
141
+ logger.done
142
+
143
+ # sleep
144
+ logger.logs "Sleep... "
145
+ self.doSleep(start_time)
146
+ logger.done
147
+
148
+ logger.log "-------------------------------------------"
149
+
150
+ DB.disconnect
151
+ GC.start
152
+ end # main while
153
+
154
+ #
155
+ logger.log "Process Finish!"
156
+ logger.log "Finish Reason: " + self.whyCantRun.to_s
157
+
158
+ #
159
+ logger.logs "Disconnect to Database... "
160
+ begin
161
+ DB.disconnect()
162
+ logger.done
163
+ rescue => e
164
+ logger.error(e)
165
+ end
166
+ end # run
167
+
168
+ end # class MyLocalProcess
169
+
170
+ end # module BlackStack
@@ -0,0 +1,142 @@
1
+ module BlackStack
2
+
3
+ # es un proceso sin conexion a base de datos, que itera infinitamente.
4
+ # en cada iteracion saluda a la central (hello), obtiene parametros (get)
5
+ class MyParentProcess < BlackStack::MyProcess
6
+ def run()
7
+ super
8
+
9
+ # creo el objeto logger
10
+ self.logger = BlackStack::RemoteLogger.new(
11
+ "#{self.fullWorkerName}.log",
12
+ BlackStack::Pampa::api_protocol,
13
+ BlackStack::Pampa::api_domain,
14
+ BlackStack::Pampa::api_port,
15
+ BlackStack::Pampa::api_key,
16
+ self.id_client # ID of the client that has this thread assigned
17
+ )
18
+
19
+ #
20
+ pid = nil
21
+ while (true)
22
+ begin
23
+ GC.start # 331 - avoid lack of memory
24
+ #DB.disconnect # este proceso esta desacoplado de la conexion a la base de datos
25
+
26
+ # reseteo en contador nested del logger
27
+ self.logger.reset()
28
+
29
+ # get the start time
30
+ start_time = Time.now
31
+
32
+ # consulto a la central por la division asignada
33
+ url = "#{BlackStack::Pampa::api_url}/api1.3/pampa/hello.json"
34
+ logger.logs("Hello to the central... ")
35
+ res = BlackStack::Netting::call_post(url, {
36
+ 'api_key' => BlackStack::Pampa::api_key,
37
+ 'name' => self.fullWorkerName }.merge( BlackStack::RemoteHost.new.poll )
38
+ )
39
+ parsed = JSON.parse(res.body)
40
+ if (parsed['status'] != BlackStack::Netting::SUCCESS)
41
+ self.logger.logf("Error: " + parsed['status'].to_s)
42
+ else
43
+ self.logger.done
44
+
45
+ logger.logs("Get worker data... ")
46
+ url = "#{BlackStack::Pampa::api_url}/api1.3/pampa/get.json"
47
+ res = BlackStack::Netting::call_post(url, {
48
+ 'api_key' => BlackStack::Pampa::api_key,
49
+ 'name' => self.fullWorkerName }.merge( BlackStack::RemoteHost.new.poll )
50
+ )
51
+ parsed = JSON.parse(res.body)
52
+ if (parsed['status'] != BlackStack::Netting::SUCCESS)
53
+ self.logger.logf("Error: " + parsed['status'].to_s)
54
+ else
55
+ # map response
56
+ self.id = parsed['id']
57
+ self.assigned_process = parsed['assigned_process']
58
+ self.id_client = parsed['id_client']
59
+ self.id_division = parsed['id_division']
60
+ self.division_name = parsed['division_name']
61
+ self.ws_url = parsed['ws_url']
62
+ self.ws_port = parsed['ws_port']
63
+ self.logger.logf "done (#{self.division_name})"
64
+
65
+ #
66
+ self.logger.logs "Notify division... "
67
+ if self.division_name.to_s.size == 0
68
+ self.logger.logf "no division assigned"
69
+ else
70
+ self.notify # notifico a la division
71
+ self.logger.done
72
+
73
+ #
74
+ self.logger.logs "Spawn child process... "
75
+ # lanzo el proceso
76
+ if self.assigned_process.to_s.size > 0
77
+ command = "ruby #{self.assigned_process} name=#{self.worker_name} division=#{self.division_name}"
78
+ pid = Process.spawn(command)
79
+ logger.logf "done (pid=#{pid.to_s})"
80
+
81
+ logger.log("Wait to child process to finish.")
82
+ Process.wait(pid)
83
+ else
84
+ if self.assigned_process.to_s.size == 0
85
+ self.logger.logf "no process assigned"
86
+ end
87
+ end # if self.assigned_process.to_s.size > 0
88
+ end # if self.division_name.to_s.size == 0
89
+ end # if (parsed['status'] != "success") <-- #{BlackStack::Pampa::api_url}/api1.3/pampa/get.json
90
+ end # if (parsed['status'] != "success") <-- #{BlackStack::Pampa::api_url}/api1.3/pampa/hello.json
91
+
92
+ #
93
+ logger.logs "Sleep... "
94
+ self.doSleep(start_time)
95
+ logger.done
96
+
97
+ logger.log "-------------------------------------------"
98
+
99
+ rescue Interrupt => e
100
+ logger.reset
101
+
102
+ logger.log "Interrupt signal!"
103
+
104
+ logger.logs "Kill process... "
105
+ if (pid!=nil)
106
+ system("taskkill /im #{pid.to_s} /f /t >nul 2>&1")
107
+ end
108
+ logger.done
109
+
110
+ logger.logs "Disconnect to Database... "
111
+ begin
112
+ # DB.disconnect()
113
+ logger.done
114
+ rescue => e
115
+ logger.error(e)
116
+ end
117
+
118
+ logger.log "Process is out."
119
+ exit(0)
120
+
121
+ rescue => e
122
+ begin
123
+ logger.log "Unhandled exception: #{e.to_s}\r\n#{e.backtrace.join("\r\n").to_s}"
124
+ logger.logs "Sleep #{self.minimum_enlapsed_seconds.to_s} seconds... "
125
+ sleep(self.minimum_enlapsed_seconds)
126
+ logger.done
127
+ rescue => e
128
+ puts "Fatal error: #{e.to_s}"
129
+ print "Sleep #{self.minimum_enlapsed_seconds.to_s} seconds... "
130
+ sleep(self.minimum_enlapsed_seconds)
131
+ puts
132
+ end
133
+
134
+ end # rescue
135
+
136
+ end # while
137
+
138
+ end # def run()
139
+
140
+ end # class MyParentProcess
141
+
142
+ end # module BlackStack
@@ -0,0 +1,265 @@
1
+ module BlackStack
2
+
3
+ class MyProcess
4
+ DEFAULT_MINIMUM_ENLAPSED_SECONDS = 60
5
+
6
+ attr_accessor :assigned_process_changed, :assigned_division_changed, :verify_configuration
7
+ attr_accessor :logger, :id, :worker_name, :division_name, :minimum_enlapsed_seconds, :assigned_process, :id_client, :id_division, :ws_url, :ws_port
8
+ attr_accessor :email, :password
9
+
10
+ # constructor
11
+ def initialize(
12
+ the_worker_name,
13
+ the_division_name,
14
+ the_minimum_enlapsed_seconds=MyProcess::DEFAULT_MINIMUM_ENLAPSED_SECONDS,
15
+ the_verify_configuration=true,
16
+ the_email=nil,
17
+ the_password=nil
18
+ )
19
+ self.assigned_process_changed = false
20
+ self.assigned_division_changed = false
21
+ self.assigned_process = File.expand_path($0)
22
+ self.worker_name = "#{the_worker_name}"
23
+ self.division_name = the_division_name
24
+ self.minimum_enlapsed_seconds = the_minimum_enlapsed_seconds
25
+ self.verify_configuration = the_verify_configuration
26
+ self.email = the_email
27
+ self.password = the_password
28
+ end
29
+
30
+ # retrieves the id of the current process
31
+ def pid()
32
+ Process.pid.to_s
33
+ end
34
+
35
+ # Retorna un array de hashes.
36
+ # => Cada elemento del hash tiene la forma: {:executablepath, :pid, :ppid},
37
+ # => donde imagename es el patch completo del proceso, pid es el id del proceso
38
+ # => y ppid es el id del proceso padre.
39
+ def list()
40
+ a = []
41
+ s = `wmic process get executablepath,processid,parentprocessid`
42
+ s.split(/\n+/).each { |e|
43
+ aux = e.strip.scan(/^(.+)\s+(\d+)\s+(\d+)$/)[0]
44
+ if (aux!=nil)
45
+ if (aux.size>=3)
46
+ a << {
47
+ :executablepath => aux[0].strip.to_s,
48
+ :pid => aux[2].to_s, # TODO: deberia ser aux[1], pero por algo que no entiendo ahora el pid viene en aux[2]
49
+ :ppid => aux[1].to_s, # TODO: deberia ser aux[2], pero por algo que no entiendo ahora el pid viene en aux[1]
50
+ }
51
+ end
52
+ end
53
+ }
54
+ a
55
+ end
56
+
57
+ # ejecuta TASKKILL /F /PID #{the_pid} y retorna el output del comando
58
+ def self.kill(the_pid)
59
+ system("TASKKILL /F /PID #{the_pid}")
60
+ end
61
+
62
+ # obtiene la diferencia en segundos entre la hora actual y el parametro the_start_time.
63
+ # si la diferencia es mayor al atributo minimum_enlapsed_seconds, entonces duerme el tiempo restante.
64
+ def doSleep(the_start_time)
65
+ # si el proceso tardo menos del minimum_enlapsed_seconds, entonces duermo el tiempo restante
66
+ end_time = Time.now
67
+ elapsed_seconds = end_time - the_start_time # in seconds
68
+ if (elapsed_seconds < self.minimum_enlapsed_seconds)
69
+ sleep_seconds = self.minimum_enlapsed_seconds - elapsed_seconds
70
+ sleep(sleep_seconds)
71
+ end
72
+ end
73
+
74
+ # This function works in windows only
75
+ # TODO: Esta funcion no retorna la mac address completa
76
+ # TODO: Validar que no se retorne una macaddress virtual, con todos valores en 0
77
+ def self.macaddress()
78
+ BlackStack::SimpleHostMonitoring.macaddress
79
+ end
80
+
81
+ def self.fullWorkerName(name)
82
+ "#{Socket.gethostname}.#{MyProcess.macaddress}.#{name}"
83
+ end
84
+
85
+ def fullWorkerName()
86
+ MyProcess.fullWorkerName(self.worker_name)
87
+ end
88
+
89
+ # saluda a la central
90
+ def hello()
91
+ # me notifico a la central. obtengo asignacion si ya la tenia
92
+ url = "#{BlackStack::Pampa::api_url}/api1.3/pampa/hello.json"
93
+ res = BlackStack::Netting::call_post(url, {
94
+ 'api_key' => BlackStack::Pampa::api_key,
95
+ 'name' => self.fullWorkerName }.merge( BlackStack::RemoteHost.new.poll )
96
+ )
97
+ parsed = JSON.parse(res.body)
98
+ if (parsed['status'] != BlackStack::Netting::SUCCESS)
99
+ raise parsed['status'].to_s
100
+ end
101
+ end # hello()
102
+
103
+ # notifico mis parametros (assigned_process, id_client) a la division asignada
104
+ def set(new_assigned_process, new_id_client)
105
+ if (self.ws_url.to_s.size > 0 && self.ws_port.to_s.size > 0)
106
+ url = "#{BlackStack::Pampa::api_protocol}://#{self.ws_url.to_s}:#{self.ws_port.to_s}/api1.3/pampa/notify.json"
107
+ res = BlackStack::Netting::call_post(url, {
108
+ 'api_key' => BlackStack::Pampa::api_key,
109
+ 'name' => self.fullWorkerName,
110
+ 'assigned_process' => new_assigned_process,
111
+ 'id_client' => new_id_client }.merge( BlackStack::RemoteHost.new.poll )
112
+ )
113
+ end
114
+ end
115
+
116
+ # obtiene sus parametros de la central
117
+ def get()
118
+ # me notifico a la central. obtengo asignacion que tenga
119
+ url = "#{BlackStack::Pampa::api_url}/api1.3/pampa/get.json"
120
+ res = BlackStack::Netting::call_post(url, {
121
+ 'api_key' => BlackStack::Pampa::api_key,
122
+ 'name' => self.fullWorkerName }.merge( BlackStack::RemoteHost.new.poll )
123
+ )
124
+ parsed = JSON.parse(res.body)
125
+ if (parsed['status'] != BlackStack::Netting::SUCCESS)
126
+ raise parsed['status'].to_s
127
+ else
128
+ if self.verify_configuration
129
+ # si ya tenia un proceso asignado, y ahora se le asigna un nuevo proceso
130
+ if self.assigned_process.to_s.size > 0
131
+ a = File.expand_path(self.assigned_process)
132
+ b = File.expand_path(parsed['assigned_process'])
133
+ if a != b
134
+ self.assigned_process_changed = true
135
+ else
136
+ self.assigned_process_changed = false
137
+ end
138
+ end
139
+
140
+ # si ya tenia un proceso asignado, y ahora se le asigna un nuevo proceso
141
+ if self.id_division.to_s.size > 0
142
+ if self.id_division.to_guid != parsed['id_division'].to_guid
143
+ self.assigned_division_changed = true
144
+ else
145
+ self.assigned_division_changed = false
146
+ end
147
+ end
148
+ end # verify_configuration
149
+
150
+ # si ya tenia asignada una division, entonces le notifico mi nueva configuracion
151
+ self.set(parsed['assigned_process'], parsed['id_client'])
152
+
153
+ self.id = parsed['id']
154
+ self.assigned_process = parsed['assigned_process']
155
+ self.id_client = parsed['id_client']
156
+ self.id_division = parsed['id_division']
157
+ self.division_name = parsed['division_name']
158
+ self.ws_url = parsed['ws_url']
159
+ self.ws_port = parsed['ws_port']
160
+
161
+ # le notifico a la nueva division asignada mi nueva configuracion
162
+ self.set(parsed['assigned_process'], parsed['id_client'])
163
+ end
164
+ end # get()
165
+
166
+
167
+ # update worker configuration in the division
168
+ def updateWorker()
169
+ raise "Abstract Method."
170
+ end
171
+
172
+ # ping the central database
173
+ def ping()
174
+ # me notifico a la central.
175
+ url = "#{BlackStack::Pampa::api_url}/api1.3/pampa/ping.json"
176
+ res = BlackStack::Netting::call_post(url, {
177
+ 'api_key' => BlackStack::Pampa::api_key,
178
+ 'name' => self.fullWorkerName }.merge( BlackStack::RemoteHost.new.poll )
179
+ )
180
+ parsed = JSON.parse(res.body)
181
+ if (parsed['status'] != BlackStack::Netting::SUCCESS)
182
+ raise parsed['status'].to_s
183
+ end
184
+
185
+ # me notifico a la division.
186
+ if (self.ws_url != nil && self.ws_port != nil)
187
+ url = "#{BlackStack::Pampa::api_protocol}://#{self.ws_url.to_s}:#{self.ws_port.to_s}/api1.3/pampa/ping.json"
188
+ res = BlackStack::Netting::call_post(url, {
189
+ 'api_key' => BlackStack::Pampa::api_key,
190
+ 'name' => self.fullWorkerName }.merge( BlackStack::RemoteHost.new.poll )
191
+ )
192
+ parsed = JSON.parse(res.body)
193
+ if (parsed['status'] != "success")
194
+ raise parsed['status'].to_s
195
+ end
196
+ end # if
197
+ end # ping()
198
+
199
+ # se notifica al dispatcher de la division
200
+ def notify()
201
+ if (self.ws_url==nil || self.ws_port==nil)
202
+ raise "Cannot notify. Worker has not parameters."
203
+ end
204
+
205
+ # me notifico a la division. obtengo trabajo
206
+ url = "#{BlackStack::Pampa::api_protocol}://#{self.ws_url}:#{self.ws_port}/api1.3/pampa/notify.json"
207
+ res = BlackStack::Netting::call_post(url,
208
+ {
209
+ 'api_key' => BlackStack::Pampa::api_key,
210
+ 'name' => self.fullWorkerName,
211
+ 'assigned_process' => self.assigned_process,
212
+ 'id_client' => self.id_client,
213
+ 'id_division' => self.id_division }.merge( BlackStack::RemoteHost.new.poll )
214
+ )
215
+ parsed = JSON.parse(res.body)
216
+ if (parsed['status'] != "success")
217
+ raise parsed['status'].to_s
218
+ end
219
+ end
220
+
221
+ # Get the data object of the divison assigned to this worker.
222
+ # Needs database connections. So it's available for ChildProcess only.
223
+ def division()
224
+ raise "This is an abstract method."
225
+ end
226
+
227
+ # Get the data object of worker linked to this process.
228
+ # Needs database connections. So it's available for ChildProcess only.
229
+ def worker()
230
+ raise "This is an abstract method."
231
+ end
232
+
233
+ # retorna true si el proceso hijo (child) esta habilitado para trabajar.
234
+ def canRun?()
235
+ self.assigned_process_changed == false &&
236
+ self.assigned_division_changed == false
237
+ end
238
+
239
+ def whyCantRun()
240
+ if self.assigned_process_changed == true
241
+ return "Assigned process has changed."
242
+ elsif self.assigned_division_changed == true
243
+ return "Assigned division has changed."
244
+ else
245
+ return "unknown"
246
+ end
247
+ end
248
+
249
+ # este metodo
250
+ # ejecuta el trabajo para el que fue creado el objeto.
251
+ def process(argv)
252
+ raise "This is an abstract method."
253
+ end
254
+
255
+ # ejecuta el proceso, en modo parent, bot o child segun la clase que se implemente.
256
+ # en modo parent, hace un loop infinito.
257
+ # en modo bot o child, hace un loop hasta que el metodo canRun? retorne false.
258
+ # en modo bot o child, invoca al metodo process() en cada ciclo.
259
+ def run()
260
+ #raise "This is an abstract method"
261
+ end # run
262
+
263
+ end # class MyProcess
264
+
265
+ end # module BlackStack