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.
- {multissh3-5.83 → multissh3-5.85}/PKG-INFO +1 -1
- {multissh3-5.83 → multissh3-5.85}/README.md +0 -0
- {multissh3-5.83 → multissh3-5.85}/multiSSH3.egg-info/PKG-INFO +1 -1
- {multissh3-5.83 → multissh3-5.85}/multiSSH3.py +78 -26
- {multissh3-5.83 → multissh3-5.85}/setup.py +0 -0
- {multissh3-5.83 → multissh3-5.85}/test/test.py +0 -0
- {multissh3-5.83 → multissh3-5.85}/test/testCurses.py +0 -0
- {multissh3-5.83 → multissh3-5.85}/test/testCursesOld.py +0 -0
- {multissh3-5.83 → multissh3-5.85}/test/testPerfCompact.py +0 -0
- {multissh3-5.83 → multissh3-5.85}/test/testPerfExpand.py +0 -0
- {multissh3-5.83 → multissh3-5.85}/multiSSH3.egg-info/SOURCES.txt +0 -0
- {multissh3-5.83 → multissh3-5.85}/multiSSH3.egg-info/dependency_links.txt +0 -0
- {multissh3-5.83 → multissh3-5.85}/multiSSH3.egg-info/entry_points.txt +0 -0
- {multissh3-5.83 → multissh3-5.85}/multiSSH3.egg-info/requires.txt +0 -0
- {multissh3-5.83 → multissh3-5.85}/multiSSH3.egg-info/top_level.txt +0 -0
- {multissh3-5.83 → multissh3-5.85}/setup.cfg +0 -0
|
File without changes
|
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|