multiSSH3 5.82__py3-none-any.whl → 5.84__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
@@ -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.82'
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.84'
59
85
  VERSION = version
60
86
  __version__ = version
61
- COMMIT_DATE = '2025-07-16'
87
+ COMMIT_DATE = '2025-07-31'
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,22 @@ 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()}
583
627
  #%% ------------ Compacting Hostnames ----------------
584
628
  def __tokenize_hostname(hostname):
585
629
  """
@@ -1569,8 +1613,9 @@ def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cp
1569
1613
  return []
1570
1614
  sem = threading.Semaphore(max_connections) # Limit concurrent SSH sessions
1571
1615
  threads = [threading.Thread(target=run_command, args=(host, sem,timeout,password), daemon=True) for host in hosts]
1572
- for thread in threads:
1616
+ for thread, host in zip(threads, hosts):
1573
1617
  thread.start()
1618
+ host.thread = thread
1574
1619
  time.sleep(__thread_start_delay)
1575
1620
  return threads
1576
1621
 
@@ -2422,53 +2467,55 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
2422
2467
  sleep_interval *= 1.1
2423
2468
  for thread in threads:
2424
2469
  thread.join(timeout=3)
2425
- # update the unavailable hosts and global unavailable hosts
2426
- if willUpdateUnreachableHosts:
2427
- availableHosts = set()
2428
- for host in hosts:
2429
- if host.stderr and ('No route to host' in host.stderr[0].strip() or 'Connection timed out' in host.stderr[0].strip() or (host.stderr[-1].strip().startswith('Timeout!') and host.returncode == 124)):
2430
- unavailableHosts[host.name] = int(time.monotonic())
2431
- __globalUnavailableHosts[host.name] = int(time.monotonic())
2432
- else:
2433
- availableHosts.add(host.name)
2434
- if host.name in unavailableHosts:
2435
- del unavailableHosts[host.name]
2436
- if host.name in __globalUnavailableHosts:
2437
- del __globalUnavailableHosts[host.name]
2438
- if __DEBUG_MODE:
2439
- print(f'Unreachable hosts: {unavailableHosts}')
2440
- try:
2441
- # check for the old content, only update if the new content is different
2442
- if not os.path.exists(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')):
2443
- with open(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'),'w') as f:
2444
- f.writelines(f'{host},{expTime}' for host,expTime in unavailableHosts.items())
2445
- else:
2446
- oldDic = {}
2447
- try:
2448
- with open(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
2449
- for line in f:
2450
- line = line.strip()
2451
- if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
2452
- hostname = line.split(',')[0]
2453
- expireTime = int(line.split(',')[1])
2454
- if expireTime < time.monotonic() and hostname not in availableHosts:
2455
- oldDic[hostname] = expireTime
2456
- except:
2457
- pass
2458
- # add new entries
2459
- oldDic.update(unavailableHosts)
2460
- with open(os.path.join(tempfile.gettempdir(),getpass.getuser()+'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),'w') as f:
2461
- for key, value in oldDic.items():
2462
- f.write(f'{key},{value}\n')
2463
- os.replace(os.path.join(tempfile.gettempdir(),getpass.getuser()+'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'))
2464
- except Exception as e:
2465
- eprint(f'Error writing to temporary file: {e!r}')
2466
- import traceback
2467
- eprint(traceback.format_exc())
2468
-
2470
+ # update the unavailable hosts and global unavailable hosts
2471
+ if willUpdateUnreachableHosts:
2472
+ availableHosts = set()
2473
+ for host in hosts:
2474
+ if host.stderr and ('No route to host' in host.stderr[0].strip() or 'Connection timed out' in host.stderr[0].strip() or (host.stderr[-1].strip().startswith('Timeout!') and host.returncode == 124)):
2475
+ unavailableHosts[host.name] = int(time.monotonic())
2476
+ __globalUnavailableHosts[host.name] = int(time.monotonic())
2477
+ else:
2478
+ availableHosts.add(host.name)
2479
+ if host.name in unavailableHosts:
2480
+ del unavailableHosts[host.name]
2481
+ if host.name in __globalUnavailableHosts:
2482
+ del __globalUnavailableHosts[host.name]
2483
+ if __DEBUG_MODE:
2484
+ print(f'Unreachable hosts: {unavailableHosts}')
2485
+ try:
2486
+ # check for the old content, only update if the new content is different
2487
+ if not os.path.exists(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')):
2488
+ with open(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'),'w') as f:
2489
+ f.writelines(f'{host},{expTime}' for host,expTime in unavailableHosts.items())
2490
+ else:
2491
+ oldDic = {}
2492
+ try:
2493
+ with open(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
2494
+ for line in f:
2495
+ line = line.strip()
2496
+ if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
2497
+ hostname = line.split(',')[0]
2498
+ expireTime = int(line.split(',')[1])
2499
+ if expireTime < time.monotonic() and hostname not in availableHosts:
2500
+ oldDic[hostname] = expireTime
2501
+ except:
2502
+ pass
2503
+ # add new entries
2504
+ oldDic.update(unavailableHosts)
2505
+ with open(os.path.join(tempfile.gettempdir(),getpass.getuser()+'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),'w') as f:
2506
+ for key, value in oldDic.items():
2507
+ f.write(f'{key},{value}\n')
2508
+ os.replace(os.path.join(tempfile.gettempdir(),getpass.getuser()+'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'))
2509
+ except Exception as e:
2510
+ eprint(f'Error writing to temporary file: {e!r}')
2511
+ import traceback
2512
+ eprint(traceback.format_exc())
2513
+ if not called:
2514
+ print_output(hosts,json,greppable=greppable)
2515
+ else:
2516
+ __running_threads.update(threads)
2469
2517
  # 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)
2518
+
2472
2519
 
2473
2520
  #%% ------------ Stringfy Block ----------------
2474
2521
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.82
3
+ Version: 5.84
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=-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,,
@@ -1,6 +0,0 @@
1
- multiSSH3.py,sha256=w11UYeauLYd2iTOKcv4vQ6XpH2zFLSWfuVtpWcx5jUA,152501
2
- multissh3-5.82.dist-info/METADATA,sha256=ofXQvhQb51R__LCPPrfurTlP_zlkg93163Jotp07Y50,18093
3
- multissh3-5.82.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
- multissh3-5.82.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
- multissh3-5.82.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
- multissh3-5.82.dist-info/RECORD,,