#!/usr/bin/env python3
"""
conman-console.py — Drive a serial console via a conman server, read-only.

Connects to a conmand server (RFC 127-style multiplexer) over the network,
opens a named console, sends the commands from a .cmds file, captures all
output, and disconnects. Requires no expect/tcl — pure stdlib PTY.

This replaces the old sw-capture-remote.sh workflow that killed the serial
device holder (conflicting with conman/ser2net). Instead, it talks to conman
over TCP, which multiplexes safely with other sessions.

All endpoints are configurable via environment variables so this works on
any network with a conman server:

    CONMAN_SERVER   conman server host:port   (default: via CONSOLE_HOST)
    CONSOLE         console name to open       (required)
    CMDS_FILE       file of commands to send   (required)
    TIMEOUT         overall timeout in seconds (default: 45)
    CMD_DELAY       seconds between commands   (default: 3)
    WAKE_DELAY      seconds after connect      (default: 2)

Usage:
    CONMAN_SERVER=console-host:7890 \\
    python3 conman-console.py --console pfv-r5-core-01 --cmds switches/pfv-r5-core-01.cmds

Lines starting with '!' or '#' in the cmds file are comments (skipped).
Blank lines are skipped. The conman escape sequence (&.) is sent automatically
to disconnect. A password prompt aborts immediately (we never send creds).

Exit codes:
    0  clean run
    1  usage / setup error
    2  could not connect to conman server
    3  timeout (partial output still printed)
    4  password prompt encountered (aborted)
"""
import argparse
import os
import pty
import re
import select
import sys
import time

PWD_RE = re.compile(rb"[Pp]assword:\s*$")
MORE_RE = re.compile(rb"--\s*[Mm]ore\s*--|[Mm]ore:\s*<space>")


def main():
    ap = argparse.ArgumentParser(
        description="Drive a conman console session read-only via PTY")
    ap.add_argument("--console", required=True,
                    help="console name (e.g. pfv-r5-core-01)")
    ap.add_argument("--cmds", required=True,
                    help="command file (one command per line; !/# = comment)")
    ap.add_argument("--server",
                    default=os.environ.get("CONMAN_SERVER", ""),
                    help="conman server host:port (env: CONMAN_SERVER)")
    ap.add_argument("--timeout", type=int,
                    default=int(os.environ.get("TIMEOUT", "45")),
                    help="overall timeout seconds (env: TIMEOUT)")
    ap.add_argument("--cmd-delay", type=float,
                    default=float(os.environ.get("CMD_DELAY", "3")),
                    help="seconds between commands (env: CMD_DELAY)")
    ap.add_argument("--wake-delay", type=float,
                    default=float(os.environ.get("WAKE_DELAY", "2")),
                    help="seconds after connect before first command (env: WAKE_DELAY)")
    args = ap.parse_args()

    if not args.server:
        sys.stderr.write("ERROR: --server or CONMAN_SERVER env required\n")
        return 1

    with open(args.cmds) as f:
        cmds = [l.strip() for l in f
                if l.strip() and not l.strip().startswith(("!", "#"))]

    pid, fd = pty.fork()
    if pid == 0:
        try:
            os.execvp("conman",
                      ["conman", "-d", args.server, "-f", args.console])
        except OSError as e:
            sys.stderr.write(f"ERROR: cannot exec conman: {e}\n")
            os._exit(2)
        os._exit(2)

    output = b""
    cmd_queue = list(cmds)
    sent_disconnect = False
    start = time.time()
    last_action = 0.0
    phase = "connect"

    while time.time() - start < args.timeout:
        ready, _, _ = select.select([fd], [], [], 0.5)
        if ready:
            try:
                data = os.read(fd, 8192)
            except OSError:
                break
            if not data:
                break
            output += data

            if PWD_RE.search(output.split(b"\n")[-1] if output else b""):
                sys.stderr.write("[ABORT] password prompt detected — "
                                 "never sending credentials\n")
                os.write(fd, b"&.\n")
                break

            # Handle pagination: send space to continue
            if MORE_RE.search(output[-200:] if output else b""):
                os.write(fd, b" ")
                time.sleep(0.5)

        elapsed = time.time() - start
        gap = elapsed - last_action

        if phase == "connect" and gap >= args.wake_delay:
            os.write(fd, b"\n")
            phase = "send"
            last_action = elapsed
        elif phase == "send" and gap >= args.cmd_delay:
            if cmd_queue:
                cmd = cmd_queue.pop(0)
                os.write(fd, (cmd + "\n").encode())
                last_action = elapsed
            else:
                phase = "drain"
                last_action = elapsed
        elif phase == "drain" and gap >= args.cmd_delay:
            os.write(fd, b"&.\n")
            sent_disconnect = True
            phase = "done"
            last_action = elapsed
        elif phase == "done" and gap >= 2:
            break

    if not sent_disconnect:
        try:
            os.write(fd, b"&.\n")
        except OSError:
            pass

    try:
        os.close(fd)
    except OSError:
        pass
    try:
        os.waitpid(pid, 0)
    except ChildProcessError:
        pass

    sys.stdout.buffer.write(output)
    sys.stdout.flush()
    return 0


if __name__ == "__main__":
    sys.exit(main())
