multiSSH3 5.59__py3-none-any.whl → 5.62__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.
multiSSH3.py CHANGED
@@ -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.59'
57
+ version = '5.62'
58
58
  VERSION = version
59
59
  __version__ = version
60
- COMMIT_DATE = '2025-03-17'
60
+ COMMIT_DATE = '2025-04-17'
61
61
 
62
62
  CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
63
63
  '~/multiSSH3.config.json',
@@ -226,7 +226,7 @@ class Host:
226
226
  self.stdout = [] # the stdout of the command
227
227
  self.stderr = [] # the stderr of the command
228
228
  self.printedLines = -1 # the number of lines printed on the screen
229
- self.lineNumToReprintSet = set() # whether to reprint the last line
229
+ self.lineNumToReprintSet = set() # line numbers to reprint
230
230
  self.lastUpdateTime = time.monotonic() # the last time the output was updated
231
231
  self.files = files # the files to be copied to the host
232
232
  self.ipmi = ipmi # whether to use ipmi to connect to the host
@@ -291,6 +291,7 @@ DEFAULT_SCP = False
291
291
  DEFAULT_FILE_SYNC = False
292
292
  DEFAULT_TIMEOUT = 50
293
293
  DEFAULT_CLI_TIMEOUT = 0
294
+ DEFAULT_UNAVAILABLE_HOST_EXPIRY = 600
294
295
  DEFAULT_REPEAT = 1
295
296
  DEFAULT_INTERVAL = 0
296
297
  DEFAULT_IPMI = False
@@ -304,6 +305,8 @@ DEFAULT_ERROR_ONLY = False
304
305
  DEFAULT_NO_OUTPUT = False
305
306
  DEFAULT_NO_ENV = False
306
307
  DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
308
+ DEFAULT_NO_HISTORY = False
309
+ DEFAULT_HISTORY_FILE = '~/.mssh_history'
307
310
  DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
308
311
  DEFAULT_JSON_MODE = False
309
312
  DEFAULT_PRINT_SUCCESS_HOSTS = False
@@ -325,12 +328,6 @@ _DEFAULT_RETURN_UNFINISHED = False
325
328
  _DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
326
329
  _DEFAULT_NO_START = False
327
330
  _etc_hosts = {}
328
- _sshpassPath = None
329
- _sshPath = None
330
- _scpPath = None
331
- _ipmitoolPath = None
332
- _rsyncPath = None
333
- _shellPath = None
334
331
  __ERROR_MESSAGES_TO_IGNORE_REGEX =None
335
332
  __DEBUG_MODE = False
336
333
 
@@ -1136,20 +1133,37 @@ def __handle_reading_stream(stream,target, host):
1136
1133
  host.lastUpdateTime = time.monotonic()
1137
1134
  current_line = bytearray()
1138
1135
  lastLineCommited = True
1136
+ curser_position = 0
1137
+ previousUpdateTime = time.monotonic()
1139
1138
  for char in iter(lambda:stream.read(1), b''):
1140
1139
  if char == b'\n':
1141
- if (not lastLineCommited) and current_line:
1142
- add_line(current_line,target, host, keepLastLine=False)
1143
- elif lastLineCommited:
1144
- add_line(current_line,target, host, keepLastLine=True)
1140
+ add_line(current_line,target, host, keepLastLine=lastLineCommited)
1145
1141
  current_line = bytearray()
1146
1142
  lastLineCommited = True
1143
+ curser_position = 0
1144
+ previousUpdateTime = time.monotonic()
1145
+ continue
1147
1146
  elif char == b'\r':
1147
+ curser_position = 0
1148
+ elif char == b'\x08':
1149
+ # backspace
1150
+ if curser_position > 0:
1151
+ curser_position -= 1
1152
+ else:
1153
+ # over write the character if the curser is not at the end of the line
1154
+ if curser_position < len(current_line):
1155
+ current_line[curser_position] = char[0]
1156
+ elif curser_position == len(current_line):
1157
+ current_line.append(char[0])
1158
+ else:
1159
+ # curser is bigger than the length of the line
1160
+ current_line += b' '*(curser_position - len(current_line)) + char
1161
+ curser_position += 1
1162
+ if time.monotonic() - previousUpdateTime > 0.1:
1163
+ # if the time since the last update is more than 10ms, we update the output
1148
1164
  add_line(current_line,target, host, keepLastLine=lastLineCommited)
1149
- current_line = bytearray()
1150
1165
  lastLineCommited = False
1151
- else:
1152
- current_line.extend(char)
1166
+ previousUpdateTime = time.monotonic()
1153
1167
  if current_line:
1154
1168
  add_line(current_line,target, host, keepLastLine=lastLineCommited)
1155
1169
 
@@ -2108,7 +2122,9 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2108
2122
  if host.lineNumToReprintSet:
2109
2123
  # visible range is from host.printedLines - host_window_height + 1 to host.printedLines
2110
2124
  visibleLowerBound = host.printedLines - host_window_height + 1
2111
- for lineNumToReprint in host.lineNumToReprintSet:
2125
+ lineNumToReprintSet = host.lineNumToReprintSet
2126
+ host.lineNumToReprintSet = set()
2127
+ for lineNumToReprint in lineNumToReprintSet:
2112
2128
  # if the line is visible, we will reprint it
