Holds one long-lived /bin/sh on the ghost over a single WS. Commands are framed by appending a per-command sentinel printf that emits `\\n__OVL_END_ : \\n` after the user command, so the merged stdout/stderr stream can be parsed without a fresh shell per call.
| 720 | |
| 721 | |
| 722 | class PersistentShellWebSocketClient(SSLEnabledWebSocketBaseClient): |
| 723 | |
| 724 | """Holds one long-lived /bin/sh on the ghost over a single WS. |
| 725 | |
| 726 | Commands are framed by appending a per-command sentinel printf that emits |
| 727 | `\\n__OVL_END_<nonce>:<exit>\\n` after the user command, so the merged |
| 728 | stdout/stderr stream can be parsed without a fresh shell per call. |
| 729 | """ |
| 730 | |
| 731 | def __init__(self, state, *args, **kwargs): |
| 732 | super().__init__(state, *args, **kwargs) |
| 733 | self._opened = threading.Event() |
| 734 | self._cmd_mutex = threading.Lock() # serializes RunCommand callers |
| 735 | self._cv = threading.Condition() |
| 736 | self._buf = b'' |
| 737 | self._active_nonce = None # bytes |
| 738 | self._active_writer = None |
| 739 | self._exit_code = None |
| 740 | self._died = False |
| 741 | |
| 742 | def handshake_ok(self): |
| 743 | pass |
| 744 | |
| 745 | def opened(self): |
| 746 | self._opened.set() |
| 747 | |
| 748 | def closed(self, code, reason=None): |
| 749 | del code, reason |
| 750 | with self._cv: |
| 751 | self._died = True |
| 752 | self._cv.notify_all() |
| 753 | |
| 754 | def received_message(self, message): |
| 755 | if not message.is_binary: |
| 756 | return |
| 757 | with self._cv: |
| 758 | self._buf += message.data |
| 759 | if self._active_nonce is None: |
| 760 | # Idle stream noise: drop it. |
| 761 | self._buf = b'' |
| 762 | return |
| 763 | sentinel = b'\n__OVL_END_' + self._active_nonce + b':' |
| 764 | idx = self._buf.find(sentinel) |
| 765 | if idx == -1: |
| 766 | # Forward everything except a tail that might be a split sentinel. |
| 767 | keep = len(sentinel) - 1 |
| 768 | if len(self._buf) > keep: |
| 769 | chunk = self._buf[:-keep] if keep else self._buf |
| 770 | self._buf = self._buf[len(chunk):] |
| 771 | if self._active_writer and chunk: |
| 772 | self._active_writer(chunk) |
| 773 | return |
| 774 | if idx > 0 and self._active_writer: |
| 775 | self._active_writer(self._buf[:idx]) |
| 776 | tail = self._buf[idx + len(sentinel):] |
| 777 | nl = tail.find(b'\n') |
| 778 | if nl == -1: |
| 779 | # Sentinel found but exit code not yet complete; wait for more. |