113 lines
3.4 KiB
Python
113 lines
3.4 KiB
Python
"""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":"<text>"} 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":"<text>"} 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])
|