oflow 0.6.0 → 0.8.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.
@@ -5,47 +5,234 @@ module OFlow
5
5
  # be thought of as a container for Tasks where the Flow keeps track of the
6
6
  # Links between the Tasks.
7
7
  class Flow
8
- include HasTasks
9
- include HasLinks
10
- include HasName
8
+
11
9
  include HasErrorHandler
12
10
  include HasLog
13
11
 
12
+ # The name.
13
+ attr_reader :name
14
+ attr_reader :env
15
+
14
16
  # Create a new Flow.
15
- # @param flow [Flow] Flow containing the Flow
17
+ # @param env [Env] Env containing the Flow
16
18
  # @param name [name] Flow base name
17
- # @param options [Hash] additional options for the Flow
18
- def initialize(flow, name, options)
19
- init_name(flow, name)
20
- init_tasks()
21
- init_links()
19
+ def initialize(env, name)
20
+ @name = name.to_sym
21
+ @tasks = {}
22
+ @prepared = false
23
+ @log = nil
24
+ @error_handler = nil
25
+ @env = env
26
+ end
27
+
28
+ # Similar to a full file path. The full_name described the containment of
29
+ # the named item.
30
+ # @return [String] full name of item
31
+ def full_name()
32
+ @name.to_s
33
+ end
34
+
35
+ # Returns a log Task by looking for that Task in an attribute and then in
36
+ # the contained Tasks or Tasks in outer Flows.
37
+ # @return [Task] log Task.
38
+ def log()
39
+ return @log unless @log.nil?
40
+ lg = find_task(:log)
41
+ return lg unless lg.nil?
42
+ @env.log
43
+ end
44
+
45
+ # Returns a error_handler Task by looking for that Task in an attribute and then in
46
+ # the contained Tasks or Tasks in outer Flows.
47
+ # @return [Task] error_handler Task.
48
+ def error_handler()
49
+ return @error_handler unless @error_handler.nil?
50
+ eh = find_task(:error)
51
+ return eh unless eh.nil?
52
+ @env.error_handler
53
+ end
54
+
55
+ # Creates a Task and yield to a block with the newly create Task. Used to
56
+ # configure Tasks.
57
+ # @param name [Symbol|String] base name for the Task
58
+ # @param actor_class [Class] Class to create an Actor instance of
59
+ # @param options [Hash] optional parameters
60
+ # @param block [Proc] block to yield to with the new Task instance
61
+ # @return [Task] new Task
62
+ def task(name, actor_class, options={}, &block)
63
+ has_state = options.has_key?(:state)
64
+ unless has_state
65
+ options = options.clone
66
+ options[:state] = Task::STOPPED
67
+ end
68
+ t = Task.new(self, name, actor_class, options)
69
+ @tasks[t.name] = t
70
+ yield(t) if block_given?
71
+ t
72
+ end
73
+
74
+ # Validates the container by verifying all links on a task have been set to
75
+ # a valid destination and that destination has been resolved.
76
+ # @raise [ValidateError] if there is an error in validation
77
+ def validate()
78
+ # collects errors and raises all errors at once if there are any
79
+ errors = _validation_errors()
80
+ raise ValidateError.new(errors) unless errors.empty?
81
+ end
82
+
83
+ # Returns an Array of validation errors.
84
+ def _validation_errors()
85
+ errors = []
86
+ @tasks.each_value { |t| errors += t._validation_errors() }
87
+ errors
88
+ end
89
+
90
+ # Resolves all the Links on all the Tasks and Flows being managed as well as
91
+ # any Links in the instance itself.
92
+ def resolve_all_links()
93
+ @tasks.each_value { |t|
94
+ t.resolve_all_links()
95
+ }
96
+ @prepared = true
97
+ end
98
+
99
+ # Iterates over each Task and yields to the provided block with each Task.
100
+ # @param blk [Proc] Proc to call on each iteration
101
+ def each_task(&blk)
102
+ @tasks.each { |name,task| blk.yield(task) }
103
+ end
104
+
105
+ # Performs a recursive walk over all Tasks and yields to the provided block
106
+ # for each. Flows are followed recusively.
107
+ # @param tasks_only [true|false] indicates on Tasks and not Flows are yielded to
108
+ # @param blk [Proc] Proc to call on each iteration
109
+ def walk_tasks(tasks_only=true, &blk)
110
+ @tasks.each_value do |t|
111
+ if t.is_a?(Task)
112
+ blk.yield(t)
113
+ else
114
+ blk.yield(t) unless tasks_only
115
+ t.walk_tasks(tasks_only, &blk)
116
+ end
117
+ end
118
+ end
119
+
120
+ # Locates and return a Task with the specified name.
121
+ # @param name [String] name of the Task
122
+ # @return [Task|nil] the Task with the name specified or nil
123
+ def find_task(name)
124
+ name = name.to_sym unless name.nil?
125
+ @tasks[name]
126
+ end
127
+
128
+ # Locates and return a Task with the specified full name.
129
+ # @param name [String] full name of the Task
130
+ # @return [Task|nil] the Task with the name specified or nil
131
+ def locate(name)
132
+ name = name[1..-1] if name.start_with?(':')
133
+ name = name[0..-2] if name.end_with?(':')
134
+ path = name.split(':')
135
+ _locate(path)
136
+ end
137
+
138
+ def _locate(path)
139
+ t = @tasks[path[0].to_sym]
140
+ return t if t.nil? || 1 == path.size
141
+ t._locate(path[1..-1])
142
+ end
143
+
144
+ # Returns the number of active Tasks.
145
+ def task_count()
146
+ @tasks.size
22
147
  end
