mirror of
https://github.com/natankeddem/bale.git
synced 2026-04-23 06:50:41 +00:00
190 lines
7.3 KiB
Python
190 lines
7.3 KiB
Python
from typing import Any, Callable, Dict, List, Union
|
|
from dataclasses import dataclass
|
|
import asyncio
|
|
from asyncio.subprocess import Process, PIPE
|
|
import contextlib
|
|
import shlex
|
|
from datetime import datetime
|
|
from snapper.result import Result
|
|
from nicegui import app, ui
|
|
from nicegui.element import Element
|
|
from nicegui.events import GenericEventArguments, handle_event
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
app.add_static_files("/static", "static")
|
|
ui.add_head_html('<link href="static/xterm.css" rel="stylesheet">')
|
|
ui.add_head_html('<script type="text/javascript" src="static/xterm.js"></script>')
|
|
|
|
|
|
class Terminal(ui.element, component="../../static/terminal.js", libraries=["../../static/xterm.js"]): # type: ignore[call-arg]
|
|
def __init__(
|
|
self,
|
|
options: Dict,
|
|
on_init: Callable[..., Any] | None = None,
|
|
) -> None:
|
|
super().__init__()
|
|
self._props["options"] = options
|
|
self.is_initialized = False
|
|
if on_init:
|
|
|
|
def handle_on_init(e: GenericEventArguments) -> None:
|
|
self.is_initialized = True
|
|
handle_event(
|
|
on_init,
|
|
GenericEventArguments(sender=self, client=self.client, args=e),
|
|
)
|
|
|
|
self.on("init", handle_on_init)
|
|
|
|
def call_terminal_method(self, name: str, *args) -> None:
|
|
self.run_method("call_api_method", name, *args)
|
|
|
|
def run_method(self, name: str, *args: Any) -> None:
|
|
if not self.is_initialized:
|
|
return
|
|
super().run_method(name, *args)
|
|
|
|
|
|
class Cli:
|
|
def __init__(self, seperator: Union[bytes, None] = b"\n") -> None:
|
|
self.seperator: Union[bytes, None] = seperator
|
|
self.stdout: List[str] = []
|
|
self.stderr: List[str] = []
|
|
self._terminate: asyncio.Event = asyncio.Event()
|
|
self._busy: bool = False
|
|
self.prefix_line: str = ""
|
|
self._stdout_terminals: List[Terminal] = []
|
|
self._stderr_terminals: List[Terminal] = []
|
|
|
|
async def _wait_on_stream(self, stream: asyncio.streams.StreamReader) -> Union[str, None]:
|
|
if self.seperator is None:
|
|
buf = await stream.read(140)
|
|
else:
|
|
try:
|
|
buf = await stream.readuntil(self.seperator)
|
|
except asyncio.exceptions.IncompleteReadError as e:
|
|
buf = e.partial
|
|
except Exception as e:
|
|
raise e
|
|
return buf.decode("utf-8")
|
|
|
|
async def _read_stdout(self, stream: asyncio.streams.StreamReader) -> None:
|
|
while True:
|
|
buf = await self._wait_on_stream(stream=stream)
|
|
if buf:
|
|
self.stdout.append(buf)
|
|
for terminal in self._stdout_terminals:
|
|
terminal.call_terminal_method("write", buf)
|
|
else:
|
|
break
|
|
|
|
async def _read_stderr(self, stream: asyncio.streams.StreamReader) -> None:
|
|
while True:
|
|
buf = await self._wait_on_stream(stream=stream)
|
|
if buf:
|
|
self.stderr.append(buf)
|
|
for terminal in self._stderr_terminals:
|
|
terminal.call_terminal_method("write", buf)
|
|
else:
|
|
break
|
|
|
|
async def _controller(self, process: Process) -> None:
|
|
while process.returncode is None:
|
|
if self._terminate.is_set():
|
|
process.terminate()
|
|
try:
|
|
with contextlib.suppress(asyncio.TimeoutError):
|
|
await asyncio.wait_for(process.wait(), 0.1)
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
def terminate(self) -> None:
|
|
self._terminate.set()
|
|
|
|
async def execute(self, command: str) -> Result:
|
|
self._busy = True
|
|
c = shlex.split(command, posix=False)
|
|
try:
|
|
process = await asyncio.create_subprocess_exec(*c, stdout=PIPE, stderr=PIPE)
|
|
if process.stdout is not None and process.stderr is not None:
|
|
self.stdout.clear()
|
|
self.stderr.clear()
|
|
self._terminate.clear()
|
|
terminated = False
|
|
now = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
|
|
self.prefix_line = f"<{now}> {command}\n"
|
|
for terminal in self._stdout_terminals:
|
|
terminal.call_terminal_method("write", "\n" + self.prefix_line)
|
|
await asyncio.gather(
|
|
self._controller(process=process),
|
|
self._read_stdout(stream=process.stdout),
|
|
self._read_stderr(stream=process.stderr),
|
|
)
|
|
if self._terminate.is_set():
|
|
terminated = True
|
|
await process.wait()
|
|
except Exception as e:
|
|
raise e
|
|
finally:
|
|
self._terminate.clear()
|
|
self._busy = False
|
|
return Result(command=command, stdout_lines=self.stdout.copy(), stderr_lines=self.stderr.copy(), terminated=terminated)
|
|
|
|
async def shell(self, command: str) -> Result:
|
|
self._busy = True
|
|
try:
|
|
process = await asyncio.create_subprocess_shell(command, stdout=PIPE, stderr=PIPE)
|
|
if process.stdout is not None and process.stderr is not None:
|
|
self.stdout.clear()
|
|
self.stderr.clear()
|
|
self._terminate.clear()
|
|
now = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
|
|
self.prefix_line = f"<{now}> {command}\n"
|
|
for terminal in self._stdout_terminals:
|
|
terminal.call_terminal_method("write", "\n" + self.prefix_line)
|
|
await asyncio.gather(
|
|
self._read_stdout(stream=process.stdout),
|
|
self._read_stderr(stream=process.stderr),
|
|
)
|
|
await process.wait()
|
|
except Exception as e:
|
|
raise e
|
|
finally:
|
|
self._busy = False
|
|
return Result(command=command, stdout_lines=self.stdout.copy(), stderr_lines=self.stderr.copy(), terminated=False)
|
|
|
|
def register_stdout_terminal(self, terminal: Terminal) -> None:
|
|
if terminal not in self._stdout_terminals:
|
|
terminal.call_terminal_method("write", self.prefix_line)
|
|
for line in self.stdout:
|
|
terminal.call_terminal_method("write", line)
|
|
self._stdout_terminals.append(terminal)
|
|
|
|
def register_stderr_terminal(self, terminal: Terminal) -> None:
|
|
if terminal not in self._stderr_terminals:
|
|
for line in self.stderr:
|
|
terminal.call_terminal_method("write", line)
|
|
self._stderr_terminals.append(terminal)
|
|
|
|
def release_stdout_terminal(self, terminal: Terminal) -> None:
|
|
if terminal in self._stdout_terminals:
|
|
self._stdout_terminals.remove(terminal)
|
|
|
|
def release_stderr_terminal(self, terminal: Terminal) -> None:
|
|
if terminal in self._stderr_terminals:
|
|
self._stderr_terminals.remove(terminal)
|
|
|
|
def register_terminal(self, terminal: Terminal) -> None:
|
|
self.register_stdout_terminal(terminal=terminal)
|
|
self.register_stderr_terminal(terminal=terminal)
|
|
|
|
def release_terminal(self, terminal: Terminal) -> None:
|
|
self.release_stdout_terminal(terminal=terminal)
|
|
self.release_stderr_terminal(terminal=terminal)
|
|
|
|
@property
|
|
def is_busy(self):
|
|
return self._busy
|