3 # Copyright 2001 Google Inc. All Rights Reserved.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Simple web server for browsing dependency graph data.
19 This script is inlined into the final executable and spawned by
23 from __future__ import print_function
26 import http.server as httpserver
28 import BaseHTTPServer as httpserver
35 from urllib.request import unquote
37 from urllib2 import unquote
38 from collections import namedtuple
40 Node = namedtuple('Node', ['inputs', 'rule', 'target', 'outputs'])
42 # Ideally we'd allow you to navigate to a build edge or a build node,
43 # with appropriate views for each. But there's no way to *name* a build
44 # edge so we can only display nodes.
46 # For a given node, it has at most one input edge, which has n
47 # different inputs. This becomes node.inputs. (We leave out the
48 # outputs of the input edge due to what follows.) The node can have
49 # multiple dependent output edges. Rather than attempting to display
50 # those, they are summarized by taking the union of all their outputs.
52 # This means there's no single view that shows you all inputs and outputs
53 # of an edge. But I think it's less confusing than alternatives.
55 def match_strip(line, prefix):
56 if not line.startswith(prefix):
58 return (True, line[len(prefix):])
61 lines = iter(text.split('\n'))
69 target = next(lines)[:-1] # strip trailing colon
72 (match, rule) = match_strip(line, ' input: ')
74 (match, line) = match_strip(next(lines), ' ')
77 (match, line) = match_strip(line, '| ')
80 (match, line) = match_strip(line, '|| ')
83 inputs.append((line, type))
84 (match, line) = match_strip(next(lines), ' ')
86 match, _ = match_strip(line, ' outputs:')
88 (match, line) = match_strip(next(lines), ' ')
91 (match, line) = match_strip(next(lines), ' ')
95 return Node(inputs, rule, target, outputs)
97 def create_page(body):
98 return '''<!DOCTYPE html>
116 font-family: WebKitHack, monospace;
120 -webkit-columns: auto 2;
125 def generate_html(node):
126 document = ['<h1><tt>%s</tt></h1>' % node.target]
129 document.append('<h2>target is built using rule <tt>%s</tt> of</h2>' %
131 if len(node.inputs) > 0:
132 document.append('<div class=filelist>')
133 for input, type in sorted(node.inputs):
136 extra = ' (%s)' % type
137 document.append('<tt><a href="?%s">%s</a>%s</tt><br>' %
138 (input, input, extra))
139 document.append('</div>')
142 document.append('<h2>dependent edges build:</h2>')
143 document.append('<div class=filelist>')
144 for output in sorted(node.outputs):
145 document.append('<tt><a href="?%s">%s</a></tt><br>' %
147 document.append('</div>')
149 return '\n'.join(document)
151 def ninja_dump(target):
152 proc = subprocess.Popen([sys.argv[1], '-t', 'query', target],
153 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
154 universal_newlines=True)
155 return proc.communicate() + (proc.returncode,)
157 class RequestHandler(httpserver.BaseHTTPRequestHandler):
159 assert self.path[0] == '/'
160 target = unquote(self.path[1:])
163 self.send_response(302)
164 self.send_header('Location', '?' + sys.argv[2])
168 if not target.startswith('?'):
169 self.send_response(404)
174 ninja_output, ninja_error, exit_code = ninja_dump(target)
176 page_body = generate_html(parse(ninja_output.strip()))
178 # Relay ninja's error message.
179 page_body = '<h1><tt>%s</tt></h1>' % ninja_error
181 self.send_response(200)
183 self.wfile.write(create_page(page_body).encode('utf-8'))
185 def log_message(self, format, *args):
186 pass # Swallow console spam.
188 port = int(os.getenv("PORT", '8000'))
189 httpd = httpserver.HTTPServer(('',port), RequestHandler)
191 hostname = socket.gethostname()
192 print('Web server running on %s:%d, ctl-C to abort...' % (hostname,port) )
193 print('Web server pid %d' % os.getpid(), file=sys.stderr )
194 webbrowser.open_new('http://%s:%s' % (hostname, port) )
195 httpd.serve_forever()
196 except KeyboardInterrupt:
198 pass # Swallow console spam.