23
148
 
24
- # Add a Link from the edge of the Flow to a Task contained in the Flow.
25
- # @param label [Symbol|String] identifier for the Link
26
- # @param task_name [Symbol|String] _name base name of teh Task to link to
27
- # @param op [Symbol|String] operation to call when forwarding a request to the target Task
28
- def route(label, task_name, op)
29
- op = op.to_sym unless op.nil?
30
- label = label.to_sym unless label.nil?
31
- raise ConfigError.new("Link #{label} already exists.") unless find_link(label).nil?
32
- @links[label] = Link.new(task_name.to_sym, op, true)
149
+ # Returns the sum of all the requests in all the Tasks's queues.
150
+ # @return [Fixnum] total number of items waiting to be processed
151
+ def queue_count()
152
+ cnt = 0
153
+ @tasks.each_value { |task| cnt += task.queue_count() }
154
+ cnt
33
155
  end
34
156
 
35
- # Receive a request which is redirected to a Linked target Task.
36
- # @param op [Symbol] identifies the link that points to the destination Task or Flow
37
- # @param box [Box] contents or data for the request
38
- def receive(op, box)
39
- box = box.receive(full_name, op)
40
- lnk = find_link(op)
41
- raise LinkError.new(op) if lnk.nil? || lnk.target.nil?
42
- lnk.target.receive(lnk.op, box)
157
+ # Returns true of one or more Tasks is either processing a request or has a
158
+ # request waiting to be processed on it's input queue.
159
+ # @return [true|false] the busy state across all Tasks
160
+ def busy?
161
+ @tasks.each_value { |task| return true if task.busy? }
162
+ false
43
163
  end
44
164
 
