multiSSH3 5.68__tar.gz → 5.71__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.68 → multissh3-5.71}/PKG-INFO +1 -1
- {multissh3-5.68 → multissh3-5.71}/multiSSH3.egg-info/PKG-INFO +1 -1
- {multissh3-5.68 → multissh3-5.71}/multiSSH3.py +50 -42
- {multissh3-5.68 → multissh3-5.71}/README.md +0 -0
- {multissh3-5.68 → multissh3-5.71}/multiSSH3.egg-info/SOURCES.txt +0 -0
- {multissh3-5.68 → multissh3-5.71}/multiSSH3.egg-info/dependency_links.txt +0 -0
- {multissh3-5.68 → multissh3-5.71}/multiSSH3.egg-info/entry_points.txt +0 -0
- {multissh3-5.68 → multissh3-5.71}/multiSSH3.egg-info/requires.txt +0 -0
- {multissh3-5.68 → multissh3-5.71}/multiSSH3.egg-info/top_level.txt +0 -0
- {multissh3-5.68 → multissh3-5.71}/setup.cfg +0 -0
- {multissh3-5.68 → multissh3-5.71}/setup.py +0 -0
- {multissh3-5.68 → multissh3-5.71}/test/test.py +0 -0
- {multissh3-5.68 → multissh3-5.71}/test/testCurses.py +0 -0
- {multissh3-5.68 → multissh3-5.71}/test/testCursesOld.py +0 -0
- {multissh3-5.68 → multissh3-5.71}/test/testPerfCompact.py +0 -0
- {multissh3-5.68 → multissh3-5.71}/test/testPerfExpand.py +0 -0
|
@@ -54,10 +54,10 @@ 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.
|
|
57
|
+
version = '5.71'
|
|
58
58
|
VERSION = version
|
|
59
59
|
__version__ = version
|
|
60
|
-
COMMIT_DATE = '2025-05-
|
|
60
|
+
COMMIT_DATE = '2025-05-13'
|
|
61
61
|
|
|
62
62
|
CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
|
|
63
63
|
'~/multiSSH3.config.json',
|
|
@@ -349,7 +349,7 @@ __failedHosts = set()
|
|
|
349
349
|
__wildCharacters = ['*','?','x']
|
|
350
350
|
_no_env = DEFAULT_NO_ENV
|
|
351
351
|
_env_file = DEFAULT_ENV_FILE
|
|
352
|
-
__globalUnavailableHosts =
|
|
352
|
+
__globalUnavailableHosts = dict()
|
|
353
353
|
__ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
|
|
354
354
|
__keyPressesIn = [[]]
|
|
355
355
|
_emo = False
|
|
@@ -1191,12 +1191,13 @@ def __handle_writing_stream(stream,stop_event,host):
|
|
|
1191
1191
|
line = '> ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵')
|
|
1192
1192
|
host.output.append(line)
|
|
1193
1193
|
host.stdout.append(line)
|
|
1194
|
+
host.lineNumToPrintSet.add(len(host.output)-1)
|
|
1194
1195
|
sentInput += 1
|
|
1195
1196
|
host.lastUpdateTime = time.monotonic()
|
|
1196
1197
|
else:
|
|
1197
1198
|
time.sleep(0.01) # sleep for 10ms
|
|
1198
1199
|
if sentInput < len(__keyPressesIn) - 1 :
|
|
1199
|
-
eprint(f"Warning: {len(__keyPressesIn)-sentInput} key presses are not sent before the process is terminated!")
|
|
1200
|
+
eprint(f"Warning: {len(__keyPressesIn)-sentInput} lines of key presses are not sent before the process is terminated!")
|
|
1200
1201
|
# # send the last line
|
|
1201
1202
|
# if __keyPressesIn and __keyPressesIn[-1]:
|
|
1202
1203
|
# stream.write(''.join(__keyPressesIn[-1]).encode())
|
|
@@ -2235,7 +2236,7 @@ def generate_output(hosts, usejson = False, greppable = False):
|
|
|
2235
2236
|
hostPrintOut = f" Command:\n {host['command']}\n"
|
|
2236
2237
|
hostPrintOut += " stdout:\n "+'\n '.join(host['stdout'])
|
|
2237
2238
|
if host['stderr']:
|
|
2238
|
-
if host['stderr'][0].strip().startswith('ssh: connect to host '):
|
|
2239
|
+
if host['stderr'][0].strip().startswith('ssh: connect to host ') and host['stderr'][0].strip().endswith('Connection refused'):
|
|
2239
2240
|
host['stderr'][0] = 'SSH not reachable!'
|
|
2240
2241
|
elif host['stderr'][-1].strip().endswith('Connection timed out'):
|
|
2241
2242
|
host['stderr'][-1] = 'SSH connection timed out!'
|
|
@@ -2263,7 +2264,7 @@ def generate_output(hosts, usejson = False, greppable = False):
|
|
|
2263
2264
|
rtnStr += "User Inputs: \n "
|
|
2264
2265
|
rtnStr += '\n '.join(CMDsOut)
|
|
2265
2266
|
rtnStr += '\n'
|
|
2266
|
-
__keyPressesIn
|
|
2267
|
+
__keyPressesIn[-1].clear()
|
|
2267
2268
|
if __global_suppress_printout and not outputs:
|
|
2268
2269
|
rtnStr += 'Success'
|
|
2269
2270
|
return rtnStr
|
|
@@ -2287,7 +2288,7 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
2287
2288
|
|
|
2288
2289
|
#%% ------------ Run / Process Hosts Block ----------------
|
|
2289
2290
|
def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, no_watch, json, called, greppable,
|
|
2290
|
-
unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN,
|
|
2291
|
+
unavailableHosts:dict,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN,
|
|
2291
2292
|
curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW,
|
|
2292
2293
|
unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY):
|
|
2293
2294
|
global __globalUnavailableHosts
|
|
@@ -2317,45 +2318,47 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
2317
2318
|
thread.join(timeout=3)
|
|
2318
2319
|
# update the unavailable hosts and global unavailable hosts
|
|
2319
2320
|
if willUpdateUnreachableHosts:
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2321
|
+
availableHosts = set()
|
|
2322
|
+
for host in hosts:
|
|
2323
|
+
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)):
|
|
2324
|
+
unavailableHosts[host.name] = int(time.monotonic())
|
|
2325
|
+
__globalUnavailableHosts[host.name] = int(time.monotonic())
|
|
2326
|
+
else:
|
|
2327
|
+
availableHosts.add(host.name)
|
|
2328
|
+
if host.name in unavailableHosts:
|
|
2329
|
+
del unavailableHosts[host.name]
|
|
2330
|
+
if host.name in __globalUnavailableHosts:
|
|
2331
|
+
del __globalUnavailableHosts[host.name]
|
|
2324
2332
|
if __DEBUG_MODE:
|
|
2325
2333
|
print(f'Unreachable hosts: {unavailableHosts}')
|
|
2326
|
-
__globalUnavailableHosts.update(unavailableHosts)
|
|
2327
|
-
|
|
2328
|
-
# os.environ['__multiSSH3_UNAVAILABLE_HOSTS'] = ','.join(unavailableHosts)
|
|
2329
|
-
# create a temporary file to store the unavailable hosts
|
|
2330
2334
|
try:
|
|
2331
2335
|
# check for the old content, only update if the new content is different
|
|
2332
|
-
if not os.path.exists(os.path.join(tempfile.gettempdir(),'
|
|
2333
|
-
with open(os.path.join(tempfile.gettempdir(),'
|
|
2334
|
-
f.
|
|
2336
|
+
if not os.path.exists(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')):
|
|
2337
|
+
with open(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'),'w') as f:
|
|
2338
|
+
f.writelines(f'{host},{expTime}' for host,expTime in unavailableHosts.values())
|
|
2335
2339
|
else:
|
|
2336
2340
|
oldDic = {}
|
|
2337
2341
|
try:
|
|
2338
|
-
with open(os.path.join(tempfile.gettempdir(),'
|
|
2342
|
+
with open(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
|
|
2339
2343
|
for line in f:
|
|
2340
2344
|
line = line.strip()
|
|
2341
2345
|
if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
|
|
2342
|
-
|
|
2346
|
+
hostname = line.split(',')[0]
|
|
2347
|
+
expireTime = int(line.split(',')[1])
|
|
2348
|
+
if expireTime < time.monotonic() and hostname not in availableHosts:
|
|
2349
|
+
oldDic[hostname] = expireTime
|
|
2343
2350
|
except:
|
|
2344
2351
|
pass
|
|
2345
|
-
for key in list(oldDic.keys()):
|
|
2346
|
-
if key in reachableHosts or time.monotonic() < oldDic[key] or time.monotonic() - oldDic[key] > unavailable_host_expiry:
|
|
2347
|
-
del oldDic[key]
|
|
2348
2352
|
# add new entries
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
oldDic[host] = int(time.monotonic ())
|
|
2352
|
-
with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),'w') as f:
|
|
2353
|
+
oldDic.update(unavailableHosts)
|
|
2354
|
+
with open(os.path.join(tempfile.gettempdir(),getpass.getuser()+'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),'w') as f:
|
|
2353
2355
|
for key, value in oldDic.items():
|
|
2354
2356
|
f.write(f'{key},{value}\n')
|
|
2355
|
-
os.replace(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),os.path.join(tempfile.gettempdir(),'
|
|
2356
|
-
|
|
2357
|
+
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'))
|
|
2357
2358
|
except Exception as e:
|
|
2358
2359
|
eprint(f'Error writing to temporary file: {e!r}')
|
|
2360
|
+
import traceback
|
|
2361
|
+
eprint(traceback.format_exc())
|
|
2359
2362
|
|
|
2360
2363
|
# print the output, if the output of multiple hosts are the same, we aggragate them
|
|
2361
2364
|
if not called:
|
|
@@ -2436,7 +2439,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
2436
2439
|
copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY,no_history = DEFAULT_NO_HISTORY,
|
|
2437
2440
|
history_file = DEFAULT_HISTORY_FILE, env_file = DEFAULT_ENV_FILE,
|
|
2438
2441
|
repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
|
|
2439
|
-
shortend = False):
|
|
2442
|
+
shortend = False,tabSeperated = False):
|
|
2440
2443
|
_ = called
|
|
2441
2444
|
_ = returnUnfinished
|
|
2442
2445
|
_ = willUpdateUnreachableHosts
|
|
@@ -2460,7 +2463,10 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
2460
2463
|
commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
|
|
2461
2464
|
filePath = os.path.abspath(__file__)
|
|
2462
2465
|
programName = filePath if filePath else 'mssh'
|
|
2463
|
-
|
|
2466
|
+
if tabSeperated:
|
|
2467
|
+
return f'{programName}\t{argsStr}\t{hostStr}\t{commandStr}'
|
|
2468
|
+
else:
|
|
2469
|
+
return f'{programName} {argsStr} {hostStr} {commandStr}'
|
|
2464
2470
|
|
|
2465
2471
|
#%% ------------ Record History Block ----------------
|
|
2466
2472
|
def record_command_history(kwargs):
|
|
@@ -2484,7 +2490,7 @@ def record_command_history(kwargs):
|
|
|
2484
2490
|
for name in sig.parameters
|
|
2485
2491
|
if name in kwargs
|
|
2486
2492
|
}
|
|
2487
|
-
strCommand = getStrCommand(**wanted)
|
|
2493
|
+
strCommand = getStrCommand(**wanted,shortend=True,tabSeperated=True)
|
|
2488
2494
|
with open(history_file, 'a') as f:
|
|
2489
2495
|
# it follows <timestamp>\t<strCommand>\n
|
|
2490
2496
|
f.write(f'{int(time.time())}\t{strCommand}\n')
|
|
@@ -2566,27 +2572,29 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2566
2572
|
record_command_history(locals())
|
|
2567
2573
|
if error_only:
|
|
2568
2574
|
__global_suppress_printout = True
|
|
2569
|
-
if os.path.exists(os.path.join(tempfile.gettempdir(),'
|
|
2575
|
+
if os.path.exists(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')):
|
|
2570
2576
|
if unavailable_host_expiry <= 0:
|
|
2571
2577
|
unavailable_host_expiry = 10
|
|
2572
2578
|
try:
|
|
2573
2579
|
readed = False
|
|
2574
|
-
if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'
|
|
2580
|
+
if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')) < unavailable_host_expiry:
|
|
2575
2581
|
|
|
2576
|
-
with open(os.path.join(tempfile.gettempdir(),'
|
|
2582
|
+
with open(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
|
|
2577
2583
|
for line in f:
|
|
2578
2584
|
line = line.strip()
|
|
2579
2585
|
if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
|
|
2580
|
-
|
|
2581
|
-
|
|
2586
|
+
hostname = line.split(',')[0]
|
|
2587
|
+
expireTime = int(line.split(',')[1])
|
|
2588
|
+
if expireTime < time.monotonic() and expireTime + unavailable_host_expiry > time.monotonic():
|
|
2589
|
+
__globalUnavailableHosts[hostname] = expireTime
|
|
2582
2590
|
readed = True
|
|
2583
2591
|
if readed and not __global_suppress_printout:
|
|
2584
|
-
eprint(f"Read unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'
|
|
2592
|
+
eprint(f"Read unavailable hosts from the file {os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')}")
|
|
2585
2593
|
except Exception as e:
|
|
2586
|
-
eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'
|
|
2594
|
+
eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')!r}")
|
|
2587
2595
|
eprint(str(e))
|
|
2588
2596
|
elif '__multiSSH3_UNAVAILABLE_HOSTS' in readEnvFromFile():
|
|
2589
|
-
__globalUnavailableHosts.update(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
|
|
2597
|
+
__globalUnavailableHosts.update({host: int(time.monotonic()) for host in readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(',') if host})
|
|
2590
2598
|
if not max_connections:
|
|
2591
2599
|
max_connections = 4 * os.cpu_count()
|
|
2592
2600
|
elif max_connections == 0:
|
|
@@ -2619,7 +2627,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2619
2627
|
if skipUnreachable:
|
|
2620
2628
|
unavailableHosts = __globalUnavailableHosts
|
|
2621
2629
|
else:
|
|
2622
|
-
unavailableHosts =
|
|
2630
|
+
unavailableHosts = dict()
|
|
2623
2631
|
# set global input to empty
|
|
2624
2632
|
__keyPressesIn = [[]]
|
|
2625
2633
|
__global_suppress_printout = True
|
|
@@ -2628,7 +2636,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2628
2636
|
if skipUnreachable:
|
|
2629
2637
|
unavailableHosts = __globalUnavailableHosts
|
|
2630
2638
|
else:
|
|
2631
|
-
unavailableHosts =
|
|
2639
|
+
unavailableHosts = dict()
|
|
2632
2640
|
skipUnreachable = True
|
|
2633
2641
|
if quiet:
|
|
2634
2642
|
__global_suppress_printout = True
|
|
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
|
|
File without changes
|