1#!/usr/bin/env python
2
3"""
4This program parses the output from pcap_compile() to visualize the CFG after
5each optimize phase.
6
7Usage guide:
81. Enable optimizer debugging code when configure libpcap,
9   and build libpcap & the test programs
10       ./configure --enable-optimizer-dbg
11       make
12       make testprogs
132. Run filtertest to compile BPF expression and produce the CFG as a
14   DOT graph, save to output a.txt
15       testprogs/filtertest -g EN10MB host 192.168.1.1 > a.txt
163. Send a.txt to this program's standard input
17       cat a.txt | testprogs/visopts.py
18   (Graphviz must be installed)
194. Step 2&3 can be merged:
20       testprogs/filtertest -g EN10MB host 192.168.1.1 | testprogs/visopts.py
215. The standard output is something like this:
22       generated files under directory: /tmp/visopts-W9ekBw
23         the directory will be removed when this programs finished.
24       open this link: http://localhost:39062/expr1.html
256. Open the URL at the 3rd line in a browser.
26
27Note:
281. The CFG is translated to SVG images, expr1.html embeds them as external
29   documents. If you open expr1.html as local file using file:// protocol, some
30   browsers will deny such requests so the web page will not work properly.
31   For Chrome, you can run it using the following command to avoid this:
32       chromium --disable-web-security
33   That's why this program starts a localhost HTTP server.
342. expr1.html uses jQuery from https://ajax.googleapis.com, so it needs Internet
35   access to work.
36"""
37
38import sys, os
39import string
40import subprocess
41import json
42
43html_template = string.Template("""
44<html>
45  <head>
46    <title>BPF compiler optimization phases for $expr </title>
47    <style type="text/css">
48      .hc {
49         /* half width container */
50         display: inline-block;
51         float: left;
52         width: 50%;
53      }
54    </style>
55
56    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"/></script>
57    <!--script type="text/javascript" src="./jquery.min.js"/></script-->
58    <script type="text/javascript">
59      var expr = '$expr';
60      var exprid = 1;
61      var gcount = $gcount;
62      var logs = JSON.parse('$logs');
63      logs[gcount] = "";
64
65      var leftsvg = null;
66      var rightsvg = null;
67
68      function gurl(index) {
69         index += 1;
70         if (index < 10)
71           s = "00" + index;
72         else if (index < 100)
73           s = "0" + index;
74         else
75           s = "" + index;
76         return "./expr" + exprid + "_g" + s + ".svg"
77      }
78
79      function annotate_svgs() {
80         if (!leftsvg || !rightsvg) return;
81
82         $$.each([$$(leftsvg), $$(rightsvg)], function() {
83           $$(this).find("[id|='block'][opacity]").each(function() {
84             $$(this).removeAttr('opacity');
85            });
86          });
87
88         $$(leftsvg).find("[id|='block']").each(function() {
89           var has = $$(rightsvg).find("#" + this.id).length != 0;
90           if (!has) $$(this).attr("opacity", "0.4");
91           else {
92             $$(this).click(function() {
93                var target = $$(rightsvg).find("#" + this.id);
94                var offset = $$("#rightsvgc").offset().top + target.position().top;
95                window.scrollTo(0, offset);
96                target.focus();
97             });
98           }
99          });
100         $$(rightsvg).find("[id|='block']").each(function() {
101           var has = $$(leftsvg).find("#" + this.id).length != 0;
102           if (!has) $$(this).attr("opacity", "0.4");
103           else {
104             $$(this).click(function() {
105                var target = $$(leftsvg).find("#" + this.id);
106                var offset = $$("#leftsvgc").offset().top + target.position().top;
107                window.scrollTo(0, offset);
108                target.focus();
109             });
110           }
111          });
112      }
113
114      function init_svgroot(svg) {
115         svg.setAttribute("width", "100%");
116         svg.setAttribute("height", "100%");
117      }
118      function wait_leftsvg() {
119         if (leftsvg) return;
120         var doc = document.getElementById("leftsvgc").getSVGDocument();
121         if (doc == null) {
122            setTimeout(wait_leftsvg, 500);
123            return;
124         }
125         leftsvg = doc.documentElement;
126         //console.log(leftsvg);
127         // initialize it
128         init_svgroot(leftsvg);
129         annotate_svgs();
130      }
131      function wait_rightsvg() {
132         if (rightsvg) return;
133         var doc = document.getElementById("rightsvgc").getSVGDocument();
134         if (doc == null) {
135            setTimeout(wait_rightsvg, 500);
136            return;
137         }
138         rightsvg = doc.documentElement;
139         //console.log(rightsvg);
140         // initialize it
141         init_svgroot(rightsvg);
142         annotate_svgs();
143      }
144      function load_left(index) {
145        var url = gurl(index);
146        var frag = "<embed id='leftsvgc'  type='image/svg+xml' pluginspage='https://www.adobe.com/svg/viewer/install/' src='" + url + "'/>";
147        $$("#lsvg").html(frag);
148        $$("#lcomment").html(logs[index]);
149        $$("#lsvglink").attr("href", url);
150        leftsvg = null;
151        wait_leftsvg();
152      }
153      function load_right(index) {
154        var url = gurl(index);
155        var frag = "<embed id='rightsvgc' type='image/svg+xml' pluginspage='https://www.adobe.com/svg/viewer/install/' src='" + url + "'/>";
156        $$("#rsvg").html(frag);
157        $$("#rcomment").html(logs[index]);
158        $$("#rsvglink").attr("href", url);
159        rightsvg = null;
160        wait_rightsvg();
161      }
162
163      $$(document).ready(function() {
164        for (var i = 0; i < gcount; i++) {
165          var opt = "<option value='" + i + "'>loop" + i + " -- " + logs[i] + "</option>";
166          $$("#lselect").append(opt);
167          $$("#rselect").append(opt);
168        }
169        var on_selected = function() {
170          var index = parseInt($$(this).children("option:selected").val());
171          if (this.id == "lselect")
172             load_left(index);
173          else
174             load_right(index);
175        }
176        $$("#lselect").change(on_selected);
177        $$("#rselect").change(on_selected);
178
179        $$("#backward").click(function() {
180          var index = parseInt($$("#lselect option:selected").val());
181          if (index <= 0) return;
182          $$("#lselect").val(index - 1).change();
183          $$("#rselect").val(index).change();
184        });
185        $$("#forward").click(function() {
186          var index = parseInt($$("#rselect option:selected").val());
187          if (index >= gcount - 1) return;
188          $$("#lselect").val(index).change();
189          $$("#rselect").val(index + 1).change();
190        });
191
192        if (gcount >= 1) $$("#lselect").val(0).change();
193        if (gcount >= 2) $$("#rselect").val(1).change();
194      });
195    </script>
196  </head>
197  <body style="width: 96%">
198    <div>
199      <h1>$expr</h1>
200      <div style="text-align: center;">
201        <button id="backward" type="button">&lt;&lt;</button>
202          &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
203        <button id="forward" type="button">&gt;&gt;</button>
204      </div>
205    </div>
206    <br/>
207    <div style="clear: both;">
208       <div class="hc lc">
209        <select id="lselect"></select>
210        <a id="lsvglink" target="_blank">open this svg in browser</a>
211        <p id="lcomment"></p>
212       </div>
213       <div class="hc rc">
214        <select id="rselect"></select>
215        <a id="rsvglink" target="_blank">open this svg in browser</a>
216        <p id="rcomment"></p>
217       </div>
218    </div>
219    <br/>
220    <div style="clear: both;">
221       <div id="lsvg"  class="hc lc"></div>
222       <div id="rsvg" class="hc rc"></div>
223    </div>
224  </body>
225</html>
226""")
227
228def write_html(expr, gcount, logs):
229    logs = map(lambda s: s.strip().replace("\n", "<br/>"), logs)
230
231    global html_template
232    html = html_template.safe_substitute(expr=expr.encode("string-escape"), gcount=gcount, logs=json.dumps(logs).encode("string-escape"))
233    with file("expr1.html", "wt") as f:
234        f.write(html)
235
236def render_on_html(infile):
237    expr = None
238    gid = 1
239    log = ""
240    dot = ""
241    indot = 0
242    logs = []
243
244    for line in infile:
245        if line.startswith("machine codes for filter:"):
246            expr = line[len("machine codes for filter:"):].strip()
247            break
248        elif line.startswith("digraph BPF {"):
249            indot = 1
250            dot = line
251        elif indot:
252            dot += line
253            if line.startswith("}"):
254                indot = 2
255        else:
256            log += line
257
258        if indot == 2:
259            try:
260                p = subprocess.Popen(['dot', '-Tsvg'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
261            except OSError as ose:
262                print "Failed to run 'dot':", ose
263                print "(Is Graphviz installed?)"
264                exit(1)
265
266            svg = p.communicate(dot)[0]
267            with file("expr1_g%03d.svg" % (gid), "wt") as f:
268                f.write(svg)
269
270            logs.append(log)
271            gid += 1
272            log = ""
273            dot = ""
274            indot = 0
275
276    if indot != 0:
277        #unterminated dot graph for expression
278        return False
279    if expr is None:
280        # BPF parser encounter error(s)
281        return False
282    write_html(expr, gid - 1, logs)
283    return True
284
285def run_httpd():
286    import SimpleHTTPServer
287    import SocketServer
288
289    class MySocketServer(SocketServer.TCPServer):
290        allow_reuse_address = True
291    Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
292    httpd = MySocketServer(("localhost", 0), Handler)
293    print "open this link: http://localhost:%d/expr1.html" % (httpd.server_address[1])
294    try:
295        httpd.serve_forever()
296    except KeyboardInterrupt as e:
297        pass
298
299def main():
300    import tempfile
301    import atexit
302    import shutil
303    os.chdir(tempfile.mkdtemp(prefix="visopts-"))
304    atexit.register(shutil.rmtree, os.getcwd())
305    print "generated files under directory: %s" % os.getcwd()
306    print "  the directory will be removed when this program has finished."
307
308    if not render_on_html(sys.stdin):
309        return 1
310    run_httpd()
311    return 0
312
313if __name__ == "__main__":
314    if '-h' in sys.argv or '--help' in sys.argv:
315        print __doc__
316        exit(0)
317    exit(main())
318