multiSSH3 5.77__tar.gz → 5.80__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.77
3
+ Version: 5.80
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.77
3
+ Version: 5.80
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.77'
58
+ version = '5.80'
59
59
  VERSION = version
60
60
  __version__ = version
61
- COMMIT_DATE = '2025-06-25'
61
+ COMMIT_DATE = '2025-07-09'
62
62
 
63
63
  CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
64
64
  '~/multiSSH3.config.json',
@@ -246,6 +246,7 @@ class Host:
246
246
  self.stderr = [] # the stderr of the command
247
247
  self.lineNumToPrintSet = set() # line numbers to reprint
248
248
  self.lastUpdateTime = time.monotonic() # the last time the output was updated
249
+ self.lastPrintedUpdateTime = 0 # the last time the output was printed
249
250
  self.files = files # the files to be copied to the host
250
251
  self.ipmi = ipmi # whether to use ipmi to connect to the host
251
252
  self.shell = shell # whether to use shell to run the command
@@ -260,6 +261,9 @@ class Host:
260
261
  self.identity_file = identity_file
261
262
  self.ip = ip if ip else getIP(name)
262
263
  self.current_color_pair = [-1, -1, 1]
264
+ self.output_buffer = io.BytesIO()
265
+ self.stdout_buffer = io.BytesIO()
266
+ self.stderr_buffer = io.BytesIO()
263
267
 
264
268
  def __iter__(self):
265
269
  return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
@@ -1133,7 +1137,7 @@ def expand_hostnames(hosts):
1133
1137
 
1134
1138
 
1135
1139
  #%% ------------ Run Command Block ----------------
1136
- def __handle_reading_stream(stream,target, host):
1140
+ def __handle_reading_stream(stream,target, host,buffer:io.BytesIO):
1137
1141
  '''
1138
1142
  Read the stream and append the lines to the target list
1139
1143
 
@@ -1146,50 +1150,63 @@ def __handle_reading_stream(stream,target, host):
1146
1150
  None
1147
1151
  '''
1148
1152
  global _encoding
1149
- def add_line(current_line,target, host, keepLastLine=True):
1150
- if not keepLastLine:
1151
- target.pop()
1152
- host.output.pop()
1153
- current_line_str = current_line.decode(_encoding,errors='backslashreplace')
1153
+ def add_line(buffer,target, host):
1154
+ current_line_str = buffer.getvalue().decode(_encoding,errors='backslashreplace')
1154
1155
  target.append(current_line_str)
1155
1156
  host.output.append(current_line_str)
1156
1157
  host.lineNumToPrintSet.add(len(host.output)-1)
1157
- host.lastUpdateTime = time.monotonic()
1158
- current_line = bytearray()
1159
- lastLineCommited = True
1160
- curser_position = 0
1161
- previousUpdateTime = time.monotonic()
1158
+ buffer.seek(0)
1159
+ buffer.truncate(0)
1160
+ host.output_buffer.seek(0)
1161
+ host.output_buffer.truncate(0)
1162
+
1162
1163
  for char in iter(lambda:stream.read(1), b''):
1164
+ host.lastUpdateTime = time.monotonic()
1163
1165
  if char == b'\n':
1164
- add_line(current_line,target, host, keepLastLine=lastLineCommited)
1165
- current_line = bytearray()
1166
- lastLineCommited = True
1167
- curser_position = 0
1168
- previousUpdateTime = time.monotonic()
1166
+ add_line(buffer,target, host)
1169
1167
  continue
1170
1168
  elif char == b'\r':
1171
- curser_position = 0
1169
+ buffer.seek(0)
1170
+ host.output_buffer.seek(0)
1172
1171
  elif char == b'\x08':
1173
1172
  # backspace
1174
- if curser_position > 0:
1175
- curser_position -= 1
1173
+ if buffer.tell() > 0:
1174
+ buffer.seek(buffer.tell() - 1)
1175
+ buffer.truncate()
1176
+ if host.output_buffer.tell() > 0:
1177
+ host.output_buffer.seek(host.output_buffer.tell() - 1)
1178
+ host.output_buffer.truncate()
1176
1179
  else:
1177
- # over write the character if the curser is not at the end of the line
1178
- if curser_position < len(current_line):
1179
- current_line[curser_position] = char[0]
1180
- elif curser_position == len(current_line):
1181
- current_line.append(char[0])
1182
- else:
1183
- # curser is bigger than the length of the line
1184
- current_line += b' '*(curser_position - len(current_line)) + char[0]
1185
- curser_position += 1
1186
- if time.monotonic() - previousUpdateTime > 0.1:
1187
- # if the time since the last update is more than 10ms, we update the output
1188
- add_line(current_line,target, host, keepLastLine=lastLineCommited)
1189
- lastLineCommited = False
1190
- previousUpdateTime = time.monotonic()
1191
- if current_line:
1192
- add_line(current_line,target, host, keepLastLine=lastLineCommited)
1180
+ # normal character
1181
+ buffer.write(char)
1182
+ host.output_buffer.write(char)
1183
+ # if the length of the buffer is greater than 100, we try to decode the buffer to find if there are any unicode line change chars
1184
+ if buffer.tell() % 100 == 0 and buffer.tell() > 0:
1185
+ try:
1186
+ # try to decode the buffer to find if there are any unicode line change chars
1187
+ decodedLine = buffer.getvalue().decode(_encoding,errors='backslashreplace')
1188
+ lines = decodedLine.splitlines()
1189
+ if len(lines) > 1:
1190
+ # if there are multiple lines, we add them to the target
1191
+ for line in lines[:-1]:
1192
+ # for all lines except the last one, we add them to the target
1193
+ target.append(line)
1194
+ host.output.append(line)
1195
+ host.lineNumToPrintSet.add(len(host.output)-1)
1196
+ # we keep the last line in the buffer
1197
+ buffer.seek(0)
1198
+ buffer.truncate(0)
1199
+ buffer.write(lines[-1].encode(_encoding,errors='backslashreplace'))
1200
+ host.output_buffer.seek(0)
1201
+ host.output_buffer.truncate(0)
1202
+ host.output_buffer.write(lines[-1].encode(_encoding,errors='backslashreplace'))
1203
+
1204
+ except UnicodeDecodeError:
1205
+ # if there is a unicode decode error, we just skip this character
1206
+ continue
1207
+ if buffer.tell() > 0:
1208
+ # if there is still some data in the buffer, we add it to the target
1209
+ add_line(buffer,target, host)
1193
1210
 
1194
1211
  def __handle_writing_stream(stream,stop_event,host):
1195
1212
  '''
@@ -1208,28 +1225,28 @@ def __handle_writing_stream(stream,stop_event,host):
1208
1225
  # __keyPressesIn is a list of lists.
1209
1226
  # Each list is a list of characters to be sent to the stdin of the process at once.
1210
1227
  # We do not send the last line as it may be incomplete.
1211
- sentInput = 0
1228
+ sentInputPos = 0
1212
1229
  while not stop_event.is_set():
1213
- if sentInput < len(__keyPressesIn) - 1 :
1214
- stream.write(''.join(__keyPressesIn[sentInput]).encode(encoding=_encoding,errors='backslashreplace'))
1230
+ if sentInputPos < len(__keyPressesIn) - 1 :
1231
+ stream.write(''.join(__keyPressesIn[sentInputPos]).encode(encoding=_encoding,errors='backslashreplace'))
1215
1232
  stream.flush()
1216
- line = '> ' + ''.join(__keyPressesIn[sentInput]).encode(encoding=_encoding,errors='backslashreplace').decode(encoding=_encoding,errors='backslashreplace').replace('\n', '↵')
1233
+ line = '> ' + ''.join(__keyPressesIn[sentInputPos]).encode(encoding=_encoding,errors='backslashreplace').decode(encoding=_encoding,errors='backslashreplace').replace('\n', '↵')
1217
1234
  host.output.append(line)
1218
1235
  host.stdout.append(line)
1219
1236
  host.lineNumToPrintSet.add(len(host.output)-1)
1220
- sentInput += 1
1237
+ sentInputPos += 1
1221
1238
  host.lastUpdateTime = time.monotonic()
1222
1239
  else:
1223
1240
  time.sleep(0.01) # sleep for 10ms
1224
- if sentInput < len(__keyPressesIn) - 1 :
1225
- eprint(f"Warning: {len(__keyPressesIn)-sentInput} lines of key presses are not sent before the process is terminated!")
1241
+ if sentInputPos < len(__keyPressesIn) - 1 :
1242
+ eprint(f"Warning: {len(__keyPressesIn)-sentInputPos} lines of key presses are not sent before the process is terminated!")
1226
1243
  # # send the last line
1227
1244
  # if __keyPressesIn and __keyPressesIn[-1]:
1228
1245
  # stream.write(''.join(__keyPressesIn[-1]).encode())
1229
1246
  # stream.flush()
1230
1247
  # host.output.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
1231
1248
  # host.stdout.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
1232
- return sentInput
1249
+ return sentInputPos
1233
1250
 
1234
1251
  def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
1235
1252
  '''
@@ -1382,7 +1399,7 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
1382
1399
  else:
1383
1400
  fileArgs = host.files + [f'{host.resolvedName}:{host.command}']
1384
1401
  if useScp:
1385
- formatedCMD = [_binPaths['scp'],'-rpB'] + localExtraArgs + extraargs +['--']+fileArgs
1402
+ formatedCMD = [_binPaths['scp'],'-rp'] + localExtraArgs + extraargs +['--']+fileArgs
1386
1403
  else:
1387
1404
  formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncLocalExtraArgs + extraargs +['--']+fileArgs
1388
1405
  else:
@@ -1415,11 +1432,11 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
1415
1432
  #host.stdout = []
1416
1433
  proc = subprocess.Popen(formatedCMD,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE)
1417
1434
  # create a thread to handle stdout
1418
- stdout_thread = threading.Thread(target=__handle_reading_stream, args=(proc.stdout,host.stdout, host), daemon=True)
1435
+ stdout_thread = threading.Thread(target=__handle_reading_stream, args=(proc.stdout,host.stdout, host,host.stdout_buffer), daemon=True)
1419
1436
  stdout_thread.start()
1420
1437
  # create a thread to handle stderr
1421
1438
  #host.stderr = []
1422
- stderr_thread = threading.Thread(target=__handle_reading_stream, args=(proc.stderr,host.stderr, host), daemon=True)
1439
+ stderr_thread = threading.Thread(target=__handle_reading_stream, args=(proc.stderr,host.stderr, host,host.stderr_buffer), daemon=True)
1423
1440
  stderr_thread.start()
1424
1441
  # create a thread to handle stdin
1425
1442
  stdin_stop_event = threading.Event()
@@ -1479,9 +1496,9 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
1479
1496
  except subprocess.TimeoutExpired:
1480
1497
  pass
1481
1498
  if stdout:
1482
- __handle_reading_stream(io.BytesIO(stdout),host.stdout, host)
1499
+ __handle_reading_stream(io.BytesIO(stdout),host.stdout, host,host.stdout_buffer)
1483
1500
  if stderr:
1484
- __handle_reading_stream(io.BytesIO(stderr),host.stderr, host)
1501
+ __handle_reading_stream(io.BytesIO(stderr),host.stderr, host,host.stderr_buffer)
1485
1502
  # if the last line in host.stderr is Connection to * closed., we will remove it
1486
1503
  host.returncode = proc.poll()
1487
1504
  if host.returncode is None:
@@ -2174,6 +2191,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2174
2191
  for i in range(host_window_height - 1):
2175
2192
  _curses_add_string_to_window(window=host_window, color_pair_list=[-1, -1, 1], y=i + 1,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color)
2176
2193
  host.lineNumToPrintSet.update(range(len(host.output)))
2194
+ host.lastPrintedUpdateTime = 0
2177
2195
  # for i in range(host.printedLines, len(host.output)):
2178
2196
  # _curses_add_string_to_window(window=host_window, y=i + 1, line=host.output[i], color_pair_list=host.current_color_pair,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color)
2179
2197
  # host.printedLines = len(host.output)
@@ -2193,7 +2211,12 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2193
2211
  # print(traceback.format_exc().strip())
2194
2212
  if org_dim != stdscr.getmaxyx():
2195
2213
  return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize detected')
2214
+ if host.lastPrintedUpdateTime != host.lastUpdateTime and host.output_buffer.tell() > 0:
2215
+ # this means there is still output in the buffer, we will print it
2216
+ # we will print the output in the window
2217
+ _curses_add_string_to_window(window=host_window, y=len(host.output) + 1, line=host.output_buffer.getvalue().decode(_encoding,errors='backslashreplace'), color_pair_list=host.current_color_pair,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color,fill_char='')
2196
2218
  host_window.noutrefresh()
2219
+ host.lastPrintedUpdateTime = host.lastUpdateTime
2197
2220
  hosts_to_display, host_stats,rearrangedHosts = _get_hosts_to_display(hosts, max_num_hosts,hosts_to_display, indexOffset)
2198
2221
  curses.doupdate()
2199
2222
  last_refresh_time = time.perf_counter()
@@ -2420,7 +2443,7 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
2420
2443
  # check for the old content, only update if the new content is different
2421
2444
  if not os.path.exists(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')):
2422
2445
  with open(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'),'w') as f:
2423
- f.writelines(f'{host},{expTime}' for host,expTime in unavailableHosts.values())
2446
+ f.writelines(f'{host},{expTime}' for host,expTime in unavailableHosts.items())
2424
2447
  else:
2425
2448
  oldDic = {}
2426
2449
  try:
@@ -2545,7 +2568,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
2545
2568
  history_file = history_file, env_file = env_file,
2546
2569
  repeat = repeat,interval = interval,
2547
2570
  shortend = shortend)
2548
- commands = [command.replace('"', '\\"') for command in commands]
2571
+ commands = [command.replace('"', '\\"').replace('\n', '\\n').replace('\t', '\\t') for command in commands]
2549
2572
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
2550
2573
  filePath = os.path.abspath(__file__)
2551
2574
  programName = filePath if filePath else 'mssh'
@@ -2985,23 +3008,9 @@ def write_default_config(args,CONFIG_FILE = None):
2985
3008
  eprint(f'Printing the config file to stdout:')
2986
3009
  print(json.dumps(__configs_from_file, indent=4))
2987
3010
 
2988
- #%% ------------ Wrapper Block ----------------
2989
- def main():
2990
- global _emo
2991
- global __global_suppress_printout
2992
- global __mainReturnCode
2993
- global __failedHosts
2994
- global __ipmiiInterfaceIPPrefix
3011
+ #%% ------------ Argument Processing -----------------
3012
+ def get_parser():
2995
3013
  global _binPaths
2996
- global _env_file
2997
- global __DEBUG_MODE
2998
- global __configs_from_file
2999
- global _encoding
3000
- global __returnZero
3001
- _emo = False
3002
- # We handle the signal
3003
- signal.signal(signal.SIGINT, signal_handler)
3004
- # We parse the arguments
3005
3014
  parser = argparse.ArgumentParser(description=f'Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command. Config file chain: {CONFIG_FILE_CHAIN!r}',
3006
3015
  epilog=f'Found bins: {list(_binPaths.values())}\n Missing bins: {_binCalled - set(_binPaths.keys())}')
3007
3016
  parser.add_argument('hosts', metavar='hosts', type=str, nargs='?', help=f'Hosts to run the command on, use "," to seperate hosts. (default: {DEFAULT_HOSTS})',default=DEFAULT_HOSTS)
@@ -3052,18 +3061,20 @@ def main():
3052
3061
  parser.add_argument('--script', action='store_true', help='Run the command in script mode, short for -SCRIPT or --no_watch --skip_unreachable --no_env --no_history --greppable --error_only')
3053
3062
  parser.add_argument('-e','--encoding', type=str, help=f'The encoding to use for the output. (default: {DEFAULT_ENCODING})', default=DEFAULT_ENCODING)
3054
3063
  parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} @ {COMMIT_DATE} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
3055
-
3056
- # parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
3057
- # help='the user to use to connect to the hosts')
3058
- #args = parser.parse_args()
3064
+ return parser
3059
3065
 
3066
+ def process_args(args = None):
3067
+ parser = get_parser()
3068
+ # We handle the signal
3069
+ signal.signal(signal.SIGINT, signal_handler)
3070
+ # We parse the arguments
3060
3071
  # if python version is 3.7 or higher, use parse_intermixed_args
3061
3072
  try:
3062
- args = parser.parse_intermixed_args()
3073
+ args = parser.parse_intermixed_args(args)
3063
3074
  except Exception :
3064
3075
  #eprint(f"Error while parsing arguments: {e!r}")
3065
3076
  # try to parse the arguments using parse_known_args
3066
- args, unknown = parser.parse_known_args()
3077
+ args, unknown = parser.parse_known_args(args)
3067
3078
  # if there are unknown arguments, we will try to parse them again using parse_args
3068
3079
  if unknown:
3069
3080
  eprint(f"Warning: Unknown arguments, treating all as commands: {unknown!r}")
@@ -3076,10 +3087,14 @@ def main():
3076
3087
  args.no_history = True
3077
3088
  args.greppable = True
3078
3089
  args.error_only = True
3079
-
3080
- if args.return_zero:
3081
- __returnZero = True
3082
3090
 
