NetDesk 1.5.0 — source release
This commit is contained in:
commit
ec4955682f
227 files changed
+80669
No files matched your search
@@ -0,0 +1,112 @@
|
||||
"""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])
|
||||
Reference in new issue
Block a user