multiSSH3 5.50__tar.gz → 5.52__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: multiSSH3
3
- Version: 5.50
3
+ Version: 5.52
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.2
2
2
  Name: multiSSH3
3
- Version: 5.50
3
+ Version: 5.52
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,4 +1,11 @@
1
1
  #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.6"
4
+ # dependencies = [
5
+ # "argparse",
6
+ # "ipaddress",
7
+ # ]
8
+ # ///
2
9
  __curses_available = False
3
10
  __resource_lib_available = False
4
11
  try:
@@ -47,10 +54,10 @@ except AttributeError:
47
54
  # If neither is available, use a dummy decorator
48
55
  def cache_decorator(func):
49
56
  return func
50
- version = '5.50'
57
+ version = '5.52'
51
58
  VERSION = version
52
59
  __version__ = version
53
- COMMIT_DATE = '2025-01-30'
60
+ COMMIT_DATE = '2025-03-01'
54
61
 
55
62
  CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
56
63
  '~/multiSSH3.config.json',
@@ -483,49 +490,69 @@ def replace_magic_strings(string,keys,value,case_sensitive=False):
483
490
  string = re.sub(re.escape(key),value,string,flags=re.IGNORECASE)
484
491
  return string
485
492
 
486
- def pretty_format_table(data):
487
- version = 1.0
488
- if not data:
489
- return ''
490
- if type(data) == str:
491
- data = data.strip('\n').split('\n')
492
- data = [line.split('\t') for line in data]
493
- elif isinstance(data, dict):
494
- # flatten the 2D dict to a list of lists
495
- if isinstance(next(iter(data.values())), dict):
496
- tempData = [['key'] + list(next(iter(data.values())).keys())]
497
- tempData.extend( [[key] + list(value.values()) for key, value in data.items()])
498
- data = tempData
499
- else:
500
- # it is a dict of lists
501
- data = [[key] + list(value) for key, value in data.items()]
502
- elif type(data) != list:
503
- data = list(data)
504
- # format the list into 2d list of list of strings
505
- if isinstance(data[0], dict):
506
- tempData = [data[0].keys()]
507
- tempData.extend([list(item.values()) for item in data])
508
- data = tempData
509
- data = [[str(item) for item in row] for row in data]
510
- num_cols = len(data[0])
511
- col_widths = [0] * num_cols
512
- # Calculate the maximum width of each column
513
- for c in range(num_cols):
514
- col_widths[c] = max(len(row[c]) for row in data)
515
- # Build the row format string
516
- row_format = ' | '.join('{{:<{}}}'.format(width) for width in col_widths)
517
- # Print the header
518
- header = data[0]
519
- outTable = []
520
- outTable.append(row_format.format(*header))
521
- outTable.append('-+-'.join('-' * width for width in col_widths))
522
- for row in data[1:]:
523
- # if the row is empty, print an divider
524
- if not any(row):
525
- outTable.append('-+-'.join('-' * width for width in col_widths))
526
- else:
527
- outTable.append(row_format.format(*row))
528
- return '\n'.join(outTable) + '\n'
493
+ def pretty_format_table(data, delimiter = '\t',header = None):
494
+ version = 1.1
495
+ if not data:
496
+ return ''
497
+ if type(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 type(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
+ # Build the row format string
525
+ row_format = ' | '.join('{{:<{}}}'.format(width) for width in col_widths)
526
+ # Print the header
527
+ if not header:
528
+ header = data[0]
529
+ outTable = []
530
+ outTable.append(row_format.format(*header))
531
+ outTable.append('-+-'.join('-' * width for width in col_widths))
532
+ for row in data[1:]:
533
+ # if the row is empty, print an divider
534
+ if not any(row):
535
+ outTable.append('-+-'.join('-' * width for width in col_widths))
536
+ else:
537
+ outTable.append(row_format.format(*row))
538
+ else:
539
+ # pad / truncate header to appropriate length
540
+ if isinstance(header,str):
541
+ header = header.split(delimiter)
542
+ if len(header) < num_cols:
543
+ header += ['']*(num_cols-len(header))
544
+ elif len(header) > num_cols:
545
+ header = header[:num_cols]
546
+ outTable = []
547
+ outTable.append(row_format.format(*header))
548
+ outTable.append('-+-'.join('-' * width for width in col_widths))
549
+ for row in data:
550
+ # if the row is empty, print an divider
551
+ if not any(row):
552
+ outTable.append('-+-'.join('-' * width for width in col_widths))
553
+ else:
554
+ outTable.append(row_format.format(*row))
555
+ return '\n'.join(outTable) + '\n'
529
556
 
530
557
  # ------------ Compacting Hostnames ----------------
531
558
  def __tokenize_hostname(hostname):
@@ -1359,7 +1386,7 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
1359
1386
  # Monitor the subprocess and terminate it after the timeout
1360
1387
  host.lastUpdateTime = time.time()
1361
1388
  timeoutLineAppended = False
1362
- sleep_interval = 1.0e-8 # 10 nanoseconds
1389
+ sleep_interval = 1.0e-7 # 100 nanoseconds
1363
1390
  while proc.poll() is None: # while the process is still running
1364
1391
  if timeout > 0:
1365
1392
  if time.time() - host.lastUpdateTime > timeout:
@@ -2264,13 +2291,27 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
2264
2291
  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):
2265
2292
  global __globalUnavailableHosts
2266
2293
  global _no_env
2294
+ sleep_interval = 1.0e-7 # 0.1 microseconds
2267
2295
  threads = start_run_on_hosts(hosts, timeout=timeout,password=password,max_connections=max_connections)
2268
- if __curses_available and not nowatch and threads and not returnUnfinished and any([thread.is_alive() for thread in threads]) and sys.stdout.isatty() and os.get_terminal_size() and os.get_terminal_size().columns > 10:
2269
- curses.wrapper(curses_print, hosts, threads, min_char_len = curses_min_char_len, min_line_len = curses_min_line_len, single_window = single_window)
2296
+ if __curses_available and not nowatch and threads and not returnUnfinished and sys.stdout.isatty() and os.get_terminal_size() and os.get_terminal_size().columns > 10:
2297
+ total_sleeped = 0
2298
+ while any([host.returncode is None for host in hosts]):
2299
+ time.sleep(sleep_interval) # avoid busy-waiting
2300
+ total_sleeped += sleep_interval
2301
+ if sleep_interval < 0.001:
2302
+ sleep_interval *= 2
2303
+ elif sleep_interval < 0.01:
2304
+ sleep_interval *= 1.1
2305
+ if total_sleeped > 0.1:
2306
+ break
2307
+ if any([host.returncode is None for host in hosts]):
2308
+ curses.wrapper(curses_print, hosts, threads, min_char_len = curses_min_char_len, min_line_len = curses_min_line_len, single_window = single_window)
2270
2309
  if not returnUnfinished:
2271
2310
  # wait until all hosts have a return code
2272
2311
  while any([host.returncode is None for host in hosts]):
2273
- time.sleep(0.01)
2312
+ time.sleep(sleep_interval) # avoid busy-waiting
2313
+ if sleep_interval < 0.01:
2314
+ sleep_interval *= 1.1
2274
2315
  for thread in threads:
2275
2316
  thread.join(timeout=3)
2276
2317
  # update the unavailable hosts and global unavailable hosts
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes