Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,4 @@ uv run middleman.py

Open `localhost:3000` and pick one of the examples.

See also [DEVELOPMENT.md](DEVELOPMENT.md).

For deployment instructions via Dokku, see the [deployment guide](deploy-dokku.md).
See also [DEVELOPMENT.md](DEVELOPMENT.md).
76 changes: 0 additions & 76 deletions deploy-dokku.md

This file was deleted.

165 changes: 36 additions & 129 deletions middleman.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
import os
import random
import re
import subprocess

import sys
import urllib.request
import urllib.parse
import urllib.error
from glob import glob
from dataclasses import dataclass

Expand All @@ -25,6 +26,7 @@
import zendriver as zd

CDP_URL = os.getenv("CDP_URL", "http://127.0.0.1:9222")
CHROMEFLEET_URL = os.getenv("CHROMEFLEET_URL")

MIDDLEMAN_DEBUG = os.getenv("MIDDLEMAN_DEBUG")
MIDDLEMAN_PAUSE = os.getenv("MIDDLEMAN_PAUSE")
Expand Down Expand Up @@ -973,153 +975,58 @@ async def check_cdp() -> bool:
return False


def run_podman(args: list) -> subprocess.CompletedProcess:
cmd = ["podman"]
if os.environ.get("CONTAINER_HOST"):
cmd.append("--remote")
cmd.extend(args)
return subprocess.run(cmd, capture_output=True, text=True, check=True, encoding="utf-8", errors="replace")


async def setup_tailscale(container_id, name) -> str | None:
try:
print(f"{ARROW} Setting up Tailscale in container {container_id}...")

TAILSCALE_IP_CMD = "sudo tailscale ip"
tailscale_commands = [
"curl -fsSL https://tailscale.com/install.sh | sudo sh",
"sudo nohup tailscaled > /dev/null 2>&1 &",
f"sudo tailscale up --auth-key={os.getenv('TS_AUTHKEY')} --hostname={name}",
TAILSCALE_IP_CMD,
]

ip_address = None
for i, cmd in enumerate(tailscale_commands, 1):
print(f"{ARROW} Executing Tailscale setup: Step {i}/{len(tailscale_commands)}")
await asyncio.sleep(1)
result = run_podman(["exec", container_id, "sh", "-c", cmd])
if result.returncode == 0:
if cmd == TAILSCALE_IP_CMD and result.stdout:
ip_address = result.stdout.strip().split("\n")[0]
else:
print(f"{CROSS} Failed to execute Tailscale setup step {i}: {result.stderr}")
return None

if ip_address:
print(f"{CHECK} All Tailscale setup steps completed successfully")
print(f"{CHECK} Tailscale IP address: {ip_address}")
return ip_address
else:
print(f"{CROSS} Failed to get IP address from Tailscale")
return None

except Exception as e:
print(f"{CROSS} Error setting up Tailscale: {e}")
return None


async def launch_tailscaled_chromium() -> bool:
async def launch_chromefleet_machine() -> bool:
global CDP_URL
try:
print(f"{ARROW} Launching local Chromium container with Tailscale...")
name = f"chromium-{nanoid.generate(FRIENDLY_CHARS, 5)}"
cmd = [
"run",
"-d",
"--rm",
"--name",
name,
"--cap-add=NET_ADMIN",
"--cap-add=NET_RAW",
"--device",
"/dev/net/tun:/dev/net/tun",
"ghcr.io/remotebrowser/chromium-live",
]
result = run_podman(cmd)
container_id = result.stdout.strip()
print(f"{CHECK} Container started: name={name} id={container_id}")

await asyncio.sleep(3)
ip_address = await setup_tailscale(container_id, name)
if ip_address is None:
return False

CDP_URL = f"http://{ip_address}:9222"
print(f"{CHECK} Set CDP_URL to {CDP_URL}")
print(f"{ARROW} Checking CDP availability...")
await asyncio.sleep(3)
for attempt in range(10):
if await check_cdp():
print(f"{CHECK} Local Chromium CDP is ready")
return True

if attempt == 9:
print(f"{CROSS} Failed to connect to CDP after container launch")
return False
machine_id = nanoid.generate(FRIENDLY_CHARS, 5)
print(f"{ARROW} Launching Chromium via Chrome Fleet API at {CYAN}{CHROMEFLEET_URL}{NORMAL}...")
print(f"{ARROW} Machine ID: {CYAN}{machine_id}{NORMAL}")

await asyncio.sleep(2)

return False

except subprocess.CalledProcessError as e:
print(f"{CROSS} Failed to launch container: {e}")
return False
except Exception as e:
print(f"{CROSS} Error launching Chromium: {e}")
return False
request_url = f"{CHROMEFLEET_URL}/api/v1/start/{machine_id}"
request = urllib.request.Request(request_url, method="GET")

with urllib.request.urlopen(request) as response:
data = json.loads(response.read().decode())

async def launch_local_chromium() -> bool:
try:
print(f"{ARROW} Launching local Chromium container...")
cmd = [
"run",
"-d",
"--rm",
"--network=host",
"-p",
"3001:3001",
"-p",
"9222:9222",
"ghcr.io/remotebrowser/chromium-live",
]
if "cdp_url" not in data:
print(f"{CROSS} Chrome Fleet API response missing cdp_url field")
return False

result = run_podman(cmd)
container_id = result.stdout.strip()
print(f"{CHECK} Container started: {container_id}")
CDP_URL = data["cdp_url"]
print(f"{CHECK} Chrome Fleet launched Chromium at {CYAN}{CDP_URL}{NORMAL}")

print(f"{ARROW} Checking CDP availability...")
await asyncio.sleep(3)
for attempt in range(10):
if await check_cdp():
print(f"{CHECK} Local Chromium CDP is ready")
return True
print(f"{ARROW} Checking CDP availability...")
await asyncio.sleep(3)
for attempt in range(10):
if await check_cdp():
print(f"{CHECK} Chrome Fleet Chromium CDP is ready")
return True

if attempt == 9:
print(f"{CROSS} Failed to connect to CDP after container launch")
return False
if attempt == 9:
print(f"{CROSS} Failed to connect to CDP after Chrome Fleet launch")
return False

await asyncio.sleep(2)
await asyncio.sleep(2)

return False
return False

except subprocess.CalledProcessError as e:
print(f"{CROSS} Failed to launch container: {e}")
except urllib.error.HTTPError as e:
print(f"{CROSS} Chrome Fleet API request failed: {e.code} {e.reason}")
return False
except Exception as e:
print(f"{CROSS} Error launching Chromium: {e}")
print(f"{CROSS} Error launching Chrome Fleet Chromium: {e}")
return False


if __name__ == "__main__":
if asyncio.run(check_cdp()) is False:
print(f"{CROSS} No existing CDP found")
if os.getenv("TS_AUTHKEY"):
if asyncio.run(launch_tailscaled_chromium()) is False:
print("Fatal error: Unable to detect or launch Chromium with Tailscale!")
print("Middleman will not work properly.")
elif asyncio.run(launch_local_chromium()) is False:
print("Fatal error: Unable to launch containerized Chromium!")
if CHROMEFLEET_URL:
if asyncio.run(launch_chromefleet_machine()) is False:
print("Fatal error: Unable to launch a machine with Chrome Fleet!")
sys.exit(-1)
else:
print("Fatal error: CHROMEFLEET_URL is not set and no existing CDP found!")
sys.exit(-1)

result = asyncio.run(main())
Expand Down