3583: replace " characters with ' characters in node value to ensure graph does not...
[arvados.git] / apps / workbench / app / helpers / provenance_helper.rb
1 module ProvenanceHelper
2
3   class GenerateGraph
4     def initialize(pdata, opts)
5       @pdata = pdata
6       @opts = opts
7       @visited = {}
8       @jobs = {}
9       @node_extra = {}
10     end
11
12     def self.collection_uuid(uuid)
13       m = CollectionsHelper.match(uuid)
14       if m
15         if m[2]
16           return m[1]+m[2]
17         else
18           return m[1]
19         end
20       else
21         nil
22       end
23     end
24
25     def url_for u
26       p = { :host => @opts[:request].host,
27         :port => @opts[:request].port,
28         :protocol => @opts[:request].protocol }
29       p.merge! u
30       Rails.application.routes.url_helpers.url_for (p)
31     end
32
33     def determine_fillcolor(n)
34       fillcolor = %w(aaaaaa aaffaa aaaaff aaaaaa ffaaaa)[n || 0] || 'aaaaaa'
35       "style=filled,fillcolor=\"##{fillcolor}\""
36     end
37
38     def describe_node(uuid)
39       uuid = uuid.to_sym
40       bgcolor = determine_fillcolor @opts[:pips].andand[uuid]
41
42       rsc = ArvadosBase::resource_class_for_uuid uuid.to_s
43       if rsc
44         href = url_for ({:controller => rsc.to_s.tableize,
45                           :action => :show,
46                           :id => uuid.to_s })
47
48         #"\"#{uuid}\" [label=\"#{rsc}\\n#{uuid}\",href=\"#{href}\"];\n"
49         if rsc == Collection
50           if Collection.is_empty_blob_locator? uuid.to_s
51             # special case
52             return "\"#{uuid}\" [label=\"(empty collection)\"];\n"
53           end
54           if @pdata[uuid]
55             if @pdata[uuid][:name]
56               return "\"#{uuid}\" [label=\"#{@pdata[uuid][:name]}\",href=\"#{href}\",shape=oval,#{bgcolor}];\n"
57             else
58               files = nil
59               if @pdata[uuid].respond_to? :files
60                 files = @pdata[uuid].files
61               elsif @pdata[uuid][:files]
62                 files = @pdata[uuid][:files]
63               end
64
65               if files
66                 i = 0
67                 label = ""
68                 while i < 3 and i < files.length
69                   label += "\\n" unless label == ""
70                   label += files[i][1]
71                   i += 1
72                 end
73                 if i < files.length
74                   label += "\\n&vellip;"
75                 end
76                 extra_s = @node_extra[uuid].andand.map { |k,v|
77                   "#{k}=\"#{v}\""
78                 }.andand.join ","
79                 return "\"#{uuid}\" [label=\"#{label}\",href=\"#{href}\",shape=oval,#{bgcolor},#{extra_s}];\n"
80               end
81             end
82           end
83         end
84         return "\"#{uuid}\" [label=\"#{rsc}\",href=\"#{href}\",#{bgcolor}];\n"
85       end
86       "\"#{uuid}\" [#{bgcolor}];\n"
87     end
88
89     def job_uuid(job)
90       d = Digest::MD5.hexdigest(job[:script_parameters].to_json)
91       if @opts[:combine_jobs] == :script_only
92         uuid = "#{job[:script]}_#{d}"
93       elsif @opts[:combine_jobs] == :script_and_version
94         uuid = "#{job[:script]}_#{job[:script_version]}_#{d}"
95       else
96         uuid = "#{job[:uuid]}"
97       end
98
99       @jobs[uuid] = [] unless @jobs[uuid]
100       @jobs[uuid] << job unless @jobs[uuid].include? job
101
102       uuid
103     end
104
105     def edge(tail, head, extra)
106       if @opts[:direction] == :bottom_up
107         gr = "\"#{tail}\" -> \"#{head}\""
108       else
109         gr = "\"#{head}\" -> \"#{tail}\""
110       end
111       if extra.length > 0
112         gr += " ["
113         extra.each do |k, v|
114           gr += "#{k}=\"#{v}\","
115         end
116         gr += "]"
117       end
118       gr += ";\n"
119       gr
120     end
121
122     def script_param_edges(job, prefix, sp)
123       gr = ""
124       case sp
125       when Hash
126         sp.each do |k, v|
127           if prefix.size > 0
128             k = prefix + "::" + k.to_s
129           end
130           gr += script_param_edges(job, k.to_s, v)
131         end
132       when Array
133         i = 0
134         node = ""
135         count = 0
136         sp.each do |v|
137           if GenerateGraph::collection_uuid(v)
138             gr += script_param_edges(job, "#{prefix}[#{i}]", v)
139           elsif @opts[:all_script_parameters]
140             t = "#{v}"
141             nl = (if (count+t.length) > 60 then "\\n" else " " end)
142             count = 0 if (count+t.length) > 60
143             node += "',#{nl}'" unless node == ""
144             node = "['" if node == ""
145             node += t
146             count += t.length
147           end
148           i += 1
149         end
150         unless node == ""
151           node += "']"
152           node_value = "#{node}".gsub("\"", "'")
153           gr += "\"#{node_value}\" [label=\"#{node_value}\"];\n"
154           gr += edge(job_uuid(job), node_value, {:label => prefix})
155         end
156       when String
157         return '' if sp.empty?
158         m = GenerateGraph::collection_uuid(sp)
159         if m and (@pdata[m.intern] or (not @opts[:pdata_only]))
160           gr += edge(job_uuid(job), m, {:label => prefix})
161           gr += generate_provenance_edges(m)
162         elsif @opts[:all_script_parameters]
163           gr += "\"#{sp}\" [label=\"#{sp}\"];\n"
164           gr += edge(job_uuid(job), sp, {:label => prefix})
165         end
166       end
167       gr
168     end
169
170     def generate_provenance_edges(uuid)
171       gr = ""
172       m = GenerateGraph::collection_uuid(uuid)
173       uuid = m if m
174
175       uuid = uuid.intern if uuid
176
177       if (not uuid) or uuid.empty? or @visited[uuid]
178         return ""
179       end
180
181       if not @pdata[uuid] then
182         return describe_node(uuid)
183       else
184         @visited[uuid] = true
185       end
186
187       if m
188         # uuid is a collection
189         if not Collection.is_empty_blob_locator? uuid.to_s
190           @pdata.each do |k, job|
191             if job[:output] == uuid.to_s
192               extra = { label: 'output' }
193               gr += edge(uuid, job_uuid(job), extra)
194               gr += generate_provenance_edges(job[:uuid])
195             end
196             if job[:log] == uuid.to_s
197               gr += edge(uuid, job_uuid(job), {:label => "log"})
198               gr += generate_provenance_edges(job[:uuid])
199             end
200           end
201         end
202         gr += describe_node(uuid)
203       else
204         # uuid is something else
205         rsc = ArvadosBase::resource_class_for_uuid uuid.to_s
206
207         if rsc == Job
208           job = @pdata[uuid]
209           if job
210             gr += script_param_edges(job, "", job[:script_parameters])
211
212             if @opts[:script_version_nodes]
213               gr += describe_node(job[:script_version])
214               gr += edge(job_uuid(job), job[:script_version], {:label => "script_version"})
215             end
216           end
217         elsif rsc == Link
218           # do nothing
219         else
220           gr += describe_node(uuid)
221         end
222       end
223
224       @pdata.each do |k, link|
225         if link[:head_uuid] == uuid.to_s and link[:link_class] == "provenance"
226           href = url_for ({:controller => Link.to_s.tableize,
227                             :action => :show,
228                             :id => link[:uuid] })
229
230           gr += describe_node(link[:tail_uuid])
231           gr += edge(link[:head_uuid], link[:tail_uuid], {:label => link[:name], :href => href})
232           gr += generate_provenance_edges(link[:tail_uuid])
233         end
234       end
235
236       gr
237     end
238
239     def describe_jobs
240       gr = ""
241       @jobs.each do |k, v|
242         href = url_for ({:controller => Job.to_s.tableize,
243                           :action => :index })
244
245         gr += "\"#{k}\" [href=\"#{href}?"
246
247         n = 0
248         v.each do |u|
249           gr += "uuid%5b%5d=#{u[:uuid]}&"
250           n |= @opts[:pips][u[:uuid].intern] if @opts[:pips] and @opts[:pips][u[:uuid].intern]
251         end
252
253         gr += "\",label=\""
254
255         if @opts[:combine_jobs] == :script_only
256           gr += "#{v[0][:script]}"
257         elsif @opts[:combine_jobs] == :script_and_version
258           gr += "#{v[0][:script]}" # Just show the name but the nodes will be distinct
259         else
260           gr += "#{v[0][:script]}\\n#{v[0][:finished_at]}"
261         end
262         gr += "\",#{determine_fillcolor n}];\n"
263       end
264       gr
265     end
266
267   end
268
269   def self.create_provenance_graph(pdata, svgId, opts={})
270     if pdata.is_a? Array or pdata.is_a? ArvadosResourceList
271       p2 = {}
272       pdata.each do |k|
273         p2[k[:uuid].intern] = k if k[:uuid]
274       end
275       pdata = p2
276     end
277
278     unless pdata.is_a? Hash
279       raise "create_provenance_graph accepts Array or Hash for pdata only, pdata is #{pdata.class}"
280     end
281
282     gr = """strict digraph {
283 node [fontsize=10,shape=box];
284 edge [fontsize=10];
285 """
286
287     if opts[:direction] == :bottom_up
288       gr += "edge [dir=back];"
289     end
290
291     g = GenerateGraph.new(pdata, opts)
292
293     pdata.each do |k, v|
294       gr += g.generate_provenance_edges(k)
295     end
296
297     gr += g.describe_jobs
298
299     gr += "}"
300     svg = ""
301
302     require 'open3'
303
304     Open3.popen2("dot", "-Tsvg") do |stdin, stdout, wait_thr|
305       stdin.print(gr)
306       stdin.close
307       svg = stdout.read()
308       wait_thr.value
309       stdout.close()
310     end
311
312     svg = svg.sub(/<\?xml.*?\?>/m, "")
313     svg = svg.sub(/<!DOCTYPE.*?>/m, "")
314     svg = svg.sub(/<svg /, "<svg id=\"#{svgId}\" ")
315   end
316
317   def self.find_collections(sp)
318     c = []
319     case sp
320     when Hash
321       sp.each do |k, v|
322         c.concat(find_collections(v))
323       end
324     when Array
325       sp.each do |v|
326         c.concat(find_collections(v))
327       end
328     when String
329       if !sp.empty?
330         m = GenerateGraph::collection_uuid(sp)
331         if m
332           c << m
333         end
334       end
335     end
336     c
337   end
338 end