multiSSH3 5.73__py3-none-any.whl → 5.75__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
@@ -10,6 +10,7 @@ __curses_available = False
10
10
  __resource_lib_available = False
11
11
  try:
12
12
  import curses
13
+ import curses.panel
13
14
  __curses_available = True
14
15
  except ImportError:
15
16
  pass
@@ -54,10 +55,10 @@ except AttributeError:
54
55
  # If neither is available, use a dummy decorator
55
56
  def cache_decorator(func):
56
57
  return func
57
- version = '5.73'
58
+ version = '5.75'
58
59
  VERSION = version
59
60
  __version__ = version
60
- COMMIT_DATE = '2025-05-21'
61
+ COMMIT_DATE = '2025-06-17'
61
62
 
62
63
  CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
63
64
  '~/multiSSH3.config.json',
@@ -312,6 +313,7 @@ DEFAULT_PRINT_SUCCESS_HOSTS = False
312
313
  DEFAULT_GREPPABLE_MODE = False
313
314
  DEFAULT_SKIP_UNREACHABLE = True
314
315
  DEFAULT_SKIP_HOSTS = ''
316
+ DEFAULT_ENCODING = 'utf-8'
315
317
  SSH_STRICT_HOST_KEY_CHECKING = False
316
318
  ERROR_MESSAGES_TO_IGNORE = [
317
319
  'Pseudo-terminal will not be allocated because stdin is not a terminal',
@@ -359,6 +361,7 @@ __curses_color_table = {}
359
361
  __curses_current_color_index = 10
360
362
  __max_connections_nofile_limit_supported = 0
361
363
  __thread_start_delay = 0
364
+ _encoding = DEFAULT_ENCODING
362
365
  if __resource_lib_available:
363
366
  # Get the current limits
364
367
  _, __system_nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
@@ -1122,11 +1125,12 @@ def __handle_reading_stream(stream,target, host):
1122
1125
  Returns:
1123
1126
  None
1124
1127
  '''
1128
+ global _encoding
1125
1129
  def add_line(current_line,target, host, keepLastLine=True):
1126
1130
  if not keepLastLine:
1127
1131
  target.pop()
1128
1132
  host.output.pop()
1129
- current_line_str = current_line.decode('utf-8',errors='backslashreplace')
1133
+ current_line_str = current_line.decode(_encoding,errors='backslashreplace')
1130
1134
  target.append(current_line_str)
1131
1135
  host.output.append(current_line_str)
1132
1136
  host.lineNumToPrintSet.add(len(host.output)-1)
@@ -1157,7 +1161,7 @@ def __handle_reading_stream(stream,target, host):
1157
1161
  current_line.append(char[0])
1158
1162
  else:
1159
1163
  # curser is bigger than the length of the line
1160
- current_line += b' '*(curser_position - len(current_line)) + char
1164
+ current_line += b' '*(curser_position - len(current_line)) + char[0]
1161
1165
  curser_position += 1
1162
1166
  if time.monotonic() - previousUpdateTime > 0.1:
1163
1167
  # if the time since the last update is more than 10ms, we update the output
@@ -1180,15 +1184,16 @@ def __handle_writing_stream(stream,stop_event,host):
1180
1184
  None
1181
1185
  '''
1182
1186
  global __keyPressesIn
1187
+ global _encoding
1183
1188
  # __keyPressesIn is a list of lists.
1184
1189
  # Each list is a list of characters to be sent to the stdin of the process at once.
1185
1190
  # We do not send the last line as it may be incomplete.
1186
1191
  sentInput = 0
1187
1192
  while not stop_event.is_set():
1188
1193
  if sentInput < len(__keyPressesIn) - 1 :
1189
- stream.write(''.join(__keyPressesIn[sentInput]).encode())
1194
+ stream.write(''.join(__keyPressesIn[sentInput]).encode(encoding=_encoding,errors='backslashreplace'))
1190
1195
  stream.flush()
1191
- line = '> ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵')
1196
+ line = '> ' + ''.join(__keyPressesIn[sentInput]).encode(encoding=_encoding,errors='backslashreplace').decode(encoding=_encoding,errors='backslashreplace').replace('\n', '↵')
1192
1197
  host.output.append(line)
1193
1198
  host.stdout.append(line)
1194
1199
  host.lineNumToPrintSet.add(len(host.output)-1)
@@ -1865,9 +1870,11 @@ def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None, indexO
1865
1870
  return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}, rearrangedHosts
1866
1871
 
1867
1872
  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'):
1873
+ global _encoding
1868
1874
  _ = config_reason
1869
1875
  try:
1870
1876
  box_ansi_color = None
1877
+ refresh_all = True
1871
1878
  org_dim = stdscr.getmaxyx()
