multiSSH3 4.99__tar.gz → 5.4__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.1
2
2
  Name: multiSSH3
3
- Version: 4.99
3
+ Version: 5.4
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.1
2
2
  Name: multiSSH3
3
- Version: 4.99
3
+ Version: 5.4
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
@@ -18,6 +18,7 @@ import glob
18
18
  import shutil
19
19
  import getpass
20
20
  import uuid
21
+ import tempfile
21
22
 
22
23
  try:
23
24
  # Check if functiools.cache is available
@@ -30,7 +31,7 @@ except AttributeError:
30
31
  # If neither is available, use a dummy decorator
31
32
  def cache_decorator(func):
32
33
  return func
33
- version = '4.99'
34
+ version = '5.04'
34
35
  VERSION = version
35
36
 
36
37
  CONFIG_FILE = '/etc/multiSSH3.config.json'
@@ -94,7 +95,7 @@ __build_in_default_config = {
94
95
  'DEFAULT_JSON_MODE': False,
95
96
  'DEFAULT_PRINT_SUCCESS_HOSTS': False,
96
97
  'DEFAULT_GREPPABLE_MODE': False,
97
- 'DEFAULT_SKIP_UNREACHABLE': False,
98
+ 'DEFAULT_SKIP_UNREACHABLE': True,
98
99
  'DEFAULT_SKIP_HOSTS': '',
99
100
  'SSH_STRICT_HOST_KEY_CHECKING': False,
100
101
  'ERROR_MESSAGES_TO_IGNORE': [
@@ -179,7 +180,7 @@ __DEBUG_MODE = __configs_from_file.get('__DEBUG_MODE', __build_in_default_config
179
180
 
180
181
 
181
182
 
182
- __global_suppress_printout = True
183
+ __global_suppress_printout = False
183
184
 
184
185
  __mainReturnCode = 0
185
186
  __failedHosts = set()
@@ -979,7 +980,14 @@ def get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
979
980
  host.printedLines = 0
980
981
  return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
981
982
 
982
- def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window=DEFAULT_SINGLE_WINDOW):
983
+ # Error: integer division or modulo by zero, Reloading Configuration: min_char_len=40, min_line_len=1, single_window=False with window size (61, 186) and 1 hosts...
984
+ # Traceback (most recent call last):
985
+ # File "/usr/local/lib/python3.11/site-packages/multiSSH3.py", line 1030, in generate_display
986
+ # host_window_height = max_y // num_hosts_y
987
+ # ~~~~~~^^~~~~~~~~~~~~
988
+ # ZeroDivisionError: integer division or modulo by zero
989
+
990
+ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window=DEFAULT_SINGLE_WINDOW, config_reason= 'New Configuration'):
983
991
  try:
984
992
  org_dim = stdscr.getmaxyx()
985
993
  new_configured = True
@@ -995,9 +1003,9 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
995
1003
  min_line_len_local = max_y-1
996
1004
  # return True if the terminal is too small
997
1005
  if max_x < 2 or max_y < 2:
998
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1006
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal too small')
999
1007
  if min_char_len_local < 1 or min_line_len_local < 1:
1000
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1008
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Minimum character or line length too small')
1001
1009
  # We need to figure out how many hosts we can fit in the terminal
1002
1010
  # We will need at least 2 lines per host, one for its name, one for its output
1003
1011
  # Each line will be at least 61 characters long (60 for the output, 1 for the borders)
@@ -1005,14 +1013,14 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1005
1013
  max_num_hosts_y = max_y // (min_line_len_local + 1)
1006
1014
  max_num_hosts = max_num_hosts_x * max_num_hosts_y
1007
1015
  if max_num_hosts < 1:
1008
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1016
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal too small to display any hosts')
1009
1017
  hosts_to_display , host_stats = get_hosts_to_display(hosts, max_num_hosts)
1010
1018
  if len(hosts_to_display) == 0:
1011
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1019
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'No hosts to display')
1012
1020
  # Now we calculate the actual number of hosts we will display for x and y
1013
1021
  optimal_len_x = max(min_char_len_local, 80)
1014
- num_hosts_x = max(min(max_num_hosts_x, max_x // optimal_len_x),1)
1015
- num_hosts_y = len(hosts_to_display) // num_hosts_x
1022
+ num_hosts_x = min(max(min(max_num_hosts_x, max_x // optimal_len_x),1),len(hosts_to_display))
1023
+ num_hosts_y = len(hosts_to_display) // num_hosts_x + (len(hosts_to_display) % num_hosts_x > 0)
1016
1024
  while num_hosts_y > max_num_hosts_y:
1017
1025
  num_hosts_x += 1
1018
1026
  # round up for num_hosts_y
@@ -1024,12 +1032,12 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1024
1032
  num_hosts_x += 1
1025
1033
  num_hosts_y = len(hosts_to_display) // num_hosts_x + (len(hosts_to_display) % num_hosts_x > 0)
1026
1034
  break
1027
-
1035
+ num_hosts_y = max(num_hosts_y,1)
1028
1036
  # We calculate the size of each window
1029
1037
  host_window_height = max_y // num_hosts_y
1030
1038
  host_window_width = max_x // num_hosts_x
1031
1039
  if host_window_height < 1 or host_window_width < 1:
1032
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1040
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Host window too small')
1033
1041
 
1034
1042
  old_stat = ''
1035
1043
  old_bottom_stat = ''
@@ -1070,24 +1078,24 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1070
1078
  # with open('keylog.txt','a') as f:
1071
1079
  # f.write(str(key)+'\n')
1072
1080
  if key == 410: # 410 is the key code for resize
1073
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1081
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize requested')
1074
1082
  elif key == 95 and not __keyPressesIn[-1]: # 95 is the key code for _
1075
1083
  # if last line is empty, we will reconfigure the wh to be smaller
1076
1084
  if min_line_len != 1:
1077
- return (lineToDisplay,curserPosition , min_char_len , max(min_line_len -1,1), single_window)
1085
+ return (lineToDisplay,curserPosition , min_char_len , max(min_line_len -1,1), single_window, 'Decrease line length')
1078
1086
  elif key == 43 and not __keyPressesIn[-1]: # 43 is the key code for +
1079
1087
  # if last line is empty, we will reconfigure the wh to be larger
1080
- return (lineToDisplay,curserPosition , min_char_len , min_line_len +1, single_window)
1088
+ return (lineToDisplay,curserPosition , min_char_len , min_line_len +1, single_window, 'Increase line length')
1081
1089
  elif key == 123 and not __keyPressesIn[-1]: # 123 is the key code for {
1082
1090
  # if last line is empty, we will reconfigure the ww to be smaller
1083
1091
  if min_char_len != 1:
1084
- return (lineToDisplay,curserPosition , max(min_char_len -1,1), min_line_len, single_window)
1092
+ return (lineToDisplay,curserPosition , max(min_char_len -1,1), min_line_len, single_window, 'Decrease character length')
1085
1093
  elif key == 124 and not __keyPressesIn[-1]: # 124 is the key code for |
1086
1094
  # if last line is empty, we will toggle the single window mode
1087
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, not single_window)
1095
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, not single_window, 'Toggle single window mode')
1088
1096
  elif key == 125 and not __keyPressesIn[-1]: # 125 is the key code for }
1089
1097
  # if last line is empty, we will reconfigure the ww to be larger
1090
- return (lineToDisplay,curserPosition , min_char_len +1, min_line_len, single_window)
1098
+ return (lineToDisplay,curserPosition , min_char_len +1, min_line_len, single_window, 'Increase character length')
1091
1099
  # We handle positional keys
1092
1100
  # if the key is up arrow, we will move the line to display up
1093
1101
  elif key == 259: # 259 is the key code for up arrow
@@ -1146,7 +1154,7 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1146
1154
  curserPosition += 1
1147
1155
  # reconfigure when the terminal size changes
1148
1156
  if org_dim != stdscr.getmaxyx():
1149
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1157
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize detected')
1150
1158
  # We generate the aggregated stats if user did not input anything
1151
1159
  if not __keyPressesIn[lineToDisplay]:
1152
1160
  stats = '┍'+ f" Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']} ww: {min_char_len} wh:{min_line_len} "[:max_x - 2].center(max_x - 2, "━")
@@ -1211,11 +1219,12 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1211
1219
  # print(str(e).strip())
1212
1220
  # print(traceback.format_exc().strip())
1213
1221
  if org_dim != stdscr.getmaxyx():
1214
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1222
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize detected')
1215
1223
  new_configured = False
1216
1224
  last_refresh_time = time.perf_counter()
1217
1225
  except Exception as e:
1218
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1226
+ import traceback
1227
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, f'Error: {str(e)}',traceback.format_exc())
1219
1228
  return None
1220
1229
 
1221
1230
  def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
@@ -1254,7 +1263,12 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1254
1263
  curses.init_pair(18, curses.COLOR_BLACK, curses.COLOR_BLUE)
1255
1264
  curses.init_pair(19, curses.COLOR_BLACK, curses.COLOR_MAGENTA)
1256
1265
  curses.init_pair(20, curses.COLOR_BLACK, curses.COLOR_CYAN)
1257
- params = (-1,0 , min_char_len, min_line_len, single_window)
1266
+
1267
+ # do not generate display if the output window have a size of zero
1268
+ if stdscr.getmaxyx()[0] < 2 or stdscr.getmaxyx()[1] < 2:
1269
+ return
1270
+
1271
+ params = (-1,0 , min_char_len, min_line_len, single_window,'new config')
1258
1272
  while params:
1259
1273
  params = generate_display(stdscr, hosts, *params)
1260
1274
  if not params:
@@ -1264,8 +1278,19 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1264
1278
  break
1265
1279
  # print the current configuration
1266
1280
  stdscr.clear()
1267
- stdscr.addstr(0, 0, f"Loading Configuration: min_char_len={params[2]}, min_line_len={params[3]}, single_window={params[4]}")
1268
- stdscr.refresh()
1281
+ try:
1282
+ stdscr.addstr(0, 0, f"{params[5]}, Reloading Configuration: min_char_len={params[2]}, min_line_len={params[3]}, single_window={params[4]} with window size {stdscr.getmaxyx()} and {len(hosts)} hosts...")
1283
+ if len(params) > 6:
1284
+ # traceback is available, print it
1285
+ i = 1
1286
+ for line in params[6].split('\n'):
1287
+ stdscr.addstr(i, 0, line)
1288
+ i += 1
1289
+ stdscr.refresh()
1290
+ except:
1291
+ pass
1292
+ params = params[:5]
1293
+ time.sleep(0.01)
1269
1294
  #time.sleep(0.25)
1270
1295
 
1271
1296
 
@@ -1302,9 +1327,10 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1302
1327
  outputs[hostPrintOut] = [host['name']]
1303
1328
  else:
1304
1329
  outputs[hostPrintOut].append(host['name'])
1305
- rtnStr = ''
1330
+ rtnStr = '*'*80+'\n'
1306
1331
  for output, hosts in outputs.items():
1307
1332
  rtnStr += f"{','.join(hosts)}{output}\n"
1333
+ rtnStr += '*'*80+'\n'
1308
1334
  if __keyPressesIn[-1]:
1309
1335
  CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
1310
1336
  rtnStr += 'User Inputs: '+ '\nUser Inputs: '.join(CMDsOut)
@@ -1416,13 +1442,45 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1416
1442
  thread.join(timeout=3)
1417
1443
  # update the unavailable hosts and global unavailable hosts
1418
1444
  if willUpdateUnreachableHosts:
1445
+ unavailableHosts = set(unavailableHosts)
1419
1446
  unavailableHosts.update([host.name for host in hosts if host.stderr and ('No route to host' in host.stderr[0].strip() or host.stderr[0].strip().startswith('Timeout!'))])
1447
+ # reachable hosts = all hosts - unreachable hosts
1448
+ reachableHosts = set([host.name for host in hosts]) - unavailableHosts
1420
1449
  if __DEBUG_MODE:
1421
1450
  print(f'Unreachable hosts: {unavailableHosts}')
1422
1451
  __globalUnavailableHosts.update(unavailableHosts)
1423
- # update the os environment variable if not _no_env
1424
- if not _no_env:
1425
- os.environ['__multiSSH3_UNAVAILABLE_HOSTS'] = ','.join(unavailableHosts)
1452
+
1453
+ # os.environ['__multiSSH3_UNAVAILABLE_HOSTS'] = ','.join(unavailableHosts)
1454
+ # create a temporary file to store the unavailable hosts
1455
+ try:
1456
+ # check for the old content, only update if the new content is different
1457
+ if not os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')):
1458
+ with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'w') as f:
1459
+ f.write(f',{int(time.time())}\n'.join(unavailableHosts) + f',{int(time.time())}\n')
1460
+ else:
1461
+ oldDic = {}
1462
+ try:
1463
+ with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
1464
+ for line in f:
1465
+ line = line.strip()
1466
+ if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
1467
+ oldDic[line.split(',')[0]] = int(line.split(',')[1])
1468
+ except:
1469
+ pass
1470
+ # remove entries that are either available now or older than min(timeout,3600) seconds
1471
+ for key in list(oldDic.keys()):
1472
+ if key in reachableHosts or time.time() - oldDic[key] > min(timeout,3600):
1473
+ del oldDic[key]
1474
+ # add new entries
1475
+ for host in unavailableHosts:
1476
+ oldDic[host] = int(time.time())
1477
+ with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),'w') as f:
1478
+ for key, value in oldDic.items():
1479
+ f.write(f'{key},{value}\n')
1480
+ os.replace(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'))
1481
+
1482
+ except Exception as e:
1483
+ eprint(f'Error writing to temporary file: {e}')
1426
1484
 
1427
1485
  # print the output, if the output of multiple hosts are the same, we aggragate them
1428
1486
  if not called:
@@ -1555,10 +1613,30 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1555
1613
  global __DEBUG_MODE
1556
1614
  _emo = False
1557
1615
  _no_env = no_env
1558
- if not no_env and '__multiSSH3_UNAVAILABLE_HOSTS' in os.environ:
1559
- __globalUnavailableHosts = set(os.environ['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
1616
+ if os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')):
1617
+ if timeout <= 0:
1618
+ checkTime = DEFAULT_TIMEOUT
1619
+ else:
1620
+ checkTime = timeout
1621
+ if checkTime <= 0:
1622
+ checkTime = 60
1623
+ elif checkTime > 3600:
1624
+ checkTime = 3600
1625
+ try:
1626
+ if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')) < checkTime:
1627
+ if not __global_suppress_printout:
1628
+ eprint(f"Reading unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')}")
1629
+ with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
1630
+ for line in f:
1631
+ line = line.strip()
1632
+ if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
1633
+ if int(line.split(',')[1]) > time.time() - checkTime:
1634
+ __globalUnavailableHosts.add(line.split(',')[0])
1635
+ except Exception as e:
1636
+ eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')}")
1637
+ eprint(str(e))
1560
1638
  elif '__multiSSH3_UNAVAILABLE_HOSTS' in readEnvFromFile():
1561
- __globalUnavailableHosts = set(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
1639
+ __globalUnavailableHosts.update(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
1562
1640
  if not max_connections:
1563
1641
  max_connections = 4 * os.cpu_count()
1564
1642
  elif max_connections == 0:
@@ -1842,7 +1920,7 @@ def main():
1842
1920
  parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
1843
1921
  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)
1844
1922
  parser.add_argument("-g","--greppable", action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
1845
- parser.add_argument("-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts while using --repeat. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
1923
+ parser.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)
1846
1924
  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)
1847
1925
  parser.add_argument('--store_config_file', action='store_true', help=f'Store / generate the default config file from command line argument and current config at {CONFIG_FILE}')
1848
1926
  parser.add_argument('--debug', action='store_true', help='Print debug information')
@@ -1940,15 +2018,16 @@ def main():
1940
2018
 
1941
2019
  __ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
1942
2020
 
1943
- if not args.greppable and not args.json and not args.no_output:
1944
- __global_suppress_printout = False
2021
+ if args.no_output:
2022
+ __global_suppress_printout = True
1945
2023
 
1946
2024
  if not __global_suppress_printout:
1947
- eprint('> ' + getStrCommand(args.hosts,args.commands,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
2025
+ cmdStr = getStrCommand(args.hosts,args.commands,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
1948
2026
  nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
1949
2027
  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,
1950
2028
  extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
1951
- 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))
2029
+ 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)
2030
+ eprint('> ' + cmdStr)
1952
2031
  if args.error_only:
1953
2032
  __global_suppress_printout = True
1954
2033
 
@@ -2,7 +2,7 @@ from setuptools import setup
2
2
 
3
3
  setup(
4
4
  name='multiSSH3',
5
- version='4.99',
5
+ version='5.04',
6
6
  description='Run commands on multiple hosts via SSH',
7
7
  long_description=open('README.md').read(),
8
8
  long_description_content_type='text/markdown',
File without changes
File without changes
File without changes