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 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.95'
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 set(expand_hostnames(compact_hosts)) != set(expand_hostnames(hostSet)):
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 = textwrap.wrap(hostname, width=line_length-1, tabsize=4, replace_whitespace=False, drop_whitespace=False,
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(itertools.pairwise(host.stdout))
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(itertools.pairwise(host.stderr))
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='store_true', 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})', default=DEFAULT_FILE_SYNC)
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.95
3
+ Version: 5.97
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=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,,
@@ -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,,