plugins/mkdocs-atlas-formatting-plugin/mkdocs_atlas_formatting_plugin/atlaswebserver.py (94 lines of code) (raw):

import os import socket import time from base64 import b64encode from contextlib import closing from io import BytesIO from subprocess import Popen, DEVNULL from typing import Optional, Tuple import requests from PIL import Image from .config import ATLAS_VERSION from .logconfig import setup_logging logger = setup_logging(__name__) class NoopWebserver: """Used by Parser and Block, for unit testing.""" ENCODED = 'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlE' \ 'QVR42mNkYPhfz0AEYBxVSF+FAP5FDvcfRYWgAAAAAElFTkSuQmCC' WIDTH, HEIGHT = 10, 10 def get_image(self, uri: str) -> Tuple[str, int, int]: return f'data:image/png;base64,{self.ENCODED}', self.WIDTH, self.HEIGHT class AtlasWebServer: proc: Optional[Popen] = None standalone_jar: str = f'{os.getcwd()}/atlas-standalone.jar' standalone_url: str = f'https://github.com/Netflix/atlas/releases/download/v{ATLAS_VERSION}/atlas-standalone-{ATLAS_VERSION}.jar' webserver_host: str = '127.0.0.1' webserver_port: int = 7101 webserver_timeout: int = 30 base_url: str = f'http://{webserver_host}:{webserver_port}' def __new__(cls): """Singleton Pattern""" if not hasattr(cls, 'instance'): cls.instance = super().__new__(cls) return cls.instance def __init__(self) -> None: if not self.proc: self.download_jar(self.standalone_jar, self.standalone_url) self.start_jar(self.standalone_jar, self.webserver_host, self.webserver_port) @staticmethod def download_jar(jar: str, url: str) -> None: """Download the specified Atlas Standalone jar from the GitHub releases page.""" if os.path.isfile(jar): logger.info(f'{jar} exists, skipping download') return logger.info(f'download {url}') r = requests.get(url, stream=True, allow_redirects=True) with open(jar, 'wb') as f: for chunk in r.iter_content(chunk_size=1024): if chunk: f.write(chunk) if r.ok: logger.info(f'saved {jar}') else: with open(jar) as f: lines = f.readlines() raise ConnectionError(f'ERROR {r.status_code}: failed to download Atlas: {lines}') @staticmethod def port_is_open(host: str, port: int) -> bool: with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: if s.connect_ex((host, port)) == 0: return True else: return False def start_jar(self, jar: str, host: str, port: int) -> None: """Start the Atlas Standalone jar and wait for the webserver port to become available.""" if self.port_is_open(host, port): raise ChildProcessError(f'ERROR: another process is listening on port {port}') logger.info(f'starting atlas webserver on port {port}') self.proc = Popen(['java', '-jar', jar], stdout=DEVNULL, stderr=DEVNULL) count = 0 while True: if self.port_is_open(host, port): break else: if count > self.webserver_timeout: self.proc.terminate() raise ChildProcessError( f'ERROR: failed to access atlas webserver on ' + f'port {port} after {self.webserver_timeout}s' ) count += 1 time.sleep(1) logger.info(f'webserver startup complete in {count}s, pid={self.proc.pid}') def get_image(self, uri: str) -> Tuple[str, int, int]: """ Given an Atlas URI, fetch a PNG image from the running Atlas Standalone server. Encode the image as a Base64 string suitable for embedding as a data uri in an image tag. :param uri: Atlas URI :return: image data uri, image width, image height """ url = self.base_url + uri r = requests.get(url) if not r.ok: logger.error(f'failed to get image: code={r.status_code} text={r.text} url={url}') return '<pre>ERROR: failed to get image</pre>', 0, 0 content_type = r.headers['Content-Type'] encoded = str(b64encode(r.content), 'utf-8') data_uri = f'data:{content_type};base64,{encoded}' with BytesIO(r.content) as buffer: with Image.open(buffer) as image: width, height = image.size return data_uri, width, height def shutdown(self) -> None: self.proc.terminate() self.proc.wait()