1872
1879
  # To do this, first we need to know the size of the terminal
1873
1880
  max_y, max_x = org_dim
@@ -1951,6 +1958,33 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1951
1958
  #bottom_border.addnstr(0, 0, '-' * (max_x - 1), max_x - 1)
1952
1959
  _curses_add_string_to_window(window=bottom_border, y=0, line='-' * (max_x - 1),fill_char='-',box_ansi_color=box_ansi_color)
1953
1960
  bottom_border.refresh()
1961
+ help_window_hight = min(14, max_y)
1962
+ help_window_width = min(31, max_x)
1963
+ # Create a centered help window
1964
+ help_window_y = (max_y - help_window_hight) // 2
1965
+ help_window_x = (max_x - help_window_width) // 2
1966
+ help_window = curses.newwin(help_window_hight, help_window_width, help_window_y, help_window_x)
1967
+ help_window.leaveok(True)
1968
+ help_window.scrollok(True)
1969
+ help_window.idlok(True)
1970
+ help_window.box()
1971
+ _curses_add_string_to_window(window=help_window,y=0,line='Help', color_pair_list=[-1,-1,1], centered=True, fill_char='─', lead_str='┌', box_ansi_color=box_ansi_color)
1972
+ _curses_add_string_to_window(window=help_window,y=1,line='? : Toggle Help Menu', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1973
+ _curses_add_string_to_window(window=help_window,y=2,line='_ or + : Change window hight', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1974
+ _curses_add_string_to_window(window=help_window,y=3,line='{ or } : Change window width', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1975
+ _curses_add_string_to_window(window=help_window,y=4,line='< or > : Change host index', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1976
+ _curses_add_string_to_window(window=help_window,y=5,line='|(pipe) : Toggle single host', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1977
+ _curses_add_string_to_window(window=help_window,y=6,line='Ctrl+D : Exit', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1978
+ _curses_add_string_to_window(window=help_window,y=7,line='Ctrl+R : Force refresh', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1979
+ _curses_add_string_to_window(window=help_window,y=8,line='↑ or ↓ : Navigate history', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1980
+ _curses_add_string_to_window(window=help_window,y=9,line='← or → : Move cursor', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1981
+ _curses_add_string_to_window(window=help_window,y=10,line='PgUp/Dn : Scroll history by 5', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1982
+ _curses_add_string_to_window(window=help_window,y=11,line='Home/End: Jump cursor', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1983
+ _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)
1984
+ help_panel = curses.panel.new_panel(help_window)
1985
+ help_panel.hide()
1986
+ help_shown = False
1987
+ curses.panel.update_panels()
1954
1988
  indexOffset = 0
1955
1989
  while host_stats['running'] > 0 or host_stats['waiting'] > 0:
1956
1990
  # Check for keypress
@@ -1961,7 +1995,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1961
1995
  # When we encounter a newline, we add a new list to the list of lists. ( a new line of input )
1962
1996
  # with open('keylog.txt','a') as f:
1963
1997
  # f.write(str(key)+'\n')
1964
- if key == 410: # 410 is the key code for resize
1998
+ if key == 410 or key == curses.KEY_RESIZE: # 410 is the key code for resize
1965
1999
  return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize requested')
1966
2000
  # if the user pressed ctrl + d and the last line is empty, we will exit by adding 'exit\n' to the last line
1967
2001
  elif key == 4 and not __keyPressesIn[-1]:
@@ -1991,12 +2025,15 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1991
2025
  # We handle positional keys
1992
2026
  # if the key is up arrow, we will move the line to display up
1993
2027
  elif key == 259: # 259 is the key code for up arrow
2028
+ # also scroll curserPosition to last if it is currently at the last line and curserPosition is at 0
1994
2029
  lineToDisplay = max(lineToDisplay - 1, -len(__keyPressesIn))
2030
+ if lineToDisplay == -2 and not __keyPressesIn[-1]:
2031
+ curserPosition = len(__keyPressesIn[lineToDisplay])
1995
2032
  # if the key is down arrow, we will move the line to display down
1996
2033
  elif key == 258: # 258 is the key code for down arrow
1997
2034
  lineToDisplay = min(lineToDisplay + 1, -1)
1998
2035
  # if the key is left arrow, we will move the cursor left
1999
- elif key == 260: # 260 is the key code for left arrow
2036
+ elif key == 260: # 260 is the key code for left arrow
2000
2037
  curserPosition = min(max(curserPosition - 1, 0), len(__keyPressesIn[lineToDisplay]) -1)
2001
2038
  # if the key is right arrow, we will move the cursor right
2002
2039
  elif key == 261: # 261 is the key code for right arrow
@@ -2013,6 +2050,23 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2013
2050
  # if the key is end, we will move the cursor to the end of the line
2014
2051
  elif key == 360: # 360 is the key code for end
2015
2052
  curserPosition = len(__keyPressesIn[lineToDisplay])
2053
+ elif key == curses.KEY_REFRESH or key == curses.KEY_F5 or key == 18: # 18 is the key code for ctrl + R
2054
+ # if the key is refresh, we will refresh the screen
2055
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Refresh requested')
2056
+ elif key == curses.KEY_EXIT or key == 27: # 27 is the key code for ESC
2057
+ # if the key is exit, we will exit the program
2058
+ return
2059
+ elif key == curses.KEY_HELP or key == 63 or key == curses.KEY_F1: # 63 is the key code for ?
2060
+ # if the key is help, we will display the help message
2061
+ if not help_shown:
2062
+ help_panel.show()
2063
+ help_shown = True
2064
+ else:
2065
+ help_panel.hide()
2066
+ help_shown = False
2067
+ refresh_all = True
2068
+ #return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Help closed')
2069
+ curses.panel.update_panels()
2016
2070
  # We are left with these are keys that mofidy the current line.
2017
2071
  else:
2018
2072
  # This means the user have done scrolling and is committing to modify the current line.
@@ -2052,13 +2106,13 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2052
2106
  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} i:{indexOffset} "
2053
2107
  else:
2054
2108
  # we use the stat bar to display the key presses
2055
- encodedLine = ''.join(__keyPressesIn[lineToDisplay]).encode().decode().strip('\n') + ' '
2109
+ encodedLine = ''.join(__keyPressesIn[lineToDisplay]).encode(encoding=_encoding,errors='backslashreplace').decode(encoding=_encoding,errors='backslashreplace').strip('\n') + ' '
2056
2110
  #stats = '┍'+ f"Send CMD: {encodedLine}"[:max_x - 2].center(max_x - 2, "━")
2057
2111
  # format the stats line with chracter at curser position inverted using ansi escape sequence
2058
2112
  # displayCurserPosition is needed as the curserPosition can be larger than the length of the encodedLine. This is wanted to keep scrolling through the history less painful
2059
2113
  displayCurserPosition = min(curserPosition,len(encodedLine) -1)
2060
2114
  stats = f'Send CMD: {encodedLine[:displayCurserPosition]}\x1b[7m{encodedLine[displayCurserPosition]}\x1b[0m{encodedLine[displayCurserPosition + 1:]}'
2061
- if stats != old_stat :
2115
+ if stats != old_stat or refresh_all:
2062
2116
  old_stat = stats
2063
2117
  # calculate the real curser position in stats as we centered the stats
2064
2118
  # if 'Send CMD: ' in stats:
@@ -2078,7 +2132,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2078
2132
  #target_length = max_x - 2 + len('\x1b[33m\x1b[0m\x1b[31m\x1b[0m\x1b[32m\x1b[0m')
2079
2133
  #bottom_stats = '└'+ f" Total: {len(hosts)} Running: \x1b[33m{host_stats['running']}\x1b[0m Failed: \x1b[31m{host_stats['failed']}\x1b[0m Finished: \x1b[32m{host_stats['finished']}\x1b[0m Waiting: {host_stats['waiting']} "[:target_length].center(target_length, "─")
2080
2134
  bottom_stats = f" Total: {len(hosts)} Running: \x1b[33m{host_stats['running']}\x1b[0m Failed: \x1b[31m{host_stats['failed']}\x1b[0m Finished: \x1b[32m{host_stats['finished']}\x1b[0m Waiting: {host_stats['waiting']} "
2081
- if bottom_stats != old_bottom_stat:
2135
+ if bottom_stats != old_bottom_stat or refresh_all:
2082
2136
  old_bottom_stat = bottom_stats
2083
2137
  #bottom_border.clear()
2084
2138
  #bottom_border.addnstr(0, 0, bottom_stats, max_x - 1)
@@ -2087,6 +2141,9 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2087
2141
  # set the maximum refresh rate to 100 Hz
2088
2142
  if time.perf_counter() - last_refresh_time < 0.01:
2089
2143
  time.sleep(max(0,0.01 - time.perf_counter() + last_refresh_time))
2144
+ if refresh_all:
2145
+ rearrangedHosts = set(hosts_to_display)
2146
+ refresh_all = False
2090
2147
  #stdscr.clear()
2091
2148
  for host_window, host in zip(host_windows, hosts_to_display):
2092
2149
  # we will only update the window if there is new output or the window is not fully printed
@@ -2201,6 +2258,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
2201
2258
  def generate_output(hosts, usejson = False, greppable = False):
2202
2259
  global __keyPressesIn
2203
2260
  global __global_suppress_printout
2261
+ global __encoding
2204
2262
  if __global_suppress_printout:
2205
2263
  # remove hosts with returncode 0
2206
2264
  hosts = [dict(host) for host in hosts if host.returncode != 0]
@@ -2234,7 +2292,7 @@ def generate_output(hosts, usejson = False, greppable = False):
2234
2292
  rtnStr += pretty_format_table(rtnList)
2235
2293
  rtnStr += '*'*80+'\n'
2236
2294
  if __keyPressesIn[-1]:
2237
- CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
2295
+ CMDsOut = [''.join(cmd).encode(encoding=_encoding,errors='backslashreplace').decode(encoding=_encoding,errors='backslashreplace').replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
2238
2296
  rtnStr += 'User Inputs: '+ '\nUser Inputs: '.join(CMDsOut)
2239
2297
  #rtnStr += '\n'
2240
2298
  else:
@@ -2265,7 +2323,7 @@ def generate_output(hosts, usejson = False, greppable = False):
2265
2323
  if not __global_suppress_printout or outputs:
2266
2324
  rtnStr += '*'*80+'\n'
2267
2325
  if __keyPressesIn[-1]:
2268
- CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
2326
+ CMDsOut = [''.join(cmd).encode(encoding=_encoding,errors='backslashreplace').decode(encoding=_encoding,errors='backslashreplace').replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
2269
2327
  #rtnStr += f"Key presses: {''.join(__keyPressesIn).encode('unicode_escape').decode()}\n"
2270
2328
  #rtnStr += f"Key presses: {__keyPressesIn}\n"
2271
2329
  rtnStr += "User Inputs: \n "
@@ -2863,6 +2921,7 @@ def generate_default_config(args):
2863
2921
  'DEFAULT_GREPPABLE_MODE': args.greppable,
2864
2922
  'DEFAULT_SKIP_UNREACHABLE': args.skip_unreachable,
2865
2923
  'DEFAULT_SKIP_HOSTS': args.skip_hosts,
2924
+ 'DEFAULT_ENCODING': args.encoding,
2866
2925
  'SSH_STRICT_HOST_KEY_CHECKING': SSH_STRICT_HOST_KEY_CHECKING,
2867
2926
  'ERROR_MESSAGES_TO_IGNORE': ERROR_MESSAGES_TO_IGNORE,
2868
2927
  }
@@ -2918,6 +2977,7 @@ def main():
2918
2977
  global _env_file
2919
2978
  global __DEBUG_MODE
2920
2979
  global __configs_from_file
2980
+ global _encoding
2921
2981
  _emo = False
2922
2982
  # We handle the signal
2923
2983
  signal.signal(signal.SIGINT, signal_handler)
@@ -2969,6 +3029,7 @@ def main():
2969
3029
  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)
2970
3030
  parser.add_argument('-hf','--history_file', type=str, help=f'The file to store the history. (default: {DEFAULT_HISTORY_FILE})', default=DEFAULT_HISTORY_FILE)
2971
3031
  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')
3032
+ parser.add_argument('-e','--encoding', type=str, help=f'The encoding to use for the output. (default: {DEFAULT_ENCODING})', default=DEFAULT_ENCODING)
2972
3033
  parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} @ {COMMIT_DATE} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
2973
3034
 
2974
3035
  # parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
@@ -3057,6 +3118,8 @@ def main():
3057
3118
  # set timeout to the default script timeout if timeout is not set
3058
3119
  if args.timeout == DEFAULT_CLI_TIMEOUT:
3059
3120
  args.timeout = DEFAULT_TIMEOUT
3121
+
3122
+ _encoding = args.encoding
3060
3123
 
3061
3124
  if not __global_suppress_printout:
3062
3125
  cmdStr = getStrCommand(args.hosts,args.commands,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.73
3
+ Version: 5.75
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=5pDQ7zSeNmfzu-o081Z7VOfW_8Ke48WPHi0BxnBnHAw,149915
2
+ multissh3-5.75.dist-info/METADATA,sha256=1_pBC-nNbRg_aR3LFNNInE5Whrn9hBaQMhnxEkO2IOg,18093
3
+ multissh3-5.75.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
+ multissh3-5.75.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
+ multissh3-5.75.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
+ multissh3-5.75.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- multiSSH3.py,sha256=rR0U9P-d7N9t8JggNoK0j16xuOsTqexwNB4S1qTJj5E,145261
2
- multissh3-5.73.dist-info/METADATA,sha256=4XdpwJqBEXLsyqwkr_nIiqy6ngDGu5KrttYZM424NQ4,18093
3
- multissh3-5.73.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
- multissh3-5.73.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
- multissh3-5.73.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
- multissh3-5.73.dist-info/RECORD,,