multiSSH3 5.0__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: 5.0
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: 5.0
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
@@ -31,7 +31,7 @@ except AttributeError:
31
31
  # If neither is available, use a dummy decorator
32
32
  def cache_decorator(func):
33
33
  return func
34
- version = '5.00'
34
+ version = '5.04'
35
35
  VERSION = version
36
36
 
37
37
  CONFIG_FILE = '/etc/multiSSH3.config.json'
@@ -980,7 +980,14 @@ def get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
980
980
  host.printedLines = 0
981
981
  return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
982
982
 
983
- 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'):
984
991
  try:
985
992
  org_dim = stdscr.getmaxyx()
986
993
  new_configured = True
@@ -996,9 +1003,9 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
996
1003
  min_line_len_local = max_y-1
997
1004
  # return True if the terminal is too small
998
1005
  if max_x < 2 or max_y < 2:
999
- 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')
1000
1007
  if min_char_len_local < 1 or min_line_len_local < 1:
1001
- 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')
1002
1009
  # We need to figure out how many hosts we can fit in the terminal
1003
1010
  # We will need at least 2 lines per host, one for its name, one for its output
1004
1011
  # Each line will be at least 61 characters long (60 for the output, 1 for the borders)
@@ -1006,14 +1013,14 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1006
1013
  max_num_hosts_y = max_y // (min_line_len_local + 1)
1007
1014
  max_num_hosts = max_num_hosts_x * max_num_hosts_y
1008
1015
  if max_num_hosts < 1:
1009
- 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')
1010
1017
  hosts_to_display , host_stats = get_hosts_to_display(hosts, max_num_hosts)
1011
1018
  if len(hosts_to_display) == 0:
1012
- 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')
1013
1020
  # Now we calculate the actual number of hosts we will display for x and y
1014
1021
  optimal_len_x = max(min_char_len_local, 80)
1015
- num_hosts_x = max(min(max_num_hosts_x, max_x // optimal_len_x),1)
1016
- 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)
1017
1024
  while num_hosts_y > max_num_hosts_y:
1018
1025
  num_hosts_x += 1
1019
1026
  # round up for num_hosts_y
@@ -1025,12 +1032,12 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1025
1032
  num_hosts_x += 1
1026
1033
  num_hosts_y = len(hosts_to_display) // num_hosts_x + (len(hosts_to_display) % num_hosts_x > 0)
1027
1034
  break
1028
-
1035
+ num_hosts_y = max(num_hosts_y,1)
1029
1036
  # We calculate the size of each window
1030
1037
  host_window_height = max_y // num_hosts_y
1031
1038
  host_window_width = max_x // num_hosts_x
1032
1039
  if host_window_height < 1 or host_window_width < 1:
1033
- 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')
1034
1041
 
1035
1042
  old_stat = ''
1036
1043
  old_bottom_stat = ''
@@ -1071,24 +1078,24 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1071
1078
  # with open('keylog.txt','a') as f:
1072
1079
  # f.write(str(key)+'\n')
1073
1080
  if key == 410: # 410 is the key code for resize
1074
- 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')
1075
1082
  elif key == 95 and not __keyPressesIn[-1]: # 95 is the key code for _
1076
1083
  # if last line is empty, we will reconfigure the wh to be smaller
1077
1084
  if min_line_len != 1:
1078
- 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')
1079
1086
  elif key == 43 and not __keyPressesIn[-1]: # 43 is the key code for +
1080
1087
  # if last line is empty, we will reconfigure the wh to be larger
1081
- 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')
1082
1089
  elif key == 123 and not __keyPressesIn[-1]: # 123 is the key code for {
1083
1090
  # if last line is empty, we will reconfigure the ww to be smaller
1084
1091
  if min_char_len != 1:
1085
- 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')
1086
1093
  elif key == 124 and not __keyPressesIn[-1]: # 124 is the key code for |
1087
1094
  # if last line is empty, we will toggle the single window mode
1088
- 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')
1089
1096
  elif key == 125 and not __keyPressesIn[-1]: # 125 is the key code for }
1090
1097
  # if last line is empty, we will reconfigure the ww to be larger
1091
- 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')
1092
1099
  # We handle positional keys
1093
1100
  # if the key is up arrow, we will move the line to display up
1094
1101
  elif key == 259: # 259 is the key code for up arrow
@@ -1147,7 +1154,7 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1147
1154
  curserPosition += 1
1148
1155
  # reconfigure when the terminal size changes
1149
1156
  if org_dim != stdscr.getmaxyx():
1150
- 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')
1151
1158
  # We generate the aggregated stats if user did not input anything
1152
1159
  if not __keyPressesIn[lineToDisplay]:
1153
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, "━")
@@ -1212,11 +1219,12 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1212
1219
  # print(str(e).strip())
1213
1220
  # print(traceback.format_exc().strip())
1214
1221
  if org_dim != stdscr.getmaxyx():
1215
- 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')
1216
1223
  new_configured = False
1217
1224
  last_refresh_time = time.perf_counter()
1218
1225
  except Exception as e:
1219
- 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())
1220
1228
  return None
1221
1229
 
1222
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):
@@ -1255,7 +1263,12 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1255
1263
  curses.init_pair(18, curses.COLOR_BLACK, curses.COLOR_BLUE)
1256
1264
  curses.init_pair(19, curses.COLOR_BLACK, curses.COLOR_MAGENTA)
1257
1265
  curses.init_pair(20, curses.COLOR_BLACK, curses.COLOR_CYAN)
1258
- 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')
1259
1272
  while params:
1260
1273
  params = generate_display(stdscr, hosts, *params)
1261
1274
  if not params:
@@ -1265,8 +1278,19 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1265
1278
  break
1266
1279
  # print the current configuration
1267
1280
  stdscr.clear()
1268
- stdscr.addstr(0, 0, f"Loading Configuration: min_char_len={params[2]}, min_line_len={params[3]}, single_window={params[4]}")
1269
- 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)
1270
1294
  #time.sleep(0.25)
1271
1295
 
1272
1296
 
@@ -1418,7 +1442,10 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1418
1442
  thread.join(timeout=3)
1419
1443
  # update the unavailable hosts and global unavailable hosts
1420
1444
  if willUpdateUnreachableHosts:
1445
+ unavailableHosts = set(unavailableHosts)
1421
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
1422
1449
  if __DEBUG_MODE:
1423
1450
  print(f'Unreachable hosts: {unavailableHosts}')
1424
1451
  __globalUnavailableHosts.update(unavailableHosts)
@@ -1427,18 +1454,31 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1427
1454
  # create a temporary file to store the unavailable hosts
1428
1455
  try:
1429
1456
  # check for the old content, only update if the new content is different
1430
- if not os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')):
1431
- with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS'),'w') as f:
1432
- f.write(','.join(unavailableHosts))
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')
1433
1460
  else:
1461
+ oldDic = {}
1434
1462
  try:
1435
- with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS'),'r') as f:
1436
- oldSet = set(f.read().strip().split(','))
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])
1437
1468
  except:
1438
- oldSet = None
1439
- if not oldSet or set(oldSet) != unavailableHosts:
1440
- with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS'),'w') as f:
1441
- f.write(','.join(unavailableHosts))
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
+
1442
1482
  except Exception as e:
1443
1483
  eprint(f'Error writing to temporary file: {e}')
1444
1484
 
@@ -1573,23 +1613,27 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1573
1613
  global __DEBUG_MODE
1574
1614
  _emo = False
1575
1615
  _no_env = no_env
1576
- if os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')):
1616
+ if os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')):
1577
1617
  if timeout <= 0:
1578
1618
  checkTime = DEFAULT_TIMEOUT
1579
1619
  else:
1580
1620
  checkTime = timeout
1581
1621
  if checkTime <= 0:
1582
1622
  checkTime = 60
1623
+ elif checkTime > 3600:
1624
+ checkTime = 3600
1583
1625
  try:
1584
- if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')) < checkTime:
1626
+ if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')) < checkTime:
1585
1627
  if not __global_suppress_printout:
1586
- eprint(f"Reading unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')}")
1587
- with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS'),'r') as f:
1588
- __globalUnavailableHosts.update(f.read().strip().split(','))
1589
- if __DEBUG_MODE:
1590
- eprint(f"Unavailable hosts: {__globalUnavailableHosts}")
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])
1591
1635
  except Exception as e:
1592
- eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')}")
1636
+ eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')}")
1593
1637
  eprint(str(e))
1594
1638
  elif '__multiSSH3_UNAVAILABLE_HOSTS' in readEnvFromFile():
1595
1639
  __globalUnavailableHosts.update(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
@@ -2,7 +2,7 @@ from setuptools import setup
2
2
 
3
3
  setup(
4
4
  name='multiSSH3',
5
- version='5.00',
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