multiSSH3 5.62__tar.gz → 5.64__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.62
3
+ Version: 5.64
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.62
3
+ Version: 5.64
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
@@ -54,10 +54,10 @@ except AttributeError:
54
54
  # If neither is available, use a dummy decorator
55
55
  def cache_decorator(func):
56
56
  return func
57
- version = '5.62'
57
+ version = '5.64'
58
58
  VERSION = version
59
59
  __version__ = version
60
- COMMIT_DATE = '2025-04-17'
60
+ COMMIT_DATE = '2025-05-02'
61
61
 
62
62
  CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
63
63
  '~/multiSSH3.config.json',
@@ -225,8 +225,7 @@ class Host:
225
225
  self.output = [] # the output of the command for curses
226
226
  self.stdout = [] # the stdout of the command
227
227
  self.stderr = [] # the stderr of the command
228
- self.printedLines = -1 # the number of lines printed on the screen
229
- self.lineNumToReprintSet = set() # line numbers to reprint
228
+ self.lineNumToPrintSet = set() # line numbers to reprint
230
229
  self.lastUpdateTime = time.monotonic() # the last time the output was updated
231
230
  self.files = files # the files to be copied to the host
232
231
  self.ipmi = ipmi # whether to use ipmi to connect to the host
@@ -248,7 +247,7 @@ class Host:
248
247
  def __repr__(self):
249
248
  # return the complete data structure
250
249
  return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr}, \
251
- output={self.output}, printedLines={self.printedLines}, lineNumToReprintSet={self.lineNumToReprintSet}, files={self.files}, ipmi={self.ipmi}, \
250
+ output={self.output}, lineNumToPrintSet={self.lineNumToPrintSet}, files={self.files}, ipmi={self.ipmi}, \
252
251
  interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, \
253
252
  extraargs={self.extraargs}, resolvedName={self.resolvedName}, i={self.i}, uuid={self.uuid}), \
254
253
  identity_file={self.identity_file}, ip={self.ip}, current_color_pair={self.current_color_pair}"
@@ -1129,7 +1128,7 @@ def __handle_reading_stream(stream,target, host):
1129
1128
  current_line_str = current_line.decode('utf-8',errors='backslashreplace')
1130
1129
  target.append(current_line_str)
1131
1130
  host.output.append(current_line_str)
1132
- host.lineNumToReprintSet.add(len(host.output)-1)
1131
+ host.lineNumToPrintSet.add(len(host.output)-1)
1133
1132
  host.lastUpdateTime = time.monotonic()
1134
1133
  current_line = bytearray()
1135
1134
  lastLineCommited = True
@@ -1419,12 +1418,12 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
1419
1418
  if host.output and timeoutLineAppended and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
1420
1419
  host.output.pop()
1421
1420
  host.output.append(timeoutLine)
1422
- host.lineNumToReprintSet.add(len(host.output)-1)
1421
+ host.lineNumToPrintSet.add(len(host.output)-1)
1423
1422
  timeoutLineAppended = True
1424
1423
  elif host.output and timeoutLineAppended and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
1425
1424
  host.output.pop()
1426
1425
  host.output.append('')
1427
- host.lineNumToReprintSet.add(len(host.output)-1)
1426
+ host.lineNumToPrintSet.add(len(host.output)-1)
1428
1427
  timeoutLineAppended = False
1429
1428
  if _emo:
1430
1429
  host.stderr.append('Ctrl C detected, Emergency Stop!')
@@ -1855,10 +1854,10 @@ def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
1855
1854
  new_hosts_to_display = (running_hosts + failed_hosts + finished_hosts + waiting_hosts)[:max_num_hosts]
1856
1855
  if not hosts_to_display:
1857
1856
  return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
1858
- # we will compare the new_hosts_to_display with the old one, if some hosts are not in their original position, we will change its printedLines to 0
1857
+ # we will compare the new_hosts_to_display with the old one, if some hosts are not in their original position, we will reprint all lines
1859
1858
  for i, host in enumerate(new_hosts_to_display):
1860
1859
  if host not in hosts_to_display or i != hosts_to_display.index(host):
1861
- host.printedLines = 0
1860
+ host.lineNumToPrintSet.update(range(len(host.output)))
1862
1861
  return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
1863
1862
 
1864
1863
  def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window=DEFAULT_SINGLE_WINDOW, config_reason = 'New Configuration'):
@@ -1924,7 +1923,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1924
1923
  global __keyPressesIn
1925
1924
  stdscr.nodelay(True)
1926
1925
  # we generate a stats window at the top of the screen
1927
- stat_window = curses.newwin(1, max_x, 0, 0)
1926
+ stat_window = curses.newwin(1, max_x+1, 0, 0)
1928
1927
  stat_window.leaveok(True)
1929
1928
  # We create a window for each host
1930
1929
  host_windows = []
@@ -1935,7 +1934,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1935
1934
  x = (i % num_hosts_x) * host_window_width
1936
1935
  #print(f"Creating a window at {y},{x}")
1937
1936
  # We create the window
1938
- host_window = curses.newwin(host_window_height, host_window_width, y, x)
1937
+ host_window = curses.newwin(host_window_height, host_window_width + 1, y, x)
1939
1938
  host_window.idlok(True)
1940
1939
  host_window.scrollok(True)
1941
1940
  host_window.leaveok(True)
@@ -2084,60 +2083,33 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2084
2083
  hosts_to_display, host_stats = _get_hosts_to_display(hosts, max_num_hosts,hosts_to_display)
2085
2084
  for host_window, host in zip(host_windows, hosts_to_display):
2086
2085
  # we will only update the window if there is new output or the window is not fully printed
2087
- if new_configured or host.printedLines < len(host.output):
2086
+ if new_configured:
2087
+ host.lineNumToPrintSet.update(range(len(host.output)))
2088
+ linePrintOut = f'{host.name}:[{host.command}]'.replace('\n', ' ').replace('\r', ' ').strip()
2089
+ _curses_add_string_to_window(window=host_window, y=0, line=linePrintOut, color_pair_list=[-1, -1, 1],centered=True,fill_char='─',lead_str='┼',box_ansi_color=box_ansi_color)
2090
+ # clear the window
2091
+ for i in range(host_window_height - 1):
2092
+ _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)
2093
+ # for i in range(host.printedLines, len(host.output)):
2094
+ # _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)
2095
+ # host.printedLines = len(host.output)
2096
+ if host.lineNumToPrintSet:
2088
2097
  try:
2089
- if new_configured:
2090
- host.printedLines = 0
2091
- #host_window.clear()
2092
- # we will try to center the name of the host with ┼ at the beginning and end and ─ in between
2093
- #linePrintOut = f'┼{(host.name+":["+host.command+"]")[:host_window_width - 2].center(host_window_width - 1, "─")}'.replace('\n', ' ').replace('\r', ' ').strip()
2094
- #linePrintOut = f'┼{(host.name+":["+host.command+"]")[:host_window_width - 2].center(host_window_width - 1, "─")}'.replace('\n', ' ').replace('\r', ' ').strip()
2095
- #host_window.addnstr(0, 0, linePrintOut, host_window_width - 1)
2096
- linePrintOut = f'{host.name}:[{host.command}]'.replace('\n', ' ').replace('\r', ' ').strip()
2097
- _curses_add_string_to_window(window=host_window, y=0, line=linePrintOut, color_pair_list=[-1, -1, 1],centered=True,fill_char='─',lead_str='┼',box_ansi_color=box_ansi_color)
2098
- #_add_line_with_ansi_colors(window=host_window, y=0, x=0, line=linePrintOut, n=host_window_width - 1, color_pair_list = host.current_color_pair)
2099
- # we will display the latest outputs of the host as much as we can
2100
- #for i, line in enumerate(host.output[-(host_window_height - 1):]):
2101
- # print(f"Printng a line at {i + 1} with length of {len('│'+line[:host_window_width - 1])}")
2102
- # time.sleep(10)
2103
- #linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip()
2104
- #host_window.addnstr(i + 1, 0, linePrintOut, host_window_width - 1)
2105
- #_curses_add_string_to_window(window=host_window, y=i + 1, line=line, color_pair_list=host.current_color_pair,lead_str='│')
2106
- # we draw the rest of the available lines
2107
- # for i in range(len(host.output), host_window_height - 1):
2108
- # # print(f"Printng a line at {i + 1} with length of {len('│')}")
2109
- # host_window.addnstr(i + 1, 0, '│'.ljust(host_window_width - 1, ' '), host_window_width - 1)
2110
- for i in range(host.printedLines, len(host.output)):
2111
- _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)
2112
- for i in range(len(host.output), host_window_height - 1):
2113
- _curses_add_string_to_window(window=host_window, y=i + 1,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color)
2114
- host.printedLines = len(host.output)
2115
- host_window.refresh()
2098
+ # visible range is from len(host.output) - host_window_height + 1 to len(host.output)
2099
+ visibleLowerBound = max(0, len(host.output) - host_window_height + 1)
2100
+ lineNumToPrintSet = host.lineNumToPrintSet.copy()
2101
+ host.lineNumToPrintSet = set()
2102
+ for lineNumToReprint in sorted(lineNumToPrintSet):
2103
+ # if the line is visible, we will reprint it
2104
+ if visibleLowerBound <= lineNumToReprint <= len(host.output):
2105
+ _curses_add_string_to_window(window=host_window, y=lineNumToReprint + 1, line=host.output[lineNumToReprint], color_pair_list=host.current_color_pair,lead_str='',keep_top_n_lines=1,box_ansi_color=box_ansi_color)
2116
2106
  except Exception as e:
2117
2107
  # import traceback
2118
2108
  # print(str(e).strip())
2119
2109
  # print(traceback.format_exc().strip())
2120
2110
  if org_dim != stdscr.getmaxyx():
2121
2111
  return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize detected')
2122
- if host.lineNumToReprintSet:
2123
- # visible range is from host.printedLines - host_window_height + 1 to host.printedLines
2124
- visibleLowerBound = host.printedLines - host_window_height + 1
2125
- lineNumToReprintSet = host.lineNumToReprintSet
2126
- host.lineNumToReprintSet = set()
2127
- for lineNumToReprint in lineNumToReprintSet:
2128
- # if the line is visible, we will reprint it
2129
- if visibleLowerBound <= lineNumToReprint <= host.printedLines:
2130
- if visibleLowerBound <= 0:
2131
- # this means all lines are visible
2132
- linePos = lineNumToReprint
2133
- else:
2134
- # calculate the position of the line to reprint
2135
- linePos = lineNumToReprint - visibleLowerBound
2136
- # Note: color can be incorrect if repainting an old line with new colors already initialized,
2137
- # Thus we will not use any presistent color pair for old lines
2138
- cpl = host.current_color_pair if lineNumToReprint == host.printedLines else [-1,-1,1]
2139
- _curses_add_string_to_window(window=host_window, y=linePos + 1, line=host.output[lineNumToReprint], color_pair_list=cpl,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color)
2140
- host_window.refresh()
2112
+ host_window.refresh()
2141
2113
  new_configured = False
2142
2114
  last_refresh_time = time.perf_counter()
2143
2115
  except Exception as e:
@@ -2225,7 +2197,10 @@ def generate_output(hosts, usejson = False, greppable = False):
2225
2197
  # remove hosts with returncode 0
2226
2198
  hosts = [dict(host) for host in hosts if host.returncode != 0]
2227
2199
  if not hosts:
2228
- return 'Success'
2200
+ if usejson:
2201
+ return '{"Success": true}'
2202
+ else:
2203
+ return 'Success'
2229
2204
  else:
2230
2205
  hosts = [dict(host) for host in hosts]
2231
2206
  if usejson:
@@ -2976,6 +2951,7 @@ def main():
2976
2951
  parser.add_argument('-ci','--copy_id', action='store_true', help='Copy the ssh id to the hosts')
2977
2952
  parser.add_argument('-I','-nh','--no_history', action='store_true', help=f'Do not record the command to history. Default: {DEFAULT_NO_HISTORY}', default=DEFAULT_NO_HISTORY)
2978
2953
  parser.add_argument('-hf','--history_file', type=str, help=f'The file to store the history. (default: {DEFAULT_HISTORY_FILE})', default=DEFAULT_HISTORY_FILE)
2954
+ parser.add_argument('--script', action='store_true', help='Run the command in script mode, short for -SCRIPT or --no_watch --skip_unreachable --no_env --no_history --greppable --error_only')
2979
2955
  parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} @ {COMMIT_DATE} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
2980
2956
 
2981
2957
  # parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
@@ -2993,6 +2969,14 @@ def main():
2993
2969
  if unknown:
2994
2970
  eprint(f"Warning: Unknown arguments, treating all as commands: {unknown!r}")
2995
2971
  args.commands += unknown
2972
+
2973
+ if args.script:
2974
+ args.no_watch = True
2975
+ args.skip_unreachable = True
2976
+ args.no_env = True
2977
+ args.no_history = True
2978
+ args.greppable = True
2979
+ args.error_only = True
2996
2980
 
2997
2981
  if args.generate_config_file or args.store_config_file:
2998
2982
  if args.store_config_file:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes