Skip to content

Commit

Permalink
fixup! Add serve_header.py for rapid testing on Compiler Explorer
Browse files Browse the repository at this point in the history
  • Loading branch information
falbrechtskirchinger committed Apr 26, 2022
1 parent c6f40b7 commit 7793bbb
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 33 deletions.
9 changes: 5 additions & 4 deletions scripts/serve_header/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,22 @@ Install the `mkcert` certificate authority into your trust store(s):
$ mkcert -install
```

Then proceed to create a certificate for `localhost`:
Then proceed to create a certificate for `localhost` in the project root directory:
```
$ mkcert localhost
```

Next, create the configuration file 'serve_header.yml' in the project root directory. An example configuration can also be found in `scripts/serve_header/serve_header.yml.example`.
In the absence of a configuration file these defaults are used:
```yaml
root: .
https:
enabled: true
cert_file: localhost.pem
key_file: localhost-key.pem
```
An annotated example configuration can be found in `scripts/serve_header/serve_header.yml.example`.

This configuration serves the `json.hpp` header on `https://localhost:8443/single_include/nlohmann/json.hpp`.
This configuration serves the `single_include/nlohmann/json.hpp` header on `https://localhost:8443/json.hpp`.

Make sure you have the following python dependencies installed:
```
Expand All @@ -44,7 +45,7 @@ $ make serve_header

Open [Compiler Explorer](https://godbolt.org/) and try it out:
```cpp
#include <https://localhost:8443/single_include/nlohmann/json.hpp>
#include <https://localhost:8443/json.hpp>
using namespace nlohmann;
#include <iostream>
Expand Down
61 changes: 32 additions & 29 deletions scripts/serve_header/serve_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

class ExitHandler(logging.StreamHandler):
def __init__(self, level):
"""."""
super().__init__()
self.level = level

Expand All @@ -48,9 +49,11 @@ def is_project_root(test_dir='.'):

class WorkTree:
def __init__(self, root_dir, tree_dir):
"""."""
self.root_dir = root_dir
self.tree_dir = tree_dir
self.rel_dir = os.path.relpath(tree_dir, root_dir)
self.name = os.path.basename(tree_dir)
self.include_dir = os.path.abspath(os.path.join(tree_dir, INCLUDE))
self.header = os.path.abspath(os.path.join(tree_dir, SINGLE_INCLUDE, HEADER))
self.rel_header = os.path.relpath(self.header, root_dir)
Expand All @@ -60,12 +63,12 @@ def __init__(self, root_dir, tree_dir):
t = datetime.fromtimestamp(t)
self.build_time = t.strftime(DATETIME_FORMAT)

"""Work tree hash is derived from tree_dir"""
def __hash__(self):
"""."""
return hash((self.tree_dir))

"""Work tree identity is based on tree_dir"""
def __eq__(self, other):
"""."""
if not isinstance(other, type(self)):
return NotImplemented
return self.tree_dir == other.tree_dir
Expand All @@ -76,29 +79,30 @@ def update_dirty(self, path):

path = os.path.abspath(path)
if os.path.commonpath([path, self.include_dir]) == self.include_dir:
logging.info(f'worktree is dirty: {self.rel_dir}')
logging.info(f'worktree is dirty: {self.name}')
self.dirty = True

def amalgamate_header(self):
if not self.dirty:
return

logging.info(f'amalgamating header in worktree: {self.rel_dir}')
logging.info(f'amalgamating header in worktree: {self.name}')

mtime = os.path.getmtime(self.header)
subprocess.run(['make', 'amalgamate'], cwd=self.tree_dir,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if mtime == os.path.getmtime(self.header):
logging.info(f'no changes in worktree: {self.rel_dir}')
logging.info(f'no changes in worktree: {self.name}')
else:
self.build_count += 1
self.build_time = datetime.now().strftime(DATETIME_FORMAT)
logging.info(f'amalgamated header in worktree: {self.rel_dir}')
logging.info(f'amalgamated header in worktree: {self.name}')

self.dirty = False

class WorkTrees(FileSystemEventHandler):
def __init__(self, root_dir):
"""."""
super().__init__()
self.root_dir = root_dir
self.trees = set()
Expand All @@ -117,7 +121,7 @@ def scan(self, base_dir):
if not scan_dir.endswith('/_deps/json-src'):
tree = WorkTree(self.root_dir, scan_dir)
if not tree in self.trees:
logging.info('adding work tree: %s', tree.rel_dir)
logging.info(f'adding work tree: {tree.rel_dir} ({tree.name})')
self.trees.add(tree)
with os.scandir(scan_dir) as it:
for entry in it:
Expand All @@ -137,7 +141,7 @@ def on_any_event(self, event):
if event.is_directory:
if event.event_type == 'created':
# check for new work tree
pass
self.scan(event.src_path)
elif event.event_type == 'deleted':
# check for deleted work tree
pass
Expand All @@ -151,9 +155,8 @@ def stop(self):
self.observer.join()

class HeaderRequestHandler(SimpleHTTPRequestHandler):
"""Request handler for serving json.hpp header"""

def __init__(self, request, client_address, server):
"""."""
self.worktrees = server.worktrees
self.worktree = None
super().__init__(request, client_address, server,
Expand All @@ -178,7 +181,7 @@ def send_head(self):
self.worktree = self.worktrees.find(path)
if self.worktree is not None:
self.worktree.amalgamate_header()
logging.info(f'serving header ({self.worktree.build_count}): {self.worktree.rel_header}')
logging.info(f'serving header in {self.worktree.name} ({self.worktree.build_count}): {self.worktree.rel_header}')
return super().send_head()
logging.info(f'invalid request path: {self.path}')
super().send_error(HTTPStatus.NOT_FOUND, 'Not Found')
Expand Down Expand Up @@ -224,9 +227,8 @@ def log_message(self, fmt, *args):
pass

class DualStackServer(ThreadingHTTPServer):
"""ThreadingHTTPServer which ensures dual-stack is not disabled"""

def __init__(self, addr, worktrees):
"""."""
self.worktrees = worktrees
super().__init__(addr, HeaderRequestHandler)

Expand All @@ -237,15 +239,6 @@ def server_bind(self):
socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
return super().server_bind()

def get_best_family(*address):
infos = socket.getaddrinfo(
*address,
type=socket.SOCK_STREAM,
flags=socket.AI_PASSIVE,
)
family, _, _, _, sockaddr = next(iter(infos))
return family, sockaddr

if __name__ == '__main__':
# exit code
ec = 0
Expand All @@ -265,28 +258,38 @@ def get_best_family(*address):
log.error("working directory doesn't look like project root")

# load config
with open(os.path.realpath(CONFIG_FILE), 'r') as f:
config = yaml.safe_load(f)
config = {}
config_file = os.path.abspath(CONFIG_FILE)
try:
with open(config_file, 'r') as f:
config = yaml.safe_load(f)
except FileNotFoundError:
log.info(f'cannot find configuration file: {config_file}')
log.info('using default configuration')

# find and monitor work trees
worktrees = WorkTrees(config.get('root', '.'))

# start web server
DualStackServer.address_family, addr = get_best_family(config.get('bind', None),
config.get('port', 8443))
infos = socket.getaddrinfo(config.get('bind', None), config.get('port', 8443),
type=socket.SOCK_STREAM, flags=socket.AI_PASSIVE)
DualStackServer.address_family = infos[0][0]
HeaderRequestHandler.protocol_version = 'HTTP/1.0'
with DualStackServer(addr, worktrees) as httpd:
with DualStackServer(infos[0][4], worktrees) as httpd:
scheme = 'HTTP'
https = config.get('https', {})
if https.get('enabled', False):
if https.get('enabled', True):
cert_file = https.get('cert_file', 'localhost.pem')
key_file = https.get('key_file', 'localhost-key.pem')
ssl.minimum_version = ssl.TLSVersion.TLSv1_3
ssl.maximum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED
httpd.socket = ssl.wrap_socket(httpd.socket,
certfile=https['cert_file'], keyfile=https['key_file'],
certfile=cert_file, keyfile=key_file,
server_side=True, ssl_version=ssl.PROTOCOL_TLS)
scheme = 'HTTPS'
host, port = httpd.socket.getsockname()[:2]
log.info(f'serving {scheme} on {host} port {port}')
log.info('press Ctrl+C to exit')
httpd.serve_forever()

except KeyboardInterrupt:
Expand Down

0 comments on commit 7793bbb

Please sign in to comment.