xref: /petsc/lib/petsc/bin/maint/petsclinter/petsclinter/queue_main.py (revision 4c7cc9c8e01791debde927bc0816c9a347055c8f)
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