45
- # Returns true if the Flow has a Link identified by the op.
46
- # @param op [Symbol] identifies the Link in question
47
- def has_input(op)
48
- !find_link(op).nil?
165
+ # Calls the stop() method on all Tasks.
166
+ def stop()
167
+ @tasks.each_value { |task| task.stop() }
168
+ end
169
+
170
+ # Calls the step() method one Task that is stopped and has an item in the
171
+ # queue. The Tasks with the highest backed_up() value is selected.
172
+ def step()
173
+ max = 0.0
174
+ best = nil
175
+ walk_tasks() do |t|
176
+ if Task::STOPPED == t.state
177
+ bu = t.backed_up()
178
+ if max < bu
179
+ best = t
180
+ max = bu
181
+ end
182
+ end
183
+ end
184
+ best.step() unless best.nil?
185
+ best
186
+ end
187
+
188
+ # Calls the start() method on all Tasks.
189
+ def start()
190
+ raise ValidateError.new("#{full_name} not validated.") unless @prepared
191
+ @tasks.each_value { |task| task.start() }
192
+ end
193
+
194
+ # Wakes up all the Tasks in the Flow.
195
+ def wakeup()
196
+ @tasks.each_value { |t| t.wakeup() }
197
+ end
198
+
199
+ # Wakes up all the Tasks in the Flow and waits for the system to become idle
200
+ # before returning.
201
+ def flush()
202
+ wakeup()
203
+ @tasks.each_value { |t| t.flush() }
204
+ while busy?
205
+ sleep(0.2)
206
+ end
207
+ end
208
+
209
+ # Sets the state of all Tasks recursively. This should not be called
210
+ # directly.
211
+ def state=(s)
212
+ @tasks.each_value do |task|
213
+ task.state = s
214
+ end
215
+ end
216
+
217
+ # Shuts down all Tasks.
218
+ # @param flush_first [true|false] flag indicating shutdown should occur after the system becomes idle
219
+ def shutdown(flush_first=false)
220
+ # block all tasks first so threads can empty queues
221
+ @tasks.each_value do |task|
222
+ task.state = Task::BLOCKED
223
+ end
224
+ # shutdown and wait for queues to empty if necessary
225
+ @tasks.each_value do |task|
226
+ task.shutdown(flush_first)
227
+ end
228
+ @tasks = {}
229
+ end
230
+
231
+ # Clears out all Tasks and Flows and resets the object back to a empty state.
232
+ def clear()
233
+ shutdown()
234
+ @tasks = {}
235
+ _clear()
49
236
  end
50
237
 
51
238
  # Returns a String describing the Flow.
@@ -57,13 +244,6 @@ module OFlow
57
244
  @tasks.each_value { |t|
58
245
  lines << t.describe(detail, indent + 2)
59
246
  }
60
- @links.each { |local,link|
61
- if link.ingress
62
- lines << " #{i}#{local} * #{link.target_name}:#{link.op}"
63
- else
64
- lines << " #{i}#{local} => #{link.target_name}:#{link.op}"
65
- end
66
- }
67
247
  lines << i + "}"
68
248
  lines.join("\n")
69
249
  end
@@ -0,0 +1,293 @@
1
+
2
+ require 'ox'
3
+
4
+ module OFlow
5
+
6
+ class Graffle
7
+ attr_accessor :name
8
+ attr_accessor :line_info
9
+ attr_accessor :task_info
10
+
11
+ def self.load(env, filename)
12
+ doc = Ox.load_file(filename, mode: :generic)
13
+ g = Graffle.new(env, filename, doc)
14
+ env.debug(g.to_s()) if Logger::Severity::DEBUG >= ::OFlow::Env.log_level
15
+ end
16
+
17
+ def initialize(env, filename, doc)
18
+ @line_info = { } # key is id
19
+ @task_info = { } # key is id
20
+ @name = File.basename(filename, '.graffle')
21
+
22
+ nodes = doc.locate('plist/dict')[0].nodes
23
+ nodes = Graffle.get_key_value(nodes, 'GraphicsList')
24
+
25
+ raise ConfigError.new("Empty flow.") if nodes.nil?
26
+
27
+ nodes.each do |node|
28
+ load_node(node)
29
+ end
30
+
31
+ # set label in lines
32
+ task_info.each do |id,ti|
33
+ next if ti.line_id.nil?
34
+ if !(li = line_info[ti.line_id]).nil?
35
+ li.label, li.op = ti.first_option
36
+ end
37
+ end
38
+ task_info.each do |_, ti|
39
+ if ti.name.nil? && !(n = ti.options[:flow]).nil?
40
+ @name = n.strip
41
+ end
42
+ end
43
+ env.flow(@name.to_sym) { |f|
44
+ task_info.each_value { |ti|
45
+ next unless ti.line_id.nil?
46
+ next if ti.name.nil?
47
+ c = ti.get_class()
48
+ next if c.nil?
49
+ f.task(ti.name, c, ti.options) { |t|
50
+ t.bounds = ti.bounds
51
+ t.color = ti.color
52
+ t.shape = ti.shape
53
+ line_info.each_value { |li|
54
+ next unless li.tail == ti.id
55
+ target_info = task_info[li.head]
56
+ if target_info.nil?
57
+ flow_name, task_name, op = li.op.split('/')
58
+ t.link(li.label, task_name, op, flow_name)
59
+ else
60
+ t.link(li.label, target_info.name, li.op)
61
+ end
62
+ }
63
+ }
64
+ }
65
+ }
66
+ end
67
+
68
+ def load_node(node)
69
+ nodes = node.nodes
70
+ case Graffle.get_key_value(nodes, 'Class')
71
+ when 'LineGraphic'
72
+ line = LineInfo.new(nodes)
73
+ @line_info[line.id] = line
74
+ when 'ShapedGraphic'
75
+ task = TaskInfo.new(nodes)
76
+ @task_info[task.id] = task unless task.nil?
77
+ end
78
+ end
79
+
80
+ def to_s()
81
+ s = "Graffle{\n name: #{@name}\n tasks{\n"
82
+ task_info.each { |_,ti| s += " #{ti}\n" }
83
+ s += " }\n lines{\n"
84
+ line_info.each { |_,li| s += " #{li}\n" }
85
+ s += " }\n}\n"
86
+ s
87
+ end
88
+
89
+ def self.get_key_value(nodes, key)
90
+ return if nodes.nil?
91
+ nodes.each_with_index do |node,i|
92
+ next unless 'key' == node.name && key == node.text
93
+ n = nodes[i + 1]
94
+ if 'dict' == n.name || 'array' == n.name
95
+ return n.nodes
96
+ else
97
+ return n.text
98
+ end
99
+ end
100
+ nil
101
+ end
102
+
103
+ def self.get_text(nodes)
104
+ return nil if nodes.nil?
105
+ strip_rtf(get_key_value(nodes, 'Text'))
106
+ end
107
+
108
+ def self.strip_rtf(rtf)
109
+ return rtf unless rtf.start_with?('{\rtf')
110
+ depth = 0
111
+ txt = ''
112
+ ctrl = nil
113
+ str = nil
114
+ rtf.each_char do |c|
115
+ case c
116
+ when ' '
117
+ if !str.nil?
118
+ str << ' ' unless 1 < depth
119
+ elsif !ctrl.nil?
120
+ txt << rtf_ctrl_char(ctrl) unless 1 < depth
121
+ ctrl = nil
122
+ str = ''
123
+ else
124
+ str = ''
125
+ end
126
+ when "\n", "\r"
127
+ if !str.nil? # should not happen but...
128
+ str << "\n" unless 1 < depth
129
+ elsif !ctrl.nil?
130
+ txt << rtf_ctrl_char(ctrl) unless 1 < depth
131
+ ctrl = nil
132
+ end
133
+ when '\\'
134
+ if !str.nil?
135
+ txt << str unless 1 < depth
136
+ str = nil
137
+ elsif !ctrl.nil?
138
+ txt << rtf_ctrl_char(ctrl) unless 1 < depth
139
+ end
140
+ ctrl = ''
141
+ when '{'
142
+ if !ctrl.nil?
143
+ txt << rtf_ctrl_char(ctrl) unless 1 < depth
144
+ ctrl = nil
145
+ end
146
+ depth += 1
147
+ when '}'
148
+ if !ctrl.nil?
149
+ txt << rtf_ctrl_char(ctrl) unless 1 < depth
150
+ ctrl = nil
151
+ end
152
+ depth -= 1
153
+ else
154
+ if !ctrl.nil?
155
+ ctrl << c
156
+ elsif 1 < depth
157
+ # ignore
158
+ else
159
+ str = '' if str.nil?
160
+ str << c
161
+ end
162
+ end
163
+ end
164
+ txt << str unless str.nil?
165
+ txt
166
+ end
167
+
168
+ def self.rtf_ctrl_char(ctrl)
169
+ c = ''
170
+ case ctrl
171
+ when '', 'line'
172
+ c = "\n"
173
+ when 'tab'
174
+ c = "\t"
175
+ else
176
+ if "'" == ctrl[0]
177
+ c = ctrl[1..3].hex().chr
178
+ end
179
+ end
180
+ c
181
+ end
182
+
183
+ class Element
184
+ attr_accessor :id
185
+
186
+ def initialize(nodes)
187
+ return if nodes.nil?
188
+ @id = Graffle.get_key_value(nodes, 'ID')
189
+ end
190
+
191
+ end # Element
192
+
193
+ class LineInfo < Element
194
+ attr_accessor :tail
195
+ attr_accessor :head
196
+ attr_accessor :label
197
+ attr_accessor :op
198
+
199
+ def initialize(nodes)
200
+ super
201
+ @tail = Graffle.get_key_value(Graffle.get_key_value(nodes, 'Tail'), 'ID')
202
+ @head = Graffle.get_key_value(Graffle.get_key_value(nodes, 'Head'), 'ID')
203
+ end
204
+
205
+ def to_s()
206
+ "LineInfo{id:#{@id}, tail:#{@tail}, head:#{@head}, label: #{label}:#{op}}"
207
+ end
208
+ end # LineInfo
209
+
210
+ class TaskInfo < Element
211
+ attr_accessor :line_id
212
+ attr_accessor :name
213
+ attr_accessor :options
214
+ attr_accessor :color
215
+ attr_accessor :shape
216
+ attr_accessor :bounds
217
+
218
+ def initialize(nodes)
219
+ super
220
+ if (ln = Graffle.get_key_value(nodes, 'Line')).nil?
221
+ @line_id = nil
222
+ else
223
+ @line_id = Graffle.get_key_value(ln, 'ID')
224
+ end
225
+ @shape = Graffle.get_key_value(nodes, 'Shape')
226
+ color_node = Graffle.get_key_value(Graffle.get_key_value(Graffle.get_key_value(nodes, 'Style'), 'fill'), 'Color')
227
+ if color_node.nil?
228
+ @color = nil
229
+ else
230
+ @color = [Graffle.get_key_value(color_node, 'r').to_f,
231
+ Graffle.get_key_value(color_node, 'g').to_f,
232
+ Graffle.get_key_value(color_node, 'b').to_f]
233
+ end
234
+ unless (@bounds = Graffle.get_key_value(nodes, 'Bounds')).nil?
235
+ @bounds = @bounds.delete('{}').split(',')
236
+ @bounds.map! { |s| s.to_i }
237
+ end
238
+ @options = { }
239
+ text = Graffle.get_text(Graffle.get_key_value(nodes, 'Text'))
240
+ unless text.nil?
241
+ text.split("\n").each do |line|
242
+ pair = line.split(':', 2)
243
+ if 1 == pair.length
244
+ @name = pair[0]
245
+ else
246
+ k = pair[0]
247
+ if 0 == k.length
248
+ k = nil
249
+ else
250
+ k = k.to_sym
251
+ end
252
+ @options[k] = pair[1]
253
+ end
254
+ end
255
+ end
256
+ end
257
+
258
+ def first_option()
259
+ target = nil
260
+ op = nil
261
+ if name.nil?
262
+ options.each { |k,v| target, op = k, v; break }
263
+ else
264
+ target = name
265
+ end
266
+ [target, op]
267
+ end
268
+
269
+ def get_class()
270
+ return nil if options.nil?
271
+ return nil if (s = options[:class]).nil?
272
+ s.strip!
273
+ c = nil
274
+ begin
275
+ # TBD search all modules and classes for a match that is also an Actor
276
+
277
+ # Assume the environment is safe since it is being used to create
278
+ # processes from user provided code anyway.
279
+ c = Object.class_eval(s)
280
+ rescue Exception
281
+ c = ::OFlow::Actors.module_eval(s)
282
+ end
283
+ c
284
+ end
285
+
286
+ def to_s()
287
+ "TaskInfo{id:#{@id}, line_id:#{line_id}, name: #{name}, options: #{options}, bounds: #{@bounds}, shape: #{@shape}, color: #{@color}}"
288
+ end
289
+
290
+ end # TaskInfo
291
+
292
+ end # Graffle
293
+ end # OFlow