2113
2129
  if visibleLowerBound <= lineNumToReprint <= host.printedLines:
2114
2130
  if visibleLowerBound <= 0:
@@ -2121,7 +2137,6 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2121
2137
  # Thus we will not use any presistent color pair for old lines
2122
2138
  cpl = host.current_color_pair if lineNumToReprint == host.printedLines else [-1,-1,1]
2123
2139
  _curses_add_string_to_window(window=host_window, y=linePos + 1, line=host.output[lineNumToReprint], color_pair_list=cpl,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color)
2124
- host.lineNumToReprintSet = set()
2125
2140
  host_window.refresh()
2126
2141
  new_configured = False
2127
2142
  last_refresh_time = time.perf_counter()
@@ -2206,7 +2221,13 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
2206
2221
  def generate_output(hosts, usejson = False, greppable = False):
2207
2222
  global __keyPressesIn
2208
2223
  global __global_suppress_printout
2209
- hosts = [dict(host) for host in hosts]
2224
+ if __global_suppress_printout:
2225
+ # remove hosts with returncode 0
2226
+ hosts = [dict(host) for host in hosts if host.returncode != 0]
2227
+ if not hosts:
2228
+ return 'Success'
2229
+ else:
2230
+ hosts = [dict(host) for host in hosts]
2210
2231
  if usejson:
2211
2232
  # [print(dict(host)) for host in hosts]
2212
2233
  #print(json.dumps([dict(host) for host in hosts],indent=4))
@@ -2217,10 +2238,15 @@ def generate_output(hosts, usejson = False, greppable = False):
2217
2238
  rtnList = [['host_name','return_code','output_type','output']]
2218
2239
  for host in hosts:
2219
2240
  #header = f"{host['name']} | rc: {host['returncode']} | "
2241
+ hostAdded = False
2220
2242
  for line in host['stdout']:
2221
2243
  rtnList.append([host['name'],f"rc: {host['returncode']}",'stdout',line])
2244
+ hostAdded = True
2222
2245
  for line in host['stderr']:
2223
2246
  rtnList.append([host['name'],f"rc: {host['returncode']}",'stderr',line])
2247
+ hostAdded = True
2248
+ if not hostAdded:
2249
+ rtnList.append([host['name'],f"rc: {host['returncode']}",'N/A','<EMPTY>'])
2224
2250
  rtnList.append(['','','',''])
2225
2251
  rtnStr += pretty_format_table(rtnList)
2226
2252
  rtnStr += '*'*80+'\n'
@@ -2231,8 +2257,6 @@ def generate_output(hosts, usejson = False, greppable = False):
2231
2257
  else:
2232
2258
  outputs = {}
2233
2259
  for host in hosts:
2234
- if __global_suppress_printout and host['returncode'] == 0:
2235
- continue
2236
2260
  hostPrintOut = f" Command:\n {host['command']}\n"
2237
2261
  hostPrintOut += " stdout:\n "+'\n '.join(host['stdout'])
2238
2262
  if host['stderr']:
@@ -2248,11 +2272,11 @@ def generate_output(hosts, usejson = False, greppable = False):
2248
2272
  rtnStr = ''
2249
2273
  for output, hostSet in outputs.items():
2250
2274
  compact_hosts = sorted(compact_hostnames(hostSet))
2275
+ rtnStr += '*'*80+'\n'
2251
2276
  if __global_suppress_printout:
2252
2277
  rtnStr += f'Abnormal returncode produced by {",".join(compact_hosts)}:\n'
2253
2278
  rtnStr += output+'\n'
2254
2279
  else:
2255
- rtnStr += '*'*80+'\n'
2256
2280
  rtnStr += f'These hosts: "{",".join(compact_hosts)}" have a response of:\n'
2257
2281
  rtnStr += output+'\n'
2258
2282
  if not __global_suppress_printout or outputs:
@@ -2287,12 +2311,15 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
2287
2311
  return rtnStr
2288
2312
 
2289
2313
  #%% ------------ Run / Process Hosts Block ----------------
2290
- def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
2314
+ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, no_watch, json, called, greppable,
2315
+ unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN,
2316
+ curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW,
2317
+ unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY):
2291
2318
  global __globalUnavailableHosts
2292
2319
  global _no_env
2293
2320
  sleep_interval = 1.0e-7 # 0.1 microseconds
2294
2321
  threads = start_run_on_hosts(hosts, timeout=timeout,password=password,max_connections=max_connections)
2295
- if __curses_available and not nowatch and threads and not returnUnfinished and sys.stdout.isatty() and os.get_terminal_size() and os.get_terminal_size().columns > 10:
2322
+ if __curses_available and not no_watch and threads and not returnUnfinished and sys.stdout.isatty() and os.get_terminal_size() and os.get_terminal_size().columns > 10:
2296
2323
  total_sleeped = 0
2297
2324
  while any([host.returncode is None for host in hosts]):
2298
2325
  time.sleep(sleep_interval) # avoid busy-waiting
@@ -2340,13 +2367,13 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
2340
2367
  oldDic[line.split(',')[0]] = int(line.split(',')[1])
2341
2368
  except:
2342
2369
  pass
2343
- # remove entries that are either available now or older than min(timeout,3600) seconds
2344
2370
  for key in list(oldDic.keys()):
2345
- if key in reachableHosts or time.monotonic() < oldDic[key] or time.monotonic() - oldDic[key] > min(timeout,3600):
2371
+ if key in reachableHosts or time.monotonic() < oldDic[key] or time.monotonic() - oldDic[key] > unavailable_host_expiry:
2346
2372
  del oldDic[key]
2347
2373
  # add new entries
2348
2374
  for host in unavailableHosts:
2349
- oldDic[host] = int(time.monotonic ())
2375
+ if host not in oldDic:
2376
+ oldDic[host] = int(time.monotonic ())
2350
2377
  with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),'w') as f:
2351
2378
  for key, value in oldDic.items():
2352
2379
  f.write(f'{key},{value}\n')
@@ -2385,20 +2412,24 @@ def formHostStr(host) -> str:
2385
2412
 
2386
2413
  @cache_decorator
2387
2414
  def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
2388
- nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
2415
+ no_watch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
2389
2416
  files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,
2390
2417
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
2391
2418
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
2392
2419
  file_sync = False, error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
2393
- copy_id = False,
2420
+ copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY, no_history = DEFAULT_NO_HISTORY,
2421
+ history_file = DEFAULT_HISTORY_FILE, env_file = DEFAULT_ENV_FILE,
2422
+ repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
2394
2423
  shortend = False) -> str:
2395
2424
  argsList = []
2396
2425
  if oneonone: argsList.append('--oneonone' if not shortend else '-11')
2397
2426
  if timeout and timeout != DEFAULT_TIMEOUT: argsList.append(f'--timeout={timeout}' if not shortend else f'-t={timeout}')
2427
+ if repeat and repeat != DEFAULT_REPEAT: argsList.append(f'--repeat={repeat}' if not shortend else f'-r={repeat}')
2428
+ if interval and interval != DEFAULT_INTERVAL: argsList.append(f'--interval={interval}' if not shortend else f'-i={interval}')
2398
2429
  if password and password != DEFAULT_PASSWORD: argsList.append(f'--password="{password}"' if not shortend else f'-p="{password}"')
2399
2430
  if identity_file and identity_file != DEFAULT_IDENTITY_FILE: argsList.append(f'--key="{identity_file}"' if not shortend else f'-k="{identity_file}"')
2400
2431
  if copy_id: argsList.append('--copy_id' if not shortend else '-ci')
2401
- if nowatch: argsList.append('--nowatch' if not shortend else '-q')
2432
+ if no_watch: argsList.append('--no_watch' if not shortend else '-q')
2402
2433
  if json: argsList.append('--json' if not shortend else '-j')
2403
2434
  if max_connections and max_connections != DEFAULT_MAX_CONNECTIONS: argsList.append(f'--max_connections={max_connections}' if not shortend else f'-m={max_connections}')
2404
2435
  if files: argsList.extend([f'--file="{file}"' for file in files] if not shortend else [f'-f="{file}"' for file in files])
@@ -2409,7 +2440,11 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
2409
2440
  if username and username != DEFAULT_USERNAME: argsList.append(f'--username="{username}"' if not shortend else f'-u="{username}"')
2410
2441
  if extraargs and extraargs != DEFAULT_EXTRA_ARGS: argsList.append(f'--extraargs="{extraargs}"' if not shortend else f'-ea="{extraargs}"')
2411
2442
  if skipUnreachable: argsList.append('--skip_unreachable' if not shortend else '-su')
2443
+ if unavailable_host_expiry and unavailable_host_expiry != DEFAULT_UNAVAILABLE_HOST_EXPIRY: argsList.append(f'--unavailable_host_expiry={unavailable_host_expiry}' if not shortend else f'-uhe={unavailable_host_expiry}')
2412
2444
  if no_env: argsList.append('--no_env')
2445
+ if env_file and env_file != DEFAULT_ENV_FILE: argsList.append(f'--env_file="{env_file}"' if not shortend else f'-ef="{env_file}"')
2446
+ if no_history: argsList.append('--no_history' if not shortend else '-nh')
2447
+ if history_file and history_file != DEFAULT_HISTORY_FILE: argsList.append(f'--history_file="{history_file}"' if not shortend else f'-hf="{history_file}"')
2413
2448
  if greppable: argsList.append('--greppable' if not shortend else '-g')
2414
2449
  if error_only: argsList.append('--error_only' if not shortend else '-eo')
2415
2450
  if skip_hosts and skip_hosts != DEFAULT_SKIP_HOSTS: argsList.append(f'--skip_hosts="{skip_hosts}"' if not shortend else f'-sh="{skip_hosts}"')
@@ -2417,13 +2452,15 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
2417
2452
  return ' '.join(argsList)
2418
2453
 
2419
2454
  def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
2420
- nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
2455
+ no_watch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
2421
2456
  files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
2422
2457
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
2423
2458
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
2424
2459
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
2425
2460
  single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
2426
- copy_id = False,
2461
+ copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY,no_history = DEFAULT_NO_HISTORY,
2462
+ history_file = DEFAULT_HISTORY_FILE, env_file = DEFAULT_ENV_FILE,
2463
+ repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
2427
2464
  shortend = False):
2428
2465
  _ = called
2429
2466
  _ = returnUnfinished
@@ -2436,24 +2473,66 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
2436
2473
  hostStr = formHostStr(hosts)
2437
2474
  files = frozenset(files) if files else None
2438
2475
  argsStr = __formCommandArgStr(oneonone = oneonone, timeout = timeout,password = password,
2439
- nowatch = nowatch,json = json,max_connections=max_connections,
2440
- files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,scp=scp,gather_mode = gather_mode,
2441
- username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,no_env=no_env,
2442
- greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only, identity_file = identity_file,
2443
- copy_id = copy_id,
2476
+ no_watch = no_watch,json = json,max_connections=max_connections,
2477
+ files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,
2478
+ scp=scp,gather_mode = gather_mode,username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,
2479
+ no_env=no_env, greppable=greppable,skip_hosts = skip_hosts,
2480
+ file_sync = file_sync,error_only = error_only, identity_file = identity_file,
2481
+ copy_id = copy_id, unavailable_host_expiry =unavailable_host_expiry,no_history = no_history,
2482
+ history_file = history_file, env_file = env_file,
2483
+ repeat = repeat,interval = interval,
2444
2484
  shortend = shortend)
2445
2485
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
2446
- return f'multissh {argsStr} {hostStr} {commandStr}'
2486
+ programName = sys.argv[0] if (sys.argv and sys.argv[0]) else 'mssh'
2487
+ return f'{programName} {argsStr} {hostStr} {commandStr}'
2488
+
2489
+ #%% ------------ Record History Block ----------------
2490
+ def record_command_history(kwargs):
2491
+ '''
2492
+ Record the command history to a file
2493
+
2494
+ Args:
2495
+ args (str): The command arguments to record
2496
+
2497
+ Returns:
2498
+ None
2499
+ '''
2500
+ global __global_suppress_printout
2501
+ global __DEBUG_MODE
2502
+ try:
2503
+ history_file = os.path.expanduser(kwargs.get('history_file', DEFAULT_HISTORY_FILE))
2504
+ import inspect
2505
+ sig = inspect.signature(getStrCommand)
2506
+ wanted = {
2507
+ name: kwargs[name]
2508
+ for name in sig.parameters
2509
+ if name in kwargs
2510
+ }
2511
+ strCommand = getStrCommand(**wanted)
2512
+ with open(history_file, 'a') as f:
2513
+ # it follows <timestamp>\t<strCommand>\n
2514
+ f.write(f'{int(time.time())}\t{strCommand}\n')
2515
+ f.flush()
2516
+ os.fsync(f.fileno())
2517
+ if __DEBUG_MODE:
2518
+ eprint(f'Command history recorded to {history_file}')
2519
+ except Exception as e:
2520
+ eprint(f'Error recording command history: {e!r}')
2521
+ if __DEBUG_MODE:
2522
+ import traceback
2523
+ eprint(traceback.format_exc().strip())
2447
2524
 
2448
2525
  #%% ------------ Main Block ----------------
2449
2526
  def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
2450
- nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
2527
+ no_watch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
2451
2528
  files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
2452
2529
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
2453
2530
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
2454
2531
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
2455
2532
  single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False,identity_file = DEFAULT_IDENTITY_FILE,
2456
- copy_id = False):
2533
+ copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY,no_history = True,
2534
+ history_file = DEFAULT_HISTORY_FILE,
2535
+ ):
2457
2536
  f'''
2458
2537
  Run the command on the hosts, aka multissh. main function
2459
2538
 
@@ -2463,7 +2542,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2463
2542
  oneonone (bool, optional): Whether to run the commands one on one. Defaults to {DEFAULT_ONE_ON_ONE}.
2464
2543
  timeout (int, optional): The timeout for the command. Defaults to {DEFAULT_TIMEOUT}.
2465
2544
  password (str, optional): The password for the hosts. Defaults to {DEFAULT_PASSWORD}.
2466
- nowatch (bool, optional): Whether to print the output. Defaults to {DEFAULT_NO_WATCH}.
2545
+ no_watch (bool, optional): Whether to print the output. Defaults to {DEFAULT_NO_WATCH}.
2467
2546
  json (bool, optional): Whether to print the output in JSON format. Defaults to {DEFAULT_JSON_MODE}.
2468
2547
  called (bool, optional): Whether the function is called by another function. Defaults to {_DEFAULT_CALLED}.
2469
2548
  max_connections (int, optional): The maximum number of concurrent SSH sessions. Defaults to 4 * os.cpu_count().
@@ -2489,6 +2568,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2489
2568
  quiet (bool, optional): Whether to suppress all verbose printout, added for compatibility, avoid using. Defaults to False.
2490
2569
  identity_file (str, optional): The identity file to use for the ssh connection. Defaults to {DEFAULT_IDENTITY_FILE}.
2491
2570
  copy_id (bool, optional): Whether to copy the id to the hosts. Defaults to False.
2571
+ unavailable_host_expiry (int, optional): The time in seconds to keep the unavailable hosts in the global unavailable hosts. Defaults to {DEFAULT_UNAVAILABLE_HOST_EXPIRY}.
2572
+ no_history (bool, optional): Whether to not save the history of the command. Defaults to True.
2573
+ history_file (str, optional): The file to save the history of the command. Defaults to {DEFAULT_HISTORY_FILE}.
2492
2574
 
2493
2575
  Returns:
2494
2576
  list: A list of Host objects
@@ -2503,24 +2585,23 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2503
2585
  global __keyPressesIn
2504
2586
  _emo = False
2505
2587
  _no_env = no_env
2588
+ if not no_history:
2589
+ _ = history_file
2590
+ record_command_history(locals())
2591
+ if error_only:
2592
+ __global_suppress_printout = True
2506
2593
  if os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')):
2507
- if timeout <= 0:
2508
- checkTime = DEFAULT_TIMEOUT
2509
- else:
2510
- checkTime = timeout
2511
- if checkTime <= 0:
2512
- checkTime = 60
2513
- elif checkTime > 3600:
2514
- checkTime = 3600
2594
+ if unavailable_host_expiry <= 0:
2595
+ unavailable_host_expiry = 10
2515
2596
  try:
2516
2597
  readed = False
2517
- if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')) < checkTime:
2598
+ if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')) < unavailable_host_expiry:
2518
2599
 
2519
2600
  with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
2520
2601
  for line in f:
2521
2602
  line = line.strip()
2522
2603
  if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
2523
- if int(line.split(',')[1]) < time.monotonic() and int(line.split(',')[1]) + checkTime > time.monotonic():
2604
+ if int(line.split(',')[1]) < time.monotonic() and int(line.split(',')[1]) + unavailable_host_expiry > time.monotonic():
2524
2605
  __globalUnavailableHosts.add(line.split(',')[0])
2525
2606
  readed = True
2526
2607
  if readed and not __global_suppress_printout:
@@ -2623,7 +2704,11 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2623
2704
  eprint(f"> {command}")
2624
2705
  os.system(command)
2625
2706
  if hosts:
2626
- processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
2707
+ processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
2708
+ returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
2709
+ unavailableHosts=unavailableHosts,willUpdateUnreachableHosts=willUpdateUnreachableHosts,
2710
+ curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,
2711
+ single_window=single_window,unavailable_host_expiry=unavailable_host_expiry)
2627
2712
  else:
2628
2713
  eprint(f"Warning: ssh-copy-id not found in {_binPaths} , skipping copy id to the hosts")
2629
2714
  if not commands:
@@ -2678,7 +2763,12 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2678
2763
  if not __global_suppress_printout:
2679
2764
  eprint(f"Running command: {command!r} on host: {host!r}")
2680
2765
  if not __global_suppress_printout: eprint('-'*80)
2681
- if not no_start: processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
2766
+ if not no_start:
2767
+ processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
2768
+ returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
2769
+ unavailableHosts=unavailableHosts,willUpdateUnreachableHosts=willUpdateUnreachableHosts,
2770
+ curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,
2771
+ single_window=single_window,unavailable_host_expiry=unavailable_host_expiry)
2682
2772
  return hosts
2683
2773
  else:
2684
2774
  allHosts = []
@@ -2704,7 +2794,11 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2704
2794
  if no_start:
2705
2795
  eprint(f"Warning: no_start is set, the command will not be started. As we are in interactive mode, no action will be done.")
2706
2796
  else:
2707
- processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
2797
+ processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
2798
+ returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
2799
+ unavailableHosts=unavailableHosts,willUpdateUnreachableHosts=willUpdateUnreachableHosts,
2800
+ curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,
2801
+ single_window=single_window,unavailable_host_expiry=unavailable_host_expiry)
2708
2802
  return hosts
2709
2803
  for command in commands:
2710
2804
  hosts = []
@@ -2721,7 +2815,12 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2721
2815
  eprint('-'*80)
2722
2816
  eprint(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
2723
2817
  eprint('-'*80)
2724
- if not no_start: processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
2818
+ if not no_start:
2819
+ processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
2820
+ returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
2821
+ unavailableHosts=unavailableHosts,willUpdateUnreachableHosts=willUpdateUnreachableHosts,
2822
+ curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,
2823
+ single_window=single_window,unavailable_host_expiry=unavailable_host_expiry)
2725
2824
  allHosts += hosts
2726
2825
  return allHosts
2727
2826
 
@@ -2751,12 +2850,13 @@ def generate_default_config(args):
2751
2850
  'DEFAULT_FILE_SYNC': args.file_sync,
2752
2851
  'DEFAULT_TIMEOUT': DEFAULT_TIMEOUT,
2753
2852
  'DEFAULT_CLI_TIMEOUT': args.timeout,
2853
+ 'DEFAULT_UNAVAILABLE_HOST_EXPIRY': args.unavailable_host_expiry,
2754
2854
  'DEFAULT_REPEAT': args.repeat,
2755
2855
  'DEFAULT_INTERVAL': args.interval,
2756
2856
  'DEFAULT_IPMI': args.ipmi,
2757
2857
  'DEFAULT_IPMI_INTERFACE_IP_PREFIX': args.ipmi_interface_ip_prefix,
2758
2858
  'DEFAULT_INTERFACE_IP_PREFIX': args.interface_ip_prefix,
2759
- 'DEFAULT_NO_WATCH': args.nowatch,
2859
+ 'DEFAULT_NO_WATCH': args.no_watch,
2760
2860
  'DEFAULT_CURSES_MINIMUM_CHAR_LEN': args.window_width,
2761
2861
  'DEFAULT_CURSES_MINIMUM_LINE_LEN': args.window_height,
2762
2862
  'DEFAULT_SINGLE_WINDOW': args.single_window,
@@ -2764,6 +2864,8 @@ def generate_default_config(args):
2764
2864
  'DEFAULT_NO_OUTPUT': args.no_output,
2765
2865
  'DEFAULT_NO_ENV': args.no_env,
2766
2866
  'DEFAULT_ENV_FILE': args.env_file,
2867
+ 'DEFAULT_NO_HISTORY': args.no_history,
2868
+ 'DEFAULT_HISTORY_FILE': args.history_file,
2767
2869
  'DEFAULT_MAX_CONNECTIONS': args.max_connections if args.max_connections != 4 * os.cpu_count() else None,
2768
2870
  'DEFAULT_JSON_MODE': args.json,
2769
2871
  'DEFAULT_PRINT_SUCCESS_HOSTS': args.success_hosts,
@@ -2839,38 +2941,41 @@ def main():
2839
2941
  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)
2840
2942
  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)
2841
2943
  parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
2842
- parser.add_argument('-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 as source and destination will be the same in this mode. (default: {DEFAULT_FILE_SYNC})', default=DEFAULT_FILE_SYNC)
2843
- parser.add_argument('--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)
2844
- parser.add_argument('-gm','--gather_mode', action='store_true', help=f'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)
2944
+ 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)
2945
+ 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)
2946
+ parser.add_argument('-G','-gm','--gather_mode', action='store_true', help=f'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)
2845
2947
  #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")
2846
- parser.add_argument("-t","--timeout", type=int, help=f"Timeout for each command in seconds (default: {DEFAULT_CLI_TIMEOUT} (disabled))", default=DEFAULT_CLI_TIMEOUT)
2948
+ parser.add_argument("-t","--timeout", type=int, help=f"Timeout for each command in seconds. Set default value via DEFAULT_CLI_TIMEOUT in config file. Use 0 for disabling timeout. (default: {DEFAULT_CLI_TIMEOUT})", default=DEFAULT_CLI_TIMEOUT)
2949
+ parser.add_argument('-T','--use_script_timeout',action='store_true', help=f'Use shortened timeout suitable to use in a script. Set value via DEFAULT_TIMEOUT field in config file. (current: {DEFAULT_TIMEOUT})', default=False)
2847
2950
  parser.add_argument("-r","--repeat", type=int, help=f"Repeat the command for a number of times (default: {DEFAULT_REPEAT})", default=DEFAULT_REPEAT)
2848
2951
  parser.add_argument("-i","--interval", type=int, help=f"Interval between repeats in seconds (default: {DEFAULT_INTERVAL})", default=DEFAULT_INTERVAL)
2849
- parser.add_argument("--ipmi", action='store_true', help=f"Use ipmitool to run the command. (default: {DEFAULT_IPMI})", default=DEFAULT_IPMI)
2952
+ parser.add_argument('-M',"--ipmi", action='store_true', help=f"Use ipmitool to run the command. (default: {DEFAULT_IPMI})", default=DEFAULT_IPMI)
2850
2953
  parser.add_argument("-mpre","--ipmi_interface_ip_prefix", type=str, help=f"The prefix of the IPMI interfaces (default: {DEFAULT_IPMI_INTERFACE_IP_PREFIX})", default=DEFAULT_IPMI_INTERFACE_IP_PREFIX)
2851
2954
  parser.add_argument("-pre","--interface_ip_prefix", type=str, help=f"The prefix of the for the interfaces (default: {DEFAULT_INTERFACE_IP_PREFIX})", default=DEFAULT_INTERFACE_IP_PREFIX)
2852
- parser.add_argument("-q","-nw","--nowatch","--quiet", action='store_true', help=f"Quiet mode, no curses watch, only print the output. (default: {DEFAULT_NO_WATCH})", default=DEFAULT_NO_WATCH)
2955
+ parser.add_argument('-S',"-q","-nw","--no_watch","--quiet", action='store_true', help=f"Quiet mode, no curses watch, only print the output. (default: {DEFAULT_NO_WATCH})", default=DEFAULT_NO_WATCH)
2853
2956
  parser.add_argument("-ww",'--window_width', type=int, help=f"The minimum character length of the curses window. (default: {DEFAULT_CURSES_MINIMUM_CHAR_LEN})", default=DEFAULT_CURSES_MINIMUM_CHAR_LEN)
2854
2957
  parser.add_argument("-wh",'--window_height', type=int, help=f"The minimum line height of the curses window. (default: {DEFAULT_CURSES_MINIMUM_LINE_LEN})", default=DEFAULT_CURSES_MINIMUM_LINE_LEN)
2855
- parser.add_argument('-sw','--single_window', action='store_true', help=f'Use a single window for all hosts. (default: {DEFAULT_SINGLE_WINDOW})', default=DEFAULT_SINGLE_WINDOW)
2856
- parser.add_argument('-eo','--error_only', action='store_true', help=f'Only print the error output. (default: {DEFAULT_ERROR_ONLY})', default=DEFAULT_ERROR_ONLY)
2857
- parser.add_argument("-no","--no_output", action='store_true', help=f"Do not print the output. (default: {DEFAULT_NO_OUTPUT})", default=DEFAULT_NO_OUTPUT)
2858
- parser.add_argument('--no_env', action='store_true', help=f'Do not load the command line environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
2958
+ 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)
2959
+ 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)
2960
+ 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)
2961
+ 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)
2859
2962
  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)
2860
2963
  parser.add_argument("-m","--max_connections", type=int, help=f"Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
2861
2964
  parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
2862
- parser.add_argument("--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as wells. (default: {DEFAULT_PRINT_SUCCESS_HOSTS})", default=DEFAULT_PRINT_SUCCESS_HOSTS)
2863
- parser.add_argument("-g","--greppable",'--table', action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
2965
+ parser.add_argument('-w',"--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as well. (default: {DEFAULT_PRINT_SUCCESS_HOSTS})", default=DEFAULT_PRINT_SUCCESS_HOSTS)
2966
+ parser.add_argument('-P',"-g","--greppable",'--table', action='store_true', help=f"Output in greppable table. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
2864
2967
  group = parser.add_mutually_exclusive_group()
2865
- group.add_argument("-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
2866
- group.add_argument("-nsu","--no_skip_unreachable",dest = 'skip_unreachable', action='store_false', help=f"Do not skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {not DEFAULT_SKIP_UNREACHABLE})", default=not DEFAULT_SKIP_UNREACHABLE)
2867
-
2868
- parser.add_argument("-sh","--skip_hosts", type=str, help=f"Skip the hosts in the list. (default: {DEFAULT_SKIP_HOSTS if DEFAULT_SKIP_HOSTS else 'None'})", default=DEFAULT_SKIP_HOSTS)
2968
+ group.add_argument('-x',"-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
2969
+ group.add_argument('-a',"-nsu","--no_skip_unreachable",dest = 'skip_unreachable', action='store_false', help=f"Do not skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {not DEFAULT_SKIP_UNREACHABLE})", default=not DEFAULT_SKIP_UNREACHABLE)
2970
+ parser.add_argument('-uhe','--unavailable_host_expiry', type=int, help=f"Time in seconds to expire the unavailable hosts (default: {DEFAULT_UNAVAILABLE_HOST_EXPIRY})", default=DEFAULT_UNAVAILABLE_HOST_EXPIRY)
2971
+ parser.add_argument('-X',"-sh","--skip_hosts", type=str, help=f"Skip the hosts in the list. (default: {DEFAULT_SKIP_HOSTS if DEFAULT_SKIP_HOSTS else 'None'})", default=DEFAULT_SKIP_HOSTS)
2869
2972
  parser.add_argument('--generate_config_file', action='store_true', help=f'Store / generate the default config file from command line argument and current config at --config_file / stdout')
2870
2973
  parser.add_argument('--config_file', type=str,nargs='?', help=f'Additional config file to use, will pioritize over config chains. When using with store_config_file, will store the resulting config file at this location. Use without a path will use multiSSH3.config.json',const='multiSSH3.config.json',default=None)
2871
2974
  parser.add_argument('--store_config_file',type = str,nargs='?',help=f'Store the default config file from command line argument and current config. Same as --store_config_file --config_file=<path>',const='multiSSH3.config.json')
2872
2975
  parser.add_argument('--debug', action='store_true', help='Print debug information')
2873
2976
  parser.add_argument('-ci','--copy_id', action='store_true', help='Copy the ssh id to the hosts')
2977
+ parser.add_argument('-I','-nh','--no_history', action='store_true', help=f'Do not record the command to history. Default: {DEFAULT_NO_HISTORY}', default=DEFAULT_NO_HISTORY)
2978
+ parser.add_argument('-hf','--history_file', type=str, help=f'The file to store the history. (default: {DEFAULT_HISTORY_FILE})', default=DEFAULT_HISTORY_FILE)
2874
2979
  parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} @ {COMMIT_DATE} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
2875
2980
 
2876
2981
  # parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
@@ -2900,9 +3005,10 @@ def main():
2900
3005
  else:
2901
3006
  configFileToWriteTo = args.config_file
2902
3007
  write_default_config(args,configFileToWriteTo)
2903
- if not args.commands and configFileToWriteTo:
2904
- with open(configFileToWriteTo,'r') as f:
2905
- eprint(f"Config file content: \n{f.read()}")
3008
+ if not args.commands:
3009
+ if configFileToWriteTo:
3010
+ with open(configFileToWriteTo,'r') as f:
3011
+ eprint(f"Config file content: \n{f.read()}")
2906
3012
  sys.exit(0)
2907
3013
  if args.config_file:
2908
3014
  if os.path.exists(args.config_file):
@@ -2941,14 +3047,27 @@ def main():
2941
3047
 
2942
3048
  if args.no_output:
2943
3049
  __global_suppress_printout = True
3050
+
3051
+ if args.unavailable_host_expiry <= 0:
3052
+ eprint(f"Warning: The unavailable host expiry time {args.unavailable_host_expiry} is less than 0, setting it to 10 seconds.")
3053
+ args.unavailable_host_expiry = 10
3054
+
3055
+ if args.use_script_timeout:
3056
+ # set timeout to the default script timeout if timeout is not set
3057
+ if args.timeout == DEFAULT_CLI_TIMEOUT:
3058
+ args.timeout = DEFAULT_TIMEOUT
2944
3059
 
2945
3060
  if not __global_suppress_printout:
2946
- cmdStr = getStrCommand(args.hosts,args.commands,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
2947
- nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
3061
+ cmdStr = getStrCommand(args.hosts,args.commands,
3062
+ oneonone=args.oneonone,timeout=args.timeout,password=args.password,
3063
+ no_watch=args.no_watch,json=args.json,called=args.no_output,max_connections=args.max_connections,
2948
3064
  files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,gather_mode = args.gather_mode,username=args.username,
2949
3065
  extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
2950
3066
  curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key,
2951
- copy_id=args.copy_id)
3067
+ copy_id=args.copy_id,unavailable_host_expiry=args.unavailable_host_expiry,no_history=args.no_history,
3068
+ history_file = args.history_file,
3069
+ env_file = args.env_file,
3070
+ repeat = args.repeat,interval = args.interval)
2952
3071
  eprint('> ' + cmdStr)
2953
3072
  if args.error_only:
2954
3073
  __global_suppress_printout = True
@@ -2961,11 +3080,13 @@ def main():
2961
3080
  if not __global_suppress_printout: eprint(f"Running the {i+1}/{args.repeat} time") if args.repeat > 1 else None
2962
3081
  hosts = run_command_on_hosts(args.hosts,args.commands,
2963
3082
  oneonone=args.oneonone,timeout=args.timeout,password=args.password,
2964
- nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
3083
+ no_watch=args.no_watch,json=args.json,called=args.no_output,max_connections=args.max_connections,
2965
3084
  files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,gather_mode = args.gather_mode,username=args.username,
2966
3085
  extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
2967
3086
  curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key,
2968
- copy_id=args.copy_id)
3087
+ copy_id=args.copy_id,unavailable_host_expiry=args.unavailable_host_expiry,no_history=args.no_history,
3088
+ history_file = args.history_file,
3089
+ )
2969
3090
  #print('*'*80)
2970
3091
 
2971
3092
  #if not __global_suppress_printout: eprint('-'*80)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.59
3
+ Version: 5.62
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
@@ -108,7 +108,7 @@ An example .ssh/config:
108
108
  Host *
109
109
  StrictHostKeyChecking no
110
110
  ControlMaster auto
111
- ControlPath /tmp/%u_ssh_sockets_%r@%h-%p
111
+ ControlPath /run/user/%i/ssh_sockets_%C
112
112
  ControlPersist 3600
113
113
  ```
114
114
 
@@ -181,7 +181,7 @@ options:
181
181
  The prefix of the IPMI interfaces (default: )
182
182
  -pre INTERFACE_IP_PREFIX, --interface_ip_prefix INTERFACE_IP_PREFIX
183
183
  The prefix of the for the interfaces (default: None)
184
- -q, -nw, --nowatch, --quiet
184
+ -q, -nw, --no_watch, --quiet
185
185
  Quiet mode, no curses watch, only print the output. (default: False)
186
186
  -ww WINDOW_WIDTH, --window_width WINDOW_WIDTH
187
187
  The minimum character length of the curses window. (default: 40)
@@ -291,7 +291,7 @@ mssh [options] <hosts> <commands>
291
291
  | `-j` | `--json` | Output results in JSON format. |
292
292
  | | `--success_hosts` | Also display hosts where commands succeeded. |
293
293
  | `-g` | `--greppable` | Output results in a greppable format. |
294
- | `-nw` | `--nowatch` | Do not use curses mode; use simple output instead. |
294
+ | `-nw` | `--no_watch` | Do not use curses mode; use simple output instead. |
295
295
  | `-su` | `--skipunreachable` | Skip hosts that are unreachable. |
296
296
  | `-sh` | `--skiphosts` | Comma-separated list of hosts to skip. |
297
297
  | `-V` | `--version` | Display the script version and exit. |
@@ -0,0 +1,6 @@
1
+ multiSSH3.py,sha256=aIPftXdNHKopUv-DIOPcBxQd9IQhCAg4mNLgxefh3zM,145959
2
+ multissh3-5.62.dist-info/METADATA,sha256=TEWayZf__9rv11wQTJp8kSY7IULgqQHOPf9CJefLooI,18093
3
+ multissh3-5.62.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
4
+ multissh3-5.62.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
+ multissh3-5.62.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
+ multissh3-5.62.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.1.0)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +0,0 @@
1
- multiSSH3.py,sha256=j_9ukdE0WHkS8b0Ls94JRdM7cBJZqv0GUQ2mmgRF_dk,139237
2
- multissh3-5.59.dist-info/METADATA,sha256=6Ckopi4bXvlQQTuvkdoRN_7VtQsKInmOho0WSU6Tr24,18092
3
- multissh3-5.59.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
4
- multissh3-5.59.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
- multissh3-5.59.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
- multissh3-5.59.dist-info/RECORD,,