multiSSH3 5.83__tar.gz → 5.85__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.83
3
+ Version: 5.85
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
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.83
3
+ Version: 5.85
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
@@ -43,22 +43,48 @@ import tempfile
43
43
  import math
44
44
  from itertools import count
45
45
  import queue
46
-
46
+ import typing
47
47
  try:
48
48
  # Check if functiools.cache is available
49
- cache_decorator = functools.cache
50
- except AttributeError:
51
- try:
52
- # Check if functools.lru_cache is available
53
- cache_decorator = functools.lru_cache(maxsize=None)
54
- except AttributeError:
55
- # If neither is available, use a dummy decorator
56
- def cache_decorator(func):
57
- return func
58
- version = '5.83'
49
+ # cache_decorator = functools.cache
50
+ def cache_decorator(user_function):
51
+ def _make_hashable(item):
52
+ if isinstance(item, typing.Mapping):
53
+ # Sort items so that {'a':1, 'b':2} and {'b':2, 'a':1} hash the same
54
+ return tuple(
55
+ ( _make_hashable(k), _make_hashable(v) )
56
+ for k, v in sorted(item.items(), key=lambda item: item[0])
57
+ )
58
+ if isinstance(item, (list, set, tuple)):
59
+ return tuple(_make_hashable(e) for e in item)
60
+ # Fallback: assume item is already hashable
61
+ return item
62
+ def decorating_function(user_function):
63
+ # Create the real cached function
64
+ cached_func = functools.lru_cache(maxsize=None)(user_function)
65
+ @functools.wraps(user_function)
66
+ def wrapper(*args, **kwargs):
67
+ # Convert all args/kwargs to hashable equivalents
68
+ hashable_args = tuple(_make_hashable(a) for a in args)
69
+ hashable_kwargs = {
70
+ k: _make_hashable(v) for k, v in kwargs.items()
71
+ }
72
+ # Call the lru-cached version
73
+ return cached_func(*hashable_args, **hashable_kwargs)
74
+ # Expose cache statistics and clear method
75
+ wrapper.cache_info = cached_func.cache_info
76
+ wrapper.cache_clear = cached_func.cache_clear
77
+ return wrapper
78
+ return decorating_function(user_function)
79
+ except :
80
+ # If lrucache is not available, use a dummy decorator
81
+ print('Warning: functools.lru_cache is not available, multiSSH3 will run slower without cache.',file=sys.stderr)
82
+ def cache_decorator(func):
83
+ return func
84
+ version = '5.85'
59
85
  VERSION = version
60
86
  __version__ = version
61
- COMMIT_DATE = '2025-07-21'
87
+ COMMIT_DATE = '2025-08-13'
62
88
 
63
89
  CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
64
90
  '~/multiSSH3.config.json',
@@ -264,6 +290,7 @@ class Host:
264
290
  self.output_buffer = io.BytesIO()
265
291
  self.stdout_buffer = io.BytesIO()
266
292
  self.stderr_buffer = io.BytesIO()
293
+ self.thread = None
267
294
 
268
295
  def __iter__(self):
269
296
  return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
@@ -386,6 +413,7 @@ __max_connections_nofile_limit_supported = 0
386
413
  __thread_start_delay = 0
387
414
  _encoding = DEFAULT_ENCODING
388
415
  __returnZero = DEFAULT_RETURN_ZERO
416
+ __running_threads = set()
389
417
  if __resource_lib_available:
390
418
  # Get the current limits
391
419
  _, __system_nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
@@ -580,6 +608,35 @@ def pretty_format_table(data, delimiter = '\t',header = None):
580
608
  outTable.append(row_format.format(*row))
581
609
  return '\n'.join(outTable) + '\n'
582
610
 
611
+ def join_threads(threads=__running_threads,timeout=None):
612
+ '''
613
+ Join threads
614
+
615
+ @params:
616
+ threads: The threads to join
617
+ timeout: The timeout
618
+
619
+ @returns:
620
+ None
621
+ '''
622
+ global __running_threads
623
+ for thread in threads:
624
+ thread.join(timeout=timeout)
625
+ if threads is __running_threads:
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
+
583
640
  #%% ------------ Compacting Hostnames ----------------
584
641
  def __tokenize_hostname(hostname):
585
642
  """
@@ -1569,8 +1626,9 @@ def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cp
1569
1626
  return []
1570
1627
  sem = threading.Semaphore(max_connections) # Limit concurrent SSH sessions
1571
1628
  threads = [threading.Thread(target=run_command, args=(host, sem,timeout,password), daemon=True) for host in hosts]
1572
- for thread in threads:
1629
+ for thread, host in zip(threads, hosts):
1573
1630
  thread.start()
1631
+ host.thread = thread
1574
1632
  time.sleep(__thread_start_delay)
1575
1633
  return threads
1576
1634
 
@@ -2465,10 +2523,12 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
2465
2523
  eprint(f'Error writing to temporary file: {e!r}')
2466
2524
  import traceback
2467
2525
  eprint(traceback.format_exc())
2468
-
2526
+ if not called:
2527
+ print_output(hosts,json,greppable=greppable)
2528
+ else:
2529
+ __running_threads.update(threads)
2469
2530
  # print the output, if the output of multiple hosts are the same, we aggragate them
2470
- if not called:
2471
- print_output(hosts,json,greppable=greppable)
2531
+
2472
2532
 
2473
2533
  #%% ------------ Stringfy Block ----------------
2474
2534
 
@@ -2565,7 +2625,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
2565
2625
  history_file = history_file, env_file = env_file,
2566
2626
  repeat = repeat,interval = interval,
2567
2627
  shortend = shortend)
2568
- commands = [command.replace('"', '\\"').replace('\n', '\\n').replace('\t', '\\t') for command in commands]
2628
+ commands = [command.replace('"', '\\"').replace('\n', '\\n').replace('\t', '\\t') for command in format_commands(commands)]
2569
2629
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
2570
2630
  filePath = os.path.abspath(__file__)
2571
2631
  programName = filePath if filePath else 'mssh'
@@ -2714,15 +2774,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2714
2774
  if max_connections > __max_connections_nofile_limit_supported * 2:
2715
2775
  # we need to throttle thread start to avoid hitting the nofile limit
2716
2776
  __thread_start_delay = 0.001
2717
- if not commands:
2718
- commands = []
2719
- else:
2720
- commands = [commands] if isinstance(commands,str) else commands
2721
- # reformat commands into a list of strings, join the iterables if they are not strings
2722
- try:
2723
- commands = [' '.join(command) if not isinstance(command,str) else command for command in commands]
2724
- except:
2725
- 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.")
2777
+ commands = format_commands(commands)
2726
2778
  #verify_ssh_config()
2727
2779
  # load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
2728
2780
  if called:
File without changes
File without changes
File without changes
File without changes
File without changes