multiSSH3 5.56__py3-none-any.whl → 5.58__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
@@ -54,7 +54,7 @@ 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.56'
57
+ version = '5.58'
58
58
  VERSION = version
59
59
  __version__ = version
60
60
  COMMIT_DATE = '2025-03-06'
@@ -69,7 +69,7 @@ CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
69
69
 
70
70
  # TODO: Add terminal TUI with history support
71
71
 
72
- # ------------ Pre Helper Functions ----------------
72
+ #%% ------------ Pre Helper Functions ----------------
73
73
  def eprint(*args, **kwargs):
74
74
  try:
75
75
  print(*args, file=sys.stderr, **kwargs)
@@ -216,7 +216,7 @@ def _get_i():
216
216
  '''
217
217
  return next(_i_counter)
218
218
 
219
- # ------------ Host Object ----------------
219
+ #%% ------------ Host Object ----------------
220
220
  class Host:
221
221
  def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None,shell=False,i = -1,uuid=uuid.uuid4(),ip = None):
222
222
  self.name = name # the name of the host (hostname or IP address)
@@ -255,7 +255,7 @@ identity_file={self.identity_file}, ip={self.ip}, current_color_pair={self.curre
255
255
  def __str__(self):
256
256
  return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
257
257
 
258
- # ------------ Load Defaults ( Config ) File ----------------
258
+ #%% ------------ Load Defaults ( Config ) File ----------------
259
259
  def load_config_file(config_file):
260
260
  '''
261
261
  Load the config file to global variables
@@ -276,103 +276,101 @@ def load_config_file(config_file):
276
276
  return {}
277
277
  return config
278
278
 
279
- if True:
280
- AUTHOR = 'Yufei Pan'
281
- AUTHOR_EMAIL = 'pan@zopyr.us'
282
- DEFAULT_HOSTS = 'all'
283
- DEFAULT_USERNAME = None
284
- DEFAULT_PASSWORD = ''
285
- DEFAULT_IDENTITY_FILE = None
286
- DEDAULT_SSH_KEY_SEARCH_PATH = '~/.ssh/'
287
- DEFAULT_USE_KEY = False
288
- DEFAULT_EXTRA_ARGS = None
289
- DEFAULT_ONE_ON_ONE = False
290
- DEFAULT_SCP = False
291
- DEFAULT_FILE_SYNC = False
292
- DEFAULT_TIMEOUT = 50
293
- DEFAULT_CLI_TIMEOUT = 0
294
- DEFAULT_REPEAT = 1
295
- DEFAULT_INTERVAL = 0
296
- DEFAULT_IPMI = False
297
- DEFAULT_IPMI_INTERFACE_IP_PREFIX = ''
298
- DEFAULT_INTERFACE_IP_PREFIX = None
299
- DEFAULT_NO_WATCH = False
300
- DEFAULT_CURSES_MINIMUM_CHAR_LEN = 40
301
- DEFAULT_CURSES_MINIMUM_LINE_LEN = 1
302
- DEFAULT_SINGLE_WINDOW = False
303
- DEFAULT_ERROR_ONLY = False
304
- DEFAULT_NO_OUTPUT = False
305
- DEFAULT_NO_ENV = False
306
- DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
307
- DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
308
- DEFAULT_JSON_MODE = False
309
- DEFAULT_PRINT_SUCCESS_HOSTS = False
310
- DEFAULT_GREPPABLE_MODE = False
311
- DEFAULT_SKIP_UNREACHABLE = True
312
- DEFAULT_SKIP_HOSTS = ''
313
- SSH_STRICT_HOST_KEY_CHECKING = False
314
- ERROR_MESSAGES_TO_IGNORE = [
315
- 'Pseudo-terminal will not be allocated because stdin is not a terminal',
316
- 'Connection to .* closed',
317
- 'Warning: Permanently added',
318
- 'mux_client_request_session',
319
- 'disabling multiplexing',
320
- 'Killed by signal',
321
- 'Connection reset by peer',
322
- ]
323
- _DEFAULT_CALLED = True
324
- _DEFAULT_RETURN_UNFINISHED = False
325
- _DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
326
- _DEFAULT_NO_START = False
327
- _etc_hosts = {}
328
- _sshpassPath = None
329
- _sshPath = None
330
- _scpPath = None
331
- _ipmitoolPath = None
332
- _rsyncPath = None
333
- _shellPath = None
334
- __ERROR_MESSAGES_TO_IGNORE_REGEX =None
335
- __DEBUG_MODE = False
336
-
337
- # Load Config Based Default Global variables
338
- if True:
339
- __configs_from_file = {}
340
- for config_file in reversed(CONFIG_FILE_CHAIN.copy()):
341
- __configs_from_file.update(load_config_file(os.path.expanduser(config_file)))
342
- globals().update(__configs_from_file)
343
- # form the regex from the list
344
- if __ERROR_MESSAGES_TO_IGNORE_REGEX:
345
- eprint('Using __ERROR_MESSAGES_TO_IGNORE_REGEX, ignoring ERROR_MESSAGES_TO_IGNORE')
346
- __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile(__ERROR_MESSAGES_TO_IGNORE_REGEX)
347
- else:
348
- __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile('|'.join(ERROR_MESSAGES_TO_IGNORE))
349
-
350
- # Load mssh Functional Global Variables
351
- if True:
352
- __global_suppress_printout = False
353
- __mainReturnCode = 0
354
- __failedHosts = set()
355
- __wildCharacters = ['*','?','x']
356
- _no_env = DEFAULT_NO_ENV
357
- _env_file = DEFAULT_ENV_FILE
358
- __globalUnavailableHosts = set()
359
- __ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
360
- __keyPressesIn = [[]]
361
- _emo = False
362
- __curses_global_color_pairs = {(-1,-1):1}
363
- __curses_current_color_pair_index = 2 # Start from 1, as 0 is the default color pair
364
- __curses_color_table = {}
365
- __curses_current_color_index = 10
366
- __max_connections_nofile_limit_supported = 0
367
- __thread_start_delay = 0
368
- if __resource_lib_available:
369
- # Get the current limits
370
- _, __system_nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
371
- # Set the soft limit to the hard limit
372
- resource.setrlimit(resource.RLIMIT_NOFILE, (__system_nofile_limit, __system_nofile_limit))
373
- __max_connections_nofile_limit_supported = int((__system_nofile_limit - 10) / 3)
374
-
375
- # Mapping of ANSI 4-bit colors to curses colors
279
+ #%% ------------ Global Variables ----------------
280
+ AUTHOR = 'Yufei Pan'
281
+ AUTHOR_EMAIL = 'pan@zopyr.us'
282
+ DEFAULT_HOSTS = 'all'
283
+ DEFAULT_USERNAME = None
284
+ DEFAULT_PASSWORD = ''
285
+ DEFAULT_IDENTITY_FILE = None
286
+ DEDAULT_SSH_KEY_SEARCH_PATH = '~/.ssh/'
287
+ DEFAULT_USE_KEY = False
288
+ DEFAULT_EXTRA_ARGS = None
289
+ DEFAULT_ONE_ON_ONE = False
290
+ DEFAULT_SCP = False
291
+ DEFAULT_FILE_SYNC = False
292
+ DEFAULT_TIMEOUT = 50
293
+ DEFAULT_CLI_TIMEOUT = 0
294
+ DEFAULT_REPEAT = 1
295
+ DEFAULT_INTERVAL = 0
296
+ DEFAULT_IPMI = False
297
+ DEFAULT_IPMI_INTERFACE_IP_PREFIX = ''
298
+ DEFAULT_INTERFACE_IP_PREFIX = None
299
+ DEFAULT_NO_WATCH = False
300
+ DEFAULT_CURSES_MINIMUM_CHAR_LEN = 40
301
+ DEFAULT_CURSES_MINIMUM_LINE_LEN = 1
302
+ DEFAULT_SINGLE_WINDOW = False
303
+ DEFAULT_ERROR_ONLY = False
304
+ DEFAULT_NO_OUTPUT = False
305
+ DEFAULT_NO_ENV = False
306
+ DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
307
+ DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
308
+ DEFAULT_JSON_MODE = False
309
+ DEFAULT_PRINT_SUCCESS_HOSTS = False
310
+ DEFAULT_GREPPABLE_MODE = False
311
+ DEFAULT_SKIP_UNREACHABLE = True
312
+ DEFAULT_SKIP_HOSTS = ''
313
+ SSH_STRICT_HOST_KEY_CHECKING = False
314
+ ERROR_MESSAGES_TO_IGNORE = [
315
+ 'Pseudo-terminal will not be allocated because stdin is not a terminal',
316
+ 'Connection to .* closed',
317
+ 'Warning: Permanently added',
318
+ 'mux_client_request_session',
319
+ 'disabling multiplexing',
320
+ 'Killed by signal',
321
+ 'Connection reset by peer',
322
+ ]
323
+ _DEFAULT_CALLED = True
324
+ _DEFAULT_RETURN_UNFINISHED = False
325
+ _DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
326
+ _DEFAULT_NO_START = False
327
+ _etc_hosts = {}
328
+ _sshpassPath = None
329
+ _sshPath = None
330
+ _scpPath = None
331
+ _ipmitoolPath = None
332
+ _rsyncPath = None
333
+ _shellPath = None
334
+ __ERROR_MESSAGES_TO_IGNORE_REGEX =None
335
+ __DEBUG_MODE = False
336
+
337
+ #%% Load Config Based Default Global variables
338
+ __configs_from_file = {}
339
+ for config_file in reversed(CONFIG_FILE_CHAIN.copy()):
340
+ __configs_from_file.update(load_config_file(os.path.expanduser(config_file)))
341
+ globals().update(__configs_from_file)
342
+ # form the regex from the list
343
+ if __ERROR_MESSAGES_TO_IGNORE_REGEX:
344
+ eprint('Using __ERROR_MESSAGES_TO_IGNORE_REGEX, ignoring ERROR_MESSAGES_TO_IGNORE')
345
+ __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile(__ERROR_MESSAGES_TO_IGNORE_REGEX)
346
+ else:
347
+ __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile('|'.join(ERROR_MESSAGES_TO_IGNORE))
348
+
349
+ #%% Load mssh Functional Global Variables
350
+ __global_suppress_printout = False
351
+ __mainReturnCode = 0
352
+ __failedHosts = set()
353
+ __wildCharacters = ['*','?','x']
354
+ _no_env = DEFAULT_NO_ENV
355
+ _env_file = DEFAULT_ENV_FILE
356
+ __globalUnavailableHosts = set()
357
+ __ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
358
+ __keyPressesIn = [[]]
359
+ _emo = False
360
+ __curses_global_color_pairs = {(-1,-1):1}
361
+ __curses_current_color_pair_index = 2 # Start from 1, as 0 is the default color pair
362
+ __curses_color_table = {}
363
+ __curses_current_color_index = 10
364
+ __max_connections_nofile_limit_supported = 0
365
+ __thread_start_delay = 0
366
+ if __resource_lib_available:
367
+ # Get the current limits
368
+ _, __system_nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
369
+ # Set the soft limit to the hard limit
370
+ resource.setrlimit(resource.RLIMIT_NOFILE, (__system_nofile_limit, __system_nofile_limit))
371
+ __max_connections_nofile_limit_supported = int((__system_nofile_limit - 10) / 3)
372
+
373
+ #%% Mapping of ANSI 4-bit colors to curses colors
376
374
  if __curses_available:
377
375
  ANSI_TO_CURSES_COLOR = {
378
376
  30: curses.COLOR_BLACK,
@@ -392,7 +390,7 @@ if __curses_available:
392
390
  96: curses.COLOR_CYAN, # Bright Cyan
393
391
  97: curses.COLOR_WHITE # Bright White
394
392
  }
395
- # ------------ Exportable Help Functions ----------------
393
+ #%% ------------ Exportable Help Functions ----------------
396
394
  # check if command sshpass is available
397
395
  _binPaths = {}
398
396
  def check_path(program_name):
@@ -491,73 +489,74 @@ def replace_magic_strings(string,keys,value,case_sensitive=False):
491
489
  return string
492
490
 
493
491
  def pretty_format_table(data, delimiter = '\t',header = None):
494
- version = 1.11
495
- if not data:
496
- return ''
497
- if isinstance(data, str):
498
- data = data.strip('\n').split('\n')
499
- data = [line.split(delimiter) for line in data]
500
- elif isinstance(data, dict):
501
- # flatten the 2D dict to a list of lists
502
- if isinstance(next(iter(data.values())), dict):
503
- tempData = [['key'] + list(next(iter(data.values())).keys())]
504
- tempData.extend( [[key] + list(value.values()) for key, value in data.items()])
505
- data = tempData
506
- else:
507
- # it is a dict of lists
508
- data = [[key] + list(value) for key, value in data.items()]
509
- elif not isinstance(data, list):
510
- data = list(data)
511
- # format the list into 2d list of list of strings
512
- if isinstance(data[0], dict):
513
- tempData = [data[0].keys()]
514
- tempData.extend([list(item.values()) for item in data])
515
- data = tempData
516
- data = [[str(item) for item in row] for row in data]
517
- num_cols = len(data[0])
518
- col_widths = [0] * num_cols
519
- # Calculate the maximum width of each column
520
- for c in range(num_cols):
521
- #col_widths[c] = max(len(row[c]) for row in data)
522
- # handle ansii escape sequences
523
- col_widths[c] = max(len(re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]','',row[c])) for row in data)
524
- if header:
525
- header_widths = [len(re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', col)) for col in header]
526
- col_widths = [max(col_widths[i], header_widths[i]) for i in range(num_cols)]
527
- # Build the row format string
528
- row_format = ' | '.join('{{:<{}}}'.format(width) for width in col_widths)
529
- # Print the header
530
- if not header:
531
- header = data[0]
532
- outTable = []
533
- outTable.append(row_format.format(*header))
534
- outTable.append('-+-'.join('-' * width for width in col_widths))
535
- for row in data[1:]:
536
- # if the row is empty, print an divider
537
- if not any(row):
538
- outTable.append('-+-'.join('-' * width for width in col_widths))
539
- else:
540
- outTable.append(row_format.format(*row))
541
- else:
542
- # pad / truncate header to appropriate length
543
- if isinstance(header,str):
544
- header = header.split(delimiter)
545
- if len(header) < num_cols:
546
- header += ['']*(num_cols-len(header))
547
- elif len(header) > num_cols:
548
- header = header[:num_cols]
549
- outTable = []
550
- outTable.append(row_format.format(*header))
551
- outTable.append('-+-'.join('-' * width for width in col_widths))
552
- for row in data:
553
- # if the row is empty, print an divider
554
- if not any(row):
555
- outTable.append('-+-'.join('-' * width for width in col_widths))
556
- else:
557
- outTable.append(row_format.format(*row))
558
- return '\n'.join(outTable) + '\n'
559
-
560
- # ------------ Compacting Hostnames ----------------
492
+ version = 1.11
493
+ _ = version
494
+ if not data:
495
+ return ''
496
+ if isinstance(data, str):
497
+ data = data.strip('\n').split('\n')
498
+ data = [line.split(delimiter) for line in data]
499
+ elif isinstance(data, dict):
500
+ # flatten the 2D dict to a list of lists
501
+ if isinstance(next(iter(data.values())), dict):
502
+ tempData = [['key'] + list(next(iter(data.values())).keys())]
503
+ tempData.extend( [[key] + list(value.values()) for key, value in data.items()])
504
+ data = tempData
505
+ else:
506
+ # it is a dict of lists
507
+ data = [[key] + list(value) for key, value in data.items()]
508
+ elif not isinstance(data, list):
509
+ data = list(data)
510
+ # format the list into 2d list of list of strings
511
+ if isinstance(data[0], dict):
512
+ tempData = [data[0].keys()]
513
+ tempData.extend([list(item.values()) for item in data])
514
+ data = tempData
515
+ data = [[str(item) for item in row] for row in data]
516
+ num_cols = len(data[0])
517
+ col_widths = [0] * num_cols
518
+ # Calculate the maximum width of each column
519
+ for c in range(num_cols):
520
+ #col_widths[c] = max(len(row[c]) for row in data)
521
+ # handle ansii escape sequences
522
+ col_widths[c] = max(len(re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]','',row[c])) for row in data)
523
+ if header:
524
+ header_widths = [len(re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', col)) for col in header]
525
+ col_widths = [max(col_widths[i], header_widths[i]) for i in range(num_cols)]
526
+ # Build the row format string
527
+ row_format = ' | '.join('{{:<{}}}'.format(width) for width in col_widths)
528
+ # Print the header
529
+ if not header:
530
+ header = data[0]
531
+ outTable = []
532
+ outTable.append(row_format.format(*header))
533
+ outTable.append('-+-'.join('-' * width for width in col_widths))
534
+ for row in data[1:]:
535
+ # if the row is empty, print an divider
536
+ if not any(row):
537
+ outTable.append('-+-'.join('-' * width for width in col_widths))
538
+ else:
539
+ outTable.append(row_format.format(*row))
540
+ else:
541
+ # pad / truncate header to appropriate length
542
+ if isinstance(header,str):
543
+ header = header.split(delimiter)
544
+ if len(header) < num_cols:
545
+ header += ['']*(num_cols-len(header))
546
+ elif len(header) > num_cols:
547
+ header = header[:num_cols]
548
+ outTable = []
549
+ outTable.append(row_format.format(*header))
550
+ outTable.append('-+-'.join('-' * width for width in col_widths))
551
+ for row in data:
552
+ # if the row is empty, print an divider
553
+ if not any(row):
554
+ outTable.append('-+-'.join('-' * width for width in col_widths))
555
+ else:
556
+ outTable.append(row_format.format(*row))
557
+ return '\n'.join(outTable) + '\n'
558
+
559
+ #%% ------------ Compacting Hostnames ----------------
561
560
  def __tokenize_hostname(hostname):
562
561
  """
563
562
  Tokenize the hostname into a list of tokens.
@@ -913,7 +912,7 @@ def compact_hostnames(Hostnames,verify = True):
913
912
  compact_hosts = hostSet
914
913
  return compact_hosts
915
914
 
916
- # ------------ Expanding Hostnames ----------------
915
+ #%% ------------ Expanding Hostnames ----------------
917
916
  @cache_decorator
918
917
  def __validate_expand_hostname(hostname):
919
918
  '''
@@ -1078,7 +1077,7 @@ def __expand_hostnames(hosts) -> dict:
1078
1077
  # seperated by .
1079
1078
  # If so, we expand the IP address range
1080
1079
  iplist = []
1081
- if re.match(r'^((((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?))?)|(\[((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])}|x|\*|\?))?\]))(\.((((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?)(-((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?))?)|(\[((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?)(-((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?))?\]))){2}(\.(((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?))?)|(\[((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])}|x|\*|\?))?\]))$', host):
1080
+ if re.match(r'^((((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])|x|\*|\?))?)|(\[((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])}|x|\*|\?))?\]))(\.((((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)|x|\*|\?)(-((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)|x|\*|\?))?)|(\[((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)|x|\*|\?)(-((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)|x|\*|\?))?\]))){2}(\.(((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])|x|\*|\?))?)|(\[((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])}|x|\*|\?))?\]))$', host):
1082
1081
  hostSetToAdd = sorted(__expandIPv4Address(frozenset([host])),key=ipaddress.IPv4Address)
1083
1082
  iplist = hostSetToAdd
1084
1083
  else:
@@ -1113,7 +1112,7 @@ def expand_hostnames(hosts):
1113
1112
  return __expand_hostnames(hosts)
1114
1113
 
1115
1114
 
1116
- # ------------ Run Command Block ----------------
1115
+ #%% ------------ Run Command Block ----------------
1117
1116
  def __handle_reading_stream(stream,target, host):
1118
1117
  '''
1119
1118
  Read the stream and append the lines to the target list
@@ -1494,7 +1493,7 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
1494
1493
  host.scp = True
1495
1494
  run_command(host,sem,timeout,passwds,retry_limit=retry_limit - 1)
1496
1495
 
1497
- # ------------ Start Threading Block ----------------
1496
+ #%% ------------ Start Threading Block ----------------
1498
1497
  def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cpu_count()):
1499
1498
  '''
1500
1499
  Start running the command on the hosts. Wrapper function for run_command
@@ -1518,7 +1517,7 @@ def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cp
1518
1517
  time.sleep(__thread_start_delay)
1519
1518
  return threads
1520
1519
 
1521
- # ------------ Display Block ----------------
1520
+ #%% ------------ Display Block ----------------
1522
1521
  def __approximate_color_8bit(color):
1523
1522
  """
1524
1523
  Approximate an 8-bit color (0-255) to the nearest curses color.
@@ -1614,7 +1613,6 @@ def __get_curses_color_pair(fg, bg):
1614
1613
  if __curses_current_color_pair_index >= curses.COLOR_PAIRS:
1615
1614
  print("Warning: Maximum number of color pairs reached, wrapping around.")
1616
1615
  __curses_current_color_pair_index = 1
1617
- # TODO: avoid initializing the same fg and bg color
1618
1616
  curses.init_pair(__curses_current_color_pair_index, fg, bg)
1619
1617
  __curses_global_color_pairs[(fg, bg)] = __curses_current_color_pair_index
1620
1618
  __curses_current_color_pair_index += 1
@@ -1804,7 +1802,7 @@ def _curses_add_string_to_window(window, line = '', y = 0, x = 0, number_of_char
1804
1802
  continue
1805
1803
  if parse_ansi_colors and segment.startswith("\x1b["):
1806
1804
  # Parse ANSI escape sequence
1807
- newAttr = __parse_ansi_escape_sequence_to_curses_attr(segment,color_pair_list)
1805
+ _ = __parse_ansi_escape_sequence_to_curses_attr(segment,color_pair_list)
1808
1806
  else:
1809
1807
  # Add text with current color
1810
1808
  if charsWritten < numChar and len(segment) > 0:
@@ -1845,13 +1843,12 @@ def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
1845
1843
  return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
1846
1844
  # 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
1847
1845
  for i, host in enumerate(new_hosts_to_display):
1848
- if host not in hosts_to_display:
1849
- host.printedLines = 0
1850
- elif i != hosts_to_display.index(host):
1846
+ if host not in hosts_to_display or i != hosts_to_display.index(host):
1851
1847
  host.printedLines = 0
1852
1848
  return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
1853
1849
 
1854
- 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'):
1850
+ 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'):
1851
+ _ = config_reason
1855
1852
  try:
1856
1853
  box_ansi_color = None
1857
1854
  org_dim = stdscr.getmaxyx()
@@ -2160,23 +2157,23 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
2160
2157
  # generate some debug information before display initialization
2161
2158
  try:
2162
2159
  stdscr.clear()
2163
- _curses_add_string_to_window(window=stdscr, y=0, line='Initializing display...', n=stdscr.getmaxyx()[1] - 1)
2160
+ _curses_add_string_to_window(window=stdscr, y=0, line='Initializing display...', number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
2164
2161
  # print the size
2165
- _curses_add_string_to_window(window=stdscr, y=1, line=f"Terminal size: {stdscr.getmaxyx()}", n=stdscr.getmaxyx()[1] - 1)
2162
+ _curses_add_string_to_window(window=stdscr, y=1, line=f"Terminal size: {stdscr.getmaxyx()}",number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
2166
2163
  # print the number of hosts
2167
- _curses_add_string_to_window(window=stdscr, y=2, line=f"Number of hosts: {len(hosts)}", n=stdscr.getmaxyx()[1] - 1)
2164
+ _curses_add_string_to_window(window=stdscr, y=2, line=f"Number of hosts: {len(hosts)}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
2168
2165
  # print the number of threads
2169
- _curses_add_string_to_window(window=stdscr, y=3, line=f"Number of threads: {len(threads)}", n=stdscr.getmaxyx()[1] - 1)
2166
+ _curses_add_string_to_window(window=stdscr, y=3, line=f"Number of threads: {len(threads)}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
2170
2167
  # print the minimum character length
2171
- _curses_add_string_to_window(window=stdscr, y=4, line=f"Minimum character length: {min_char_len}", n=stdscr.getmaxyx()[1] - 1)
2168
+ _curses_add_string_to_window(window=stdscr, y=4, line=f"Minimum character length: {min_char_len}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
2172
2169
  # print the minimum line length
2173
- _curses_add_string_to_window(window=stdscr, y=5, line=f"Minimum line length: {min_line_len}", n=stdscr.getmaxyx()[1] - 1)
2170
+ _curses_add_string_to_window(window=stdscr, y=5, line=f"Minimum line length: {min_line_len}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
2174
2171
  # print the single window mode
2175
- _curses_add_string_to_window(window=stdscr, y=6, line=f"Single window mode: {single_window}", n=stdscr.getmaxyx()[1] - 1)
2172
+ _curses_add_string_to_window(window=stdscr, y=6, line=f"Single window mode: {single_window}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
2176
2173
  # print COLORS and COLOR_PAIRS count
2177
- _curses_add_string_to_window(window=stdscr, y=7, line=f"len(COLORS): {curses.COLORS} len(COLOR_PAIRS): {curses.COLOR_PAIRS}", n=stdscr.getmaxyx()[1] - 1)
2174
+ _curses_add_string_to_window(window=stdscr, y=7, line=f"len(COLORS): {curses.COLORS} len(COLOR_PAIRS): {curses.COLOR_PAIRS}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
2178
2175
  # print if can change color
2179
- _curses_add_string_to_window(window=stdscr, y=8, line=f"Real color capability: {curses.can_change_color()}", n=stdscr.getmaxyx()[1] - 1)
2176
+ _curses_add_string_to_window(window=stdscr, y=8, line=f"Real color capability: {curses.can_change_color()}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
2180
2177
  stdscr.refresh()
2181
2178
  except:
2182
2179
  pass
@@ -2205,7 +2202,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
2205
2202
  time.sleep(0.01)
2206
2203
  #time.sleep(0.25)
2207
2204
 
2208
- # ------------ Generate Output Block ----------------
2205
+ #%% ------------ Generate Output Block ----------------
2209
2206
  def generate_output(hosts, usejson = False, greppable = False):
2210
2207
  global __keyPressesIn
2211
2208
  global __global_suppress_printout
@@ -2234,8 +2231,7 @@ def generate_output(hosts, usejson = False, greppable = False):
2234
2231
  else:
2235
2232
  outputs = {}
2236
2233
  for host in hosts:
2237
- if __global_suppress_printout:
2238
- if host['returncode'] == 0:
2234
+ if __global_suppress_printout and host['returncode'] == 0:
2239
2235
  continue
2240
2236
  hostPrintOut = f" Command:\n {host['command']}\n"
2241
2237
  hostPrintOut += " stdout:\n "+'\n '.join(host['stdout'])
@@ -2271,7 +2267,7 @@ def generate_output(hosts, usejson = False, greppable = False):
2271
2267
  __keyPressesIn = [[]]
2272
2268
  if __global_suppress_printout and not outputs:
2273
2269
  rtnStr += 'Success'
2274
- return rtnStr
2270
+ return rtnStr
2275
2271
 
2276
2272
  def print_output(hosts,usejson = False,quiet = False,greppable = False):
2277
2273
  '''
@@ -2290,7 +2286,7 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
2290
2286
  print(rtnStr)
2291
2287
  return rtnStr
2292
2288
 
2293
- # ------------ Run / Process Hosts Block ----------------
2289
+ #%% ------------ Run / Process Hosts Block ----------------
2294
2290
  def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
2295
2291
  global __globalUnavailableHosts
2296
2292
  global _no_env
@@ -2363,7 +2359,7 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
2363
2359
  if not called:
2364
2360
  print_output(hosts,json,greppable=greppable)
2365
2361
 
2366
- # ------------ Stringfy Block ----------------
2362
+ #%% ------------ Stringfy Block ----------------
2367
2363
  @cache_decorator
2368
2364
  def formHostStr(host) -> str:
2369
2365
  """
@@ -2429,6 +2425,13 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
2429
2425
  single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
2430
2426
  copy_id = False,
2431
2427
  shortend = False):
2428
+ _ = called
2429
+ _ = returnUnfinished
2430
+ _ = willUpdateUnreachableHosts
2431
+ _ = no_start
2432
+ _ = curses_min_char_len
2433
+ _ = curses_min_line_len
2434
+ _ = single_window
2432
2435
  hosts = hosts if isinstance(hosts,str) else frozenset(hosts)
2433
2436
  hostStr = formHostStr(hosts)
2434
2437
  files = frozenset(files) if files else None
@@ -2442,7 +2445,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
2442
2445
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
2443
2446
  return f'multissh {argsStr} {hostStr} {commandStr}'
2444
2447
 
2445
- # ------------ Main Block ----------------
2448
+ #%% ------------ Main Block ----------------
2446
2449
  def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
2447
2450
  nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
2448
2451
  files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
@@ -2548,7 +2551,6 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2548
2551
  try:
2549
2552
  commands = [' '.join(command) if not isinstance(command,str) else command for command in commands]
2550
2553
  except:
2551
- pass
2552
2554
  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.")
2553
2555
  #verify_ssh_config()
2554
2556
  # load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
@@ -2687,7 +2689,6 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2687
2689
  if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
2688
2690
  continue
2689
2691
  if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
2690
- # TODO: use ip to determine if we skip the host or not, also for unavailable hosts
2691
2692
  if file_sync:
2692
2693
  eprint(f"Error: file sync mode need to be specified with at least one path to sync.")
2693
2694
  return []
@@ -2723,7 +2724,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2723
2724
  allHosts += hosts
2724
2725
  return allHosts
2725
2726
 
2726
- # ------------ Default Config Functions ----------------
2727
+ #%% ------------ Default Config Functions ----------------
2727
2728
  def generate_default_config(args):
2728
2729
  '''
2729
2730
  Get the default config
@@ -2812,7 +2813,7 @@ def write_default_config(args,CONFIG_FILE = None):
2812
2813
  eprint(f'Printing the config file to stdout:')
2813
2814
  print(json.dumps(__configs_from_file, indent=4))
2814
2815
 
2815
- # ------------ Wrapper Block ----------------
2816
+ #%% ------------ Wrapper Block ----------------
2816
2817
  def main():
2817
2818
  global _emo
2818
2819
  global __global_suppress_printout
@@ -2878,7 +2879,7 @@ def main():
2878
2879
  # if python version is 3.7 or higher, use parse_intermixed_args
2879
2880
  try:
2880
2881
  args = parser.parse_intermixed_args()
2881
- except Exception as e:
2882
+ except Exception :
2882
2883
  #eprint(f"Error while parsing arguments: {e!r}")
2883
2884
  # try to parse the arguments using parse_known_args
2884
2885
  args, unknown = parser.parse_known_args()
@@ -2987,8 +2988,8 @@ def main():
2987
2988
  if args.success_hosts and not __global_suppress_printout:
2988
2989
  eprint(f'succeeded_hosts: {",".join(sorted(compact_hostnames(succeededHosts)))}')
2989
2990
 
2990
- if threading.active_count() > 1:
2991
- if not __global_suppress_printout: eprint(f'Remaining active thread: {threading.active_count()}')
2991
+ if threading.active_count() > 1 and not __global_suppress_printout:
2992
+ eprint(f'Remaining active thread: {threading.active_count()}')
2992
2993
  # os.system(f'pkill -ef {os.path.basename(__file__)}')
2993
2994
  # os._exit(mainReturnCode)
2994
2995
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: multiSSH3
3
- Version: 5.56
3
+ Version: 5.58
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=CMm3IwWYvN7uuNBXFcYA86AG4wykFuAUtjo5Lct93mw,139201
2
+ multissh3-5.58.dist-info/METADATA,sha256=6dKv2fRT1IYqbt0o636OwXiNr3gijG-p26kTDXvO4tQ,18092
3
+ multissh3-5.58.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
4
+ multissh3-5.58.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
+ multissh3-5.58.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
+ multissh3-5.58.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: setuptools (76.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +0,0 @@
1
- multiSSH3.py,sha256=Ybn3_67O49Cah7uuGUvxnWBiZ7GfOT9wFAe8RMQ7qZY,139586
2
- multissh3-5.56.dist-info/METADATA,sha256=1QxXM8Sf530brcOGDSFRGlMCbRZkQoBxwWmubIjL7ZQ,18092
3
- multissh3-5.56.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
4
- multissh3-5.56.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
- multissh3-5.56.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
- multissh3-5.56.dist-info/RECORD,,