mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
330 lines
10 KiB
Python
Executable File
330 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Spy Thread Client - Real-time Game State Inspector
|
|
|
|
Monitor workflow execution and game state via spy thread socket interface.
|
|
|
|
Usage:
|
|
# Get current FPS
|
|
python3 spy_client.py get fps
|
|
|
|
# Get all stats
|
|
python3 spy_client.py status
|
|
|
|
# Pause/resume execution
|
|
python3 spy_client.py pause
|
|
python3 spy_client.py resume
|
|
|
|
# Watch mode (continuous monitoring)
|
|
python3 spy_client.py watch --interval 1
|
|
|
|
# Monitor specific stat
|
|
python3 spy_client.py watch fps --interval 0.5
|
|
|
|
# Record stats to CSV
|
|
python3 spy_client.py record stats.csv --duration 30
|
|
|
|
# Help
|
|
python3 spy_client.py --help
|
|
"""
|
|
|
|
import argparse
|
|
import socket
|
|
import sys
|
|
import time
|
|
import csv
|
|
from datetime import datetime
|
|
from typing import Dict, Optional, List
|
|
import signal
|
|
|
|
|
|
class SpyClient:
|
|
"""Client for spy thread debugger"""
|
|
|
|
def __init__(self, host: str = "127.0.0.1", port: int = 9999, timeout: float = 5.0):
|
|
self.host = host
|
|
self.port = port
|
|
self.timeout = timeout
|
|
|
|
def send_command(self, command: str) -> Optional[str]:
|
|
"""Send command to spy thread and get response"""
|
|
try:
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.settimeout(self.timeout)
|
|
s.connect((self.host, self.port))
|
|
s.send(f"{command}\n".encode())
|
|
response = s.recv(4096).decode().strip()
|
|
s.close()
|
|
return response
|
|
except ConnectionRefusedError:
|
|
print(f"✗ Connection refused: {self.host}:{self.port}")
|
|
print(" Is the game running with spy thread enabled?")
|
|
return None
|
|
except socket.timeout:
|
|
print(f"✗ Timeout: Could not reach {self.host}:{self.port}")
|
|
return None
|
|
except Exception as e:
|
|
print(f"✗ Error: {e}")
|
|
return None
|
|
|
|
def parse_response(self, response: str) -> Dict[str, str]:
|
|
"""Parse response from spy thread"""
|
|
result = {}
|
|
for line in response.split('\n'):
|
|
if '=' in line:
|
|
key, value = line.split('=', 1)
|
|
result[key] = value
|
|
return result
|
|
|
|
def get_stat(self, stat_name: str) -> Optional[str]:
|
|
"""Get single stat value"""
|
|
response = self.send_command(f"get {stat_name}")
|
|
if response:
|
|
parsed = self.parse_response(response)
|
|
return parsed.get(stat_name)
|
|
return None
|
|
|
|
def get_all_stats(self) -> Optional[Dict[str, str]]:
|
|
"""Get all stats"""
|
|
response = self.send_command("status")
|
|
if response:
|
|
return self.parse_response(response)
|
|
return None
|
|
|
|
def pause(self) -> bool:
|
|
"""Pause execution"""
|
|
response = self.send_command("pause")
|
|
return response is not None and "true" in response
|
|
|
|
def resume(self) -> bool:
|
|
"""Resume execution"""
|
|
response = self.send_command("resume")
|
|
return response is not None and "false" in response
|
|
|
|
def get_help(self) -> Optional[str]:
|
|
"""Get help from spy thread"""
|
|
return self.send_command("help")
|
|
|
|
|
|
def format_stat(key: str, value: str) -> str:
|
|
"""Format stat for display"""
|
|
# Try to convert to float for better formatting
|
|
try:
|
|
fval = float(value)
|
|
if key.endswith('_time') or key == 'elapsed_time':
|
|
return f"{fval:.2f}ms" if fval < 1000 else f"{fval/1000:.2f}s"
|
|
elif key == 'fps':
|
|
return f"{fval:.1f} FPS"
|
|
elif key.endswith('_used') or key.endswith('memory'):
|
|
mb = fval / (1024 * 1024)
|
|
return f"{mb:.1f} MB"
|
|
else:
|
|
return str(int(fval))
|
|
except ValueError:
|
|
return value
|
|
|
|
|
|
def cmd_get(args, client: SpyClient):
|
|
"""Handle 'get' command"""
|
|
value = client.get_stat(args.stat)
|
|
if value:
|
|
print(f"{args.stat}={format_stat(args.stat, value)}")
|
|
else:
|
|
sys.exit(1)
|
|
|
|
|
|
def cmd_status(args, client: SpyClient):
|
|
"""Handle 'status' command"""
|
|
stats = client.get_all_stats()
|
|
if stats:
|
|
print("\n╔════════════════════════════════════════╗")
|
|
print("║ WORKFLOW EXECUTION STATUS ║")
|
|
print("╚════════════════════════════════════════╝\n")
|
|
|
|
for key, value in sorted(stats.items()):
|
|
formatted = format_stat(key, value)
|
|
print(f" {key:.<30} {formatted:>10}")
|
|
|
|
print()
|
|
else:
|
|
sys.exit(1)
|
|
|
|
|
|
def cmd_pause(args, client: SpyClient):
|
|
"""Handle 'pause' command"""
|
|
if client.pause():
|
|
print("✓ Execution paused")
|
|
else:
|
|
print("✗ Failed to pause")
|
|
sys.exit(1)
|
|
|
|
|
|
def cmd_resume(args, client: SpyClient):
|
|
"""Handle 'resume' command"""
|
|
if client.resume():
|
|
print("✓ Execution resumed")
|
|
else:
|
|
print("✗ Failed to resume")
|
|
sys.exit(1)
|
|
|
|
|
|
def cmd_watch(args, client: SpyClient):
|
|
"""Handle 'watch' command - continuous monitoring"""
|
|
stat_name = args.stat if args.stat else None
|
|
interval = args.interval
|
|
|
|
print(f"Watching {'all stats' if not stat_name else stat_name} (interval: {interval}s)")
|
|
print("Press Ctrl+C to stop\n")
|
|
|
|
try:
|
|
while True:
|
|
if stat_name:
|
|
# Watch single stat
|
|
value = client.get_stat(stat_name)
|
|
if value:
|
|
formatted = format_stat(stat_name, value)
|
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
print(f"[{timestamp}] {stat_name}={formatted}")
|
|
else:
|
|
# Watch all stats
|
|
stats = client.get_all_stats()
|
|
if stats:
|
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
print(f"\n[{timestamp}] Status:")
|
|
for key, value in sorted(stats.items()):
|
|
formatted = format_stat(key, value)
|
|
print(f" {key:.<30} {formatted:>10}")
|
|
|
|
time.sleep(interval)
|
|
except KeyboardInterrupt:
|
|
print("\n\nWatch stopped")
|
|
|
|
|
|
def cmd_record(args, client: SpyClient):
|
|
"""Handle 'record' command - log stats to CSV"""
|
|
filename = args.filename
|
|
duration = args.duration
|
|
interval = args.interval
|
|
|
|
print(f"Recording to {filename} for {duration}s (interval: {interval}s)")
|
|
|
|
start_time = time.time()
|
|
elapsed = 0.0
|
|
records = []
|
|
|
|
try:
|
|
while elapsed < duration:
|
|
stats = client.get_all_stats()
|
|
if stats:
|
|
record = {'timestamp': datetime.now().isoformat(), 'elapsed': elapsed}
|
|
record.update(stats)
|
|
records.append(record)
|
|
|
|
elapsed = time.time() - start_time
|
|
print(f" {elapsed:.1f}s / {duration}s - {len(records)} records")
|
|
|
|
time.sleep(interval)
|
|
except KeyboardInterrupt:
|
|
print("\nRecording stopped early")
|
|
|
|
# Write to CSV
|
|
if records:
|
|
try:
|
|
with open(filename, 'w', newline='') as f:
|
|
fieldnames = records[0].keys()
|
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
writer.writeheader()
|
|
writer.writerows(records)
|
|
print(f"✓ Recorded {len(records)} entries to {filename}")
|
|
except Exception as e:
|
|
print(f"✗ Failed to write CSV: {e}")
|
|
sys.exit(1)
|
|
else:
|
|
print("✗ No records collected")
|
|
sys.exit(1)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Spy Thread Client - Monitor workflow execution and game state",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
Get current FPS:
|
|
python3 spy_client.py get fps
|
|
|
|
Get all statistics:
|
|
python3 spy_client.py status
|
|
|
|
Watch FPS in real-time (update every 0.5s):
|
|
python3 spy_client.py watch fps --interval 0.5
|
|
|
|
Watch all stats (update every 1s):
|
|
python3 spy_client.py watch --interval 1
|
|
|
|
Record all stats to CSV for 30 seconds:
|
|
python3 spy_client.py record stats.csv --duration 30
|
|
|
|
Pause and resume execution:
|
|
python3 spy_client.py pause
|
|
python3 spy_client.py resume
|
|
"""
|
|
)
|
|
|
|
parser.add_argument('--host', default='127.0.0.1', help='Spy thread host (default: 127.0.0.1)')
|
|
parser.add_argument('--port', type=int, default=9999, help='Spy thread port (default: 9999)')
|
|
parser.add_argument('--timeout', type=float, default=5.0, help='Socket timeout in seconds (default: 5.0)')
|
|
|
|
subparsers = parser.add_subparsers(dest='command', help='Command to execute')
|
|
|
|
# 'get' command
|
|
get_parser = subparsers.add_parser('get', help='Get single stat')
|
|
get_parser.add_argument('stat', help='Stat name (fps, frame_count, elapsed_time, etc.)')
|
|
get_parser.set_defaults(func=cmd_get)
|
|
|
|
# 'status' command
|
|
status_parser = subparsers.add_parser('status', help='Get all statistics')
|
|
status_parser.set_defaults(func=cmd_status)
|
|
|
|
# 'pause' command
|
|
pause_parser = subparsers.add_parser('pause', help='Pause execution')
|
|
pause_parser.set_defaults(func=cmd_pause)
|
|
|
|
# 'resume' command
|
|
resume_parser = subparsers.add_parser('resume', help='Resume execution')
|
|
resume_parser.set_defaults(func=cmd_resume)
|
|
|
|
# 'watch' command
|
|
watch_parser = subparsers.add_parser('watch', help='Watch stats in real-time')
|
|
watch_parser.add_argument('stat', nargs='?', default=None, help='Specific stat to watch (optional)')
|
|
watch_parser.add_argument('--interval', type=float, default=1.0, help='Update interval in seconds (default: 1.0)')
|
|
watch_parser.set_defaults(func=cmd_watch)
|
|
|
|
# 'record' command
|
|
record_parser = subparsers.add_parser('record', help='Record stats to CSV file')
|
|
record_parser.add_argument('filename', help='Output CSV filename')
|
|
record_parser.add_argument('--duration', type=float, default=30.0, help='Record duration in seconds (default: 30.0)')
|
|
record_parser.add_argument('--interval', type=float, default=0.5, help='Sample interval in seconds (default: 0.5)')
|
|
record_parser.set_defaults(func=cmd_record)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Show help if no command
|
|
if not args.command:
|
|
parser.print_help()
|
|
sys.exit(0)
|
|
|
|
# Create client
|
|
client = SpyClient(host=args.host, port=args.port, timeout=args.timeout)
|
|
|
|
# Execute command
|
|
try:
|
|
args.func(args, client)
|
|
except AttributeError:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|