panopticon-cli 1.1.3__tar.gz → 1.1.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {panopticon_cli-1.1.3/panopticon_cli.egg-info → panopticon_cli-1.1.6}/PKG-INFO +1 -1
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon/cli.py +196 -37
- panopticon_cli-1.1.6/panopticon/conpty.py +211 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6/panopticon_cli.egg-info}/PKG-INFO +1 -1
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon_cli.egg-info/SOURCES.txt +1 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/pyproject.toml +1 -1
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/tests/test_cli.py +7 -6
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/LICENSE +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/README.md +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon/__init__.py +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon/memory.py +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon/observer.py +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon/policies.py +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon/sentinel.py +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon_cli.egg-info/dependency_links.txt +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon_cli.egg-info/entry_points.txt +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon_cli.egg-info/requires.txt +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon_cli.egg-info/top_level.txt +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/setup.cfg +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/tests/test_memory.py +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/tests/test_observer.py +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/tests/test_policies.py +0 -0
- {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/tests/test_sentinel.py +0 -0
|
@@ -32,6 +32,9 @@ class CLIWrapper:
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
self.process = None
|
|
35
|
+
self.master_fd = None
|
|
36
|
+
self.master_fd_in = None
|
|
37
|
+
self.conpty_hPC = None
|
|
35
38
|
self.buffer = []
|
|
36
39
|
self.running = False
|
|
37
40
|
self.lock = threading.Lock()
|
|
@@ -51,27 +54,81 @@ class CLIWrapper:
|
|
|
51
54
|
f"AdversarialLogic"
|
|
52
55
|
)
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
stdin=subprocess.PIPE,
|
|
60
|
-
bufsize=0,
|
|
61
|
-
env=env,
|
|
57
|
+
use_pty = (
|
|
58
|
+
sys.platform != "win32"
|
|
59
|
+
and sys.stdin is not None
|
|
60
|
+
and sys.stdin.isatty()
|
|
61
|
+
and sys.stdout.isatty()
|
|
62
62
|
)
|
|
63
|
+
use_conpty = (
|
|
64
|
+
sys.platform == "win32"
|
|
65
|
+
and sys.stdin is not None
|
|
66
|
+
and sys.stdin.isatty()
|
|
67
|
+
and sys.stdout.isatty()
|
|
68
|
+
)
|
|
69
|
+
self.master_fd = None
|
|
70
|
+
self.master_fd_in = None
|
|
71
|
+
self.conpty_hPC = None
|
|
72
|
+
|
|
73
|
+
if use_pty:
|
|
74
|
+
import pty
|
|
75
|
+
|
|
76
|
+
master_fd, slave_fd = pty.openpty()
|
|
77
|
+
self.master_fd = master_fd
|
|
78
|
+
self.master_fd_in = master_fd
|
|
79
|
+
self.process = subprocess.Popen(
|
|
80
|
+
self.command,
|
|
81
|
+
stdout=slave_fd,
|
|
82
|
+
stderr=slave_fd,
|
|
83
|
+
stdin=slave_fd,
|
|
84
|
+
bufsize=0,
|
|
85
|
+
env=env,
|
|
86
|
+
close_fds=True,
|
|
87
|
+
)
|
|
88
|
+
os.close(slave_fd)
|
|
89
|
+
use_stdin_thread = True
|
|
90
|
+
elif use_conpty:
|
|
91
|
+
from panopticon.conpty import spawn_conpty
|
|
92
|
+
|
|
93
|
+
proc, hPC, master_in, master_out = spawn_conpty(self.command, env=env)
|
|
94
|
+
self.process = proc
|
|
95
|
+
self.conpty_hPC = hPC
|
|
96
|
+
self.master_fd = master_out
|
|
97
|
+
self.master_fd_in = master_in
|
|
98
|
+
use_stdin_thread = True
|
|
99
|
+
else:
|
|
100
|
+
stdin_target = subprocess.PIPE
|
|
101
|
+
use_stdin_thread = True
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
sys.platform != "win32"
|
|
105
|
+
and sys.stdin is not None
|
|
106
|
+
and sys.stdin.isatty()
|
|
107
|
+
):
|
|
108
|
+
stdin_target = sys.stdin
|
|
109
|
+
use_stdin_thread = False
|
|
110
|
+
|
|
111
|
+
self.process = subprocess.Popen(
|
|
112
|
+
self.command,
|
|
113
|
+
stdout=subprocess.PIPE,
|
|
114
|
+
stderr=subprocess.STDOUT,
|
|
115
|
+
stdin=stdin_target,
|
|
116
|
+
bufsize=0,
|
|
117
|
+
env=env,
|
|
118
|
+
)
|
|
63
119
|
|
|
64
|
-
t_out = threading.Thread(target=self._read_stdout)
|
|
120
|
+
t_out = threading.Thread(target=self._read_stdout, daemon=True)
|
|
65
121
|
t_out.start()
|
|
66
122
|
self.threads.append(t_out)
|
|
67
123
|
|
|
68
|
-
t_eval = threading.Thread(target=self._eval_loop)
|
|
124
|
+
t_eval = threading.Thread(target=self._eval_loop, daemon=True)
|
|
69
125
|
t_eval.start()
|
|
70
126
|
self.threads.append(t_eval)
|
|
71
127
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
128
|
+
if use_stdin_thread:
|
|
129
|
+
t_in = threading.Thread(target=self._read_stdin, daemon=True)
|
|
130
|
+
t_in.start()
|
|
131
|
+
self.threads.append(t_in)
|
|
75
132
|
|
|
76
133
|
# Register cleanup on exit
|
|
77
134
|
atexit.register(self._cleanup)
|
|
@@ -89,6 +146,29 @@ class CLIWrapper:
|
|
|
89
146
|
self.running = False
|
|
90
147
|
self._stop_event.set()
|
|
91
148
|
|
|
149
|
+
if getattr(self, "conpty_hPC", None) is not None:
|
|
150
|
+
try:
|
|
151
|
+
from panopticon.conpty import close_conpty
|
|
152
|
+
close_conpty(self.conpty_hPC)
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
self.conpty_hPC = None
|
|
156
|
+
|
|
157
|
+
if getattr(self, "master_fd_in", None) is not None:
|
|
158
|
+
try:
|
|
159
|
+
if self.master_fd_in != getattr(self, "master_fd", None):
|
|
160
|
+
os.close(self.master_fd_in)
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
self.master_fd_in = None
|
|
164
|
+
|
|
165
|
+
if getattr(self, "master_fd", None) is not None:
|
|
166
|
+
try:
|
|
167
|
+
os.close(self.master_fd)
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
self.master_fd = None
|
|
171
|
+
|
|
92
172
|
# Close subprocess pipes FIRST before thread cleanup
|
|
93
173
|
if self.process:
|
|
94
174
|
try:
|
|
@@ -118,31 +198,16 @@ class CLIWrapper:
|
|
|
118
198
|
if thread.is_alive():
|
|
119
199
|
thread.join(timeout=1)
|
|
120
200
|
|
|
121
|
-
def _stdin_ready(self) -> bool:
|
|
122
|
-
stdin_obj = sys.stdin
|
|
123
|
-
if sys.platform == "win32":
|
|
124
|
-
try:
|
|
125
|
-
import msvcrt
|
|
126
|
-
except ImportError:
|
|
127
|
-
pass
|
|
128
|
-
else:
|
|
129
|
-
if stdin_obj.isatty():
|
|
130
|
-
return msvcrt.kbhit()
|
|
131
|
-
|
|
132
|
-
try:
|
|
133
|
-
import select
|
|
134
|
-
|
|
135
|
-
return bool(select.select([stdin_obj], [], [], 0.1)[0])
|
|
136
|
-
except Exception:
|
|
137
|
-
return False
|
|
138
|
-
|
|
139
201
|
def _read_stdout(self):
|
|
140
202
|
# Flaw 2 Fix: Safely decode multi-byte UTF-8 emojis incrementally
|
|
141
203
|
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
142
204
|
try:
|
|
143
205
|
while self.running and self.process and self.process.poll() is None:
|
|
144
206
|
try:
|
|
145
|
-
|
|
207
|
+
if getattr(self, "master_fd", None) is not None:
|
|
208
|
+
byte_chunk = os.read(self.master_fd, 1)
|
|
209
|
+
else:
|
|
210
|
+
byte_chunk = self.process.stdout.read(1)
|
|
146
211
|
if not byte_chunk:
|
|
147
212
|
break
|
|
148
213
|
|
|
@@ -161,7 +226,8 @@ class CLIWrapper:
|
|
|
161
226
|
finally:
|
|
162
227
|
try:
|
|
163
228
|
if (
|
|
164
|
-
self
|
|
229
|
+
getattr(self, "master_fd", None) is None
|
|
230
|
+
and self.process
|
|
165
231
|
and self.process.stdout
|
|
166
232
|
and (not self.process.stdout.closed)
|
|
167
233
|
):
|
|
@@ -169,7 +235,40 @@ class CLIWrapper:
|
|
|
169
235
|
except Exception:
|
|
170
236
|
pass
|
|
171
237
|
|
|
238
|
+
def _stdin_ready(self) -> bool:
|
|
239
|
+
if sys.platform == "win32":
|
|
240
|
+
if (
|
|
241
|
+
sys.stdin is not None
|
|
242
|
+
and getattr(sys.stdin, "isatty", lambda: False)()
|
|
243
|
+
and hasattr(sys.stdin, "fileno")
|
|
244
|
+
):
|
|
245
|
+
try:
|
|
246
|
+
import msvcrt
|
|
247
|
+
|
|
248
|
+
return msvcrt.kbhit()
|
|
249
|
+
except ImportError:
|
|
250
|
+
return False
|
|
251
|
+
return True
|
|
252
|
+
else:
|
|
253
|
+
import select
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
rlist, _, _ = select.select([sys.stdin], [], [], 0)
|
|
257
|
+
return bool(rlist)
|
|
258
|
+
except Exception:
|
|
259
|
+
return False
|
|
260
|
+
|
|
172
261
|
def _read_stdin(self):
|
|
262
|
+
if sys.platform == "win32":
|
|
263
|
+
if (
|
|
264
|
+
sys.stdin is not None
|
|
265
|
+
and getattr(sys.stdin, "isatty", lambda: False)()
|
|
266
|
+
and hasattr(sys.stdin, "fileno")
|
|
267
|
+
and sys.stdin.fileno() == 0
|
|
268
|
+
):
|
|
269
|
+
self._read_stdin_windows()
|
|
270
|
+
return
|
|
271
|
+
|
|
173
272
|
stdin_buffer = getattr(sys.stdin, "buffer", None)
|
|
174
273
|
if stdin_buffer is None:
|
|
175
274
|
return
|
|
@@ -186,10 +285,66 @@ class CLIWrapper:
|
|
|
186
285
|
continue
|
|
187
286
|
|
|
188
287
|
try:
|
|
189
|
-
|
|
288
|
+
read_func = getattr(stdin_buffer, "read1", stdin_buffer.read)
|
|
289
|
+
data = read_func(1024)
|
|
190
290
|
if not data:
|
|
191
291
|
break
|
|
192
|
-
if self
|
|
292
|
+
if getattr(self, "master_fd_in", None) is not None:
|
|
293
|
+
os.write(self.master_fd_in, data)
|
|
294
|
+
elif getattr(self, "master_fd", None) is not None:
|
|
295
|
+
os.write(self.master_fd, data)
|
|
296
|
+
elif self.process.stdin and not self.process.stdin.closed:
|
|
297
|
+
self.process.stdin.write(data)
|
|
298
|
+
self.process.stdin.flush()
|
|
299
|
+
except (OSError, ValueError, BrokenPipeError):
|
|
300
|
+
break
|
|
301
|
+
except Exception:
|
|
302
|
+
pass
|
|
303
|
+
finally:
|
|
304
|
+
try:
|
|
305
|
+
if (
|
|
306
|
+
getattr(self, "master_fd", None) is None
|
|
307
|
+
and self.process
|
|
308
|
+
and self.process.stdin
|
|
309
|
+
and (not self.process.stdin.closed)
|
|
310
|
+
):
|
|
311
|
+
self.process.stdin.close()
|
|
312
|
+
except Exception:
|
|
313
|
+
pass
|
|
314
|
+
|
|
315
|
+
def _read_stdin_windows(self):
|
|
316
|
+
try:
|
|
317
|
+
import msvcrt
|
|
318
|
+
except ImportError:
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
while (
|
|
323
|
+
self.running
|
|
324
|
+
and self.process
|
|
325
|
+
and self.process.poll() is None
|
|
326
|
+
and not self._stop_event.is_set()
|
|
327
|
+
):
|
|
328
|
+
if not msvcrt.kbhit():
|
|
329
|
+
time.sleep(0.1)
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
char = msvcrt.getwch()
|
|
334
|
+
if char == "\x03":
|
|
335
|
+
if self.process.poll() is None:
|
|
336
|
+
try:
|
|
337
|
+
self.process.send_signal(signal.CTRL_C_EVENT)
|
|
338
|
+
except Exception:
|
|
339
|
+
pass
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
data = char.encode("utf-8")
|
|
343
|
+
if getattr(self, "master_fd_in", None) is not None:
|
|
344
|
+
os.write(self.master_fd_in, data)
|
|
345
|
+
elif getattr(self, "master_fd", None) is not None:
|
|
346
|
+
os.write(self.master_fd, data)
|
|
347
|
+
elif self.process.stdin and not self.process.stdin.closed:
|
|
193
348
|
self.process.stdin.write(data)
|
|
194
349
|
self.process.stdin.flush()
|
|
195
350
|
except (OSError, ValueError, BrokenPipeError):
|
|
@@ -248,8 +403,12 @@ class CLIWrapper:
|
|
|
248
403
|
|
|
249
404
|
try:
|
|
250
405
|
# Write the injection payload as binary
|
|
251
|
-
|
|
252
|
-
|
|
406
|
+
payload = (e.course_correction + "\n").encode("utf-8")
|
|
407
|
+
if getattr(self, "master_fd_in", None) is not None:
|
|
408
|
+
os.write(self.master_fd_in, payload)
|
|
409
|
+
elif getattr(self, "master_fd", None) is not None:
|
|
410
|
+
os.write(self.master_fd, payload)
|
|
411
|
+
elif self.process.stdin and not self.process.stdin.closed:
|
|
253
412
|
self.process.stdin.write(payload)
|
|
254
413
|
self.process.stdin.flush()
|
|
255
414
|
except Exception as ex:
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import subprocess
|
|
4
|
+
import shutil
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
if sys.platform == "win32":
|
|
8
|
+
import ctypes
|
|
9
|
+
from ctypes import wintypes
|
|
10
|
+
import msvcrt
|
|
11
|
+
|
|
12
|
+
kernel32 = ctypes.windll.kernel32
|
|
13
|
+
|
|
14
|
+
class COORD(ctypes.Structure):
|
|
15
|
+
_fields_ = [("X", wintypes.SHORT), ("Y", wintypes.SHORT)]
|
|
16
|
+
|
|
17
|
+
class STARTUPINFOW(ctypes.Structure):
|
|
18
|
+
_fields_ = [
|
|
19
|
+
("cb", wintypes.DWORD),
|
|
20
|
+
("lpReserved", wintypes.LPWSTR),
|
|
21
|
+
("lpDesktop", wintypes.LPWSTR),
|
|
22
|
+
("lpTitle", wintypes.LPWSTR),
|
|
23
|
+
("dwX", wintypes.DWORD),
|
|
24
|
+
("dwY", wintypes.DWORD),
|
|
25
|
+
("dwXSize", wintypes.DWORD),
|
|
26
|
+
("dwYSize", wintypes.DWORD),
|
|
27
|
+
("dwXCountChars", wintypes.DWORD),
|
|
28
|
+
("dwYCountChars", wintypes.DWORD),
|
|
29
|
+
("dwFillAttribute", wintypes.DWORD),
|
|
30
|
+
("dwFlags", wintypes.DWORD),
|
|
31
|
+
("wShowWindow", wintypes.WORD),
|
|
32
|
+
("cbReserved2", wintypes.WORD),
|
|
33
|
+
("lpReserved2", ctypes.POINTER(ctypes.c_byte)),
|
|
34
|
+
("hStdInput", wintypes.HANDLE),
|
|
35
|
+
("hStdOutput", wintypes.HANDLE),
|
|
36
|
+
("hStdError", wintypes.HANDLE),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
class STARTUPINFOEXW(ctypes.Structure):
|
|
40
|
+
_fields_ = [
|
|
41
|
+
("StartupInfo", STARTUPINFOW),
|
|
42
|
+
("lpAttributeList", wintypes.LPVOID),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
class PROCESS_INFORMATION(ctypes.Structure):
|
|
46
|
+
_fields_ = [
|
|
47
|
+
("hProcess", wintypes.HANDLE),
|
|
48
|
+
("hThread", wintypes.HANDLE),
|
|
49
|
+
("dwProcessId", wintypes.DWORD),
|
|
50
|
+
("dwThreadId", wintypes.DWORD),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
HPCON = wintypes.HANDLE
|
|
54
|
+
EXTENDED_STARTUPINFO_PRESENT = 0x00080000
|
|
55
|
+
CREATE_UNICODE_ENVIRONMENT = 0x00000400
|
|
56
|
+
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016
|
|
57
|
+
|
|
58
|
+
class ConPTYProcess:
|
|
59
|
+
def __init__(self, pi, hPC):
|
|
60
|
+
self.pi = pi
|
|
61
|
+
self.hPC = hPC
|
|
62
|
+
self.returncode = None
|
|
63
|
+
self.stdin = None
|
|
64
|
+
self.stdout = None
|
|
65
|
+
self.stderr = None
|
|
66
|
+
|
|
67
|
+
def poll(self):
|
|
68
|
+
if self.returncode is not None:
|
|
69
|
+
return self.returncode
|
|
70
|
+
res = kernel32.WaitForSingleObject(self.pi.hProcess, 0)
|
|
71
|
+
if res == 0: # WAIT_OBJECT_0
|
|
72
|
+
exit_code = wintypes.DWORD()
|
|
73
|
+
if kernel32.GetExitCodeProcess(self.pi.hProcess, ctypes.byref(exit_code)):
|
|
74
|
+
self.returncode = exit_code.value
|
|
75
|
+
else:
|
|
76
|
+
self.returncode = 0
|
|
77
|
+
self.cleanup_handles()
|
|
78
|
+
return self.returncode
|
|
79
|
+
|
|
80
|
+
def wait(self, timeout=None):
|
|
81
|
+
if self.returncode is not None:
|
|
82
|
+
return self.returncode
|
|
83
|
+
if timeout is None:
|
|
84
|
+
ms = 0xFFFFFFFF # INFINITE
|
|
85
|
+
else:
|
|
86
|
+
ms = int(timeout * 1000)
|
|
87
|
+
res = kernel32.WaitForSingleObject(self.pi.hProcess, ms)
|
|
88
|
+
if res == 0:
|
|
89
|
+
exit_code = wintypes.DWORD()
|
|
90
|
+
if kernel32.GetExitCodeProcess(self.pi.hProcess, ctypes.byref(exit_code)):
|
|
91
|
+
self.returncode = exit_code.value
|
|
92
|
+
else:
|
|
93
|
+
self.returncode = 0
|
|
94
|
+
self.cleanup_handles()
|
|
95
|
+
return self.returncode
|
|
96
|
+
elif res == 0x00000102: # WAIT_TIMEOUT
|
|
97
|
+
raise subprocess.TimeoutExpired(self.pi.dwProcessId, timeout)
|
|
98
|
+
return self.returncode
|
|
99
|
+
|
|
100
|
+
def terminate(self):
|
|
101
|
+
if self.poll() is None:
|
|
102
|
+
kernel32.TerminateProcess(self.pi.hProcess, 1)
|
|
103
|
+
|
|
104
|
+
def kill(self):
|
|
105
|
+
self.terminate()
|
|
106
|
+
|
|
107
|
+
def send_signal(self, sig):
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
def cleanup_handles(self):
|
|
111
|
+
if getattr(self, "pi", None) and self.pi.hProcess:
|
|
112
|
+
kernel32.CloseHandle(self.pi.hProcess)
|
|
113
|
+
self.pi.hProcess = 0
|
|
114
|
+
if getattr(self, "pi", None) and self.pi.hThread:
|
|
115
|
+
kernel32.CloseHandle(self.pi.hThread)
|
|
116
|
+
self.pi.hThread = 0
|
|
117
|
+
|
|
118
|
+
def spawn_conpty(command, env=None):
|
|
119
|
+
in_read, in_write = os.pipe()
|
|
120
|
+
out_read, out_write = os.pipe()
|
|
121
|
+
|
|
122
|
+
hInputRead = msvcrt.get_osfhandle(in_read)
|
|
123
|
+
hOutputWrite = msvcrt.get_osfhandle(out_write)
|
|
124
|
+
|
|
125
|
+
size = COORD(120, 30)
|
|
126
|
+
hPC = HPCON()
|
|
127
|
+
res = kernel32.CreatePseudoConsole(size, hInputRead, hOutputWrite, 0, ctypes.byref(hPC))
|
|
128
|
+
if res != 0:
|
|
129
|
+
os.close(in_read)
|
|
130
|
+
os.close(in_write)
|
|
131
|
+
os.close(out_read)
|
|
132
|
+
os.close(out_write)
|
|
133
|
+
raise RuntimeError(f"CreatePseudoConsole failed with HRESULT {res}")
|
|
134
|
+
|
|
135
|
+
attr_list_size = ctypes.c_size_t(0)
|
|
136
|
+
kernel32.InitializeProcThreadAttributeList(None, 1, 0, ctypes.byref(attr_list_size))
|
|
137
|
+
attr_list = ctypes.create_string_buffer(attr_list_size.value)
|
|
138
|
+
if not kernel32.InitializeProcThreadAttributeList(attr_list, 1, 0, ctypes.byref(attr_list_size)):
|
|
139
|
+
kernel32.ClosePseudoConsole(hPC)
|
|
140
|
+
raise RuntimeError("InitializeProcThreadAttributeList failed")
|
|
141
|
+
|
|
142
|
+
if not kernel32.UpdateProcThreadAttribute(
|
|
143
|
+
attr_list,
|
|
144
|
+
0,
|
|
145
|
+
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
|
|
146
|
+
hPC,
|
|
147
|
+
ctypes.sizeof(HPCON),
|
|
148
|
+
None,
|
|
149
|
+
None,
|
|
150
|
+
):
|
|
151
|
+
kernel32.DeleteProcThreadAttributeList(attr_list)
|
|
152
|
+
kernel32.ClosePseudoConsole(hPC)
|
|
153
|
+
raise RuntimeError("UpdateProcThreadAttribute failed")
|
|
154
|
+
|
|
155
|
+
si_ex = STARTUPINFOEXW()
|
|
156
|
+
si_ex.StartupInfo.cb = ctypes.sizeof(STARTUPINFOEXW)
|
|
157
|
+
si_ex.lpAttributeList = ctypes.cast(attr_list, wintypes.LPVOID)
|
|
158
|
+
|
|
159
|
+
pi = PROCESS_INFORMATION()
|
|
160
|
+
|
|
161
|
+
exe = shutil.which(command[0]) or command[0]
|
|
162
|
+
cmdline = subprocess.list2cmdline([exe] + command[1:])
|
|
163
|
+
cmd_buf = ctypes.create_unicode_buffer(cmdline)
|
|
164
|
+
|
|
165
|
+
env_buf = None
|
|
166
|
+
flags = EXTENDED_STARTUPINFO_PRESENT
|
|
167
|
+
if env is not None:
|
|
168
|
+
env_items = [f"{str(k)}={str(v)}" for k, v in env.items()]
|
|
169
|
+
env_str = "\0".join(sorted(env_items)) + "\0\0"
|
|
170
|
+
env_buf = ctypes.create_unicode_buffer(env_str)
|
|
171
|
+
flags |= CREATE_UNICODE_ENVIRONMENT
|
|
172
|
+
|
|
173
|
+
success = kernel32.CreateProcessW(
|
|
174
|
+
None,
|
|
175
|
+
cmd_buf,
|
|
176
|
+
None,
|
|
177
|
+
None,
|
|
178
|
+
False,
|
|
179
|
+
flags,
|
|
180
|
+
env_buf,
|
|
181
|
+
None,
|
|
182
|
+
ctypes.byref(si_ex),
|
|
183
|
+
ctypes.byref(pi),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
kernel32.DeleteProcThreadAttributeList(attr_list)
|
|
187
|
+
os.close(in_read)
|
|
188
|
+
os.close(out_write)
|
|
189
|
+
|
|
190
|
+
if not success:
|
|
191
|
+
err = kernel32.GetLastError()
|
|
192
|
+
kernel32.ClosePseudoConsole(hPC)
|
|
193
|
+
os.close(in_write)
|
|
194
|
+
os.close(out_read)
|
|
195
|
+
raise RuntimeError(f"CreateProcessW failed with error {err}")
|
|
196
|
+
|
|
197
|
+
proc = ConPTYProcess(pi, hPC)
|
|
198
|
+
return proc, hPC, in_write, out_read
|
|
199
|
+
|
|
200
|
+
def close_conpty(hPC):
|
|
201
|
+
if hPC:
|
|
202
|
+
try:
|
|
203
|
+
kernel32.ClosePseudoConsole(hPC)
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
206
|
+
else:
|
|
207
|
+
def spawn_conpty(command, env=None):
|
|
208
|
+
raise NotImplementedError("ConPTY is only supported on Windows")
|
|
209
|
+
|
|
210
|
+
def close_conpty(hPC):
|
|
211
|
+
pass
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import sys
|
|
3
|
+
from unittest.mock import patch
|
|
3
4
|
|
|
4
5
|
from panopticon.cli import CLIWrapper, strip_ansi
|
|
5
6
|
|
|
@@ -52,12 +53,12 @@ def test_read_stdin_forwards_input():
|
|
|
52
53
|
wrapper.running = True
|
|
53
54
|
wrapper.process = DummyProcess()
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
dummy_stdin = DummyStdinStream(b"hello world\n")
|
|
57
|
+
dummy_stdin.isatty = lambda: True
|
|
58
|
+
|
|
59
|
+
with patch("panopticon.cli.sys.stdin", dummy_stdin):
|
|
60
|
+
with patch.object(CLIWrapper, "_stdin_ready", return_value=True):
|
|
61
|
+
wrapper._read_stdin()
|
|
61
62
|
|
|
62
63
|
assert wrapper.process.stdin.written == b"hello world\n"
|
|
63
64
|
assert wrapper.process.stdin.closed is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|