1#!/usr/bin/env python3 2""" 3# Created: Tue Jun 21 09:44:08 2022 (-0400) 4# @author: Jacob Faibussowitsch 5""" 6from __future__ import annotations 7 8import copy 9import multiprocessing as mp 10import petsclinter as pl 11 12from .classes._diag import DiagnosticManager 13from .classes._path import Path 14from .classes._pool import WorkerPoolBase, ParallelPool 15from .classes._linter import Linter 16 17from .util._timeout import timeout 18from .util._utility import traceback_format_exception 19 20from ._error import BaseError 21from ._typing import * 22 23class MainLoopError(BaseError): 24 """ 25 Thrown by child processes when they encounter an error in the main loop 26 """ 27 def __init__(self, filename: str, *args, **kwargs) -> None: 28 super().__init__(*args, **kwargs) 29 self._main_loop_error_filename = filename 30 return 31 32@timeout(seconds=5) 33def __handle_error(error_prefix: str, filename: str, error_queue: ParallelPool.ErrorQueueType, file_queue: ParallelPool.CommandQueueType, base_e: ExceptionKind) -> None: 34 try: 35 # attempt to send the traceback back to parent 36 exception_trace = ''.join(traceback_format_exception(base_e)) 37 error_message = f'{error_prefix} {filename}\n{exception_trace}' 38 if not error_message.endswith('\n'): 39 error_message += '\n' 40 error_queue.put(error_message) 41 except Exception as send_e: 42 send_exception_trace = '' 43 try: 44 # if this fails then I guess we really are screwed 45 send_exception_trace = ''.join(traceback_format_exception(send_e)) 46 except Exception as send_e2: 47 send_exception_trace = str(send_e) + '\n\n' + str(send_e2) 48 error_queue.put(f'{error_prefix} {filename}\n{send_exception_trace}\n') 49 finally: 50 try: 51 # in case we had any work from the queue we need to release it but only after 52 # putting our exception on the queue 53 file_queue.task_done() 54 except ValueError: 55 # task_done() called more times than get(), means we threw before getting the 56 # filename 57 pass 58 return 59 60def __main_loop(cmd_queue: ParallelPool.CommandQueueType, return_queue: ParallelPool.ReturnQueueType, linter: Linter) -> None: 61 try: 62 while 1: 63 ret = cmd_queue.get() 64 assert isinstance(ret, ParallelPool.SendPacket) 65 if ret.type == WorkerPoolBase.QueueSignal.EXIT_QUEUE: 66 break 67 if ret.type == WorkerPoolBase.QueueSignal.FILE_PATH: 68 filename = ret.data 69 assert isinstance(filename, Path) 70 else: 71 raise ValueError(f'Don\'t know what to do with Queue signal: {ret.type} -> {ret.data}') 72 73 errors_left, errors_fixed, warnings, patches = linter.parse(filename).diagnostics() 74 return_queue.put( 75 ParallelPool.ReturnPacket( 76 patches=patches, 77 errors_left=errors_left, 78 errors_fixed=errors_fixed, 79 warnings=warnings 80 ) 81 ) 82 cmd_queue.task_done() 83 except Exception as exc: 84 raise MainLoopError(str(filename)) from exc 85 return 86 87class LockPrinter: 88 __slots__ = ('_verbose', '_print_prefix', '_lock') 89 90 _verbose: bool 91 _print_prefix: str 92 _lock: ParallelPool.LockType 93 94 def __init__(self, verbose: bool, print_prefix: str, lock: ParallelPool.LockType) -> None: 95 r"""Construct a `LockPrinter` 96 97 Parameters 98 ---------- 99 verbose : 100 whether to print at all 101 print_prefix : 102 the prefix string to prepend to all print output 103 lock : 104 the lock to acquire before printing 105 """ 106 self._verbose = verbose 107 self._print_prefix = print_prefix 108 self._lock = lock 109 return 110 111 def __call__(self, *args, flush: bool = True, **kwargs) -> None: 112 r"""Print stuff 113 114 Parameters 115 ---------- 116 args : optional 117 the positional stuff to print 118 flush : optional 119 whether to flush the stream after printing 120 kwargs : optional 121 additional keyword arguments to send to `print()` 122 123 Notes 124 ----- 125 If called empty (i.e. `args` and `kwargs`) this does nothing 126 """ 127 if self._verbose: 128 if args or kwargs: 129 with self._lock: 130 print(self._print_prefix, *args, flush=flush, **kwargs) 131 return 132 133def queue_main( 134 clang_lib: PathLike, 135 clang_compat_check: bool, 136 updated_check_function_map: dict[str, FunctionChecker], 137 updated_classid_map: dict[str, str], 138 updated_diagnostics_mngr: DiagnosticsManagerCls, 139 compiler_flags: list[str], 140 clang_options: CXTranslationUnit, 141 verbose: bool, 142 werror: bool, 143 error_queue: ParallelPool.ErrorQueueType, 144 return_queue: ParallelPool.ReturnQueueType, 145 file_queue: ParallelPool.CommandQueueType, 146 lock: ParallelPool.LockType 147) -> None: 148 """ 149 main function for worker processes in the queue, does pretty much the same thing the 150 main process would do in their place 151 """ 152 def update_globals() -> None: 153 from .checks import _register 154 155 _register.check_function_map = copy.deepcopy(updated_check_function_map) 156 _register.classid_map = copy.deepcopy(updated_classid_map) 157 DiagnosticManager.disabled = copy.deepcopy(updated_diagnostics_mngr.disabled) 158 return 159 160 # in case errors are thrown before setup is complete 161 error_prefix = '[UNKNOWN_CHILD]' 162 filename = 'QUEUE SETUP' 163 printbar = 15 * '=' 164 try: 165 # initialize the global variables 166 proc = mp.current_process().name 167 print_prefix = proc + ' --'[:len('[ROOT]') - len(proc)] 168 error_prefix = f'{print_prefix} Exception detected while processing' 169 170 update_globals() 171 # removing the type: ignore would require us to type-annotate sync_print in 172 # __init__.py. However, __init__.py does a version check so we cannot put stuff (like 173 # type annotations) that may require a higher version of python to even byte-compile. 174 pl.sync_print = LockPrinter(verbose, print_prefix, lock) # type: ignore[assignment] 175 pl.sync_print(printbar, 'Performing setup', printbar) 176 # initialize libclang, and create a linter instance 177 pl.util.initialize_libclang(clang_lib=clang_lib, compat_check=clang_compat_check) 178 linter = Linter(compiler_flags, clang_options=clang_options, verbose=verbose, werror=werror) 179 pl.sync_print(printbar, 'Entering queue ', printbar) 180 181 # main loop 182 __main_loop(file_queue, return_queue, linter) 183 except Exception as base_e: 184 try: 185 if isinstance(base_e, MainLoopError): 186 filename = base_e._main_loop_error_filename 187 except: 188 pass 189 try: 190 __handle_error(error_prefix, str(filename), error_queue, file_queue, base_e) 191 except: 192 pass 193 try: 194 pl.sync_print(printbar, 'Exiting queue ', printbar) 195 except: 196 pass 197 return 198