Last active
June 18, 2016 14:22
-
-
Save ochafik/9929ec51d3c4d3d613b71b5f5b45130b to your computer and use it in GitHub Desktop.
Dart Development Server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# https://gist.github.com/ochafik/9929ec51d3c4d3d613b71b5f5b45130b | |
# For SPDY / HTTP2 push: https://github.com/eigengo/opensourcejournal/blob/master/2014.1/spdynetty/spdynetty.md | |
import getopt | |
import os | |
import re | |
import sys | |
import threading | |
import urllib | |
from SimpleHTTPServer import SimpleHTTPRequestHandler | |
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler | |
from SocketServer import ThreadingMixIn | |
# Example usage: | |
# | |
# ./serve \ | |
# ads/adsense/fe/dashboard/web@/adsense/apps/dashboard/_/resources \ | |
# default_app/web@/ | |
# | |
# List of (served_path, served_path_with_slash, directory) tuples. | |
served_directories = [] | |
# List of directories from which packages should be resolved. | |
root_directories = [] | |
verbose = False | |
push_imported_files = False | |
def main(): | |
global served_directories | |
global root_directories | |
global verbose | |
global push_imported_files | |
port = 8080 | |
hostname = '0.0.0.0' | |
# The bazel root directory. | |
bazel_root = os.getcwd() | |
try: | |
opts, args = getopt.getopt( | |
sys.argv[1:], | |
"p:r:v", | |
[ | |
"verbose", | |
"port=", | |
"root=", | |
"add-relative-root=", | |
"hostname", | |
"push" | |
]) | |
except getopt.GetoptError as err: | |
print str(err) | |
sys.exit(2) | |
# See http://www.bazel.io/docs/output_directories.html | |
relative_roots = [ | |
"blaze-genfiles", | |
"blaze-bin", | |
"blaze-out", | |
"bazel-genfiles", | |
"bazel-bin", | |
"bazel-out", | |
"../READONLY", | |
] | |
for o, a in opts: | |
if o in ("-v", "--verbose"): | |
verbose = True | |
elif o in ("-p", "--port"): | |
port = int(a) | |
elif o == "--push": | |
push_imported_files = True | |
elif o == "--hostname": | |
hostname = a | |
elif o in ("-r", "--root"): | |
bazel_root = a | |
elif o in ("--add-relative-root"): | |
relative_roots.append(a) | |
else: | |
assert False, "unhandled option" | |
for arg in args: | |
parts = arg.split("@") | |
if len(parts) == 2: | |
[directory, served_path] = parts | |
else: | |
[directory] = parts | |
served_path = "/" | |
if served_path == "/": | |
served_path_with_slash = served_path | |
elif served_path.endswith("/"): | |
served_path_with_slash = served_path | |
served_path = served_path[:-1] | |
else: | |
served_path_with_slash = served_path + "/" | |
directory = os.path.relpath(directory, bazel_root) | |
served_directories.append((served_path, served_path_with_slash, directory)) | |
root_directories.append(bazel_root) | |
for sub in relative_roots: | |
dir = os.path.join(bazel_root, sub) | |
if os.path.isdir(dir): | |
root_directories.append(dir) | |
if verbose: | |
print "Served dirs:", served_directories | |
print " Root dirs: ", root_directories | |
print 'Starting server, use <Ctrl-C> to stop' | |
ThreadedHTTPServer((hostname, port), DartRequestHandler).serve_forever() | |
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): | |
"""Handle requests in a separate thread.""" | |
dart_package_re = re.compile('^(.*?/package(?:s/|:))([^/]+)/(.*)$') | |
# TODO(ochafik): Handle DDC ES6 modules preloading. | |
# dart_package_uri_re = re.compile('package:([^/]+)/(.*?)') | |
dart_import_re = re.compile('import\s*[\'"]([^:]+|package:([^\'"]+?))[\'"]', re.MULTILINE) | |
class DartRequestHandler(SimpleHTTPRequestHandler): | |
resolved_paths = {} | |
def resolve_path(self): | |
global served_directories | |
global verbose | |
if self.path in self.resolved_paths: | |
return self.resolved_paths[self.path] | |
path = urllib.unquote(self.path) | |
candidates = [] | |
m = dart_package_re.match(path) | |
if m: | |
file = os.path.join(m.group(2).replace(".", "/"), "lib", m.group(3)) | |
candidates += [ | |
file, | |
os.path.join("third_party", "dart", file), | |
] | |
else: | |
for served_path, served_path_with_slash, directory in served_directories: | |
if path == served_path: | |
candidates.append(os.path.join(directory, "index.html")) | |
break | |
elif path.startswith(served_path_with_slash): | |
candidates.append(os.path.join(directory, path[len(served_path_with_slash):])) | |
break | |
# if verbose: | |
# print "Candidates: " + ", ".join(candidates) | |
file = self.find_file(candidates) | |
self.resolved_paths[self.path] = file | |
return file | |
def do_GET(self): | |
global served_directories | |
global verbose | |
googleCommand = self.headers.get('X-Google-Command', None) | |
if googleCommand: | |
print "X-Google-Command = ", googleCommand | |
file = self.resolve_path() | |
if file: | |
etag = "\"%s\"" % int(os.path.getmtime(file)) | |
if self.headers.get('If-None-Match', None) == etag: | |
self.send_response(304) | |
self.end_headers() | |
return | |
content_type = 'application/dart' if file.endswith(".dart") else self.guess_type(file) | |
# Don't open as text as file size wouldn't match if newlines are modified | |
mode = 'rb' | |
try: | |
# Try to open the file 2 times | |
# TODO(ochafik): Print / understand errors (fuse-specific?). | |
try: | |
f = open(file, mode) | |
except IOError: | |
try: | |
f = open(file, mode) | |
except IOError: | |
f = open(file, mode) | |
if push_imported_files: | |
# Server push for Dart sources. | |
content = f.read() | |
f.close() | |
links = [] | |
for dart_import_m in dart_import_re.finditer(content): | |
dart_import = dart_import_m.group(1) | |
packages_base = self.get_packages_base() | |
link = None | |
if dart_import.startswith("package:"): | |
link = os.path.join(packages_base, dart_import[len("package:"):]) | |
else: | |
link = os.path.join(os.path.dirname(self.path), dart_import) | |
links.append("<%s>; rel=preload; as=script" % link) | |
self.send_response(200) | |
self.send_header("Content-Length", len(content)) | |
self.send_header("Content-Type", content_type) | |
self.send_header("ETag", etag) | |
if len(links) > 0: | |
self.send_header("Link", ", ".join(links)) | |
if verbose: | |
print "Links for ", self.path, ":\n\t", "\n\t".join(links) | |
self.end_headers() | |
self.wfile.write(content) | |
self.wfile.close(); | |
f.close() | |
else: | |
self.send_response(200) | |
self.send_header("Content-Length", os.fstat(f.fileno()).st_size) | |
self.send_header("Content-Type", content_type) | |
self.send_header("ETag", etag) | |
self.end_headers() | |
self.copyfile(f, self.wfile) | |
f.close() | |
return | |
except IOError: | |
self.send_error(404, "File not found: " + file) | |
self.send_error(404, "File not found") | |
def get_packages_base(self): | |
path = self.path | |
m = dart_package_re.match(path) | |
return m.group(1) if m else os.path.dirname(path) + "packages/" | |
def find_file(self, candidates): | |
global root_directories | |
for root_directory in root_directories: | |
for candidate in candidates: | |
file = os.path.join(root_directory, candidate) | |
if os.path.exists(file): | |
return file | |
return None | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment