blob: ef94c45888d8d480c534d607b8d9a4433bc37166 [file] [edit]
import http.server
import socketserver
import urllib.request
import urllib.error
import subprocess
import json
import os
import argparse
import re
class ProxyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, chromium_dir=None, **kwargs):
self.chromium_dir = chromium_dir
super().__init__(*args, **kwargs)
def end_headers(self):
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
self.send_header('Pragma', 'no-cache')
self.send_header('Expires', '0')
super().end_headers()
def get_luci_token(self):
try:
result = subprocess.run(['luci-auth', 'token'], capture_output=True, text=True, check=True)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"Error getting luci-auth token: {e}")
return None
except FileNotFoundError:
print("luci-auth command not found. Please ensure it is installed and in your PATH.")
return None
def do_GET(self):
parsed_path = urllib.parse.urlparse(self.path)
if parsed_path.path == '/api/builders':
self.handle_api_builders()
elif parsed_path.path == '/api/trigger_mapping':
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
self.end_headers()
self.wfile.write(json.dumps(self.get_trigger_mapping()).encode('utf-8'))
elif parsed_path.path == '/api/gn_args':
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
self.end_headers()
self.wfile.write(json.dumps(self.get_gn_args_mapping()).encode('utf-8'))
elif parsed_path.path == '/api/swarming_dimensions':
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
self.end_headers()
self.wfile.write(json.dumps(self.get_swarming_dimensions_mapping()).encode('utf-8'))
elif parsed_path.path == '/api/benchmarks':
self.handle_api_benchmarks()
else:
super().do_GET()
def do_POST(self):
if self.path == '/api/swarming_bots':
self.handle_api_swarming_bots()
else:
self.send_error(404)
def get_trigger_mapping(self):
import os, re, subprocess, json, tempfile, tarfile
mapping = {}
# Method 1: Try gob-curl against Gitiles API (+archive)
try:
archive_url = "https://chrome-internal.googlesource.com/chrome/src-internal/+archive/main/infra/config/subprojects/chrome/ci.tar.gz"
with tempfile.NamedTemporaryFile(suffix='.tar.gz') as temp_archive:
subprocess.run(
f'gob-curl "{archive_url}" > "{temp_archive.name}"',
shell=True, check=True
)
with tarfile.open(temp_archive.name, 'r:gz') as tar:
for member in tar.getmembers():
if member.name.endswith('.star'):
f = tar.extractfile(member)
if f:
content = f.read().decode('utf-8')
current_name = None
for line in content.split('\n'):
name_match = re.search(r'name\s*=\s*"([^"]+)"', line)
if name_match:
current_name = name_match.group(1)
trigger = None
trigger_match = re.search(r'triggered_by\s*=\s*\["([^"]+)"\]', line)
parent_match = re.search(r'parent\s*=\s*"([^"]+)"', line)
if trigger_match:
trigger = trigger_match.group(1)
elif parent_match:
trigger = parent_match.group(1)
if trigger and current_name:
if trigger.startswith('ci/'):
trigger = trigger[3:]
mapping[current_name] = trigger
if mapping:
return mapping
except Exception as e:
print(f"gob-curl fetch failed: {e}. Falling back to local chromium-dir...")
# Method 2: Fallback to local chromium_dir
if not self.chromium_dir:
return mapping
config_dir = os.path.join(self.chromium_dir, "internal/infra/config/subprojects/chrome/ci")
if not os.path.exists(config_dir):
print(f"Warning: Config directory not found at {config_dir}")
return mapping
star_files = [f for f in os.listdir(config_dir) if f.endswith('.star')]
for filename in star_files:
filepath = os.path.join(config_dir, filename)
try:
with open(filepath, 'r') as f:
content = f.read()
current_name = None
for line in content.split('\n'):
name_match = re.search(r'name\s*=\s*"([^"]+)"', line)
if name_match:
current_name = name_match.group(1)
trigger = None
trigger_match = re.search(r'triggered_by\s*=\s*\["([^"]+)"\]', line)
parent_match = re.search(r'parent\s*=\s*"([^"]+)"', line)
if trigger_match:
trigger = trigger_match.group(1)
elif parent_match:
trigger = parent_match.group(1)
if trigger and current_name:
if trigger.startswith('ci/'):
trigger = trigger[3:]
mapping[current_name] = trigger
except Exception:
pass
return mapping
def get_gn_args_mapping(self):
import urllib.request, base64, ast
url = "https://chromium.googlesource.com/chromium/src/+/main/tools/mb/mb_config.pyl?format=TEXT"
try:
req = urllib.request.Request(url)
with urllib.request.urlopen(req) as response:
content = base64.b64decode(response.read()).decode("utf-8")
pyl = ast.literal_eval(content)
gn_args_map = {}
for group, builders in pyl.get('builder_groups', {}).items():
if group != 'chromium.perf':
continue
for builder, config_name in builders.items():
if isinstance(config_name, dict):
# Handle case where builder maps to a nested dict instead of string
for k, v in config_name.items():
if k == "chromium.perf": config_name = v
if not isinstance(config_name, str):
continue
mixins = pyl.get('configs', {}).get(config_name, [])
args_parts = []
for mixin in mixins:
mixin_dict = pyl.get('mixins', {}).get(mixin, {})
if 'gn_args' in mixin_dict:
args_parts.append(mixin_dict['gn_args'])
if args_parts:
gn_args_map[builder] = " ".join(args_parts)
return gn_args_map
except Exception as e:
print(f"Error fetching mb_config.pyl: {e}")
return {}
def get_swarming_dimensions_mapping(self):
import urllib.request, json, base64
url = "https://chromium.googlesource.com/chromium/src/+/refs/heads/main/testing/buildbot/chromium.perf.json?format=TEXT"
try:
req = urllib.request.Request(url)
with urllib.request.urlopen(req) as response:
content = base64.b64decode(response.read()).decode("utf-8")
data = json.loads(content)
dims_map = {}
for builder, config in data.items():
if builder.startswith("AAAA"): continue
scripts = config.get("isolated_scripts", [])
if not scripts: continue
swarming = scripts[0].get("swarming", {})
dimensions = swarming.get("dimensions", {})
dims_map[builder] = dimensions
return dims_map
except Exception as e:
print(f"Error fetching chromium.perf.json: {e}")
return {}
def handle_api_benchmarks(self):
import tarfile
import io
import urllib.request
import glob
benchmarks_mapping = {}
if self.chromium_dir:
schedule_dir = os.path.join(self.chromium_dir, "tools", "perf", "core", "schedule")
if os.path.exists(schedule_dir):
for filepath in glob.glob(os.path.join(schedule_dir, "*.csv")):
benchmark_name = os.path.basename(filepath).replace(".csv", "")
try:
with open(filepath, 'r') as f:
lines = f.readlines()
for line in lines[1:]:
parts = line.strip().split(',')
if len(parts) >= 1:
bot = parts[0]
if bot not in benchmarks_mapping:
benchmarks_mapping[bot] = []
benchmarks_mapping[bot].append(benchmark_name)
except Exception as e:
pass
if not benchmarks_mapping:
url = "https://chromium.googlesource.com/chromium/src/+archive/refs/heads/main/tools/perf/core/schedule.tar.gz"
try:
req = urllib.request.Request(url)
resp = urllib.request.urlopen(req).read()
with tarfile.open(fileobj=io.BytesIO(resp), mode="r:gz") as tar:
for member in tar.getmembers():
if member.name.endswith(".csv"):
benchmark_name = member.name.replace(".csv", "")
f = tar.extractfile(member)
if f:
content = f.read().decode('utf-8')
lines = content.splitlines()
for line in lines[1:]:
parts = line.strip().split(',')
if len(parts) >= 1:
bot = parts[0]
if bot not in benchmarks_mapping:
benchmarks_mapping[bot] = []
benchmarks_mapping[bot].append(benchmark_name)
except Exception as e:
print("Failed to fetch schedule tarball:", e)
for bot in benchmarks_mapping:
benchmarks_mapping[bot] = sorted(list(set(benchmarks_mapping[bot])))
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(benchmarks_mapping).encode('utf-8'))
def handle_api_swarming_bots(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
req_data = json.loads(post_data)
dimensions_dict = req_data.get('dimensions', {})
swarming_server = req_data.get('server', 'https://chrome-swarming.appspot.com')
token = self.get_luci_token()
if not token:
self.send_error(500, "Failed to get luci-auth token")
return
dimensions_list = [{"key": k, "value": str(v)} for k, v in dimensions_dict.items()]
url = f"{swarming_server}/prpc/swarming.v2.Bots/ListBots"
payload = json.dumps({
"dimensions": dimensions_list
}).encode("utf-8")
req = urllib.request.Request(url, data=payload, method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Accept", "application/json")
req.add_header("Authorization", f"Bearer {token}")
try:
with urllib.request.urlopen(req) as response:
resp = response.read().decode("utf-8")
if resp.startswith(")]}'\n"):
resp = resp[5:]
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
self.send_header('Pragma', 'no-cache')
self.send_header('Expires', '0')
self.end_headers()
self.wfile.write(resp.encode('utf-8'))
except Exception as e:
print(f"Error fetching ListBots: {e}")
self.send_error(500, str(e))
def handle_api_builders(self):
token = self.get_luci_token()
if not token:
self.send_response(500)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(b'{"error": "Failed to get luci-auth token. Please run bb auth-login."}')
return
url = 'https://cr-buildbucket.appspot.com/prpc/buildbucket.v2.Builders/ListBuilders'
payload = json.dumps({
"project": "chrome",
"bucket": "ci",
"pageSize": 1000
}).encode('utf-8')
req = urllib.request.Request(url, data=payload, method='POST')
req.add_header('Content-Type', 'application/json')
req.add_header('Accept', 'application/json')
req.add_header('Authorization', f'Bearer {token}')
try:
with urllib.request.urlopen(req) as response:
resp_data = response.read().decode('utf-8')
# Strip pRPC prefix
if resp_data.startswith(")]}'\n"):
resp_data = resp_data[5:]
json_data = json.loads(resp_data)
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(json_data).encode('utf-8'))
except urllib.error.HTTPError as e:
self.send_response(e.code)
self.send_header('Content-type', 'application/json')
self.end_headers()
err_msg = e.read().decode('utf-8')
self.wfile.write(json.dumps({"error": f"Buildbucket API Error: {err_msg}"}).encode('utf-8'))
except Exception as e:
self.send_response(500)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Hardware Dashboard Proxy Server")
parser.add_argument("--port", type=int, default=8081, help="Port to run the server on")
parser.add_argument("--chromium-dir", type=str, help="Path to chromium/src directory for parsing configs")
args = parser.parse_args()
# Create a handler factory with the chromium_dir baked in
def handler_factory(*h_args, **h_kwargs):
return ProxyHTTPRequestHandler(*h_args, chromium_dir=args.chromium_dir, **h_kwargs)
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("", args.port), handler_factory) as httpd:
print(f"Serving at port {args.port}. Stop the old Python server and run this instead!")
if not args.chromium_dir:
print("Note: --chromium-dir was not provided. Builder mapping will fallback to unmapped.")
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass