multiSSH3 5.95__py3-none-any.whl → 5.97__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 +61 -12
- {multissh3-5.95.dist-info → multissh3-5.97.dist-info}/METADATA +1 -1
- multissh3-5.97.dist-info/RECORD +6 -0
- multissh3-5.95.dist-info/RECORD +0 -6
- {multissh3-5.95.dist-info → multissh3-5.97.dist-info}/WHEEL +0 -0
- {multissh3-5.95.dist-info → multissh3-5.97.dist-info}/entry_points.txt +0 -0
- {multissh3-5.95.dist-info → multissh3-5.97.dist-info}/top_level.txt +0 -0
multiSSH3.py
CHANGED
|
@@ -84,7 +84,7 @@ 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.97'
|
|
88
88
|
VERSION = version
|
|
89
89
|
__version__ = version
|
|
90
90
|
COMMIT_DATE = '2025-10-21'
|
|
@@ -420,6 +420,11 @@ __thread_start_delay = 0
|
|
|
420
420
|
_encoding = DEFAULT_ENCODING
|
|
421
421
|
__returnZero = DEFAULT_RETURN_ZERO
|
|
422
422
|
__running_threads = set()
|
|
423
|
+
__control_master_string = '''Host *
|
|
424
|
+
ControlMaster auto
|
|
425
|
+
ControlPath /run/user/%i/ssh_sockets_%C
|
|
426
|
+
ControlPersist 3600
|
|
427
|
+
'''
|
|
423
428
|
if __resource_lib_available:
|
|
424
429
|
# Get the current limits
|
|
425
430
|
_, __system_nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
@@ -1388,14 +1393,14 @@ def compact_hostnames(Hostnames,verify = True):
|
|
|
1388
1393
|
# hostSet = frozenset(Hostnames)
|
|
1389
1394
|
# else:
|
|
1390
1395
|
# hostSet = Hostnames
|
|
1391
|
-
hostSet = frozenset(
|
|
1396
|
+
hostSet = frozenset(expand_hostnames(
|
|
1392
1397
|
hostname.strip()
|
|
1393
1398
|
for hostnames_str in Hostnames
|
|
1394
1399
|
for hostname in hostnames_str.split(',')
|
|
1395
|
-
)
|
|
1400
|
+
))
|
|
1396
1401
|
compact_hosts = __compact_hostnames(hostSet)
|
|
1397
1402
|
if verify:
|
|
1398
|
-
if
|
|
1403
|
+
if frozenset(expand_hostnames(compact_hosts)) != hostSet:
|
|
1399
1404
|
if not __global_suppress_printout:
|
|
1400
1405
|
eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
|
|
1401
1406
|
compact_hosts = hostSet
|
|
@@ -2892,12 +2897,13 @@ def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_thresh
|
|
|
2892
2897
|
def mergeOutputs(outputs_by_hostname, merge_groups, remaining_hostnames, diff_display_threshold, line_length):
|
|
2893
2898
|
output = []
|
|
2894
2899
|
output.append(('┌'+'─'*(line_length-2) + '┐'))
|
|
2900
|
+
hostnameWrapper = textwrap.TextWrapper(width=line_length - 1, tabsize=4, replace_whitespace=False, drop_whitespace=False, break_on_hyphens=False,initial_indent='├─ ', subsequent_indent='│- ')
|
|
2901
|
+
hostnameWrapper.wordsep_simple_re = re.compile(r'([,]+)')
|
|
2895
2902
|
for merging_hostnames in merge_groups:
|
|
2896
2903
|
mergeOutput(merging_hostnames, outputs_by_hostname, output, diff_display_threshold,line_length)
|
|
2897
2904
|
output.append('\033[0m├'+'─'*(line_length-2) + '┤')
|
|
2898
2905
|
for hostname in remaining_hostnames:
|
|
2899
|
-
hostnameLines =
|
|
2900
|
-
initial_indent='├─ ', subsequent_indent='│- ')
|
|
2906
|
+
hostnameLines = hostnameWrapper.wrap(','.join(compact_hostnames([hostname])))
|
|
2901
2907
|
output.extend(line.ljust(line_length - 1) + '│' for line in hostnameLines)
|
|
2902
2908
|
output.extend(line.ljust(line_length - 1) + '│' for line in outputs_by_hostname[hostname])
|
|
2903
2909
|
output.append('\033[0m├'+'─'*(line_length-2) + '┤')
|
|
@@ -2917,7 +2923,7 @@ def pre_merge_hosts(hosts):
|
|
|
2917
2923
|
# Create merged hosts
|
|
2918
2924
|
merged_hosts = []
|
|
2919
2925
|
for group in output_groups.values():
|
|
2920
|
-
group[0].name = ','.join(host.name for host in group)
|
|
2926
|
+
group[0].name = ','.join(compact_hostnames(host.name for host in group))
|
|
2921
2927
|
merged_hosts.append(group[0])
|
|
2922
2928
|
return merged_hosts
|
|
2923
2929
|
|
|
@@ -2930,6 +2936,7 @@ def get_host_raw_output(hosts, terminal_width):
|
|
|
2930
2936
|
max_length = 20
|
|
2931
2937
|
hosts = pre_merge_hosts(hosts)
|
|
2932
2938
|
for host in hosts:
|
|
2939
|
+
max_length = max(max_length, len(max(host.name.split(','), key=len)) + 3)
|
|
2933
2940
|
hostPrintOut = ["│█ EXECUTED COMMAND:"]
|
|
2934
2941
|
for line in host.command.splitlines():
|
|
2935
2942
|
hostPrintOut.extend(text_wrapper.wrap(line))
|
|
@@ -2948,7 +2955,7 @@ def get_host_raw_output(hosts, terminal_width):
|
|
|
2948
2955
|
lineBag.add((prevLine,1))
|
|
2949
2956
|
lineBag.add((1,host.stdout[0]))
|
|
2950
2957
|
if len(host.stdout) > 1:
|
|
2951
|
-
lineBag.update(
|
|
2958
|
+
lineBag.update(zip(host.stdout, host.stdout[1:]))
|
|
2952
2959
|
lineBag.update(host.stdout)
|
|
2953
2960
|
prevLine = host.stdout[-1]
|
|
2954
2961
|
if host.stderr:
|
|
@@ -2970,7 +2977,7 @@ def get_host_raw_output(hosts, terminal_width):
|
|
|
2970
2977
|
lineBag.add((2,host.stderr[0]))
|
|
2971
2978
|
lineBag.update(host.stderr)
|
|
2972
2979
|
if len(host.stderr) > 1:
|
|
2973
|
-
lineBag.update(
|
|
2980
|
+
lineBag.update(zip(host.stderr, host.stderr[1:]))
|
|
2974
2981
|
prevLine = host.stderr[-1]
|
|
2975
2982
|
hostPrintOut.append(f"│░ RETURN CODE: {host.returncode}")
|
|
2976
2983
|
lineBag.add((prevLine,f"{host.returncode}"))
|
|
@@ -3757,7 +3764,7 @@ def get_parser():
|
|
|
3757
3764
|
parser.add_argument('-ea','--extraargs',type=str,help=f'Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.Use "=" ! Ex. -ea="--delete" (default: {DEFAULT_EXTRA_ARGS})',default=DEFAULT_EXTRA_ARGS)
|
|
3758
3765
|
parser.add_argument("-11",'--oneonone', action='store_true', help=f"Run one corresponding command on each host. (default: {DEFAULT_ONE_ON_ONE})", default=DEFAULT_ONE_ON_ONE)
|
|
3759
3766
|
parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
|
|
3760
|
-
parser.add_argument('-s','-fs','--file_sync', action='
|
|
3767
|
+
parser.add_argument('-s','-fs','--file_sync',nargs='?', action='append', help=f'Operate in file sync mode, sync path in <COMMANDS> from this machine to <HOSTS>. Treat --file <FILE> and <COMMANDS> both as source and source and destination will be the same in this mode. Infer destination from source path. (default: {DEFAULT_FILE_SYNC})',const=True, default=[DEFAULT_FILE_SYNC])
|
|
3761
3768
|
parser.add_argument('-W','--scp', action='store_true', help=f'Use scp for copying files instead of rsync. Need to use this on windows. (default: {DEFAULT_SCP})', default=DEFAULT_SCP)
|
|
3762
3769
|
parser.add_argument('-G','-gm','--gather_mode', action='store_true', help='Gather files from the hosts instead of sending files to the hosts. Will send remote files specified in <FILE> to local path specified in <COMMANDS> (default: False)', default=False)
|
|
3763
3770
|
#parser.add_argument("-d",'-c',"--destination", type=str, help="The destination of the files. Same as specify with commands. Added for compatibility. Use #HOST# or #HOSTNAME# to replace the host name in the destination")
|
|
@@ -3799,6 +3806,7 @@ def get_parser():
|
|
|
3799
3806
|
parser.add_argument('-e','--encoding', type=str, help=f'The encoding to use for the output. (default: {DEFAULT_ENCODING})', default=DEFAULT_ENCODING)
|
|
3800
3807
|
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)
|
|
3801
3808
|
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)
|
|
3809
|
+
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.')
|
|
3802
3810
|
parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} @ {COMMIT_DATE} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
|
|
3803
3811
|
return parser
|
|
3804
3812
|
|
|
@@ -3828,7 +3836,18 @@ def process_args(args = None):
|
|
|
3828
3836
|
args.no_history = True
|
|
3829
3837
|
args.greppable = True
|
|
3830
3838
|
args.error_only = True
|
|
3831
|
-
|
|
3839
|
+
|
|
3840
|
+
if args.file_sync:
|
|
3841
|
+
for path in args.file_sync:
|
|
3842
|
+
if path and isinstance(path, str):
|
|
3843
|
+
if args.file:
|
|
3844
|
+
if path not in args.file:
|
|
3845
|
+
args.file.append(path)
|
|
3846
|
+
else:
|
|
3847
|
+
args.file = [path]
|
|
3848
|
+
args.file_sync = any(args.file_sync)
|
|
3849
|
+
else:
|
|
3850
|
+
args.file_sync = False
|
|
3832
3851
|
if args.unavailable_host_expiry <= 0:
|
|
3833
3852
|
eprint(f"Warning: The unavailable host expiry time {args.unavailable_host_expiry} is less than 0, setting it to 10 seconds.")
|
|
3834
3853
|
args.unavailable_host_expiry = 10
|
|
@@ -3847,7 +3866,7 @@ def process_config_file(args):
|
|
|
3847
3866
|
else:
|
|
3848
3867
|
configFileToWriteTo = args.config_file
|
|
3849
3868
|
write_default_config(args,configFileToWriteTo)
|
|
3850
|
-
if not args.commands:
|
|
3869
|
+
if not args.commands and not args.file:
|
|
3851
3870
|
if configFileToWriteTo:
|
|
3852
3871
|
with open(configFileToWriteTo,'r') as f:
|
|
3853
3872
|
eprint(f"Config file content: \n{f.read()}")
|
|
@@ -3889,6 +3908,35 @@ def process_keys(args):
|
|
|
3889
3908
|
eprint(f"Warning: Identity file {args.key!r} not found. Passing to ssh anyway. Proceed with caution.")
|
|
3890
3909
|
return args
|
|
3891
3910
|
|
|
3911
|
+
def process_control_master_config(args):
|
|
3912
|
+
global __control_master_string
|
|
3913
|
+
if args.add_control_master_config:
|
|
3914
|
+
try:
|
|
3915
|
+
if not os.path.exists(os.path.expanduser('~/.ssh')):
|
|
3916
|
+
os.makedirs(os.path.expanduser('~/.ssh'),mode=0o700)
|
|
3917
|
+
ssh_config_file = os.path.expanduser('~/.ssh/config')
|
|
3918
|
+
if not os.path.exists(ssh_config_file):
|
|
3919
|
+
with open(ssh_config_file,'w') as f:
|
|
3920
|
+
f.write(__control_master_string)
|
|
3921
|
+
os.chmod(ssh_config_file,0o644)
|
|
3922
|
+
else:
|
|
3923
|
+
with open(ssh_config_file,'r') as f:
|
|
3924
|
+
ssh_config_content = f.readlines()
|
|
3925
|
+
if set(map(str.strip,ssh_config_content)).issuperset(set(map(str.strip,__control_master_string.splitlines()))):
|
|
3926
|
+
eprint("ControlMaster configuration already exists in ~/.ssh/config, skipping adding:")
|
|
3927
|
+
eprint(__control_master_string)
|
|
3928
|
+
else:
|
|
3929
|
+
with open(ssh_config_file,'a') as f:
|
|
3930
|
+
f.write('\n# Added by multiSSH3.py to speed up subsequent connections\n')
|
|
3931
|
+
f.write(__control_master_string)
|
|
3932
|
+
eprint("ControlMaster configuration added to ~/.ssh/config.")
|
|
3933
|
+
except Exception as e:
|
|
3934
|
+
eprint(f"Error adding ControlMaster configuration: {e}")
|
|
3935
|
+
import traceback
|
|
3936
|
+
traceback.print_exc()
|
|
3937
|
+
if not args.commands and not args.file:
|
|
3938
|
+
_exit_with_code(0, "Done configuring ControlMaster.")
|
|
3939
|
+
return args
|
|
3892
3940
|
|
|
3893
3941
|
def set_global_with_args(args):
|
|
3894
3942
|
global _emo
|
|
@@ -3925,6 +3973,7 @@ def main():
|
|
|
3925
3973
|
args = process_config_file(args)
|
|
3926
3974
|
args = process_commands(args)
|
|
3927
3975
|
args = process_keys(args)
|
|
3976
|
+
args = process_control_master_config(args)
|
|
3928
3977
|
set_global_with_args(args)
|
|
3929
3978
|
|
|
3930
3979
|
if args.use_script_timeout:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
multiSSH3.py,sha256=AXZqfsQ3c0O0qIGUedwGChpLnkxPX_pDtDEtBj_L7Rk,181554
|
|
2
|
+
multissh3-5.97.dist-info/METADATA,sha256=26GBKHydWcT4nR5HO3St3ubhaH6zp2-_ajR6RvOX6rI,18093
|
|
3
|
+
multissh3-5.97.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
4
|
+
multissh3-5.97.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
5
|
+
multissh3-5.97.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
6
|
+
multissh3-5.97.dist-info/RECORD,,
|
multissh3-5.95.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
multiSSH3.py,sha256=PWEXxU31f8mCRPzHqrWyG7DJcKXn99jmyTCT5mfuZoQ,179383
|
|
2
|
-
multissh3-5.95.dist-info/METADATA,sha256=GfNp-SWNCY8KyBjmuiK7JWbJWURo0oZ5ltBRAYlC4dw,18093
|
|
3
|
-
multissh3-5.95.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
4
|
-
multissh3-5.95.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
5
|
-
multissh3-5.95.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
6
|
-
multissh3-5.95.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|