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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.76
3
+ Version: 5.78
4
4
  Summary: Run commands on multiple hosts via SSH
5
5
  Home-page: https://github.com/yufei-pan/multiSSH3
6
6
  Author: Yufei Pan
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.76
3
+ Version: 5.78
4
4
  Summary: Run commands on multiple hosts via SSH
5
5
  Home-page: https://github.com/yufei-pan/multiSSH3
6
6
  Author: Yufei Pan
@@ -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.76'
58
+ version = '5.78'
59
59
  VERSION = version
60
60
  __version__ = version
61
- COMMIT_DATE = '2025-06-25'
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(current_line,target, host, keepLastLine=True):
1150
- if not keepLastLine:
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
- host.lastUpdateTime = time.monotonic()
1158
- current_line = bytearray()
1159
- lastLineCommited = True
1160
- curser_position = 0
1161
- previousUpdateTime = time.monotonic()
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(current_line,target, host, keepLastLine=lastLineCommited)
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
- curser_position = 0
1169
+ buffer.seek(0)
1170
+ host.output_buffer.seek(0)
1172
1171
  elif char == b'\x08':
1173
1172
  # backspace
1174
- if curser_position > 0:
1175
- curser_position -= 1
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
- # over write the character if the curser is not at the end of the line
1178
- if curser_position < len(current_line):
1179
- current_line[curser_position] = char[0]
1180
- elif curser_position == len(current_line):
1181
- current_line.append(char[0])
1182
- else:
1183
- # curser is bigger than the length of the line
1184
- current_line += b' '*(curser_position - len(current_line)) + char[0]
1185
- curser_position += 1
1186
- if time.monotonic() - previousUpdateTime > 0.1:
1187
- # if the time since the last update is more than 10ms, we update the output
1188
- add_line(current_line,target, host, keepLastLine=lastLineCommited)
1189
- lastLineCommited = False
1190
- previousUpdateTime = time.monotonic()
1191
- if current_line:
1192
- add_line(current_line,target, host, keepLastLine=lastLineCommited)
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
- sentInput = 0
1228
+ sentInputPos = 0
1212
1229
  while not stop_event.is_set():
1213
- if sentInput < len(__keyPressesIn) - 1 :
1214
- stream.write(''.join(__keyPressesIn[sentInput]).encode(encoding=_encoding,errors='backslashreplace'))
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[sentInput]).encode(encoding=_encoding,errors='backslashreplace').decode(encoding=_encoding,errors='backslashreplace').replace('\n', '↵')
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
- sentInput += 1
1237
+ sentInputPos += 1
1221
1238
  host.lastUpdateTime = time.monotonic()
1222
1239
  else:
1223
1240
  time.sleep(0.01) # sleep for 10ms
1224
- if sentInput < len(__keyPressesIn) - 1 :
1225
- eprint(f"Warning: {len(__keyPressesIn)-sentInput} lines of key presses are not sent before the process is terminated!")
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 sentInput
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