#!/usr/bin/env python import contextlib import random import signal import subprocess import threading import time import requests import sys port = int(sys.argv[1]) NGROK_CLI = [ 'ngrok', 'start', '--none', '--region', 'eu', ] own = None web_addr = 'http://localhost:4040/api' addr = 'localhost:%d' % port # FIXME: this is for xdist to avoid workers running ngrok at the # exact same time, use lockfile instead time.sleep(random.SystemRandom().randint(1, 10)) # try to find out if ngrok is running, and if it's not attempt # to start it try: requests.get(web_addr) except requests.exceptions.ConnectionError: own = subprocess.Popen(NGROK_CLI, stdout=subprocess.DEVNULL) for _ in range(5): time.sleep(1) with contextlib.suppress(requests.exceptions.ConnectionError): requests.get(web_addr) break else: sys.exit("Unable to connect to ngrok") requests.post(f'{web_addr}/tunnels', json={ 'name': str(port), 'proto': 'http', 'addr': addr, 'schemes': ['https'], 'inspect': True, }).raise_for_status() tunnel = f'{web_addr}/tunnels/{port}' for _ in range(10): time.sleep(2) r = requests.get(tunnel) # not created yet, wait and retry if r.status_code == 404: continue # check for weird responses r.raise_for_status() print(r.json()['public_url'], flush=True) sys.stdout.close() break else: sys.exit("ngrok tunnel creation failed (?)") shutdown = threading.Event() def cleanup(_sig, _frame): requests.delete(tunnel) for _ in range(10): time.sleep(1) r = requests.get(tunnel) # check if deletion is done if r.status_code == 404: break r.raise_for_status() else: raise sys.exit("ngrok tunnel deletion failed") r = requests.get(f'{web_addr}/tunnels') if not r.ok: sys.exit(f'{r.reason} {r.text}') # FIXME: if we started ngrok, we should probably wait for all tunnels to be # closed then terminate ngrok? This is likely a situation where the # worker which created the ngrok instance finished early... if own and not r.json()['tunnels']: # no more tunnels and we started ngrok -> try to kill it own.terminate() own.wait(30) shutdown.set() # don't know why but signal.sigwait doesn't seem to take SIGTERM in account so # we need the cursed version signal.signal(signal.SIGTERM, cleanup) signal.signal(signal.SIGINT, cleanup) shutdown.wait()