qssh 0.2.3__tar.gz → 0.2.4__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.
- {qssh-0.2.3/src/qssh.egg-info → qssh-0.2.4}/PKG-INFO +1 -1
- {qssh-0.2.3 → qssh-0.2.4}/pyproject.toml +1 -1
- {qssh-0.2.3 → qssh-0.2.4}/src/qssh/__init__.py +1 -1
- {qssh-0.2.3 → qssh-0.2.4}/src/qssh/connector.py +95 -40
- {qssh-0.2.3 → qssh-0.2.4/src/qssh.egg-info}/PKG-INFO +1 -1
- {qssh-0.2.3 → qssh-0.2.4}/LICENSE +0 -0
- {qssh-0.2.3 → qssh-0.2.4}/README.md +0 -0
- {qssh-0.2.3 → qssh-0.2.4}/setup.cfg +0 -0
- {qssh-0.2.3 → qssh-0.2.4}/src/qssh/cli.py +0 -0
- {qssh-0.2.3 → qssh-0.2.4}/src/qssh/py.typed +0 -0
- {qssh-0.2.3 → qssh-0.2.4}/src/qssh/session.py +0 -0
- {qssh-0.2.3 → qssh-0.2.4}/src/qssh.egg-info/SOURCES.txt +0 -0
- {qssh-0.2.3 → qssh-0.2.4}/src/qssh.egg-info/dependency_links.txt +0 -0
- {qssh-0.2.3 → qssh-0.2.4}/src/qssh.egg-info/entry_points.txt +0 -0
- {qssh-0.2.3 → qssh-0.2.4}/src/qssh.egg-info/requires.txt +0 -0
- {qssh-0.2.3 → qssh-0.2.4}/src/qssh.egg-info/top_level.txt +0 -0
|
@@ -173,13 +173,35 @@ class SSHConnector:
|
|
|
173
173
|
channel: SSH channel
|
|
174
174
|
"""
|
|
175
175
|
import threading
|
|
176
|
-
import
|
|
176
|
+
import ctypes
|
|
177
177
|
import time
|
|
178
|
+
from ctypes import wintypes
|
|
178
179
|
|
|
179
|
-
#
|
|
180
|
+
# Windows Console API constants
|
|
181
|
+
STD_INPUT_HANDLE = -10
|
|
182
|
+
ENABLE_PROCESSED_INPUT = 0x0001
|
|
183
|
+
ENABLE_LINE_INPUT = 0x0002
|
|
184
|
+
ENABLE_ECHO_INPUT = 0x0004
|
|
185
|
+
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
|
|
186
|
+
|
|
187
|
+
kernel32 = ctypes.windll.kernel32
|
|
188
|
+
|
|
189
|
+
# Get console handle
|
|
190
|
+
stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
|
|
191
|
+
|
|
192
|
+
# Save original console mode
|
|
193
|
+
original_mode = wintypes.DWORD()
|
|
194
|
+
kernel32.GetConsoleMode(stdin_handle, ctypes.byref(original_mode))
|
|
195
|
+
|
|
196
|
+
# Set console to raw mode (disable processed input to capture Ctrl+C)
|
|
197
|
+
new_mode = original_mode.value & ~(ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT)
|
|
198
|
+
new_mode |= ENABLE_VIRTUAL_TERMINAL_INPUT
|
|
199
|
+
kernel32.SetConsoleMode(stdin_handle, new_mode)
|
|
200
|
+
|
|
201
|
+
# Ignore SIGINT at Python level as backup
|
|
180
202
|
original_sigint = signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
181
203
|
|
|
182
|
-
running = [True]
|
|
204
|
+
running = [True]
|
|
183
205
|
|
|
184
206
|
def read_output():
|
|
185
207
|
"""Read from channel and print to stdout."""
|
|
@@ -191,11 +213,9 @@ class SSHConnector:
|
|
|
191
213
|
sys.stdout.write(data.decode("utf-8", errors="replace"))
|
|
192
214
|
sys.stdout.flush()
|
|
193
215
|
else:
|
|
194
|
-
# Empty data means connection closed
|
|
195
216
|
running[0] = False
|
|
196
217
|
break
|
|
197
218
|
|
|
198
|
-
# Check if channel is closed
|
|
199
219
|
if channel.closed or channel.exit_status_ready():
|
|
200
220
|
running[0] = False
|
|
201
221
|
break
|
|
@@ -205,53 +225,88 @@ class SSHConnector:
|
|
|
205
225
|
running[0] = False
|
|
206
226
|
break
|
|
207
227
|
|
|
208
|
-
#
|
|
228
|
+
# Structure for reading console input
|
|
229
|
+
class KEY_EVENT_RECORD(ctypes.Structure):
|
|
230
|
+
_fields_ = [
|
|
231
|
+
("bKeyDown", wintypes.BOOL),
|
|
232
|
+
("wRepeatCount", wintypes.WORD),
|
|
233
|
+
("wVirtualKeyCode", wintypes.WORD),
|
|
234
|
+
("wVirtualScanCode", wintypes.WORD),
|
|
235
|
+
("uChar", ctypes.c_wchar),
|
|
236
|
+
("dwControlKeyState", wintypes.DWORD),
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
class INPUT_RECORD_UNION(ctypes.Union):
|
|
240
|
+
_fields_ = [("KeyEvent", KEY_EVENT_RECORD)]
|
|
241
|
+
|
|
242
|
+
class INPUT_RECORD(ctypes.Structure):
|
|
243
|
+
_fields_ = [
|
|
244
|
+
("EventType", wintypes.WORD),
|
|
245
|
+
("Event", INPUT_RECORD_UNION),
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
KEY_EVENT = 0x0001
|
|
249
|
+
|
|
250
|
+
# Virtual key codes for special keys
|
|
251
|
+
VK_MAP = {
|
|
252
|
+
0x26: '\x1b[A', # Up
|
|
253
|
+
0x28: '\x1b[B', # Down
|
|
254
|
+
0x27: '\x1b[C', # Right
|
|
255
|
+
0x25: '\x1b[D', # Left
|
|
256
|
+
0x24: '\x1b[H', # Home
|
|
257
|
+
0x23: '\x1b[F', # End
|
|
258
|
+
0x2D: '\x1b[2~', # Insert
|
|
259
|
+
0x2E: '\x1b[3~', # Delete
|
|
260
|
+
0x21: '\x1b[5~', # Page Up
|
|
261
|
+
0x22: '\x1b[6~', # Page Down
|
|
262
|
+
}
|
|
263
|
+
|
|
209
264
|
output_thread = threading.Thread(target=read_output, daemon=True)
|
|
210
265
|
output_thread.start()
|
|
211
266
|
|
|
212
267
|
try:
|
|
268
|
+
input_record = INPUT_RECORD()
|
|
269
|
+
events_read = wintypes.DWORD()
|
|
270
|
+
|
|
213
271
|
while running[0] and not channel.closed:
|
|
214
|
-
# Check
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
272
|
+
# Check if input is available
|
|
273
|
+
events_available = wintypes.DWORD()
|
|
274
|
+
kernel32.GetNumberOfConsoleInputEvents(stdin_handle, ctypes.byref(events_available))
|
|
275
|
+
|
|
276
|
+
if events_available.value > 0:
|
|
277
|
+
# Read input event
|
|
278
|
+
kernel32.ReadConsoleInputW(
|
|
279
|
+
stdin_handle,
|
|
280
|
+
ctypes.byref(input_record),
|
|
281
|
+
1,
|
|
282
|
+
ctypes.byref(events_read)
|
|
283
|
+
)
|
|
218
284
|
|
|
219
|
-
if
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
'Q': '\x1b[6~', # Page Down
|
|
237
|
-
}
|
|
238
|
-
if char2 in key_map:
|
|
239
|
-
channel.send(key_map[char2])
|
|
240
|
-
elif char == '\x08':
|
|
241
|
-
# Backspace
|
|
242
|
-
channel.send('\x7f')
|
|
243
|
-
else:
|
|
244
|
-
# Send character as-is (includes Ctrl+C as \x03, Ctrl+D as \x04, etc.)
|
|
245
|
-
channel.send(char)
|
|
285
|
+
if input_record.EventType == KEY_EVENT:
|
|
286
|
+
key_event = input_record.Event.KeyEvent
|
|
287
|
+
|
|
288
|
+
if key_event.bKeyDown:
|
|
289
|
+
vk = key_event.wVirtualKeyCode
|
|
290
|
+
char = key_event.uChar
|
|
291
|
+
|
|
292
|
+
# Check for special keys
|
|
293
|
+
if vk in VK_MAP:
|
|
294
|
+
channel.send(VK_MAP[vk])
|
|
295
|
+
elif char == '\r':
|
|
296
|
+
channel.send('\r')
|
|
297
|
+
elif char == '\x08': # Backspace
|
|
298
|
+
channel.send('\x7f')
|
|
299
|
+
elif char:
|
|
300
|
+
# Send character including control chars (Ctrl+C = \x03)
|
|
301
|
+
channel.send(char)
|
|
246
302
|
else:
|
|
247
|
-
# Small delay to prevent CPU spinning
|
|
248
303
|
time.sleep(0.01)
|
|
249
304
|
|
|
250
305
|
finally:
|
|
251
306
|
running[0] = False
|
|
252
|
-
# Wait for output thread to finish
|
|
253
307
|
output_thread.join(timeout=1.0)
|
|
254
|
-
# Restore original
|
|
308
|
+
# Restore original console mode
|
|
309
|
+
kernel32.SetConsoleMode(stdin_handle, original_mode.value)
|
|
255
310
|
signal.signal(signal.SIGINT, original_sigint)
|
|
256
311
|
|
|
257
312
|
def _unix_interactive_shell(self, channel) -> None:
|
|
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
|