multiSSH3 5.76__tar.gz → 5.78__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.
Potentially problematic release.
This version of multiSSH3 might be problematic. Click here for more details.
- {multissh3-5.76 → multissh3-5.78}/PKG-INFO +1 -1
- {multissh3-5.76 → multissh3-5.78}/multiSSH3.egg-info/PKG-INFO +1 -1
- {multissh3-5.76 → multissh3-5.78}/multiSSH3.py +73 -49
- {multissh3-5.76 → multissh3-5.78}/README.md +0 -0
- {multissh3-5.76 → multissh3-5.78}/multiSSH3.egg-info/SOURCES.txt +0 -0
- {multissh3-5.76 → multissh3-5.78}/multiSSH3.egg-info/dependency_links.txt +0 -0
- {multissh3-5.76 → multissh3-5.78}/multiSSH3.egg-info/entry_points.txt +0 -0
- {multissh3-5.76 → multissh3-5.78}/multiSSH3.egg-info/requires.txt +0 -0
- {multissh3-5.76 → multissh3-5.78}/multiSSH3.egg-info/top_level.txt +0 -0
- {multissh3-5.76 → multissh3-5.78}/setup.cfg +0 -0
- {multissh3-5.76 → multissh3-5.78}/setup.py +0 -0
- {multissh3-5.76 → multissh3-5.78}/test/test.py +0 -0
- {multissh3-5.76 → multissh3-5.78}/test/testCurses.py +0 -0
- {multissh3-5.76 → multissh3-5.78}/test/testCursesOld.py +0 -0
- {multissh3-5.76 → multissh3-5.78}/test/testPerfCompact.py +0 -0
- {multissh3-5.76 → multissh3-5.78}/test/testPerfExpand.py +0 -0
|
@@ -55,10 +55,10 @@ except AttributeError:
|
|
|
55
55
|
# If neither is available, use a dummy decorator
|
|
56
56
|
def cache_decorator(func):
|
|
57
57
|
return func
|
|
58
|
-
version = '5.
|
|
58
|
+
version = '5.78'
|
|
59
59
|
VERSION = version
|
|
60
60
|
__version__ = version
|
|
61
|
-
COMMIT_DATE = '2025-06-
|
|
61
|
+
COMMIT_DATE = '2025-06-26'
|
|
62
62
|
|
|
63
63
|
CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
|
|
64
64
|
'~/multiSSH3.config.json',
|
|
@@ -246,6 +246,7 @@ class Host:
|
|
|
246
246
|
self.stderr = [] # the stderr of the command
|
|
247
247
|
self.lineNumToPrintSet = set() # line numbers to reprint
|
|
248
248
|
self.lastUpdateTime = time.monotonic() # the last time the output was updated
|
|
249
|
+
self.lastPrintedUpdateTime = 0 # the last time the output was printed
|
|
249
250
|
self.files = files # the files to be copied to the host
|
|
250
251
|
self.ipmi = ipmi # whether to use ipmi to connect to the host
|
|
251
252
|
self.shell = shell # whether to use shell to run the command
|
|
@@ -260,6 +261,9 @@ class Host:
|
|
|
260
261
|
self.identity_file = identity_file
|
|
261
262
|
self.ip = ip if ip else getIP(name)
|
|
262
263
|
self.current_color_pair = [-1, -1, 1]
|
|
264
|
+
self.output_buffer = io.BytesIO()
|
|
265
|
+
self.stdout_buffer = io.BytesIO()
|
|
266
|
+
self.stderr_buffer = io.BytesIO()
|
|
263
267
|
|
|
264
268
|
def __iter__(self):
|
|
265
269
|
return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
|
|
@@ -1133,7 +1137,7 @@ def expand_hostnames(hosts):
|
|
|
1133
1137
|
|
|
1134
1138
|
|
|
1135
1139
|
#%% ------------ Run Command Block ----------------
|
|
1136
|
-
def __handle_reading_stream(stream,target, host):
|
|
1140
|
+
def __handle_reading_stream(stream,target, host,buffer:io.BytesIO):
|
|
1137
1141
|
'''
|
|
1138
1142
|
Read the stream and append the lines to the target list
|
|
1139
1143
|
|
|
@@ -1146,50 +1150,63 @@ def __handle_reading_stream(stream,target, host):
|
|
|
1146
1150
|
None
|
|
1147
1151
|
'''
|
|
1148
1152
|
global _encoding
|
|
1149
|
-
def add_line(
|
|
1150
|
-
|
|
1151
|
-
target.pop()
|
|
1152
|
-
host.output.pop()
|
|
1153
|
-
current_line_str = current_line.decode(_encoding,errors='backslashreplace')
|
|
1153
|
+
def add_line(buffer,target, host):
|
|
1154
|
+
current_line_str = buffer.getvalue().decode(_encoding,errors='backslashreplace')
|
|
1154
1155
|
target.append(current_line_str)
|
|
1155
1156
|
host.output.append(current_line_str)
|
|
1156
1157
|
host.lineNumToPrintSet.add(len(host.output)-1)
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1158
|
+
buffer.seek(0)
|
|
1159
|
+
buffer.truncate(0)
|
|
1160
|
+
host.output_buffer.seek(0)
|
|
1161
|
+
host.output_buffer.truncate(0)
|
|
1162
|
+
|
|
1162
1163
|
for char in iter(lambda:stream.read(1), b''):
|
|
1164
|
+
host.lastUpdateTime = time.monotonic()
|
|
1163
1165
|
if char == b'\n':
|
|
1164
|
-
add_line(
|
|
1165
|
-
current_line = bytearray()
|
|
1166
|
-
lastLineCommited = True
|
|
1167
|
-
curser_position = 0
|
|
1168
|
-
previousUpdateTime = time.monotonic()
|
|
1166
|
+
add_line(buffer,target, host)
|
|
1169
1167
|
continue
|
|
1170
1168
|
elif char == b'\r':
|
|
1171
|
-
|
|
1169
|
+
buffer.seek(0)
|
|
1170
|
+
host.output_buffer.seek(0)
|
|
1172
1171
|
elif char == b'\x08':
|
|
1173
1172
|
# backspace
|
|
1174
|
-
if
|
|
1175
|
-
|
|
1173
|
+
if buffer.tell() > 0:
|
|
1174
|
+
buffer.seek(buffer.tell() - 1)
|
|
1175
|
+
buffer.truncate()
|
|
1176
|
+
if host.output_buffer.tell() > 0:
|
|
1177
|
+
host.output_buffer.seek(host.output_buffer.tell() - 1)
|
|
1178
|
+
host.output_buffer.truncate()
|
|
1176
1179
|
else:
|
|
1177
|
-
#
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
#
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1180
|
+
# normal character
|
|
1181
|
+
buffer.write(char)
|
|
1182
|
+
host.output_buffer.write(char)
|
|
1183
|
+
# if the length of the buffer is greater than 100, we try to decode the buffer to find if there are any unicode line change chars
|
|
1184
|
+
if buffer.tell() % 100 == 0 and buffer.tell() > 0:
|
|
1185
|
+
try:
|
|
1186
|
+
# try to decode the buffer to find if there are any unicode line change chars
|
|
1187
|
+
decodedLine = buffer.getvalue().decode(_encoding,errors='backslashreplace')
|
|
1188
|
+
lines = decodedLine.splitlines()
|
|
1189
|
+
if len(lines) > 1:
|
|
1190
|
+
# if there are multiple lines, we add them to the target
|
|
1191
|
+
for line in lines[:-1]:
|
|
1192
|
+
# for all lines except the last one, we add them to the target
|
|
1193
|
+
target.append(line)
|
|
1194
|
+
host.output.append(line)
|
|
1195
|
+
host.lineNumToPrintSet.add(len(host.output)-1)
|
|
1196
|
+
# we keep the last line in the buffer
|
|
1197
|
+
buffer.seek(0)
|
|
1198
|
+
buffer.truncate(0)
|
|
1199
|
+
buffer.write(lines[-1].encode(_encoding,errors='backslashreplace'))
|
|
1200
|
+
host.output_buffer.seek(0)
|
|
1201
|
+
host.output_buffer.truncate(0)
|
|
1202
|
+
host.output_buffer.write(lines[-1].encode(_encoding,errors='backslashreplace'))
|
|
1203
|
+
|
|
1204
|
+
except UnicodeDecodeError:
|
|
1205
|
+
# if there is a unicode decode error, we just skip this character
|
|
1206
|
+
continue
|
|
1207
|
+
if buffer.tell() > 0:
|
|
1208
|
+
# if there is still some data in the buffer, we add it to the target
|
|
1209
|
+
add_line(buffer,target, host)
|
|
1193
1210
|
|
|
1194
1211
|
def __handle_writing_stream(stream,stop_event,host):
|
|
1195
1212
|
'''
|
|
@@ -1208,28 +1225,28 @@ def __handle_writing_stream(stream,stop_event,host):
|
|
|
1208
1225
|
# __keyPressesIn is a list of lists.
|
|
1209
1226
|
# Each list is a list of characters to be sent to the stdin of the process at once.
|
|
1210
1227
|
# We do not send the last line as it may be incomplete.
|
|
1211
|
-
|
|
1228
|
+
sentInputPos = 0
|
|
1212
1229
|
while not stop_event.is_set():
|
|
1213
|
-
if
|
|
1214
|
-
stream.write(''.join(__keyPressesIn[
|
|
1230
|
+
if sentInputPos < len(__keyPressesIn) - 1 :
|
|
1231
|
+
stream.write(''.join(__keyPressesIn[sentInputPos]).encode(encoding=_encoding,errors='backslashreplace'))
|
|
1215
1232
|
stream.flush()
|
|
1216
|
-
line = '> ' + ''.join(__keyPressesIn[
|
|
1233
|
+
line = '> ' + ''.join(__keyPressesIn[sentInputPos]).encode(encoding=_encoding,errors='backslashreplace').decode(encoding=_encoding,errors='backslashreplace').replace('\n', '↵')
|
|
1217
1234
|
host.output.append(line)
|
|
1218
1235
|
host.stdout.append(line)
|
|
1219
1236
|
host.lineNumToPrintSet.add(len(host.output)-1)
|
|
1220
|
-
|
|
1237
|
+
sentInputPos += 1
|
|
1221
1238
|
host.lastUpdateTime = time.monotonic()
|
|
1222
1239
|
else:
|
|
1223
1240
|
time.sleep(0.01) # sleep for 10ms
|
|
1224
|
-
if
|
|
1225
|
-
eprint(f"Warning: {len(__keyPressesIn)-
|
|
1241
|
+
if sentInputPos < len(__keyPressesIn) - 1 :
|
|
1242
|
+
eprint(f"Warning: {len(__keyPressesIn)-sentInputPos} lines of key presses are not sent before the process is terminated!")
|
|
1226
1243
|
# # send the last line
|
|
1227
1244
|
# if __keyPressesIn and __keyPressesIn[-1]:
|
|
1228
1245
|
# stream.write(''.join(__keyPressesIn[-1]).encode())
|
|
1229
1246
|
# stream.flush()
|
|
1230
1247
|
# host.output.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
|
|
1231
1248
|
# host.stdout.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
|
|
1232
|
-
return
|
|
1249
|
+
return sentInputPos
|
|
1233
1250
|
|
|
1234
1251
|
def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
|
|
1235
1252
|
'''
|
|
@@ -1415,11 +1432,11 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
|
|
|
1415
1432
|
#host.stdout = []
|
|
1416
1433
|
proc = subprocess.Popen(formatedCMD,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE)
|
|
1417
1434
|
# create a thread to handle stdout
|
|
1418
|
-
stdout_thread = threading.Thread(target=__handle_reading_stream, args=(proc.stdout,host.stdout, host), daemon=True)
|
|
1435
|
+
stdout_thread = threading.Thread(target=__handle_reading_stream, args=(proc.stdout,host.stdout, host,host.stdout_buffer), daemon=True)
|
|
1419
1436
|
stdout_thread.start()
|
|
1420
1437
|
# create a thread to handle stderr
|
|
1421
1438
|
#host.stderr = []
|
|
1422
|
-
stderr_thread = threading.Thread(target=__handle_reading_stream, args=(proc.stderr,host.stderr, host), daemon=True)
|
|
1439
|
+
stderr_thread = threading.Thread(target=__handle_reading_stream, args=(proc.stderr,host.stderr, host,host.stderr_buffer), daemon=True)
|
|
1423
1440
|
stderr_thread.start()
|
|
1424
1441
|
# create a thread to handle stdin
|
|
1425
1442
|
stdin_stop_event = threading.Event()
|
|
@@ -1479,9 +1496,9 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
|
|
|
1479
1496
|
except subprocess.TimeoutExpired:
|
|
1480
1497
|
pass
|
|
1481
1498
|
if stdout:
|
|
1482
|
-
__handle_reading_stream(io.BytesIO(stdout),host.stdout, host)
|
|
1499
|
+
__handle_reading_stream(io.BytesIO(stdout),host.stdout, host,host.stdout_buffer)
|
|
1483
1500
|
if stderr:
|
|
1484
|
-
__handle_reading_stream(io.BytesIO(stderr),host.stderr, host)
|
|
1501
|
+
__handle_reading_stream(io.BytesIO(stderr),host.stderr, host,host.stderr_buffer)
|
|
1485
1502
|
# if the last line in host.stderr is Connection to * closed., we will remove it
|
|
1486
1503
|
host.returncode = proc.poll()
|
|
1487
1504
|
if host.returncode is None:
|
|
@@ -2174,6 +2191,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
|
|
|
2174
2191
|
for i in range(host_window_height - 1):
|
|
2175
2192
|
_curses_add_string_to_window(window=host_window, color_pair_list=[-1, -1, 1], y=i + 1,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color)
|
|
2176
2193
|
host.lineNumToPrintSet.update(range(len(host.output)))
|
|
2194
|
+
host.lastPrintedUpdateTime = 0
|
|
2177
2195
|
# for i in range(host.printedLines, len(host.output)):
|
|
2178
2196
|
# _curses_add_string_to_window(window=host_window, y=i + 1, line=host.output[i], color_pair_list=host.current_color_pair,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color)
|
|
2179
2197
|
# host.printedLines = len(host.output)
|
|
@@ -2193,7 +2211,12 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
|
|
|
2193
2211
|
# print(traceback.format_exc().strip())
|
|
2194
2212
|
if org_dim != stdscr.getmaxyx():
|
|
2195
2213
|
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize detected')
|
|
2214
|
+
if host.lastPrintedUpdateTime != host.lastUpdateTime and host.output_buffer.tell() > 0:
|
|
2215
|
+
# this means there is still output in the buffer, we will print it
|
|
2216
|
+
# we will print the output in the window
|
|
2217
|
+
_curses_add_string_to_window(window=host_window, y=len(host.output) + 1, line=host.output_buffer.getvalue().decode(_encoding,errors='backslashreplace'), color_pair_list=host.current_color_pair,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color,fill_char='')
|
|
2196
2218
|
host_window.noutrefresh()
|
|
2219
|
+
host.lastPrintedUpdateTime = host.lastUpdateTime
|
|
2197
2220
|
hosts_to_display, host_stats,rearrangedHosts = _get_hosts_to_display(hosts, max_num_hosts,hosts_to_display, indexOffset)
|
|
2198
2221
|
curses.doupdate()
|
|
2199
2222
|
last_refresh_time = time.perf_counter()
|
|
@@ -2545,6 +2568,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
2545
2568
|
history_file = history_file, env_file = env_file,
|
|
2546
2569
|
repeat = repeat,interval = interval,
|
|
2547
2570
|
shortend = shortend)
|
|
2571
|
+
commands = [command.replace('"', '\\"') for command in commands]
|
|
2548
2572
|
commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
|
|
2549
2573
|
filePath = os.path.abspath(__file__)
|
|
2550
2574
|
programName = filePath if filePath else 'mssh'
|
|
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
|