{title} {status}
Open to load output.
#!/usr/bin/env python3
"""Collect named diagnostics and render a browsable, static support report."""
import argparse
from datetime import datetime, timezone
import html
import json
import os
from pathlib import Path
import re
import shlex
import shutil
import signal
import subprocess
import time
class Report:
def __init__(self, directory, timeout=300):
self.directory = Path(directory).resolve()
self.outputs = self.directory / 'outputs'
self.outputs.mkdir(parents=True, exist_ok=False)
self.timeout = timeout
self.records = []
self.created = datetime.now(timezone.utc).isoformat()
def run(self, name, command, stdin_path=None):
filename = f'{len(self.records) + 1:03d}-{name}.txt'
path = self.outputs / filename
display = shlex.join(command)
if stdin_path:
display += ' < [filesystem passphrase file]'
print(f'Collecting {filename}: {display}', flush=True)
start = time.monotonic()
status = 0
timed_out = False
with path.open('wb') as output:
source = None
try:
if stdin_path:
source = open(stdin_path, 'rb')
process = subprocess.Popen(command, cwd=self.directory, stdin=source or subprocess.DEVNULL,
stdout=output, stderr=subprocess.STDOUT, start_new_session=True)
try:
status = process.wait(timeout=self.timeout)
except subprocess.TimeoutExpired:
timed_out = True
os.killpg(process.pid, signal.SIGTERM)
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
process.wait()
status = 124
output.write(f'\n[Timed out after {self.timeout:g} seconds]\n'.encode())
except BaseException:
os.killpg(process.pid, signal.SIGKILL)
process.wait()
raise
except OSError as error:
status = 127
output.write(f'{error}\n'.encode())
finally:
if source:
source.close()
record = {'file': f'outputs/{filename}', 'command': display, 'status': status,
'seconds': round(time.monotonic() - start, 2), 'timeout': timed_out,
'bytes': path.stat().st_size}
self.records.append(record)
return path
def render(self):
(self.directory / 'manifest.json').write_text(json.dumps({'created': self.created, 'commands': self.records}, indent=2) + '\n')
with (self.directory / 'kent_report.log').open('wb') as full:
full.write(f'Bcachefs support report\nCreated: {self.created}\n\nCommand index:\n'.encode())
for record in self.records:
full.write(f"{record['file']}: {record['command']} [exit {record['status']}]\n".encode())
for record in self.records:
full.write(f"\n\n{'=' * 80}\n$ {record['command']}\nFile: {record['file']}\nExit: {record['status']} | {record['seconds']}s\n\n".encode())
with (self.directory / record['file']).open('rb') as part:
shutil.copyfileobj(part, full)
entries = []
for record in self.records:
link = html.escape(record['file'], quote=True)
title = html.escape(record['command'])
status = f"exit {record['status']} · {record['seconds']}s · {record['bytes']:,} bytes"
entries.append(f'''{title}
{status}
Open to load output.
Created: CREATED. Expand a command to fetch its output. Each text file is also directly downloadable.