"""Interactive command terminal for the NetDesk host ("cmd"). Spawns a shell and streams its output, so a connected viewer gets a working console: run commands and see their output live (dir, ipconfig, ping, python scripts, git, etc.). Uses a pipe rather than a full pseudo-console — that covers command execution; full-screen TUI apps (vim, a nested shell UI) would need a ConPTY and are a future enhancement. Wire protocol (host side): viewer -> host : {"t":"term.open","tid":N} open a shell viewer -> host : {"t":"term.data","tid":N,"d":""} send keystrokes/commands viewer -> host : {"t":"term.resize","tid":N,"cols":C,"rows":R} viewer -> host : {"t":"term.close","tid":N} host -> viewer: {"t":"term.data","tid":N,"d":""} shell output (streamed) host -> viewer: {"t":"term.exit","tid":N} shell ended Developed by Akronlogic. """ import os import subprocess import threading CREATE_NO_WINDOW = 0x08000000 class TerminalSession: """One shell process. `on_output(text: str)` is invoked from a reader thread as the shell produces output; it is called once with "" when the shell exits.""" def __init__(self, on_output, shell=None, cwd=None): self._on_output = on_output shell = shell or os.environ.get("COMSPEC", "cmd.exe") self._p = subprocess.Popen( shell, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0, cwd=cwd, creationflags=CREATE_NO_WINDOW if os.name == "nt" else 0, ) self._alive = True self._reader = threading.Thread(target=self._pump, daemon=True) self._reader.start() def _pump(self): try: while True: chunk = self._p.stdout.read(4096) if not chunk: break try: self._on_output(chunk.decode("utf-8", "replace")) except Exception: pass finally: self._alive = False try: self._on_output("") # EOF -> shell closed except Exception: pass def write(self, data): """Send keystrokes / a command line (include the trailing \\r\\n).""" if isinstance(data, str): data = data.encode("utf-8", "replace") try: self._p.stdin.write(data) self._p.stdin.flush() return True except Exception: return False def resize(self, cols, rows): # Pipe mode has no window size; accepted for API parity (ConPTY later). return None @property def alive(self): return self._alive and self._p.poll() is None def close(self): self._alive = False try: self._p.stdin.close() except Exception: pass try: self._p.terminate() except Exception: pass if __name__ == "__main__": import time buf = [] term = TerminalSession(on_output=buf.append) time.sleep(0.3) term.write("echo NETDESK_TERM_OK_777\r\n") time.sleep(0.6) term.write("cd\r\n") time.sleep(0.4) term.write("exit\r\n") time.sleep(0.4) term.close() text = "".join(buf) ok = "NETDESK_TERM_OK_777" in text print("SELFTEST", "PASS" if ok else "FAIL", "| chars:", len(text)) if not ok: print(text[:800])