3091
+ if args.unavailable_host_expiry <= 0:
3092
+ eprint(f"Warning: The unavailable host expiry time {args.unavailable_host_expiry} is less than 0, setting it to 10 seconds.")
3093
+ args.unavailable_host_expiry = 10
3094
+ return args
3095
+
3096
+ def process_config_file(args):
3097
+ global __configs_from_file
3083
3098
  if args.generate_config_file or args.store_config_file:
3084
3099
  if args.store_config_file:
3085
3100
  configFileToWriteTo = args.store_config_file
@@ -3101,11 +3116,12 @@ def main():
3101
3116
  __configs_from_file.update(load_config_file(os.path.expanduser(args.config_file)))
3102
3117
  else:
3103
3118
  eprint(f"Warning: Config file {args.config_file!r} not found, ignoring it.")
3119
+ return args
3104
3120
 
3105
- _env_file = args.env_file
3106
- __DEBUG_MODE = args.debug
3107
3121
  # if there are more than 1 commands, and every command only consists of one word,
3108
3122
  # we will ask the user to confirm if they want to run multiple commands or just one command.
3123
+
3124
+ def process_commands(args):
3109
3125
  if not args.file and len(args.commands) > 1 and all([len(command.split()) == 1 for command in args.commands]):
3110
3126
  eprint(f"Multiple one word command detected, what to do? (1/m/n)")
3111
3127
  eprint(f"1: Run 1 command [{' '.join(args.commands)}] on all hosts ( default )")
@@ -3119,7 +3135,9 @@ def main():
3119
3135
  eprint(f"\nRunning multiple commands: {', '.join(args.commands)!r} on all hosts")
3120
3136
  else:
3121
3137
  _exit_with_code(0, "Aborted by user, no commands to run")
3138
+ return args
3122
3139
 
3140
+ def process_keys(args):
3123
3141
  if args.key or args.use_key:
3124
3142
  if not args.key:
3125
3143
  args.key = find_ssh_key_file()
@@ -3128,23 +3146,43 @@ def main():
3128
3146
  args.key = find_ssh_key_file(args.key)
3129
3147
  elif not os.path.exists(args.key):
3130
3148
  eprint(f"Warning: Identity file {args.key!r} not found. Passing to ssh anyway. Proceed with caution.")
3149
+ return args
3150
+
3131
3151
 
3152
+ def set_global_with_args(args):
3153
+ global _emo
3154
+ global __ipmiiInterfaceIPPrefix
3155
+ global _env_file
3156
+ global __DEBUG_MODE
3157
+ global __configs_from_file
3158
+ global _encoding
3159
+ global __returnZero
3160
+ _emo = False
3132
3161
  __ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
3162
+ _env_file = args.env_file
3163
+ __DEBUG_MODE = args.debug
3164
+ _encoding = args.encoding
3165
+ if args.return_zero:
3166
+ __returnZero = True
3133
3167
 
3134
- if args.no_output:
3135
- __global_suppress_printout = True
3136
-
3137
- if args.unavailable_host_expiry <= 0:
3138
- eprint(f"Warning: The unavailable host expiry time {args.unavailable_host_expiry} is less than 0, setting it to 10 seconds.")
3139
- args.unavailable_host_expiry = 10
3168
+ #%% ------------ Wrapper Block ----------------
3169
+ def main():
3170
+ global __global_suppress_printout
3171
+ global __mainReturnCode
3172
+ global __failedHosts
3173
+ args = process_args()
3174
+ args = process_config_file(args)
3175
+ args = process_commands(args)
3176
+ args = process_keys(args)
3177
+ set_global_with_args(args)
3140
3178
 
3141
3179
  if args.use_script_timeout:
3142
3180
  # set timeout to the default script timeout if timeout is not set
3143
3181
  if args.timeout == DEFAULT_CLI_TIMEOUT:
3144
3182
  args.timeout = DEFAULT_TIMEOUT
3145
3183
 
3146
- _encoding = args.encoding
3147
-
3184
+ if args.no_output:
3185
+ __global_suppress_printout = True
3148
3186
  if not __global_suppress_printout:
3149
3187
  cmdStr = getStrCommand(args.hosts,args.commands,
3150
3188
  oneonone=args.oneonone,timeout=args.timeout,password=args.password,
@@ -3176,9 +3214,7 @@ def main():
3176
3214
  history_file = args.history_file,
3177
3215
  )
3178
3216
  #print('*'*80)
3179
-
3180
3217
  #if not __global_suppress_printout: eprint('-'*80)
3181
-
3182
3218
  succeededHosts = set()
3183
3219
  for host in hosts:
3184
3220
  if host.returncode and host.returncode != 0:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes