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()