multiSSH3 5.75__tar.gz → 5.77__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.75
3
+ Version: 5.77
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.75
3
+ Version: 5.77
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
@@ -55,10 +55,10 @@ except AttributeError:
55
55
  # If neither is available, use a dummy decorator
56
56
  def cache_decorator(func):
57
57
  return func
58
- version = '5.75'
58
+ version = '5.77'
59
59
  VERSION = version
60
60
  __version__ = version
61
- COMMIT_DATE = '2025-06-17'
61
+ COMMIT_DATE = '2025-06-25'
62
62
 
63
63
  CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
64
64
  '~/multiSSH3.config.json',
@@ -78,6 +78,24 @@ def eprint(*args, **kwargs):
78
78
  print(f"Error: Cannot print to stderr: {e}")
79
79
  print(*args, **kwargs)
80
80
 
81
+ def _exit_with_code(code, message=None):
82
+ '''
83
+ Exit the program with a specific code and print a message
84
+
85
+ Args:
86
+ code (int): The exit code
87
+ message (str, optional): The message to print. Defaults to None.
88
+
89
+ Returns:
90
+ None
91
+ '''
92
+ global __returnZero
93
+ if message:
94
+ eprint('Exiting: '+ message)
95
+ if __returnZero:
96
+ code = 0
97
+ sys.exit(code)
98
+
81
99
  def signal_handler(sig, frame):
82
100
  '''
83
101
  Handle the Ctrl C signal
@@ -98,7 +116,7 @@ def signal_handler(sig, frame):
98
116
  # wait for 0.1 seconds to allow the threads to exit
99
117
  time.sleep(0.1)
100
118
  os.system(f'pkill -ef {os.path.basename(__file__)}')
101
- sys.exit(0)
119
+ _exit_with_code(1, 'Exiting immediately due to Ctrl C')
102
120
 
103
121
  # def input_with_timeout_and_countdown(timeout, prompt='Please enter your selection'):
104
122
  # """
@@ -303,6 +321,7 @@ DEFAULT_CURSES_MINIMUM_LINE_LEN = 1
303
321
  DEFAULT_SINGLE_WINDOW = False
304
322
  DEFAULT_ERROR_ONLY = False
305
323
  DEFAULT_NO_OUTPUT = False
324
+ DEFAULT_RETURN_ZERO = False
306
325
  DEFAULT_NO_ENV = False
307
326
  DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
308
327
  DEFAULT_NO_HISTORY = False
@@ -362,6 +381,7 @@ __curses_current_color_index = 10
362
381
  __max_connections_nofile_limit_supported = 0
363
382
  __thread_start_delay = 0
364
383
  _encoding = DEFAULT_ENCODING
384
+ __returnZero = DEFAULT_RETURN_ZERO
365
385
  if __resource_lib_available:
366
386
  # Get the current limits
367
387
  _, __system_nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
@@ -2525,6 +2545,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
2525
2545
  history_file = history_file, env_file = env_file,
2526
2546
  repeat = repeat,interval = interval,
2527
2547
  shortend = shortend)
2548
+ commands = [command.replace('"', '\\"') for command in commands]
2528
2549
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
2529
2550
  filePath = os.path.abspath(__file__)
2530
2551
  programName = filePath if filePath else 'mssh'
@@ -2761,7 +2782,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2761
2782
  else:
2762
2783
  eprint(f"Warning: ssh-copy-id not found in {_binPaths} , skipping copy id to the hosts")
2763
2784
  if not commands:
2764
- sys.exit(0)
2785
+ _exit_with_code(0, "Copy id finished, no commands to run")
2765
2786
  if files and not commands:
2766
2787
  # if files are specified but not target dir, we default to file sync mode
2767
2788
  file_sync = True
@@ -2778,8 +2799,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2778
2799
  except:
2779
2800
  pathSet.update(glob.glob(file,recursive=True))
2780
2801
  if not pathSet:
2781
- eprint(f'Warning: No source files at {files!r} are found after resolving globs!')
2782
- sys.exit(66)
2802
+ _exit_with_code(66, f'No source files at {files!r} are found after resolving globs!')
2783
2803
  else:
2784
2804
  pathSet = set(files)
2785
2805
  if file_sync:
@@ -2796,7 +2816,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2796
2816
  eprint("Error: the number of commands must be the same as the number of hosts")
2797
2817
  eprint(f"Number of commands: {len(commands)}")
2798
2818
  eprint(f"Number of hosts: {len(set(targetHostDic) - set(skipHostSet))}")
2799
- sys.exit(255)
2819
+ _exit_with_code(255, "Number of commands and hosts do not match")
2800
2820
  if not __global_suppress_printout:
2801
2821
  eprint('-'*80)
2802
2822
  eprint("Running in one on one mode")
@@ -2911,6 +2931,7 @@ def generate_default_config(args):
2911
2931
  'DEFAULT_SINGLE_WINDOW': args.single_window,
2912
2932
  'DEFAULT_ERROR_ONLY': args.error_only,
2913
2933
  'DEFAULT_NO_OUTPUT': args.no_output,
2934
+ 'DEFAULT_RETURN_ZERO': args.return_zero,
2914
2935
  'DEFAULT_NO_ENV': args.no_env,
2915
2936
  'DEFAULT_ENV_FILE': args.env_file,
2916
2937
  'DEFAULT_NO_HISTORY': args.no_history,
@@ -2945,8 +2966,7 @@ def write_default_config(args,CONFIG_FILE = None):
2945
2966
  elif inStr.lower().strip().startswith('o'):
2946
2967
  backup = False
2947
2968
  else:
2948
- eprint("Aborted")
2949
- sys.exit(1)
2969
+ _exit_with_code(0, "Aborted by user, no config file written")
2950
2970
  try:
2951
2971
  if backup and os.path.exists(CONFIG_FILE):
2952
2972
  os.rename(CONFIG_FILE,CONFIG_FILE+'.bak')
@@ -2955,8 +2975,7 @@ def write_default_config(args,CONFIG_FILE = None):
2955
2975
  eprint(f"Do you want to continue writing the new config file to {CONFIG_FILE!r}? (y/n)")
2956
2976
  inStr = input_with_timeout_and_countdown(10)
2957
2977
  if not inStr or not inStr.lower().strip().startswith('y'):
2958
- eprint("Aborted")
2959
- sys.exit(1)
2978
+ _exit_with_code(0, "Aborted by user, no config file written")
2960
2979
  try:
2961
2980
  with open(CONFIG_FILE,'w') as f:
2962
2981
  json.dump(__configs_from_file,f,indent=4)
@@ -2978,6 +2997,7 @@ def main():
2978
2997
  global __DEBUG_MODE
2979
2998
  global __configs_from_file
2980
2999
  global _encoding
3000
+ global __returnZero
2981
3001
  _emo = False
2982
3002
  # We handle the signal
2983
3003
  signal.signal(signal.SIGINT, signal_handler)
@@ -3010,6 +3030,7 @@ def main():
3010
3030
  parser.add_argument('-B','-sw','--single_window', action='store_true', help=f'Use a single window for all hosts. (default: {DEFAULT_SINGLE_WINDOW})', default=DEFAULT_SINGLE_WINDOW)
3011
3031
  parser.add_argument('-R','-eo','--error_only', action='store_true', help=f'Only print the error output. (default: {DEFAULT_ERROR_ONLY})', default=DEFAULT_ERROR_ONLY)
3012
3032
  parser.add_argument('-Q',"-no","--no_output", action='store_true', help=f"Do not print the output. (default: {DEFAULT_NO_OUTPUT})", default=DEFAULT_NO_OUTPUT)
3033
+ parser.add_argument('-Z','-rz','--return_zero', action='store_true', help=f"Return 0 even if there are errors. (default: {DEFAULT_RETURN_ZERO})", default=DEFAULT_RETURN_ZERO)
3013
3034
  parser.add_argument('-C','--no_env', action='store_true', help=f'Do not load the command line environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
3014
3035
  parser.add_argument("--env_file", type=str, help=f"The file to load the mssh file based environment variables from. ( Still work with --no_env ) (default: {DEFAULT_ENV_FILE})", default=DEFAULT_ENV_FILE)
3015
3036
  parser.add_argument("-m","--max_connections", type=int, help=f"Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
@@ -3055,7 +3076,10 @@ def main():
3055
3076
  args.no_history = True
3056
3077
  args.greppable = True
3057
3078
  args.error_only = True
3058
-
3079
+
3080
+ if args.return_zero:
3081
+ __returnZero = True
3082
+
3059
3083
  if args.generate_config_file or args.store_config_file:
3060
3084
  if args.store_config_file:
3061
3085
  configFileToWriteTo = args.store_config_file
@@ -3071,7 +3095,7 @@ def main():
3071
3095
  if configFileToWriteTo:
3072
3096
  with open(configFileToWriteTo,'r') as f:
3073
3097
  eprint(f"Config file content: \n{f.read()}")
3074
- sys.exit(0)
3098
+ _exit_with_code(0)
3075
3099
  if args.config_file:
3076
3100
  if os.path.exists(args.config_file):
3077
3101
  __configs_from_file.update(load_config_file(os.path.expanduser(args.config_file)))
@@ -3094,7 +3118,7 @@ def main():
3094
3118
  elif inStr.lower().strip().startswith('m'):
3095
3119
  eprint(f"\nRunning multiple commands: {', '.join(args.commands)!r} on all hosts")
3096
3120
  else:
3097
- sys.exit(0)
3121
+ _exit_with_code(0, "Aborted by user, no commands to run")
3098
3122
 
3099
3123
  if args.key or args.use_key:
3100
3124
  if not args.key:
@@ -3179,7 +3203,7 @@ def main():
3179
3203
  # os.system(f'pkill -ef {os.path.basename(__file__)}')
3180
3204
  # os._exit(mainReturnCode)
3181
3205
 
3182
- sys.exit(__mainReturnCode)
3206
+ _exit_with_code(__mainReturnCode)
3183
3207
 
3184
3208
  if __name__ == "__main__":
3185
3209
  main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes