main
  1#!/usr/bin/env python3
  2"""
  3Start one or more servers, wait for them to be ready, run a command, then clean up.
  4
  5Usage:
  6    # Single server
  7    python scripts/with_server.py --server "npm run dev" --port 5173 -- python automation.py
  8    python scripts/with_server.py --server "npm start" --port 3000 -- python test.py
  9
 10    # Multiple servers
 11    python scripts/with_server.py \
 12      --server "cd backend && python server.py" --port 3000 \
 13      --server "cd frontend && npm run dev" --port 5173 \
 14      -- python test.py
 15"""
 16
 17import subprocess
 18import socket
 19import time
 20import sys
 21import argparse
 22
 23def is_server_ready(port, timeout=30):
 24    """Wait for server to be ready by polling the port."""
 25    start_time = time.time()
 26    while time.time() - start_time < timeout:
 27        try:
 28            with socket.create_connection(('localhost', port), timeout=1):
 29                return True
 30        except (socket.error, ConnectionRefusedError):
 31            time.sleep(0.5)
 32    return False
 33
 34
 35def main():
 36    parser = argparse.ArgumentParser(description='Run command with one or more servers')
 37    parser.add_argument('--server', action='append', dest='servers', required=True, help='Server command (can be repeated)')
 38    parser.add_argument('--port', action='append', dest='ports', type=int, required=True, help='Port for each server (must match --server count)')
 39    parser.add_argument('--timeout', type=int, default=30, help='Timeout in seconds per server (default: 30)')
 40    parser.add_argument('command', nargs=argparse.REMAINDER, help='Command to run after server(s) ready')
 41
 42    args = parser.parse_args()
 43
 44    # Remove the '--' separator if present
 45    if args.command and args.command[0] == '--':
 46        args.command = args.command[1:]
 47
 48    if not args.command:
 49        print("Error: No command specified to run")
 50        sys.exit(1)
 51
 52    # Parse server configurations
 53    if len(args.servers) != len(args.ports):
 54        print("Error: Number of --server and --port arguments must match")
 55        sys.exit(1)
 56
 57    servers = []
 58    for cmd, port in zip(args.servers, args.ports):
 59        servers.append({'cmd': cmd, 'port': port})
 60
 61    server_processes = []
 62
 63    try:
 64        # Start all servers
 65        for i, server in enumerate(servers):
 66            print(f"Starting server {i+1}/{len(servers)}: {server['cmd']}")
 67
 68            # Use shell=True to support commands with cd and &&
 69            process = subprocess.Popen(
 70                server['cmd'],
 71                shell=True,
 72                stdout=subprocess.PIPE,
 73                stderr=subprocess.PIPE
 74            )
 75            server_processes.append(process)
 76
 77            # Wait for this server to be ready
 78            print(f"Waiting for server on port {server['port']}...")
 79            if not is_server_ready(server['port'], timeout=args.timeout):
 80                raise RuntimeError(f"Server failed to start on port {server['port']} within {args.timeout}s")
 81
 82            print(f"Server ready on port {server['port']}")
 83
 84        print(f"\nAll {len(servers)} server(s) ready")
 85
 86        # Run the command
 87        print(f"Running: {' '.join(args.command)}\n")
 88        result = subprocess.run(args.command)
 89        sys.exit(result.returncode)
 90
 91    finally:
 92        # Clean up all servers
 93        print(f"\nStopping {len(server_processes)} server(s)...")
 94        for i, process in enumerate(server_processes):
 95            try:
 96                process.terminate()
 97                process.wait(timeout=5)
 98            except subprocess.TimeoutExpired:
 99                process.kill()
100                process.wait()
101            print(f"Server {i+1} stopped")
102        print("All servers stopped")
103
104
105if __name__ == '__main__':
106    main()