multiSSH3 5.96__py3-none-any.whl → 5.98__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 +231 -102
- {multissh3-5.96.dist-info → multissh3-5.98.dist-info}/METADATA +1 -1
- multissh3-5.98.dist-info/RECORD +6 -0
- multissh3-5.96.dist-info/RECORD +0 -6
- {multissh3-5.96.dist-info → multissh3-5.98.dist-info}/WHEEL +0 -0
- {multissh3-5.96.dist-info → multissh3-5.98.dist-info}/entry_points.txt +0 -0
- {multissh3-5.96.dist-info → multissh3-5.98.dist-info}/top_level.txt +0 -0
multiSSH3.py
CHANGED
|
@@ -84,10 +84,10 @@ except Exception:
|
|
|
84
84
|
print('Warning: functools.lru_cache is not available, multiSSH3 will run slower without cache.',file=sys.stderr)
|
|
85
85
|
def cache_decorator(func):
|
|
86
86
|
return func
|
|
87
|
-
version = '5.
|
|
87
|
+
version = '5.98'
|
|
88
88
|
VERSION = version
|
|
89
89
|
__version__ = version
|
|
90
|
-
COMMIT_DATE = '2025-10-
|
|
90
|
+
COMMIT_DATE = '2025-10-24'
|
|
91
91
|
|
|
92
92
|
CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
|
|
93
93
|
'~/multiSSH3.config.json',
|
|
@@ -377,6 +377,24 @@ ERROR_MESSAGES_TO_IGNORE = [
|
|
|
377
377
|
'Killed by signal',
|
|
378
378
|
'Connection reset by peer',
|
|
379
379
|
]
|
|
380
|
+
__DEFAULT_COLOR_PALETTE = {
|
|
381
|
+
'cyan': (86, 173, 188),
|
|
382
|
+
'green': (114, 180, 43),
|
|
383
|
+
'magenta': (140, 107, 200),
|
|
384
|
+
'red': (196, 38, 94),
|
|
385
|
+
'white': (227, 227, 221),
|
|
386
|
+
'yellow': (179, 180, 43),
|
|
387
|
+
'blue': (106, 126, 200),
|
|
388
|
+
'bright_black': (102, 102, 102),
|
|
389
|
+
'bright_blue': (129, 154, 255),
|
|
390
|
+
'bright_cyan': (102, 217, 239),
|
|
391
|
+
'bright_green': (126, 226, 46),
|
|
392
|
+
'bright_magenta': (174, 129, 255),
|
|
393
|
+
'bright_red': (249, 38, 114),
|
|
394
|
+
'bright_white': (248, 248, 242),
|
|
395
|
+
'bright_yellow': (226, 226, 46),
|
|
396
|
+
}
|
|
397
|
+
COLOR_PALETTE = __DEFAULT_COLOR_PALETTE.copy()
|
|
380
398
|
_DEFAULT_CALLED = True
|
|
381
399
|
_DEFAULT_RETURN_UNFINISHED = False
|
|
382
400
|
_DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
|
|
@@ -420,6 +438,11 @@ __thread_start_delay = 0
|
|
|
420
438
|
_encoding = DEFAULT_ENCODING
|
|
421
439
|
__returnZero = DEFAULT_RETURN_ZERO
|
|
422
440
|
__running_threads = set()
|
|
441
|
+
__control_master_string = '''Host *
|
|
442
|
+
ControlMaster auto
|
|
443
|
+
ControlPath /run/user/%i/ssh_sockets_%C
|
|
444
|
+
ControlPersist 3600
|
|
445
|
+
'''
|
|
423
446
|
if __resource_lib_available:
|
|
424
447
|
# Get the current limits
|
|
425
448
|
_, __system_nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
@@ -931,6 +954,8 @@ def get_terminal_color_capability():
|
|
|
931
954
|
return '24bit'
|
|
932
955
|
elif "256" in term:
|
|
933
956
|
return '256'
|
|
957
|
+
elif "16" in term:
|
|
958
|
+
return '16'
|
|
934
959
|
try:
|
|
935
960
|
curses.setupterm()
|
|
936
961
|
colors = curses.tigetnum("colors")
|
|
@@ -949,96 +974,120 @@ def get_terminal_color_capability():
|
|
|
949
974
|
return 'None'
|
|
950
975
|
|
|
951
976
|
@cache_decorator
|
|
952
|
-
def
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
977
|
+
def rgb_to_ansi_color_string(r, g, b):
|
|
978
|
+
"""
|
|
979
|
+
Return an ANSI escape sequence setting the foreground to (r,g,b)
|
|
980
|
+
approximated to the terminal's capability, or '' if none.
|
|
981
|
+
"""
|
|
982
|
+
cap = get_terminal_color_capability()
|
|
983
|
+
if cap == 'None':
|
|
984
|
+
return ''
|
|
985
|
+
if cap == '24bit':
|
|
986
|
+
return f'\x1b[38;2;{r};{g};{b}m'
|
|
987
|
+
if cap == '256':
|
|
988
|
+
idx = _rgb_to_256_color(r, g, b)
|
|
989
|
+
return f'\x1b[38;5;{idx}m'
|
|
990
|
+
if cap == '16':
|
|
991
|
+
idx = _rgb_to_16_color(r, g, b)
|
|
992
|
+
# 0–7 = 30–37, 8–15 = 90–97
|
|
993
|
+
if idx < 8:
|
|
994
|
+
return f'\x1b[{30 + idx}m'
|
|
995
|
+
else:
|
|
996
|
+
return f'\x1b[{90 + (idx - 8)}m'
|
|
997
|
+
if cap == '8':
|
|
998
|
+
idx = _rgb_to_8_color(r, g, b)
|
|
999
|
+
return f'\x1b[{30 + idx}m'
|
|
1000
|
+
return ''
|
|
974
1001
|
|
|
975
|
-
|
|
976
|
-
|
|
1002
|
+
def _rgb_to_256_color(r, g, b):
|
|
1003
|
+
"""
|
|
1004
|
+
Map (r,g,b) to the 256-color cube or grayscale ramp.
|
|
977
1005
|
"""
|
|
978
|
-
|
|
979
|
-
r
|
|
980
|
-
|
|
1006
|
+
# if it’s already gray, use the 232–255 grayscale ramp
|
|
1007
|
+
if r == g == b:
|
|
1008
|
+
# 24 shades from 232 to 255
|
|
1009
|
+
return 232 + int(round(r / 255 * 23))
|
|
1010
|
+
# else map each channel to 0–5
|
|
1011
|
+
def to6(v):
|
|
1012
|
+
return int(round(v / 255 * 5))
|
|
1013
|
+
r6, g6, b6 = to6(r), to6(g), to6(b)
|
|
1014
|
+
return 16 + 36 * r6 + 6 * g6 + b6
|
|
1015
|
+
|
|
1016
|
+
def _rgb_to_16_color(r, g, b):
|
|
981
1017
|
"""
|
|
982
|
-
|
|
1018
|
+
Pick the nearest of the 16 ANSI standard colors.
|
|
1019
|
+
Returns an index 0-15.
|
|
1020
|
+
"""
|
|
1021
|
+
palette = [
|
|
1022
|
+
(0, 0, 0), # 0 black
|
|
1023
|
+
(128, 0, 0), # 1 red
|
|
1024
|
+
(0, 128, 0), # 2 green
|
|
1025
|
+
(128, 128, 0), # 3 yellow
|
|
1026
|
+
(0, 0, 128), # 4 blue
|
|
1027
|
+
(128, 0, 128), # 5 magenta
|
|
1028
|
+
(0, 128, 128), # 6 cyan
|
|
1029
|
+
(192, 192, 192), # 7 white (light gray)
|
|
1030
|
+
(128, 128, 128), # 8 bright black (dark gray)
|
|
1031
|
+
(255, 0, 0), # 9 bright red
|
|
1032
|
+
(0, 255, 0), # 10 bright green
|
|
1033
|
+
(255, 255, 0), # 11 bright yellow
|
|
1034
|
+
(0, 0, 255), # 12 bright blue
|
|
1035
|
+
(255, 0, 255), # 13 bright magenta
|
|
1036
|
+
(0, 255, 255), # 14 bright cyan
|
|
1037
|
+
(255, 255, 255), # 15 bright white
|
|
1038
|
+
]
|
|
1039
|
+
best_idx = 0
|
|
983
1040
|
best_dist = float('inf')
|
|
984
|
-
for i, (pr, pg, pb) in enumerate(
|
|
985
|
-
|
|
986
|
-
dg = pg - g
|
|
987
|
-
db = pb - b
|
|
988
|
-
dist = dr*dr + dg*dg + db*db
|
|
1041
|
+
for i, (pr, pg, pb) in enumerate(palette):
|
|
1042
|
+
dist = (r - pr)**2 + (g - pg)**2 + (b - pb)**2
|
|
989
1043
|
if dist < best_dist:
|
|
990
1044
|
best_dist = dist
|
|
991
|
-
|
|
992
|
-
return
|
|
1045
|
+
best_idx = i
|
|
1046
|
+
return best_idx
|
|
993
1047
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1048
|
+
def _rgb_to_8_color(r, g, b):
|
|
1049
|
+
"""
|
|
1050
|
+
Reduce to 8 colors by mapping to the 16-color index then clamping 0-7.
|
|
1051
|
+
"""
|
|
1052
|
+
return _rgb_to_16_color(r//2, g//2, b//2)
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def int_to_color(hash_value, min_brightness=100,max_brightness=220):
|
|
997
1056
|
r = (hash_value >> 16) & 0xFF
|
|
998
1057
|
g = (hash_value >> 8) & 0xFF
|
|
999
1058
|
b = hash_value & 0xFF
|
|
1000
|
-
|
|
1001
|
-
|
|
1059
|
+
brightness = math.sqrt(0.299 * r**2 + 0.587 * g**2 + 0.114 * b**2)
|
|
1060
|
+
if brightness < min_brightness:
|
|
1061
|
+
return int_to_color(hash(str(hash_value)), min_brightness, max_brightness)
|
|
1062
|
+
if brightness > max_brightness:
|
|
1063
|
+
return int_to_color(hash(str(hash_value)), min_brightness, max_brightness)
|
|
1002
1064
|
return (r, g, b)
|
|
1003
1065
|
|
|
1004
|
-
|
|
1066
|
+
__previous_color_rgb = ()
|
|
1005
1067
|
@cache_decorator
|
|
1006
|
-
def
|
|
1068
|
+
def int_to_unique_ansi_color(number):
|
|
1007
1069
|
'''
|
|
1008
|
-
Convert a
|
|
1070
|
+
Convert a number to a unique ANSI color code
|
|
1009
1071
|
|
|
1010
1072
|
Args:
|
|
1011
|
-
|
|
1012
|
-
|
|
1073
|
+
number (int): The number to convert
|
|
1013
1074
|
Returns:
|
|
1014
1075
|
int: The ANSI color code
|
|
1015
1076
|
'''
|
|
1016
|
-
global
|
|
1077
|
+
global __previous_color_rgb
|
|
1017
1078
|
# Use a hash function to generate a consistent integer from the string
|
|
1018
1079
|
color_capability = get_terminal_color_capability()
|
|
1019
|
-
index = None
|
|
1020
1080
|
if color_capability == 'None':
|
|
1021
1081
|
return ''
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
index = (hash(string) % 14) + 31
|
|
1025
|
-
if index > 37:
|
|
1026
|
-
index += 52 # Bright colors (90-97)
|
|
1027
|
-
elif color_capability == '8':
|
|
1028
|
-
index = (hash(string) % 6) + 31
|
|
1029
|
-
r,g,b = hashable_to_color(string)
|
|
1030
|
-
if color_capability == '256':
|
|
1031
|
-
index = rgb_to_xterm_index(r,g,b)
|
|
1032
|
-
if index:
|
|
1033
|
-
if index == __previous_ansi_color_index:
|
|
1034
|
-
return string_to_unique_ansi_color(hash(string))
|
|
1035
|
-
__previous_ansi_color_index = index
|
|
1036
|
-
if color_capability == '256':
|
|
1037
|
-
return f'\033[38;5;{index}m'
|
|
1038
|
-
else:
|
|
1039
|
-
return f'\033[{index}m'
|
|
1082
|
+
if color_capability == '24bit':
|
|
1083
|
+
r, g, b = int_to_color(number)
|
|
1040
1084
|
else:
|
|
1041
|
-
|
|
1085
|
+
# for 256 colors and below, reduce brightness threshold as we do not have many color to work with
|
|
1086
|
+
r, g, b = int_to_color(number, min_brightness=70, max_brightness=190)
|
|
1087
|
+
if sum(abs(a - b) for a, b in zip((r, g, b), __previous_color_rgb)) <= 256:
|
|
1088
|
+
r, g, b = int_to_color(hash(str(number)))
|
|
1089
|
+
__previous_color_rgb = (r, g, b)
|
|
1090
|
+
return rgb_to_ansi_color_string(r, g, b)
|
|
1042
1091
|
|
|
1043
1092
|
#%% ------------ Compacting Hostnames ----------------
|
|
1044
1093
|
def __tokenize_hostname(hostname):
|
|
@@ -1388,14 +1437,14 @@ def compact_hostnames(Hostnames,verify = True):
|
|
|
1388
1437
|
# hostSet = frozenset(Hostnames)
|
|
1389
1438
|
# else:
|
|
1390
1439
|
# hostSet = Hostnames
|
|
1391
|
-
hostSet = frozenset(
|
|
1440
|
+
hostSet = frozenset(expand_hostnames(
|
|
1392
1441
|
hostname.strip()
|
|
1393
1442
|
for hostnames_str in Hostnames
|
|
1394
1443
|
for hostname in hostnames_str.split(',')
|
|
1395
|
-
)
|
|
1444
|
+
))
|
|
1396
1445
|
compact_hosts = __compact_hostnames(hostSet)
|
|
1397
1446
|
if verify:
|
|
1398
|
-
if
|
|
1447
|
+
if frozenset(expand_hostnames(compact_hosts)) != hostSet:
|
|
1399
1448
|
if not __global_suppress_printout:
|
|
1400
1449
|
eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
|
|
1401
1450
|
compact_hosts = hostSet
|
|
@@ -2790,7 +2839,7 @@ def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_thresh
|
|
|
2790
2839
|
indexes = Counter({hostname: 0 for hostname in merging_hostnames})
|
|
2791
2840
|
working_index_keys = set(merging_hostnames)
|
|
2792
2841
|
previousBuddies = set()
|
|
2793
|
-
hostnameWrapper = textwrap.TextWrapper(width=line_length -
|
|
2842
|
+
hostnameWrapper = textwrap.TextWrapper(width=line_length -1, tabsize=4, replace_whitespace=False, drop_whitespace=False, break_on_hyphens=False,initial_indent='─ ', subsequent_indent='- ')
|
|
2794
2843
|
hostnameWrapper.wordsep_simple_re = re.compile(r'([,]+)')
|
|
2795
2844
|
diff_display_item_count = max(1,int(max(map(len, outputs_by_hostname.values())) * (1 - diff_display_threshold)))
|
|
2796
2845
|
def get_multiset_index_for_hostname(hostname):
|
|
@@ -2862,12 +2911,14 @@ def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_thresh
|
|
|
2862
2911
|
if buddy != previousBuddies:
|
|
2863
2912
|
hostnameStr = ','.join(compact_hostnames(buddy))
|
|
2864
2913
|
hostnameLines = hostnameWrapper.wrap(hostnameStr)
|
|
2865
|
-
hostnameLines = [line.ljust(line_length
|
|
2866
|
-
color =
|
|
2867
|
-
|
|
2914
|
+
hostnameLines = [line.ljust(line_length) for line in hostnameLines]
|
|
2915
|
+
color = int_to_unique_ansi_color(hash(hostnameStr)) if len(buddy) < len(merging_hostnames) else ''
|
|
2916
|
+
if color:
|
|
2917
|
+
color = f"\033[0m{color}"
|
|
2918
|
+
hostnameLines[0] = f"{color}{hostnameLines[0]}"
|
|
2868
2919
|
output.extend(hostnameLines)
|
|
2869
2920
|
previousBuddies = buddy
|
|
2870
|
-
output.append(lineToAdd.ljust(line_length
|
|
2921
|
+
output.append(lineToAdd.ljust(line_length))
|
|
2871
2922
|
currentLines[lineToAdd].difference_update(buddy)
|
|
2872
2923
|
if not currentLines[lineToAdd]:
|
|
2873
2924
|
del currentLines[lineToAdd]
|
|
@@ -2891,17 +2942,24 @@ def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_thresh
|
|
|
2891
2942
|
|
|
2892
2943
|
def mergeOutputs(outputs_by_hostname, merge_groups, remaining_hostnames, diff_display_threshold, line_length):
|
|
2893
2944
|
output = []
|
|
2894
|
-
|
|
2895
|
-
|
|
2945
|
+
color_cap = get_terminal_color_capability()
|
|
2946
|
+
if color_cap == 'None':
|
|
2947
|
+
color_line = ''
|
|
2948
|
+
color_reset = ''
|
|
2949
|
+
else:
|
|
2950
|
+
color_line = rgb_to_ansi_color_string(*COLOR_PALETTE.get('white', __DEFAULT_COLOR_PALETTE['white']))
|
|
2951
|
+
color_reset = '\033[0m'
|
|
2952
|
+
output.append(color_line+'─'*(line_length)+color_reset)
|
|
2953
|
+
hostnameWrapper = textwrap.TextWrapper(width=line_length - 1, tabsize=4, replace_whitespace=False, drop_whitespace=False, break_on_hyphens=False,initial_indent='─ ', subsequent_indent='- ')
|
|
2896
2954
|
hostnameWrapper.wordsep_simple_re = re.compile(r'([,]+)')
|
|
2897
2955
|
for merging_hostnames in merge_groups:
|
|
2898
2956
|
mergeOutput(merging_hostnames, outputs_by_hostname, output, diff_display_threshold,line_length)
|
|
2899
|
-
output.append(
|
|
2957
|
+
output.append(color_line+'─'*(line_length)+color_reset)
|
|
2900
2958
|
for hostname in remaining_hostnames:
|
|
2901
2959
|
hostnameLines = hostnameWrapper.wrap(','.join(compact_hostnames([hostname])))
|
|
2902
|
-
output.extend(line.ljust(line_length
|
|
2903
|
-
output.extend(line.ljust(line_length
|
|
2904
|
-
output.append(
|
|
2960
|
+
output.extend(line.ljust(line_length ) for line in hostnameLines)
|
|
2961
|
+
output.extend(line.ljust(line_length ) for line in outputs_by_hostname[hostname])
|
|
2962
|
+
output.append(color_line+'─'*(line_length)+color_reset)
|
|
2905
2963
|
if output:
|
|
2906
2964
|
output.pop()
|
|
2907
2965
|
# if output and output[0] and output[0].startswith('├'):
|
|
@@ -2918,7 +2976,7 @@ def pre_merge_hosts(hosts):
|
|
|
2918
2976
|
# Create merged hosts
|
|
2919
2977
|
merged_hosts = []
|
|
2920
2978
|
for group in output_groups.values():
|
|
2921
|
-
group[0].name = ','.join(host.name for host in group)
|
|
2979
|
+
group[0].name = ','.join(compact_hostnames(host.name for host in group))
|
|
2922
2980
|
merged_hosts.append(group[0])
|
|
2923
2981
|
return merged_hosts
|
|
2924
2982
|
|
|
@@ -2926,25 +2984,40 @@ def get_host_raw_output(hosts, terminal_width):
|
|
|
2926
2984
|
outputs_by_hostname = {}
|
|
2927
2985
|
line_bag_by_hostname = {}
|
|
2928
2986
|
hostnames_by_line_bag_len = {}
|
|
2929
|
-
text_wrapper = textwrap.TextWrapper(width=terminal_width -
|
|
2930
|
-
initial_indent='
|
|
2987
|
+
text_wrapper = textwrap.TextWrapper(width=terminal_width - 1, tabsize=4, replace_whitespace=False, drop_whitespace=False,
|
|
2988
|
+
initial_indent=' ', subsequent_indent='-')
|
|
2931
2989
|
max_length = 20
|
|
2990
|
+
color_cap = get_terminal_color_capability()
|
|
2991
|
+
if color_cap == 'None':
|
|
2992
|
+
color_reset_str = ''
|
|
2993
|
+
blue_str = ''
|
|
2994
|
+
cyan_str = ''
|
|
2995
|
+
green_str = ''
|
|
2996
|
+
red_str = ''
|
|
2997
|
+
else:
|
|
2998
|
+
color_reset_str = '\033[0m'
|
|
2999
|
+
blue_str = rgb_to_ansi_color_string(*COLOR_PALETTE.get('blue', __DEFAULT_COLOR_PALETTE['blue']))
|
|
3000
|
+
cyan_str = rgb_to_ansi_color_string(*COLOR_PALETTE.get('bright_cyan', __DEFAULT_COLOR_PALETTE['bright_cyan']))
|
|
3001
|
+
green_str = rgb_to_ansi_color_string(*COLOR_PALETTE.get('bright_green', __DEFAULT_COLOR_PALETTE['bright_green']))
|
|
3002
|
+
red_str = rgb_to_ansi_color_string(*COLOR_PALETTE.get('bright_red', __DEFAULT_COLOR_PALETTE['bright_red']))
|
|
2932
3003
|
hosts = pre_merge_hosts(hosts)
|
|
2933
3004
|
for host in hosts:
|
|
2934
|
-
|
|
3005
|
+
max_length = max(max_length, len(max(host.name.split(','), key=len)) + 3)
|
|
3006
|
+
hostPrintOut = [f"{cyan_str}█{color_reset_str} EXECUTED COMMAND:"]
|
|
2935
3007
|
for line in host.command.splitlines():
|
|
2936
3008
|
hostPrintOut.extend(text_wrapper.wrap(line))
|
|
2937
3009
|
# hostPrintOut.extend(itertools.chain.from_iterable(text_wrapper.wrap(line) for line in host['command'].splitlines()))
|
|
2938
3010
|
lineBag = {(0,host.command)}
|
|
2939
3011
|
prevLine = host.command
|
|
2940
3012
|
if host.stdout:
|
|
2941
|
-
hostPrintOut.append('
|
|
3013
|
+
hostPrintOut.append(f'{blue_str}▓{color_reset_str} STDOUT:')
|
|
2942
3014
|
# for line in host.stdout:
|
|
2943
3015
|
# if len(line) < terminal_width - 2:
|
|
2944
|
-
# hostPrintOut.append(f"
|
|
3016
|
+
# hostPrintOut.append(f" {line}")
|
|
2945
3017
|
# else:
|
|
2946
3018
|
# hostPrintOut.extend(text_wrapper.wrap(line))
|
|
2947
|
-
hostPrintOut.extend(f"
|
|
3019
|
+
hostPrintOut.extend(f" {line}" for line in host.stdout)
|
|
3020
|
+
max_length = max(max_length, max(map(len, host.stdout)))
|
|
2948
3021
|
# hostPrintOut.extend(text_wrapper.wrap(line) for line in host.stdout)
|
|
2949
3022
|
lineBag.add((prevLine,1))
|
|
2950
3023
|
lineBag.add((1,host.stdout[0]))
|
|
@@ -2960,22 +3033,26 @@ def get_host_raw_output(hosts, terminal_width):
|
|
|
2960
3033
|
elif host.stderr[-1].strip().endswith('No route to host'):
|
|
2961
3034
|
host.stderr[-1] = 'Cannot find host!'
|
|
2962
3035
|
if host.stderr:
|
|
2963
|
-
hostPrintOut.append('
|
|
3036
|
+
hostPrintOut.append(f'{red_str}▒{color_reset_str} STDERR:')
|
|
2964
3037
|
# for line in host.stderr:
|
|
2965
3038
|
# if len(line) < terminal_width - 2:
|
|
2966
|
-
# hostPrintOut.append(f"
|
|
3039
|
+
# hostPrintOut.append(f" {line}")
|
|
2967
3040
|
# else:
|
|
2968
3041
|
# hostPrintOut.extend(text_wrapper.wrap(line))
|
|
2969
|
-
hostPrintOut.extend(f"
|
|
3042
|
+
hostPrintOut.extend(f" {line}" for line in host.stderr)
|
|
3043
|
+
max_length = max(max_length, max(map(len, host.stderr)))
|
|
2970
3044
|
lineBag.add((prevLine,2))
|
|
2971
3045
|
lineBag.add((2,host.stderr[0]))
|
|
2972
3046
|
lineBag.update(host.stderr)
|
|
2973
3047
|
if len(host.stderr) > 1:
|
|
2974
3048
|
lineBag.update(zip(host.stderr, host.stderr[1:]))
|
|
2975
3049
|
prevLine = host.stderr[-1]
|
|
2976
|
-
|
|
3050
|
+
if host.returncode != 0:
|
|
3051
|
+
codeColor = red_str
|
|
3052
|
+
else:
|
|
3053
|
+
codeColor = green_str
|
|
3054
|
+
hostPrintOut.append(f"{codeColor}░{color_reset_str} RETURN CODE: {host.returncode}")
|
|
2977
3055
|
lineBag.add((prevLine,f"{host.returncode}"))
|
|
2978
|
-
max_length = max(max_length, max(map(len, hostPrintOut)))
|
|
2979
3056
|
outputs_by_hostname[host.name] = hostPrintOut
|
|
2980
3057
|
line_bag_by_hostname[host.name] = lineBag
|
|
2981
3058
|
hostnames_by_line_bag_len.setdefault(len(lineBag), set()).add(host.name)
|
|
@@ -3016,6 +3093,7 @@ def form_merge_groups(hostnames_by_line_bag_len, sorted_hostnames_by_line_bag_le
|
|
|
3016
3093
|
return merge_groups, remaining_hostnames
|
|
3017
3094
|
|
|
3018
3095
|
def generate_output(hosts, usejson = False, greppable = False,quiet = False,encoding = _encoding,keyPressesIn = [[]]):
|
|
3096
|
+
color_cap = get_terminal_color_capability()
|
|
3019
3097
|
if quiet:
|
|
3020
3098
|
# remove hosts with returncode 0
|
|
3021
3099
|
hosts = [host for host in hosts if host.returncode != 0]
|
|
@@ -3023,7 +3101,10 @@ def generate_output(hosts, usejson = False, greppable = False,quiet = False,enco
|
|
|
3023
3101
|
if usejson:
|
|
3024
3102
|
return '{"Success": true}'
|
|
3025
3103
|
else:
|
|
3026
|
-
|
|
3104
|
+
if color_cap == 'None':
|
|
3105
|
+
return 'Success'
|
|
3106
|
+
else:
|
|
3107
|
+
return '\033[32mSuccess\033[0m'
|
|
3027
3108
|
if usejson:
|
|
3028
3109
|
# [print(dict(host)) for host in hosts]
|
|
3029
3110
|
#print(json.dumps([dict(host) for host in hosts],indent=4))
|
|
@@ -3058,23 +3139,32 @@ def generate_output(hosts, usejson = False, greppable = False,quiet = False,enco
|
|
|
3058
3139
|
except Exception:
|
|
3059
3140
|
eprint("Warning: diff_display_threshold should be a float between 0 and 1. Setting to default value of 0.9")
|
|
3060
3141
|
diff_display_threshold = 0.9
|
|
3142
|
+
|
|
3143
|
+
color_reset_str = '' if color_cap == 'None' else '\033[0m'
|
|
3144
|
+
white_str = '' if color_cap == 'None' else rgb_to_ansi_color_string(*COLOR_PALETTE.get('white', __DEFAULT_COLOR_PALETTE['white']))
|
|
3061
3145
|
terminal_length = get_terminal_size()[0]
|
|
3062
3146
|
outputs_by_hostname, line_bag_by_hostname, hostnames_by_line_bag_len, sorted_hostnames_by_line_bag_len_keys, line_length = get_host_raw_output(hosts,terminal_length)
|
|
3063
3147
|
merge_groups ,remaining_hostnames = form_merge_groups(hostnames_by_line_bag_len, sorted_hostnames_by_line_bag_len_keys, line_bag_by_hostname, diff_display_threshold)
|
|
3064
3148
|
outputs = mergeOutputs(outputs_by_hostname, merge_groups,remaining_hostnames, diff_display_threshold,line_length)
|
|
3065
3149
|
if keyPressesIn[-1]:
|
|
3066
3150
|
CMDsOut = [''.join(cmd).encode(encoding=encoding,errors='backslashreplace').decode(encoding=encoding,errors='backslashreplace').replace('\\n', '↵') for cmd in keyPressesIn if cmd]
|
|
3067
|
-
outputs.append("
|
|
3151
|
+
outputs.append(color_reset_str + "─ User Inputs:".ljust(line_length,'─'))
|
|
3068
3152
|
cmdOut = []
|
|
3069
3153
|
for line in CMDsOut:
|
|
3070
3154
|
cmdOut.extend(textwrap.wrap(line, width=line_length-1, tabsize=4, replace_whitespace=False, drop_whitespace=False,
|
|
3071
|
-
initial_indent='
|
|
3072
|
-
outputs.extend(cmd.ljust(line_length
|
|
3155
|
+
initial_indent=' ', subsequent_indent='-'))
|
|
3156
|
+
outputs.extend(cmd.ljust(line_length) for cmd in cmdOut)
|
|
3073
3157
|
keyPressesIn[-1].clear()
|
|
3074
3158
|
if not outputs:
|
|
3075
|
-
|
|
3159
|
+
if quiet:
|
|
3160
|
+
if color_cap == 'None':
|
|
3161
|
+
return 'Success'
|
|
3162
|
+
else:
|
|
3163
|
+
return '\033[32mSuccess\033[0m'
|
|
3164
|
+
else:
|
|
3165
|
+
rtnStr = ''
|
|
3076
3166
|
else:
|
|
3077
|
-
rtnStr = '\n'.join(outputs + [
|
|
3167
|
+
rtnStr = '\n'.join(outputs + [white_str + '─' * (line_length) + color_reset_str])
|
|
3078
3168
|
return rtnStr
|
|
3079
3169
|
|
|
3080
3170
|
def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
@@ -3120,7 +3210,15 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
3120
3210
|
if total_sleeped > 0.1:
|
|
3121
3211
|
break
|
|
3122
3212
|
if any([host.returncode is None for host in hosts]):
|
|
3123
|
-
|
|
3213
|
+
try:
|
|
3214
|
+
curses.wrapper(curses_print, hosts, threads, min_char_len = curses_min_char_len, min_line_len = curses_min_line_len, single_window = single_window)
|
|
3215
|
+
except Exception:
|
|
3216
|
+
try:
|
|
3217
|
+
curses.wrapper(curses_print, hosts, threads, min_char_len = curses_min_char_len, min_line_len = curses_min_line_len, single_window = single_window)
|
|
3218
|
+
except Exception as e:
|
|
3219
|
+
eprint(f"Curses print error: {e}")
|
|
3220
|
+
import traceback
|
|
3221
|
+
print(traceback.format_exc())
|
|
3124
3222
|
if not returnUnfinished:
|
|
3125
3223
|
# wait until all hosts have a return code
|
|
3126
3224
|
while any([host.returncode is None for host in hosts]):
|
|
@@ -3800,6 +3898,7 @@ def get_parser():
|
|
|
3800
3898
|
parser.add_argument('-e','--encoding', type=str, help=f'The encoding to use for the output. (default: {DEFAULT_ENCODING})', default=DEFAULT_ENCODING)
|
|
3801
3899
|
parser.add_argument('-dt','--diff_display_threshold', type=float, help=f'The threshold of lines to display the diff when files differ. {{0-1}} Set to 0 to always display the diff. Set to 1 to disable diff. (Only merge same) (default: {DEFAULT_DIFF_DISPLAY_THRESHOLD})', default=DEFAULT_DIFF_DISPLAY_THRESHOLD)
|
|
3802
3900
|
parser.add_argument('--force_truecolor', action='store_true', help=f'Force truecolor output even when not in a truecolor terminal. (default: {FORCE_TRUECOLOR})', default=FORCE_TRUECOLOR)
|
|
3901
|
+
parser.add_argument('--add_control_master_config', action='store_true', help='Add ControlMaster configuration to ~/.ssh/config to speed up multiple connections to the same host.')
|
|
3803
3902
|
parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} @ {COMMIT_DATE} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
|
|
3804
3903
|
return parser
|
|
3805
3904
|
|
|
@@ -3859,7 +3958,7 @@ def process_config_file(args):
|
|
|
3859
3958
|
else:
|
|
3860
3959
|
configFileToWriteTo = args.config_file
|
|
3861
3960
|
write_default_config(args,configFileToWriteTo)
|
|
3862
|
-
if not args.commands:
|
|
3961
|
+
if not args.commands and not args.file:
|
|
3863
3962
|
if configFileToWriteTo:
|
|
3864
3963
|
with open(configFileToWriteTo,'r') as f:
|
|
3865
3964
|
eprint(f"Config file content: \n{f.read()}")
|
|
@@ -3901,6 +4000,35 @@ def process_keys(args):
|
|
|
3901
4000
|
eprint(f"Warning: Identity file {args.key!r} not found. Passing to ssh anyway. Proceed with caution.")
|
|
3902
4001
|
return args
|
|
3903
4002
|
|
|
4003
|
+
def process_control_master_config(args):
|
|
4004
|
+
global __control_master_string
|
|
4005
|
+
if args.add_control_master_config:
|
|
4006
|
+
try:
|
|
4007
|
+
if not os.path.exists(os.path.expanduser('~/.ssh')):
|
|
4008
|
+
os.makedirs(os.path.expanduser('~/.ssh'),mode=0o700)
|
|
4009
|
+
ssh_config_file = os.path.expanduser('~/.ssh/config')
|
|
4010
|
+
if not os.path.exists(ssh_config_file):
|
|
4011
|
+
with open(ssh_config_file,'w') as f:
|
|
4012
|
+
f.write(__control_master_string)
|
|
4013
|
+
os.chmod(ssh_config_file,0o644)
|
|
4014
|
+
else:
|
|
4015
|
+
with open(ssh_config_file,'r') as f:
|
|
4016
|
+
ssh_config_content = f.readlines()
|
|
4017
|
+
if set(map(str.strip,ssh_config_content)).issuperset(set(map(str.strip,__control_master_string.splitlines()))):
|
|
4018
|
+
eprint("ControlMaster configuration already exists in ~/.ssh/config, skipping adding:")
|
|
4019
|
+
eprint(__control_master_string)
|
|
4020
|
+
else:
|
|
4021
|
+
with open(ssh_config_file,'a') as f:
|
|
4022
|
+
f.write('\n# Added by multiSSH3.py to speed up subsequent connections\n')
|
|
4023
|
+
f.write(__control_master_string)
|
|
4024
|
+
eprint("ControlMaster configuration added to ~/.ssh/config.")
|
|
4025
|
+
except Exception as e:
|
|
4026
|
+
eprint(f"Error adding ControlMaster configuration: {e}")
|
|
4027
|
+
import traceback
|
|
4028
|
+
traceback.print_exc()
|
|
4029
|
+
if not args.commands and not args.file:
|
|
4030
|
+
_exit_with_code(0, "Done configuring ControlMaster.")
|
|
4031
|
+
return args
|
|
3904
4032
|
|
|
3905
4033
|
def set_global_with_args(args):
|
|
3906
4034
|
global _emo
|
|
@@ -3937,6 +4065,7 @@ def main():
|
|
|
3937
4065
|
args = process_config_file(args)
|
|
3938
4066
|
args = process_commands(args)
|
|
3939
4067
|
args = process_keys(args)
|
|
4068
|
+
args = process_control_master_config(args)
|
|
3940
4069
|
set_global_with_args(args)
|
|
3941
4070
|
|
|
3942
4071
|
if args.use_script_timeout:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
multiSSH3.py,sha256=oIb2TT_bbUmMRKA-7GT75w7xdW5QpsibmUb-nHrjUtM,184779
|
|
2
|
+
multissh3-5.98.dist-info/METADATA,sha256=Oqw0T08ZrfV_eIBGvKqgK7sR5clxE1uOZamYOgVX4ws,18093
|
|
3
|
+
multissh3-5.98.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
4
|
+
multissh3-5.98.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
5
|
+
multissh3-5.98.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
6
|
+
multissh3-5.98.dist-info/RECORD,,
|
multissh3-5.96.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
multiSSH3.py,sha256=xmpPdker87cPs5mcocUo3MdI4SjMsCh4u3MnwJbcGo8,179830
|
|
2
|
-
multissh3-5.96.dist-info/METADATA,sha256=QuuF4JIB_Fv_WcFiYrChIFVt8eCioYYbFJLHri8CTVA,18093
|
|
3
|
-
multissh3-5.96.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
4
|
-
multissh3-5.96.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
5
|
-
multissh3-5.96.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
6
|
-
multissh3-5.96.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|