roby 0.7
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/.gitignore +29 -0
- data/History.txt +4 -0
- data/License-fr.txt +519 -0
- data/License.txt +515 -0
- data/Manifest.txt +245 -0
- data/NOTES +4 -0
- data/README.txt +163 -0
- data/Rakefile +161 -0
- data/TODO.txt +146 -0
- data/app/README.txt +24 -0
- data/app/Rakefile +8 -0
- data/app/config/ROBOT.rb +5 -0
- data/app/config/app.yml +91 -0
- data/app/config/init.rb +7 -0
- data/app/config/roby.yml +3 -0
- data/app/controllers/.gitattributes +0 -0
- data/app/controllers/ROBOT.rb +2 -0
- data/app/data/.gitattributes +0 -0
- data/app/planners/ROBOT/main.rb +6 -0
- data/app/planners/main.rb +5 -0
- data/app/scripts/distributed +3 -0
- data/app/scripts/generate/bookmarks +3 -0
- data/app/scripts/replay +3 -0
- data/app/scripts/results +3 -0
- data/app/scripts/run +3 -0
- data/app/scripts/server +3 -0
- data/app/scripts/shell +3 -0
- data/app/scripts/test +3 -0
- data/app/tasks/.gitattributes +0 -0
- data/app/tasks/ROBOT/.gitattributes +0 -0
- data/bin/roby +210 -0
- data/bin/roby-log +168 -0
- data/bin/roby-shell +25 -0
- data/doc/images/event_generalization.png +0 -0
- data/doc/images/exception_propagation_1.png +0 -0
- data/doc/images/exception_propagation_2.png +0 -0
- data/doc/images/exception_propagation_3.png +0 -0
- data/doc/images/exception_propagation_4.png +0 -0
- data/doc/images/exception_propagation_5.png +0 -0
- data/doc/images/replay_handler_error.png +0 -0
- data/doc/images/replay_handler_error_0.png +0 -0
- data/doc/images/replay_handler_error_1.png +0 -0
- data/doc/images/roby_cycle_overview.png +0 -0
- data/doc/images/roby_replay_02.png +0 -0
- data/doc/images/roby_replay_03.png +0 -0
- data/doc/images/roby_replay_04.png +0 -0
- data/doc/images/roby_replay_event_representation.png +0 -0
- data/doc/images/roby_replay_first_state.png +0 -0
- data/doc/images/roby_replay_relations.png +0 -0
- data/doc/images/roby_replay_startup.png +0 -0
- data/doc/images/task_event_generalization.png +0 -0
- data/doc/papers.rdoc +11 -0
- data/doc/styles/allison.css +314 -0
- data/doc/styles/allison.js +316 -0
- data/doc/styles/allison.rb +276 -0
- data/doc/styles/jamis.rb +593 -0
- data/doc/tutorials/01-GettingStarted.rdoc +86 -0
- data/doc/tutorials/02-GoForward.rdoc +220 -0
- data/doc/tutorials/03-PlannedPath.rdoc +268 -0
- data/doc/tutorials/04-EventPropagation.rdoc +236 -0
- data/doc/tutorials/05-ErrorHandling.rdoc +319 -0
- data/doc/tutorials/06-Overview.rdoc +40 -0
- data/doc/videos.rdoc +69 -0
- data/ext/droby/dump.cc +175 -0
- data/ext/droby/extconf.rb +3 -0
- data/ext/graph/algorithm.cc +746 -0
- data/ext/graph/extconf.rb +7 -0
- data/ext/graph/graph.cc +529 -0
- data/ext/graph/graph.hh +183 -0
- data/ext/graph/iterator_sequence.hh +102 -0
- data/ext/graph/undirected_dfs.hh +226 -0
- data/ext/graph/undirected_graph.hh +421 -0
- data/lib/roby.rb +41 -0
- data/lib/roby/app.rb +870 -0
- data/lib/roby/app/rake.rb +56 -0
- data/lib/roby/app/run.rb +14 -0
- data/lib/roby/app/scripts/distributed.rb +13 -0
- data/lib/roby/app/scripts/generate/bookmarks.rb +162 -0
- data/lib/roby/app/scripts/replay.rb +31 -0
- data/lib/roby/app/scripts/results.rb +15 -0
- data/lib/roby/app/scripts/run.rb +26 -0
- data/lib/roby/app/scripts/server.rb +18 -0
- data/lib/roby/app/scripts/shell.rb +88 -0
- data/lib/roby/app/scripts/test.rb +40 -0
- data/lib/roby/basic_object.rb +151 -0
- data/lib/roby/config.rb +5 -0
- data/lib/roby/control.rb +747 -0
- data/lib/roby/decision_control.rb +17 -0
- data/lib/roby/distributed.rb +32 -0
- data/lib/roby/distributed/base.rb +440 -0
- data/lib/roby/distributed/communication.rb +871 -0
- data/lib/roby/distributed/connection_space.rb +592 -0
- data/lib/roby/distributed/distributed_object.rb +206 -0
- data/lib/roby/distributed/drb.rb +62 -0
- data/lib/roby/distributed/notifications.rb +539 -0
- data/lib/roby/distributed/peer.rb +550 -0
- data/lib/roby/distributed/protocol.rb +529 -0
- data/lib/roby/distributed/proxy.rb +343 -0
- data/lib/roby/distributed/subscription.rb +311 -0
- data/lib/roby/distributed/transaction.rb +498 -0
- data/lib/roby/event.rb +897 -0
- data/lib/roby/exceptions.rb +234 -0
- data/lib/roby/executives/simple.rb +30 -0
- data/lib/roby/graph.rb +166 -0
- data/lib/roby/interface.rb +390 -0
- data/lib/roby/log.rb +3 -0
- data/lib/roby/log/chronicle.rb +303 -0
- data/lib/roby/log/console.rb +72 -0
- data/lib/roby/log/data_stream.rb +197 -0
- data/lib/roby/log/dot.rb +279 -0
- data/lib/roby/log/event_stream.rb +151 -0
- data/lib/roby/log/file.rb +340 -0
- data/lib/roby/log/gui/basic_display.ui +83 -0
- data/lib/roby/log/gui/chronicle.rb +26 -0
- data/lib/roby/log/gui/chronicle_view.rb +40 -0
- data/lib/roby/log/gui/chronicle_view.ui +70 -0
- data/lib/roby/log/gui/data_displays.rb +172 -0
- data/lib/roby/log/gui/data_displays.ui +155 -0
- data/lib/roby/log/gui/notifications.rb +26 -0
- data/lib/roby/log/gui/relations.rb +248 -0
- data/lib/roby/log/gui/relations.ui +123 -0
- data/lib/roby/log/gui/relations_view.rb +185 -0
- data/lib/roby/log/gui/relations_view.ui +149 -0
- data/lib/roby/log/gui/replay.rb +327 -0
- data/lib/roby/log/gui/replay_controls.rb +200 -0
- data/lib/roby/log/gui/replay_controls.ui +259 -0
- data/lib/roby/log/gui/runtime.rb +130 -0
- data/lib/roby/log/hooks.rb +185 -0
- data/lib/roby/log/logger.rb +202 -0
- data/lib/roby/log/notifications.rb +244 -0
- data/lib/roby/log/plan_rebuilder.rb +470 -0
- data/lib/roby/log/relations.rb +1056 -0
- data/lib/roby/log/server.rb +550 -0
- data/lib/roby/log/sqlite.rb +47 -0
- data/lib/roby/log/timings.rb +164 -0
- data/lib/roby/plan-object.rb +247 -0
- data/lib/roby/plan.rb +762 -0
- data/lib/roby/planning.rb +13 -0
- data/lib/roby/planning/loops.rb +302 -0
- data/lib/roby/planning/model.rb +906 -0
- data/lib/roby/planning/task.rb +151 -0
- data/lib/roby/propagation.rb +562 -0
- data/lib/roby/query.rb +619 -0
- data/lib/roby/relations.rb +583 -0
- data/lib/roby/relations/conflicts.rb +70 -0
- data/lib/roby/relations/ensured.rb +20 -0
- data/lib/roby/relations/error_handling.rb +23 -0
- data/lib/roby/relations/events.rb +9 -0
- data/lib/roby/relations/executed_by.rb +193 -0
- data/lib/roby/relations/hierarchy.rb +239 -0
- data/lib/roby/relations/influence.rb +10 -0
- data/lib/roby/relations/planned_by.rb +63 -0
- data/lib/roby/robot.rb +7 -0
- data/lib/roby/standard_errors.rb +218 -0
- data/lib/roby/state.rb +5 -0
- data/lib/roby/state/events.rb +221 -0
- data/lib/roby/state/information.rb +55 -0
- data/lib/roby/state/pos.rb +110 -0
- data/lib/roby/state/shapes.rb +32 -0
- data/lib/roby/state/state.rb +353 -0
- data/lib/roby/support.rb +92 -0
- data/lib/roby/task-operations.rb +182 -0
- data/lib/roby/task.rb +1618 -0
- data/lib/roby/test/common.rb +399 -0
- data/lib/roby/test/distributed.rb +214 -0
- data/lib/roby/test/tasks/empty_task.rb +9 -0
- data/lib/roby/test/tasks/goto.rb +36 -0
- data/lib/roby/test/tasks/simple_task.rb +23 -0
- data/lib/roby/test/testcase.rb +519 -0
- data/lib/roby/test/tools.rb +160 -0
- data/lib/roby/thread_task.rb +87 -0
- data/lib/roby/transactions.rb +462 -0
- data/lib/roby/transactions/proxy.rb +292 -0
- data/lib/roby/transactions/updates.rb +139 -0
- data/plugins/fault_injection/History.txt +4 -0
- data/plugins/fault_injection/README.txt +37 -0
- data/plugins/fault_injection/Rakefile +18 -0
- data/plugins/fault_injection/TODO.txt +0 -0
- data/plugins/fault_injection/app.rb +52 -0
- data/plugins/fault_injection/fault_injection.rb +89 -0
- data/plugins/fault_injection/test/test_fault_injection.rb +84 -0
- data/plugins/subsystems/README.txt +40 -0
- data/plugins/subsystems/Rakefile +18 -0
- data/plugins/subsystems/app.rb +171 -0
- data/plugins/subsystems/test/app/README +24 -0
- data/plugins/subsystems/test/app/Rakefile +8 -0
- data/plugins/subsystems/test/app/config/app.yml +71 -0
- data/plugins/subsystems/test/app/config/init.rb +9 -0
- data/plugins/subsystems/test/app/config/roby.yml +3 -0
- data/plugins/subsystems/test/app/planners/main.rb +20 -0
- data/plugins/subsystems/test/app/scripts/distributed +3 -0
- data/plugins/subsystems/test/app/scripts/replay +3 -0
- data/plugins/subsystems/test/app/scripts/results +3 -0
- data/plugins/subsystems/test/app/scripts/run +3 -0
- data/plugins/subsystems/test/app/scripts/server +3 -0
- data/plugins/subsystems/test/app/scripts/shell +3 -0
- data/plugins/subsystems/test/app/scripts/test +3 -0
- data/plugins/subsystems/test/app/tasks/services.rb +15 -0
- data/plugins/subsystems/test/test_subsystems.rb +71 -0
- data/test/distributed/test_communication.rb +178 -0
- data/test/distributed/test_connection.rb +282 -0
- data/test/distributed/test_execution.rb +373 -0
- data/test/distributed/test_mixed_plan.rb +341 -0
- data/test/distributed/test_plan_notifications.rb +238 -0
- data/test/distributed/test_protocol.rb +516 -0
- data/test/distributed/test_query.rb +102 -0
- data/test/distributed/test_remote_plan.rb +491 -0
- data/test/distributed/test_transaction.rb +463 -0
- data/test/mockups/tasks.rb +27 -0
- data/test/planning/test_loops.rb +380 -0
- data/test/planning/test_model.rb +427 -0
- data/test/planning/test_task.rb +106 -0
- data/test/relations/test_conflicts.rb +42 -0
- data/test/relations/test_ensured.rb +38 -0
- data/test/relations/test_executed_by.rb +149 -0
- data/test/relations/test_hierarchy.rb +158 -0
- data/test/relations/test_planned_by.rb +54 -0
- data/test/suite_core.rb +24 -0
- data/test/suite_distributed.rb +9 -0
- data/test/suite_planning.rb +3 -0
- data/test/suite_relations.rb +8 -0
- data/test/test_bgl.rb +508 -0
- data/test/test_control.rb +399 -0
- data/test/test_event.rb +894 -0
- data/test/test_exceptions.rb +592 -0
- data/test/test_interface.rb +37 -0
- data/test/test_log.rb +114 -0
- data/test/test_log_server.rb +132 -0
- data/test/test_plan.rb +584 -0
- data/test/test_propagation.rb +210 -0
- data/test/test_query.rb +266 -0
- data/test/test_relations.rb +180 -0
- data/test/test_state.rb +414 -0
- data/test/test_support.rb +16 -0
- data/test/test_task.rb +938 -0
- data/test/test_testcase.rb +122 -0
- data/test/test_thread_task.rb +73 -0
- data/test/test_transactions.rb +569 -0
- data/test/test_transactions_proxy.rb +198 -0
- metadata +570 -0
data/lib/roby/log/dot.rb
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
require 'roby/distributed/protocol'
|
|
2
|
+
require 'roby/log'
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Roby
|
|
7
|
+
module LoggedPlan
|
|
8
|
+
attr_accessor :layout_level
|
|
9
|
+
def all_events(display)
|
|
10
|
+
known_tasks.inject(free_events.dup) do |events, task|
|
|
11
|
+
if display.displayed?(task)
|
|
12
|
+
events.merge(task.events.values.to_value_set)
|
|
13
|
+
else
|
|
14
|
+
events
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :dot_id
|
|
20
|
+
def to_dot(display, io, level)
|
|
21
|
+
@layout_level = level
|
|
22
|
+
id = io.layout_id(self)
|
|
23
|
+
@dot_id = "plan_#{id}"
|
|
24
|
+
io << "subgraph cluster_#{dot_id} {\n"
|
|
25
|
+
(known_tasks | finalized_tasks | free_events | finalized_events).
|
|
26
|
+
each do |obj|
|
|
27
|
+
obj.to_dot(display, io) if display.displayed?(obj)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
io << "};\n"
|
|
31
|
+
|
|
32
|
+
transactions.each do |trsc|
|
|
33
|
+
trsc.to_dot(display, io, level + 1)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
relations_to_dot(display, io, TaskStructure, known_tasks)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def each_displayed_relation(display, space, objects)
|
|
40
|
+
space.relations.each do |rel|
|
|
41
|
+
next unless display.relation_enabled?(rel)
|
|
42
|
+
|
|
43
|
+
objects.each do |from|
|
|
44
|
+
next unless display.displayed?(from)
|
|
45
|
+
unless display[from]
|
|
46
|
+
Roby::Log.warn "no display item for #{from} in #each_displayed_relation"
|
|
47
|
+
next
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
from.each_child_object(rel) do |to|
|
|
51
|
+
next unless display.displayed?(to)
|
|
52
|
+
unless display[to]
|
|
53
|
+
Roby::Log.warn "no display item for child in #{from} <#{rel}> #{to} in #each_displayed_relation"
|
|
54
|
+
next
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
yield(rel, from, to)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def relations_to_dot(display, io, space, objects)
|
|
64
|
+
each_displayed_relation(display, space, objects) do |rel, from, to|
|
|
65
|
+
from_id, to_id = from.dot_id, to.dot_id
|
|
66
|
+
if from_id && to_id
|
|
67
|
+
io << " #{from_id} -> #{to_id}\n"
|
|
68
|
+
else
|
|
69
|
+
Roby::Log.warn "ignoring #{from}(#{from.object_id} #{from_id}) -> #{to}(#{to.object_id} #{to_id}) in #{rel} in #{caller(1).join("\n ")}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def layout_relations(positions, display, space, objects)
|
|
75
|
+
each_displayed_relation(display, space, objects) do |rel, from, to|
|
|
76
|
+
display.task_relation(from, to, rel, from[to, rel])
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The distance from the root plan
|
|
81
|
+
attr_reader :depth
|
|
82
|
+
|
|
83
|
+
# Computes the plan depths and max_depth for this plan and all its
|
|
84
|
+
# children. +depth+ is this plan depth
|
|
85
|
+
#
|
|
86
|
+
# Returns max_depth
|
|
87
|
+
def compute_depth(depth)
|
|
88
|
+
@depth = depth
|
|
89
|
+
child_depth = transactions.
|
|
90
|
+
map { |trsc| trsc.compute_depth(depth + 1) }.
|
|
91
|
+
max
|
|
92
|
+
child_depth || depth
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def apply_layout(bounding_rects, positions, display, max_depth = nil)
|
|
96
|
+
max_depth ||= compute_depth(0)
|
|
97
|
+
|
|
98
|
+
if rect = bounding_rects[dot_id]
|
|
99
|
+
item = display[self]
|
|
100
|
+
rect[2] *= 1.2
|
|
101
|
+
rect[3] *= 1.2
|
|
102
|
+
item.z_value = Log::PLAN_LAYER + depth - max_depth
|
|
103
|
+
item.set_rect *rect
|
|
104
|
+
else
|
|
105
|
+
Roby::Log.warn "no bounding rectangle for #{self} (#{dot_id})"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
(known_tasks | finalized_tasks | free_events | finalized_events).
|
|
110
|
+
each do |obj|
|
|
111
|
+
obj.apply_layout(positions, display)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
transactions.each do |trsc|
|
|
115
|
+
trsc.apply_layout(bounding_rects, positions, display, max_depth)
|
|
116
|
+
end
|
|
117
|
+
layout_relations(positions, display, TaskStructure, known_tasks)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
module LoggedPlanObject
|
|
122
|
+
attr_reader :dot_id
|
|
123
|
+
|
|
124
|
+
def dot_label(display); display_name(display) end
|
|
125
|
+
|
|
126
|
+
# Adds the dot definition for this object in +io+
|
|
127
|
+
def to_dot(display, io)
|
|
128
|
+
return unless display.displayed?(self)
|
|
129
|
+
@dot_id ||= "plan_object_#{io.layout_id(self)}"
|
|
130
|
+
io << " #{dot_id}[label=\"#{dot_label(display).split("\n").join('\n')}\"];\n"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Applys the layout in +positions+ to this particular object
|
|
134
|
+
def apply_layout(positions, display)
|
|
135
|
+
return unless display.displayed?(self)
|
|
136
|
+
if p = positions[dot_id]
|
|
137
|
+
raise "no graphics for #{self}" unless graphics_item = display[self]
|
|
138
|
+
graphics_item.pos = p
|
|
139
|
+
else
|
|
140
|
+
STDERR.puts "WARN: #{self} has not been layouted"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
class PlanObject::DRoby
|
|
146
|
+
include LoggedPlanObject
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
class TaskEventGenerator::DRoby
|
|
150
|
+
def dot_label(display); symbol.to_s end
|
|
151
|
+
def dot_id; task.dot_id end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
module LoggedTask
|
|
155
|
+
include LoggedPlanObject
|
|
156
|
+
def dot_label(display)
|
|
157
|
+
event_names = events.values.find_all { |ev| display.displayed?(ev) }.
|
|
158
|
+
map { |ev| ev.dot_label(display) }.
|
|
159
|
+
join(" ")
|
|
160
|
+
|
|
161
|
+
own = super
|
|
162
|
+
if own.size > event_names.size then own
|
|
163
|
+
else event_names
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
module Log
|
|
169
|
+
class Layout
|
|
170
|
+
@@bkpindex = 0
|
|
171
|
+
|
|
172
|
+
def layout_id(object)
|
|
173
|
+
id = Object.address_from_id(object.object_id).to_s
|
|
174
|
+
object_ids[id] = object
|
|
175
|
+
id
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
attribute(:object_ids) { Hash.new }
|
|
179
|
+
attr_reader :dot_input
|
|
180
|
+
|
|
181
|
+
def <<(string); dot_input << string end
|
|
182
|
+
def layout(display, plan)
|
|
183
|
+
@@index ||= 0
|
|
184
|
+
@@index += 1
|
|
185
|
+
|
|
186
|
+
# Dot input file
|
|
187
|
+
@dot_input = Tempfile.new("roby_dot")
|
|
188
|
+
# Dot output file
|
|
189
|
+
dot_output = Tempfile.new("roby_layout")
|
|
190
|
+
|
|
191
|
+
dot_input << "digraph relations {\n"
|
|
192
|
+
display.layout_options.each do |k, v|
|
|
193
|
+
dot_input << " #{k}=#{v};\n"
|
|
194
|
+
end
|
|
195
|
+
plan.to_dot(display, self, 0)
|
|
196
|
+
|
|
197
|
+
# Take the signalling into account for the layout
|
|
198
|
+
display.propagated_events.each do |_, sources, to, _|
|
|
199
|
+
sources.each do |from|
|
|
200
|
+
from_id, to_id = from.dot_id, to.dot_id
|
|
201
|
+
if from_id && to_id
|
|
202
|
+
dot_input << " #{from.dot_id} -> #{to.dot_id}\n"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
dot_input << "\n};"
|
|
208
|
+
dot_input.flush
|
|
209
|
+
|
|
210
|
+
# Make sure the GUI keeps being updated while dot is processing
|
|
211
|
+
FileUtils.cp dot_input.path, "/tmp/dot-input-#{@@index}.dot"
|
|
212
|
+
system("#{display.layout_method} #{dot_input.path} > #{dot_output.path}")
|
|
213
|
+
#pid = fork do
|
|
214
|
+
# exec("#{display.layout_method} #{dot_input.path} > #{dot_output.path}")
|
|
215
|
+
#end
|
|
216
|
+
#while !Process.waitpid(pid, Process::WNOHANG)
|
|
217
|
+
# if Qt::Application.has_pending_events
|
|
218
|
+
# Qt::Application.process_events
|
|
219
|
+
# else
|
|
220
|
+
# sleep(0.05)
|
|
221
|
+
# end
|
|
222
|
+
#end
|
|
223
|
+
FileUtils.cp dot_output.path, "/tmp/dot-output-#{@@index}.dot"
|
|
224
|
+
|
|
225
|
+
# Load only task bounding boxes from dot, update arrows later
|
|
226
|
+
current_graph_id = nil
|
|
227
|
+
bounding_rects = Hash.new
|
|
228
|
+
object_pos = Hash.new
|
|
229
|
+
lines = File.open(dot_output.path) { |io| io.readlines }
|
|
230
|
+
full_line = ""
|
|
231
|
+
lines.each do |line|
|
|
232
|
+
line.chomp!
|
|
233
|
+
full_line << line
|
|
234
|
+
if line[-1] == ?\\
|
|
235
|
+
full_line.chop!
|
|
236
|
+
next
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
case full_line
|
|
240
|
+
when /((?:\w+_)+\d+) \[.*pos="(\d+),(\d+)"/
|
|
241
|
+
object_pos[$1] = Qt::PointF.new(Integer($2), Integer($3))
|
|
242
|
+
when /subgraph cluster_(plan_\d+)/
|
|
243
|
+
current_graph_id = $1
|
|
244
|
+
when /graph \[bb="(\d+),(\d+),(\d+),(\d+)"\]/
|
|
245
|
+
bb = [$1, $2, $3, $4].map do |c|
|
|
246
|
+
c = Integer(c)
|
|
247
|
+
end
|
|
248
|
+
bounding_rects[current_graph_id] = [bb[0], bb[1], bb[2] - bb[0], bb[3] - bb[1]]
|
|
249
|
+
end
|
|
250
|
+
full_line = ""
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
graph_bb = bounding_rects.delete(nil)
|
|
254
|
+
bounding_rects.each_value do |coords|
|
|
255
|
+
coords[0] -= graph_bb[0]
|
|
256
|
+
coords[1] = graph_bb[1] - coords[1] - coords[3]
|
|
257
|
+
end
|
|
258
|
+
object_pos.each do |id, pos|
|
|
259
|
+
pos.x -= graph_bb[0]
|
|
260
|
+
pos.y = graph_bb[1] - pos.y
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
@display = display
|
|
264
|
+
@plan = plan
|
|
265
|
+
@object_pos = object_pos
|
|
266
|
+
@bounding_rects = bounding_rects
|
|
267
|
+
|
|
268
|
+
ensure
|
|
269
|
+
dot_input.close! if dot_input
|
|
270
|
+
dot_output.close! if dot_output
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
attr_reader :bounding_rects, :object_pos, :display, :plan
|
|
274
|
+
def apply
|
|
275
|
+
plan.apply_layout(bounding_rects, object_pos, display)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
require 'roby/log/data_stream'
|
|
2
|
+
|
|
3
|
+
module Roby
|
|
4
|
+
module Log
|
|
5
|
+
# This class is a logger-compatible interface which read event and index logs,
|
|
6
|
+
# and may rebuild the task and event graphs from the marshalled events
|
|
7
|
+
# that are saved using for instance FileLogger
|
|
8
|
+
class EventStream < DataStream
|
|
9
|
+
def splat?; true end
|
|
10
|
+
|
|
11
|
+
# The event log
|
|
12
|
+
attr_reader :logfile
|
|
13
|
+
|
|
14
|
+
# The index of the currently displayed cycle in +index_data+
|
|
15
|
+
attr_reader :current_cycle
|
|
16
|
+
# The index of the first non-empty cycle
|
|
17
|
+
attr_reader :start_cycle
|
|
18
|
+
# A [min, max] array of the minimum and maximum times for this
|
|
19
|
+
# stream
|
|
20
|
+
def range; [start_time, logfile.range.last] end
|
|
21
|
+
|
|
22
|
+
def initialize(basename, file = nil)
|
|
23
|
+
super(basename, "roby-events")
|
|
24
|
+
if file
|
|
25
|
+
@logfile = file
|
|
26
|
+
reinit!
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
def open
|
|
30
|
+
@logfile = Roby::Log.open(name)
|
|
31
|
+
reinit!
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
def close; @logfile.close end
|
|
35
|
+
|
|
36
|
+
def index_data; logfile.index_data end
|
|
37
|
+
|
|
38
|
+
# True if the stream has been reinitialized
|
|
39
|
+
def reinit?
|
|
40
|
+
@reinit ||= (!index_data.empty? && logfile.stat.size < index_data.last[:pos])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Reinitializes the stream
|
|
44
|
+
def reinit!
|
|
45
|
+
prepare_seek(nil)
|
|
46
|
+
|
|
47
|
+
super
|
|
48
|
+
|
|
49
|
+
start_cycle = 0
|
|
50
|
+
while start_cycle < index_data.size && index_data[start_cycle][:event_count] == 4
|
|
51
|
+
start_cycle += 1
|
|
52
|
+
end
|
|
53
|
+
@start_cycle = start_cycle
|
|
54
|
+
@current_cycle = start_cycle
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# True if there is at least one sample available
|
|
58
|
+
def has_sample?
|
|
59
|
+
logfile.update_index
|
|
60
|
+
!index_data.empty? && (index_data.last[:pos] > logfile.tell)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Seek the data stream to the specified time.
|
|
64
|
+
def prepare_seek(time)
|
|
65
|
+
if !time || !current_time || time < current_time
|
|
66
|
+
clear
|
|
67
|
+
|
|
68
|
+
@current_time = nil
|
|
69
|
+
@current_cycle = start_cycle
|
|
70
|
+
logfile.rewind
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def start_time
|
|
75
|
+
return if start_cycle == index_data.size
|
|
76
|
+
Time.at(*index_data[start_cycle][:start])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The current time
|
|
80
|
+
def current_time
|
|
81
|
+
return if index_data.empty?
|
|
82
|
+
time = Time.at(*index_data[current_cycle][:start])
|
|
83
|
+
if index_data.size == current_cycle + 1
|
|
84
|
+
time += index_data[current_cycle][:end]
|
|
85
|
+
end
|
|
86
|
+
time
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The time we will reach when the next sample is processed
|
|
90
|
+
def next_time
|
|
91
|
+
return if index_data.empty?
|
|
92
|
+
if index_data.size > current_cycle + 1
|
|
93
|
+
Time.at(*index_data[current_cycle + 1][:start])
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Reads a sample of data and returns it. It will be fed to
|
|
98
|
+
# decoders' #decode method.
|
|
99
|
+
#
|
|
100
|
+
# In this stream, this is the chunk of the marshalled file which
|
|
101
|
+
# corresponds to a cycle
|
|
102
|
+
def read
|
|
103
|
+
if reinit?
|
|
104
|
+
reinit!
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
start_pos = index_data[current_cycle][:pos]
|
|
108
|
+
end_pos = if index_data.size > current_cycle + 1
|
|
109
|
+
index_data[current_cycle + 1][:pos]
|
|
110
|
+
else
|
|
111
|
+
logfile.stat.size
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
logfile.seek(start_pos)
|
|
115
|
+
logfile.read(end_pos - start_pos)
|
|
116
|
+
|
|
117
|
+
ensure
|
|
118
|
+
@current_cycle += 1
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Unmarshalls a set of data returned by #read_all and yield
|
|
122
|
+
# each sample that should be fed to the decoders
|
|
123
|
+
def self.init(data)
|
|
124
|
+
io = StringIO.new(data)
|
|
125
|
+
while !io.eof?
|
|
126
|
+
yield(Marshal.load(io))
|
|
127
|
+
end
|
|
128
|
+
rescue EOFError
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Unmarshalls one cycle of data returned by #read and feeds
|
|
132
|
+
# it to the decoders
|
|
133
|
+
def self.decode(data)
|
|
134
|
+
Marshal.load(data)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Read all data read so far in a format suitable to feed to
|
|
138
|
+
# #init_stream on the decoding side
|
|
139
|
+
def read_all
|
|
140
|
+
end_pos = if index_data.size > current_cycle + 1
|
|
141
|
+
index_data[current_cycle + 1][:pos]
|
|
142
|
+
else
|
|
143
|
+
logfile.stat.size
|
|
144
|
+
end
|
|
145
|
+
logfile.rewind
|
|
146
|
+
logfile.read(end_pos)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
require 'roby/log/logger'
|
|
2
|
+
require 'roby/distributed'
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Roby::Log
|
|
7
|
+
class Logfile < DelegateClass(File)
|
|
8
|
+
attr_reader :event_io
|
|
9
|
+
attr_reader :index_io
|
|
10
|
+
attr_reader :index_data
|
|
11
|
+
attr_reader :basename
|
|
12
|
+
def range
|
|
13
|
+
[Time.at(*index_data.first[:start]),
|
|
14
|
+
Time.at(*index_data.last[:start]) + index_data.last[:end]]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(file, allow_old_format = false, force_rebuild_index = false)
|
|
18
|
+
@event_io = if file.respond_to?(:to_str)
|
|
19
|
+
@basename = if file =~ /-events\.log$/ then $`
|
|
20
|
+
else file
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
File.open("#{basename}-events.log")
|
|
24
|
+
else
|
|
25
|
+
@basename = file.path.gsub(/-events\.log$/, '')
|
|
26
|
+
file
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if !allow_old_format
|
|
30
|
+
FileLogger.check_format(@event_io)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
index_path = "#{basename}-index.log"
|
|
34
|
+
if force_rebuild_index || !File.file?(index_path)
|
|
35
|
+
rebuild_index
|
|
36
|
+
else
|
|
37
|
+
@index_io = File.open(index_path)
|
|
38
|
+
if @index_io.stat.size == 0
|
|
39
|
+
rebuild_index
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
super(@event_io)
|
|
44
|
+
|
|
45
|
+
@index_data = Array.new
|
|
46
|
+
update_index
|
|
47
|
+
rewind
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Reads as much index data as possible
|
|
51
|
+
def update_index
|
|
52
|
+
begin
|
|
53
|
+
pos = nil
|
|
54
|
+
loop do
|
|
55
|
+
pos = index_io.tell
|
|
56
|
+
length = index_io.read(4)
|
|
57
|
+
raise EOFError unless length
|
|
58
|
+
length = length.unpack("N").first
|
|
59
|
+
index_data << Marshal.load(index_io.read(length))
|
|
60
|
+
end
|
|
61
|
+
rescue EOFError
|
|
62
|
+
index_io.seek(pos, IO::SEEK_SET)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
return if index_data.empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def rewind
|
|
69
|
+
@event_io.rewind
|
|
70
|
+
Marshal.load(@event_io)
|
|
71
|
+
@index_io.rewind
|
|
72
|
+
@index_data.clear
|
|
73
|
+
update_index
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def rebuild_index
|
|
77
|
+
STDOUT.puts "rebuilding index file for #{basename}"
|
|
78
|
+
@index_io.close if @index_io
|
|
79
|
+
@index_io = File.open("#{basename}-index.log", 'w+')
|
|
80
|
+
FileLogger.rebuild_index(@event_io, @index_io)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.open(path)
|
|
84
|
+
io = new(path)
|
|
85
|
+
if block_given?
|
|
86
|
+
begin
|
|
87
|
+
yield(io)
|
|
88
|
+
ensure
|
|
89
|
+
io.close unless io.closed?
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
io
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A logger object which marshals all available events in two files. The
|
|
98
|
+
# event log is the full log, the index log contains only the timings given
|
|
99
|
+
# to Control#cycle_end, along with the corresponding position in the event
|
|
100
|
+
# log file.
|
|
101
|
+
#
|
|
102
|
+
# You can use FileLogger.replay(io) to send the events back into the
|
|
103
|
+
# logging system (using Log.log), for instance to feed an offline display
|
|
104
|
+
class FileLogger
|
|
105
|
+
# The current log format version
|
|
106
|
+
FORMAT_VERSION = 3
|
|
107
|
+
|
|
108
|
+
@dumped = Hash.new
|
|
109
|
+
class << self
|
|
110
|
+
attr_reader :dumped
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The IO object for the event log
|
|
114
|
+
attr_reader :event_log
|
|
115
|
+
# The IO object for the index log
|
|
116
|
+
attr_reader :index_log
|
|
117
|
+
# The set of events for the current cycle. This is dumped only
|
|
118
|
+
# when the +cycle_end+ event is received
|
|
119
|
+
attr_reader :current_cycle
|
|
120
|
+
# StringIO object on which we dump the data
|
|
121
|
+
attr_reader :dump_io
|
|
122
|
+
|
|
123
|
+
def initialize(basename)
|
|
124
|
+
@current_pos = 0
|
|
125
|
+
@dump_io = StringIO.new('', 'w')
|
|
126
|
+
@current_cycle = Array.new
|
|
127
|
+
@event_log = File.open("#{basename}-events.log", 'w')
|
|
128
|
+
event_log.sync = true
|
|
129
|
+
FileLogger.write_header(@event_log)
|
|
130
|
+
@index_log = File.open("#{basename}-index.log", 'w')
|
|
131
|
+
index_log.sync = true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
attr_accessor :stats_mode
|
|
135
|
+
def splat?; false end
|
|
136
|
+
|
|
137
|
+
def dump_method(m, time, args)
|
|
138
|
+
if m == :cycle_end || !stats_mode
|
|
139
|
+
current_cycle << m << time.tv_sec << time.tv_usec << args
|
|
140
|
+
end
|
|
141
|
+
if m == :cycle_end
|
|
142
|
+
info = args.first
|
|
143
|
+
info[:pos] = event_log.tell
|
|
144
|
+
info[:event_count] = current_cycle.size
|
|
145
|
+
Marshal.dump(current_cycle, event_log)
|
|
146
|
+
|
|
147
|
+
dump_io.truncate(0)
|
|
148
|
+
dump_io.seek(0)
|
|
149
|
+
Marshal.dump(info, dump_io)
|
|
150
|
+
index_log.write [dump_io.size].pack("N")
|
|
151
|
+
index_log.write dump_io.string
|
|
152
|
+
current_cycle.clear
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
rescue
|
|
156
|
+
puts "failed to dump #{m}#{args}: #{$!.full_message}"
|
|
157
|
+
args.each do |obj|
|
|
158
|
+
unless (Marshal.dump(obj) rescue nil)
|
|
159
|
+
puts "there is a problem with"
|
|
160
|
+
pp obj
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
Roby::Log.each_hook do |klass, m|
|
|
166
|
+
define_method(m) { |time, args| dump_method(m, time, args) }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Creates an index file for +event_log+ in +index_log+
|
|
170
|
+
def self.rebuild_index(event_log, index_log)
|
|
171
|
+
event_log.rewind
|
|
172
|
+
# Skip the file header
|
|
173
|
+
Marshal.load(event_log)
|
|
174
|
+
|
|
175
|
+
current_pos = event_log.tell
|
|
176
|
+
dump_io = StringIO.new("", 'w')
|
|
177
|
+
|
|
178
|
+
loop do
|
|
179
|
+
cycle = Marshal.load(event_log)
|
|
180
|
+
info = cycle.last.last
|
|
181
|
+
info[:pos] = current_pos
|
|
182
|
+
info[:event_count] = cycle.size
|
|
183
|
+
|
|
184
|
+
dump_io.truncate(0)
|
|
185
|
+
dump_io.seek(0)
|
|
186
|
+
Marshal.dump(info, dump_io)
|
|
187
|
+
index_log.write [dump_io.size].pack("N")
|
|
188
|
+
index_log.write dump_io.string
|
|
189
|
+
|
|
190
|
+
current_pos = event_log.tell
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
rescue EOFError
|
|
194
|
+
ensure
|
|
195
|
+
event_log.rewind
|
|
196
|
+
index_log.rewind
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def self.log_format(input)
|
|
201
|
+
input.rewind
|
|
202
|
+
format = begin
|
|
203
|
+
header = Marshal.load(input)
|
|
204
|
+
case header
|
|
205
|
+
when Hash: header[:log_format]
|
|
206
|
+
when Symbol
|
|
207
|
+
if Marshal.load(input).kind_of?(Array)
|
|
208
|
+
0
|
|
209
|
+
end
|
|
210
|
+
when Array
|
|
211
|
+
1 if header[-2] == :cycle_end
|
|
212
|
+
end
|
|
213
|
+
rescue
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
unless format
|
|
217
|
+
raise "#{input.path} does not look like a Roby event log file"
|
|
218
|
+
end
|
|
219
|
+
format
|
|
220
|
+
|
|
221
|
+
ensure
|
|
222
|
+
input.rewind
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def self.check_format(input)
|
|
226
|
+
format = log_format(input)
|
|
227
|
+
if format < FORMAT_VERSION
|
|
228
|
+
raise "this is an outdated format. Please run roby-log upgrade-format"
|
|
229
|
+
elsif format > FORMAT_VERSION
|
|
230
|
+
raise "this is an unknown format version #{format}: expected #{FORMAT_VERSION}. This file can be read only by newest version of Roby"
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def self.write_header(io)
|
|
235
|
+
header = { :log_format => FORMAT_VERSION }
|
|
236
|
+
Marshal.dump(header, io)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def self.from_format_0(input, output)
|
|
240
|
+
current_cycle = []
|
|
241
|
+
while !input.eof?
|
|
242
|
+
m = Marshal.load(input)
|
|
243
|
+
args = Marshal.load(input)
|
|
244
|
+
|
|
245
|
+
current_cycle << m << args
|
|
246
|
+
if m == :cycle_end
|
|
247
|
+
Marshal.dump(current_cycle, output)
|
|
248
|
+
current_cycle.clear
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
unless current_cycle.empty?
|
|
253
|
+
Marshal.dump(current_cycle, output)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def self.from_format_1(input, output)
|
|
258
|
+
# The only difference between v1 and v2 is the header. Just copy
|
|
259
|
+
# data from input to output
|
|
260
|
+
output.write(input.read)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def self.from_format_2(input, output)
|
|
264
|
+
# In format 3, we did two things:
|
|
265
|
+
# * changed the way exceptions were dumped: instead of relying on
|
|
266
|
+
# DRbRemoteError, we are now using DRobyModel
|
|
267
|
+
# * stopped dumping Time objects and instead marshalled tv_sec
|
|
268
|
+
# and tv_usec directly
|
|
269
|
+
Exception::DRoby.class_eval do
|
|
270
|
+
def self._load(str)
|
|
271
|
+
Marshal.load(str)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
new_cycle = []
|
|
276
|
+
input_stream = EventStream.new("input", Logfile.new(input, true))
|
|
277
|
+
while !input.eof?
|
|
278
|
+
new_cycle.clear
|
|
279
|
+
begin
|
|
280
|
+
m_data = input_stream.read
|
|
281
|
+
cycle_data = Marshal.load(m_data)
|
|
282
|
+
cycle_data.each_slice(2) do |m, args|
|
|
283
|
+
time = args.shift
|
|
284
|
+
new_cycle << m << time.tv_sec << time.tv_usec << args
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
stats = new_cycle.last.first
|
|
288
|
+
reftime = stats[:start]
|
|
289
|
+
stats[:start] = [reftime.tv_sec, reftime.tv_usec]
|
|
290
|
+
new_cycle.last[0] = stats.inject(Hash.new) do |new_stats, (name, value)|
|
|
291
|
+
new_stats[name] = if value.kind_of?(Time) then value - reftime
|
|
292
|
+
else value
|
|
293
|
+
end
|
|
294
|
+
new_stats
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
Marshal.dump(new_cycle, output)
|
|
298
|
+
rescue Exception => e
|
|
299
|
+
STDERR.puts "dropped cycle because of the following error:"
|
|
300
|
+
STDERR.puts " #{e.full_message}"
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def self.to_new_format(file, into = file)
|
|
306
|
+
input = File.open(file)
|
|
307
|
+
log_format = self.log_format(input)
|
|
308
|
+
|
|
309
|
+
if log_format == FORMAT_VERSION
|
|
310
|
+
STDERR.puts "#{file} is already at format #{log_format}"
|
|
311
|
+
else
|
|
312
|
+
if into =~ /-events\.log$/
|
|
313
|
+
into = $`
|
|
314
|
+
end
|
|
315
|
+
STDERR.puts "upgrading #{file} from format #{log_format} into #{into}"
|
|
316
|
+
|
|
317
|
+
input.rewind
|
|
318
|
+
Tempfile.open('roby_to_new_format') do |output|
|
|
319
|
+
write_header(output)
|
|
320
|
+
send("from_format_#{log_format}", input, output)
|
|
321
|
+
output.flush
|
|
322
|
+
|
|
323
|
+
input.close
|
|
324
|
+
FileUtils.cp output.path, "#{into}-events.log"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
File.open("#{into}-events.log") do |event_log|
|
|
328
|
+
File.open("#{into}-index.log", 'w') do |index_log|
|
|
329
|
+
puts "rebuilding index file for #{into}"
|
|
330
|
+
rebuild_index(event_log, index_log)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
ensure
|
|
336
|
+
input.close if input && !input.closed?
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|