multiSSH3 5.84__py3-none-any.whl → 5.86__py3-none-any.whl

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.py CHANGED
@@ -81,10 +81,10 @@ except :
81
81
  print('Warning: functools.lru_cache is not available, multiSSH3 will run slower without cache.',file=sys.stderr)
82
82
  def cache_decorator(func):
83
83
  return func
84
- version = '5.84'
84
+ version = '5.86'
85
85
  VERSION = version
86
86
  __version__ = version
87
- COMMIT_DATE = '2025-07-31'
87
+ COMMIT_DATE = '2025-10-07'
88
88
 
89
89
  CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
90
90
  '~/multiSSH3.config.json',
@@ -624,6 +624,19 @@ def join_threads(threads=__running_threads,timeout=None):
624
624
  thread.join(timeout=timeout)
625
625
  if threads is __running_threads:
626
626
  __running_threads = {t for t in threads if t.is_alive()}
627
+
628
+ def format_commands(commands):
629
+ if not commands:
630
+ commands = []
631
+ else:
632
+ commands = [commands] if isinstance(commands,str) else commands
633
+ # reformat commands into a list of strings, join the iterables if they are not strings
634
+ try:
635
+ commands = [' '.join(command) if not isinstance(command,str) else command for command in commands]
636
+ except:
637
+ eprint(f"Warning: commands should ideally be a list of strings. Now mssh had failed to convert {commands!r} to a list of strings. Continuing anyway but expect failures.")
638
+ return commands
639
+
627
640
  #%% ------------ Compacting Hostnames ----------------
628
641
  def __tokenize_hostname(hostname):
629
642
  """
@@ -1951,7 +1964,7 @@ def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None, indexO
1951
1964
  rearrangedHosts.add(host)
1952
1965
  return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}, rearrangedHosts
1953
1966
 
1954
- 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'):
1967
+ 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,help_shown = False, config_reason = 'New Configuration'):
1955
1968
  global _encoding
1956
1969
  _ = config_reason
1957
1970
  try:
@@ -1970,9 +1983,9 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1970
1983
  min_line_len_local = max_y-1
1971
1984
  # return True if the terminal is too small
1972
1985
  if max_x < 2 or max_y < 2:
1973
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal too small')
1986
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window,help_shown, 'Terminal too small')
1974
1987
  if min_char_len_local < 1 or min_line_len_local < 1:
1975
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Minimum character or line length too small')
1988
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window,help_shown, 'Minimum character or line length too small')
1976
1989
  # We need to figure out how many hosts we can fit in the terminal
1977
1990
  # We will need at least 2 lines per host, one for its name, one for its output
1978
1991
  # Each line will be at least 61 characters long (60 for the output, 1 for the borders)
@@ -1980,10 +1993,10 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1980
1993
  max_num_hosts_y = max_y // (min_line_len_local + 1)
1981
1994
  max_num_hosts = max_num_hosts_x * max_num_hosts_y
1982
1995
  if max_num_hosts < 1:
1983
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal too small to display any hosts')
1996
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window,help_shown, 'Terminal too small to display any hosts')
1984
1997
  hosts_to_display , host_stats, rearrangedHosts = _get_hosts_to_display(hosts, max_num_hosts)
1985
1998
  if len(hosts_to_display) == 0:
1986
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'No hosts to display')
1999
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window,help_shown, 'No hosts to display')
1987
2000
  # Now we calculate the actual number of hosts we will display for x and y
1988
2001
  optimal_len_x = max(min_char_len_local, 80)
1989
2002
  num_hosts_x = min(max(min(max_num_hosts_x, max_x // optimal_len_x),1),len(hosts_to_display))
@@ -2004,7 +2017,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2004
2017
  host_window_height = max_y // num_hosts_y
2005
2018
  host_window_width = max_x // num_hosts_x
2006
2019
  if host_window_height < 1 or host_window_width < 1:
2007
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Host window too small')
2020
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window,help_shown, 'Host window too small')
2008
2021
 
2009
2022
  old_stat = ''
2010
2023
  old_bottom_stat = ''
@@ -2065,7 +2078,6 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2065
2078
  _curses_add_string_to_window(window=help_window,y=12,line='Esc : Clear line', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
2066
2079
  help_panel = curses.panel.new_panel(help_window)
2067
2080
  help_panel.hide()
2068
- help_shown = False
2069
2081
  curses.panel.update_panels()
2070
2082
  indexOffset = 0
2071
2083
  while host_stats['running'] > 0 or host_stats['waiting'] > 0:
@@ -2078,7 +2090,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2078
2090
  # with open('keylog.txt','a') as f:
2079
2091
  # f.write(str(key)+'\n')
2080
2092
  if key == 410 or key == curses.KEY_RESIZE: # 410 is the key code for resize
2081
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize requested')
2093
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window,help_shown, 'Terminal resize requested')
2082
2094
  # if the user pressed ctrl + d and the last line is empty, we will exit by adding 'exit\n' to the last line
2083
2095
  elif key == 4 and not __keyPressesIn[-1]:
2084
2096
  __keyPressesIn[-1].extend('exit\n')
@@ -2086,20 +2098,20 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2086
2098
  elif key == 95 and not __keyPressesIn[-1]: # 95 is the key code for _
2087
2099
  # if last line is empty, we will reconfigure the wh to be smaller
2088
2100
  if min_line_len != 1:
2089
- return (lineToDisplay,curserPosition , min_char_len , max(min_line_len -1,1), single_window, 'Decrease line length')
2101
+ return (lineToDisplay,curserPosition , min_char_len , max(min_line_len -1,1), single_window,help_shown, 'Decrease line length')
2090
2102
  elif key == 43 and not __keyPressesIn[-1]: # 43 is the key code for +
2091
2103
  # if last line is empty, we will reconfigure the wh to be larger
2092
- return (lineToDisplay,curserPosition , min_char_len , min_line_len +1, single_window, 'Increase line length')
2104
+ return (lineToDisplay,curserPosition , min_char_len , min_line_len +1, single_window,help_shown, 'Increase line length')
2093
2105
  elif key == 123 and not __keyPressesIn[-1]: # 123 is the key code for {
2094
2106
  # if last line is empty, we will reconfigure the ww to be smaller
2095
2107
  if min_char_len != 1:
2096
- return (lineToDisplay,curserPosition , max(min_char_len -1,1), min_line_len, single_window, 'Decrease character length')
2108
+ return (lineToDisplay,curserPosition , max(min_char_len -1,1), min_line_len, single_window,help_shown, 'Decrease character length')
2097
2109
  elif key == 124 and not __keyPressesIn[-1]: # 124 is the key code for |
2098
2110
  # if last line is empty, we will toggle the single window mode
2099
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, not single_window, 'Toggle single window mode')
2111
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, not single_window,help_shown, 'Toggle single window mode')
2100
2112
  elif key == 125 and not __keyPressesIn[-1]: # 125 is the key code for }
2101
2113
  # if last line is empty, we will reconfigure the ww to be larger
2102
- return (lineToDisplay,curserPosition , min_char_len +1, min_line_len, single_window, 'Increase character length')
2114
+ return (lineToDisplay,curserPosition , min_char_len +1, min_line_len, single_window,help_shown, 'Increase character length')
2103
2115
  elif key == 60 and not __keyPressesIn[-1]: # 60 is the key code for <
2104
2116
  indexOffset = (indexOffset - 1 ) % len(hosts)
2105
2117
  elif key == 62 and not __keyPressesIn[-1]: # 62 is the key code for >
@@ -2134,11 +2146,11 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2134
2146
  curserPosition = len(__keyPressesIn[lineToDisplay])
2135
2147
  elif key == curses.KEY_REFRESH or key == curses.KEY_F5 or key == 18: # 18 is the key code for ctrl + R
2136
2148
  # if the key is refresh, we will refresh the screen
2137
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Refresh requested')
2149
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window,help_shown, 'Refresh requested')
2138
2150
  elif key == curses.KEY_EXIT or key == 27: # 27 is the key code for ESC
2139
2151
  # if the key is exit, we will exit the program
2140
2152
  return
2141
- elif key == curses.KEY_HELP or key == 63 or key == curses.KEY_F1: # 63 is the key code for ?
2153
+ elif key == curses.KEY_HELP or key == 63 or key == curses.KEY_F1 or key == 8: # 63 is the key code for ?
2142
2154
  # if the key is help, we will display the help message
2143
2155
  if not help_shown:
2144
2156
  help_panel.show()
@@ -2181,7 +2193,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2181
2193
  curserPosition += 1
2182
2194
  # reconfigure when the terminal size changes
2183
2195
  if org_dim != stdscr.getmaxyx():
2184
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize detected')
2196
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window,help_shown, 'Terminal resize detected')
2185
2197
  # We generate the aggregated stats if user did not input anything
2186
2198
  if not __keyPressesIn[lineToDisplay]:
2187
2199
  #stats = '┍'+ f" Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']} ww: {min_char_len} wh:{min_line_len} "[:max_x - 2].center(max_x - 2, "━")
@@ -2255,7 +2267,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2255
2267
  # print(str(e).strip())
2256
2268
  # print(traceback.format_exc().strip())
2257
2269
  if org_dim != stdscr.getmaxyx():
2258
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize detected')
2270
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window,help_shown, 'Terminal resize detected')
2259
2271
  if host.lastPrintedUpdateTime != host.lastUpdateTime and host.output_buffer.tell() > 0:
2260
2272
  # this means there is still output in the buffer, we will print it
2261
2273
  # we will print the output in the window
@@ -2263,11 +2275,14 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2263
2275
  host_window.noutrefresh()
2264
2276
  host.lastPrintedUpdateTime = host.lastUpdateTime
2265
2277
  hosts_to_display, host_stats,rearrangedHosts = _get_hosts_to_display(hosts, max_num_hosts,hosts_to_display, indexOffset)
2278
+ if help_shown:
2279
+ help_window.touchwin()
2280
+ help_window.noutrefresh()
2266
2281
  curses.doupdate()
2267
2282
  last_refresh_time = time.perf_counter()
2268
2283
  except Exception as e:
2269
2284
  import traceback
2270
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, f'Error: {str(e)}',traceback.format_exc())
2285
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window,help_shown, f'Error: {str(e)}',traceback.format_exc())
2271
2286
  return None
2272
2287
 
2273
2288
  def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
@@ -2317,7 +2332,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
2317
2332
  stdscr.refresh()
2318
2333
  except:
2319
2334
  pass
2320
- params = (-1,0 , min_char_len, min_line_len, single_window,'new config')
2335
+ params = (-1,0 , min_char_len, min_line_len, single_window,False,'new config')
2321
2336
  while params:
2322
2337
  params = __generate_display(stdscr, hosts, *params)
2323
2338
  if not params:
@@ -2328,17 +2343,17 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
2328
2343
  # print the current configuration
2329
2344
  stdscr.clear()
2330
2345
  try:
2331
- stdscr.addstr(0, 0, f"{params[5]}, Reloading Configuration: min_char_len={params[2]}, min_line_len={params[3]}, single_window={params[4]} with window size {stdscr.getmaxyx()} and {len(hosts)} hosts...")
2332
- if len(params) > 6:
2346
+ stdscr.addstr(0, 0, f"{params[6]}, Reloading Configuration: min_char_len={params[2]}, min_line_len={params[3]}, single_window={params[4]} with window size {stdscr.getmaxyx()} and {len(hosts)} hosts...")
2347
+ if len(params) > 7:
2333
2348
  # traceback is available, print it
2334
2349
  i = 1
2335
- for line in params[6].split('\n'):
2350
+ for line in params[7].split('\n'):
2336
2351
  stdscr.addstr(i, 0, line)
2337
2352
  i += 1
2338
2353
  stdscr.refresh()
2339
2354
  except:
2340
2355
  pass
2341
- params = params[:5] + ('new config',)
2356
+ params = params[:6] + ('new config',)
2342
2357
  time.sleep(0.01)
2343
2358
  #time.sleep(0.25)
2344
2359
 
@@ -2612,7 +2627,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
2612
2627
  history_file = history_file, env_file = env_file,
2613
2628
  repeat = repeat,interval = interval,
2614
2629
  shortend = shortend)
2615
- commands = [command.replace('"', '\\"').replace('\n', '\\n').replace('\t', '\\t') for command in commands]
2630
+ commands = [command.replace('"', '\\"').replace('\n', '\\n').replace('\t', '\\t') for command in format_commands(commands)]
2616
2631
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
2617
2632
  filePath = os.path.abspath(__file__)
2618
2633
  programName = filePath if filePath else 'mssh'
@@ -2761,15 +2776,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2761
2776
  if max_connections > __max_connections_nofile_limit_supported * 2:
2762
2777
  # we need to throttle thread start to avoid hitting the nofile limit
2763
2778
  __thread_start_delay = 0.001
2764
- if not commands:
2765
- commands = []
2766
- else:
2767
- commands = [commands] if isinstance(commands,str) else commands
2768
- # reformat commands into a list of strings, join the iterables if they are not strings
2769
- try:
2770
- commands = [' '.join(command) if not isinstance(command,str) else command for command in commands]
2771
- except:
2772
- eprint(f"Warning: commands should ideally be a list of strings. Now mssh had failed to convert {commands!r} to a list of strings. Continuing anyway but expect failures.")
2779
+ commands = format_commands(commands)
2773
2780
  #verify_ssh_config()
2774
2781
  # load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
2775
2782
  if called:
@@ -3090,9 +3097,9 @@ def get_parser():
3090
3097
  parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
3091
3098
  parser.add_argument('-w',"--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as well. (default: {DEFAULT_PRINT_SUCCESS_HOSTS})", default=DEFAULT_PRINT_SUCCESS_HOSTS)
3092
3099
  parser.add_argument('-P',"-g","--greppable",'--table', action='store_true', help=f"Output in greppable table. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
3093
- group = parser.add_mutually_exclusive_group()
3094
- group.add_argument('-x',"-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
3095
- group.add_argument('-a',"-nsu","--no_skip_unreachable",dest = 'skip_unreachable', action='store_false', help=f"Do not skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {not DEFAULT_SKIP_UNREACHABLE})", default=not DEFAULT_SKIP_UNREACHABLE)
3100
+ su_group = parser.add_mutually_exclusive_group()
3101
+ su_group.add_argument('-x',"-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
3102
+ su_group.add_argument('-a',"-nsu","--no_skip_unreachable",dest = 'skip_unreachable', action='store_false', help=f"Do not skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {not DEFAULT_SKIP_UNREACHABLE})", default=not DEFAULT_SKIP_UNREACHABLE)
3096
3103
  parser.add_argument('-uhe','--unavailable_host_expiry', type=int, help=f"Time in seconds to expire the unavailable hosts (default: {DEFAULT_UNAVAILABLE_HOST_EXPIRY})", default=DEFAULT_UNAVAILABLE_HOST_EXPIRY)
3097
3104
  parser.add_argument('-X',"-sh","--skip_hosts", type=str, help=f"Skip the hosts in the list. (default: {DEFAULT_SKIP_HOSTS if DEFAULT_SKIP_HOSTS else 'None'})", default=DEFAULT_SKIP_HOSTS)
3098
3105
  parser.add_argument('--generate_config_file', action='store_true', help=f'Store / generate the default config file from command line argument and current config at --config_file / stdout')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.84
3
+ Version: 5.86
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
@@ -0,0 +1,6 @@
1
+ multiSSH3.py,sha256=Mbr7AsHObwHIkMCuS9bsfmfMFWNQ4NngBlM-oVCXAlg,154547
2
+ multissh3-5.86.dist-info/METADATA,sha256=r4ndEU9KGZ5RMn-AtlxzUazkrnbLbT7LFkQWgF9RwqY,18093
3
+ multissh3-5.86.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
+ multissh3-5.86.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
+ multissh3-5.86.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
+ multissh3-5.86.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- multiSSH3.py,sha256=-CzKDCUgMrgzmvtiLGgbQG0n_Gzk6IEqvUieLzJ29y0,154177
2
- multissh3-5.84.dist-info/METADATA,sha256=9ZD6QwIhVXZXymckMv_aocvIzy2NHjyA_EnDaUhZn1s,18093
3
- multissh3-5.84.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
- multissh3-5.84.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
- multissh3-5.84.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
- multissh3-5.84.dist-info/RECORD,,