quill
SignalHandler.h
1 
7 #pragma once
8 
9 #include "quill/backend/ThreadUtilities.h"
10 
11 #include "quill/Logger.h"
12 #include "quill/core/Attributes.h"
13 #include "quill/core/LogLevel.h"
14 #include "quill/core/LoggerBase.h"
15 #include "quill/core/LoggerManager.h"
16 #include "quill/core/MacroMetadata.h"
17 #include "quill/core/QuillError.h"
18 
19 #include <atomic>
20 #include <csignal>
21 #include <cstdint>
22 #include <cstdlib>
23 #include <cstring>
24 #include <string>
25 
26 #if defined(_WIN32)
27  #if !defined(WIN32_LEAN_AND_MEAN)
28  #define WIN32_LEAN_AND_MEAN
29  #endif
30 
31  #if !defined(NOMINMAX)
32  // Mingw already defines this, so no need to redefine
33  #define NOMINMAX
34  #endif
35 
36  #include <chrono>
37  #include <thread>
38  #include <windows.h>
39 #else
40  #include <unistd.h>
41 #endif
42 
43 QUILL_BEGIN_NAMESPACE
44 
49 {
53  std::vector<int> catchable_signals{SIGTERM, SIGINT, SIGABRT, SIGFPE, SIGILL, SIGSEGV};
54 
62  uint32_t timeout_seconds = 20u;
63 
71  std::string logger;
72 };
73 
74 namespace detail
75 {
76 /***/
78 {
79 public:
81  SignalHandlerContext& operator=(SignalHandlerContext const&) = delete;
82 
83  /***/
84  QUILL_EXPORT static SignalHandlerContext& instance() noexcept
85  {
86  static SignalHandlerContext instance;
87  return instance;
88  }
89 
90  /***/
91  QUILL_NODISCARD static LoggerBase* get_logger() noexcept
92  {
93  LoggerBase* logger_base{nullptr};
94 
95  if (!instance().logger_name.empty())
96  {
97  logger_base = LoggerManager::instance().get_logger(instance().logger_name);
98  }
99 
100  // This also checks if the logger was found above
101  if (!logger_base || !logger_base->is_valid_logger())
102  {
103  logger_base = LoggerManager::instance().get_valid_logger(excluded_logger_name_substr);
104  }
105 
106  return logger_base;
107  }
108 
109  static constexpr std::string_view excluded_logger_name_substr = {"__csv__"};
110 
111  std::string logger_name;
112  std::atomic<int32_t> signal_number{0};
113  std::atomic<uint32_t> lock{0};
114  std::atomic<uint32_t> backend_thread_id{0};
115  std::atomic<uint32_t> signal_handler_timeout_seconds{20};
116  std::atomic<bool> should_reraise_signal{true};
117 
118 private:
119  SignalHandlerContext() = default;
120  ~SignalHandlerContext() = default;
121 };
122 
123 #define QUILL_SIGNAL_HANDLER_LOG(logger, log_level, fmt, ...) \
124  do \
125  { \
126  if (logger->template should_log_statement<log_level>()) \
127  { \
128  static constexpr quill::MacroMetadata macro_metadata{ \
129  "SignalHandler:~", "", fmt, nullptr, log_level, quill::MacroMetadata::Event::Log}; \
130  \
131  logger->template log_statement<false>(&macro_metadata, ##__VA_ARGS__); \
132  } \
133  } while (0)
134 
135 /***/
136 template <typename TFrontendOptions>
137 void on_signal(int32_t signal_number)
138 {
139  // This handler can be entered by multiple threads.
140  uint32_t const lock = SignalHandlerContext::instance().lock.fetch_add(1);
141 
142  if (lock != 0)
143  {
144  // We only allow the first thread to enter the signal handler
145 
146  // sleep until a signal is delivered that either terminates the process or causes the
147  // invocation of a signal-catching function.
148 
149 #if defined(_WIN32)
150  std::this_thread::sleep_for(std::chrono::hours{24000});
151 #else
152  pause();
153 #endif
154  }
155 
156 #if defined(_WIN32)
157  // nothing to do, windows do not have alarm
158 #else
159  // Store the original signal number for the alarm
160  SignalHandlerContext::instance().signal_number.store(signal_number);
161 
162  // Set up an alarm to crash after 20 seconds by redelivering the original signal,
163  // in case anything else goes wrong
164  alarm(SignalHandlerContext::instance().signal_handler_timeout_seconds.load());
165 #endif
166 
167  // Get the id of this thread in the handler and make sure it is not the backend worker thread
168  uint32_t const backend_thread_id = SignalHandlerContext::instance().backend_thread_id.load();
169  uint32_t const current_thread_id = get_thread_id();
170  bool const should_reraise_signal = SignalHandlerContext::instance().should_reraise_signal.load();
171 
172  if ((backend_thread_id == 0) || (current_thread_id == backend_thread_id))
173  {
174  // backend worker thread is not running or the signal handler is called in the backend worker thread
175  if (signal_number == SIGINT || signal_number == SIGTERM)
176  {
177  std::exit(EXIT_SUCCESS);
178  }
179 
180  if (should_reraise_signal)
181  {
182  // for other signals expect SIGINT and SIGTERM we re-raise
183  std::signal(signal_number, SIG_DFL);
184  std::raise(signal_number);
185  }
186  }
187  else
188  {
189  // This means signal handler is running on a frontend thread, we can log and flush
190  LoggerBase* logger_base = SignalHandlerContext::instance().get_logger();
191 
192  if (logger_base)
193  {
194 #if defined(_WIN32)
195  int32_t const signal_desc = signal_number;
196 #else
197  char const* const signal_desc = ::strsignal(signal_number);
198 #endif
199 
200  auto logger = reinterpret_cast<LoggerImpl<TFrontendOptions>*>(logger_base);
201  QUILL_SIGNAL_HANDLER_LOG(logger, LogLevel::Info, "Received signal: {} (signum: {})",
202  signal_desc, signal_number);
203 
204  if (signal_number == SIGINT || signal_number == SIGTERM)
205  {
206  // For SIGINT and SIGTERM, we are shutting down gracefully
207  // Pass `0` to avoid calling std::this_thread::sleep_for()
208  logger->flush_log(0);
209  std::exit(EXIT_SUCCESS);
210  }
211 
212  if (should_reraise_signal)
213  {
214  QUILL_SIGNAL_HANDLER_LOG(logger, LogLevel::Critical,
215  "Program terminated unexpectedly due to signal: {} (signum: {})",
216  signal_desc, signal_number);
217 
218  // This is here in order to flush the above log statement
219  logger->flush_log(0);
220 
221  // Reset to the default signal handler and re-raise the signal
222  std::signal(signal_number, SIG_DFL);
223  std::raise(signal_number);
224  }
225  else
226  {
227  logger->flush_log(0);
228  }
229  }
230  }
231 }
232 } // namespace detail
233 
237 #if defined(_WIN32)
238 namespace detail
239 {
240 /***/
241 inline char const* get_error_message(DWORD ex_code)
242 {
243  switch (ex_code)
244  {
245  case EXCEPTION_ACCESS_VIOLATION:
246  return "EXCEPTION_ACCESS_VIOLATION";
247  case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
248  return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED";
249  case EXCEPTION_BREAKPOINT:
250  return "EXCEPTION_BREAKPOINT";
251  case EXCEPTION_DATATYPE_MISALIGNMENT:
252  return "EXCEPTION_DATATYPE_MISALIGNMENT";
253  case EXCEPTION_FLT_DENORMAL_OPERAND:
254  return "EXCEPTION_FLT_DENORMAL_OPERAND";
255  case EXCEPTION_FLT_DIVIDE_BY_ZERO:
256  return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
257  case EXCEPTION_FLT_INEXACT_RESULT:
258  return "EXCEPTION_FLT_INEXACT_RESULT";
259  case EXCEPTION_FLT_INVALID_OPERATION:
260  return "EXCEPTION_FLT_INVALID_OPERATION";
261  case EXCEPTION_FLT_OVERFLOW:
262  return "EXCEPTION_FLT_OVERFLOW";
263  case EXCEPTION_FLT_STACK_CHECK:
264  return "EXCEPTION_FLT_STACK_CHECK";
265  case EXCEPTION_FLT_UNDERFLOW:
266  return "EXCEPTION_FLT_UNDERFLOW";
267  case EXCEPTION_ILLEGAL_INSTRUCTION:
268  return "EXCEPTION_ILLEGAL_INSTRUCTION";
269  case EXCEPTION_IN_PAGE_ERROR:
270  return "EXCEPTION_IN_PAGE_ERROR";
271  case EXCEPTION_INT_DIVIDE_BY_ZERO:
272  return "EXCEPTION_INT_DIVIDE_BY_ZERO";
273  case EXCEPTION_INT_OVERFLOW:
274  return "EXCEPTION_INT_OVERFLOW";
275  case EXCEPTION_INVALID_DISPOSITION:
276  return "EXCEPTION_INVALID_DISPOSITION";
277  case EXCEPTION_NONCONTINUABLE_EXCEPTION:
278  return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
279  case EXCEPTION_PRIV_INSTRUCTION:
280  return "EXCEPTION_PRIV_INSTRUCTION";
281  case EXCEPTION_SINGLE_STEP:
282  return "EXCEPTION_SINGLE_STEP";
283  case EXCEPTION_STACK_OVERFLOW:
284  return "EXCEPTION_STACK_OVERFLOW";
285  default:
286  return "Unrecognized Exception";
287  }
288 }
289 
290 /***/
291 template <typename TFrontendOptions>
292 BOOL WINAPI on_console_signal(DWORD signal)
293 {
294  uint32_t const backend_thread_id = SignalHandlerContext::instance().backend_thread_id.load();
295  uint32_t const current_thread_id = get_thread_id();
296 
297  // Check if the signal handler is running from a caller thread and if the signal is CTRL+C or CTRL+BREAK
298  if ((backend_thread_id != 0) && (current_thread_id != backend_thread_id) &&
299  (signal == CTRL_C_EVENT || signal == CTRL_BREAK_EVENT))
300  {
301  // Log the interruption and flush log messages
302  LoggerBase* logger_base = SignalHandlerContext::instance().get_logger();
303 
304  if (logger_base)
305  {
306  auto logger = reinterpret_cast<LoggerImpl<TFrontendOptions>*>(logger_base);
307  QUILL_SIGNAL_HANDLER_LOG(logger, LogLevel::Info,
308  "Program interrupted by Ctrl+C or Ctrl+Break signal");
309 
310  // Pass `0` to avoid calling std::this_thread::sleep_for()
311  logger->flush_log(0);
312  std::exit(EXIT_SUCCESS);
313  }
314  }
315 
316  return TRUE;
317 }
318 
319 /***/
320 template <typename TFrontendOptions>
321 LONG WINAPI on_exception(EXCEPTION_POINTERS* exception_p)
322 {
323  uint32_t const backend_thread_id = SignalHandlerContext::instance().backend_thread_id.load();
324  uint32_t const current_thread_id = get_thread_id();
325 
326  // Check if the signal handler is running from a caller thread and if the signal is CTRL+C or CTRL+BREAK
327  if ((backend_thread_id != 0) && (current_thread_id != backend_thread_id))
328  {
329  // Log the interruption and flush log messages
330  LoggerBase* logger_base = SignalHandlerContext::instance().get_logger();
331 
332  if (logger_base)
333  {
334  auto logger = reinterpret_cast<LoggerImpl<TFrontendOptions>*>(logger_base);
335 
336  QUILL_SIGNAL_HANDLER_LOG(logger, LogLevel::Info, "Received exception: {} (Code: {})",
337  get_error_message(exception_p->ExceptionRecord->ExceptionCode),
338  exception_p->ExceptionRecord->ExceptionCode);
339 
340  QUILL_SIGNAL_HANDLER_LOG(logger, LogLevel::Critical,
341  "Program terminated unexpectedly due to exception: {} (Code: {})",
342  get_error_message(exception_p->ExceptionRecord->ExceptionCode),
343  exception_p->ExceptionRecord->ExceptionCode);
344 
345  // Pass `0` to avoid calling std::this_thread::sleep_for()
346  logger->flush_log(0);
347  }
348  }
349 
350  // FATAL Exception: It doesn't necessarily stop here. we pass on continue search
351  // If nobody catches it, then it will exit anyhow.
352  // The risk here is if someone is catching this and returning "EXCEPTION_EXECUTE_HANDLER"
353  // but won't shut down then the app will be running with quill shutdown.
354  return EXCEPTION_CONTINUE_SEARCH;
355 }
356 
357 /***/
358 template <typename TFrontendOptions>
359 void init_exception_handler()
360 {
361  SetUnhandledExceptionFilter(on_exception<TFrontendOptions>);
362 
363  if (!SetConsoleCtrlHandler(on_console_signal<TFrontendOptions>, TRUE))
364  {
365  QUILL_THROW(QuillError{"Failed to call SetConsoleCtrlHandler"});
366  }
367 }
368 } // namespace detail
369 
374 template <typename TFrontendOptions>
375 void init_signal_handler(std::vector<int> const& catchable_signals = std::vector<int>{
376  SIGTERM, SIGINT, SIGABRT, SIGFPE, SIGILL, SIGSEGV})
377 {
378  for (auto const& catchable_signal : catchable_signals)
379  {
380  // setup a signal handler per signal in the array
381  if (std::signal(catchable_signal, detail::on_signal<TFrontendOptions>) == SIG_ERR)
382  {
383  QUILL_THROW(QuillError{"Failed to setup signal handler for signal: " + std::to_string(catchable_signal)});
384  }
385  }
386 }
387 #else
388 namespace detail
389 {
390 /***/
391 inline void on_alarm(int32_t signal_number)
392 {
393  if (SignalHandlerContext::instance().signal_number.load() == 0)
394  {
395  // Will only happen if SIGALRM is the first signal we receive
396  SignalHandlerContext::instance().signal_number = signal_number;
397  }
398 
399  // We will raise the original signal back
400  std::signal(SignalHandlerContext::instance().signal_number, SIG_DFL);
401  std::raise(SignalHandlerContext::instance().signal_number);
402 }
403 
404 template <typename TFrontendOptions>
405 void init_signal_handler(std::vector<int> const& catchable_signals)
406 {
407  for (auto const& catchable_signal : catchable_signals)
408  {
409  if (catchable_signal == SIGALRM)
410  {
411  QUILL_THROW(QuillError{"SIGALRM can not be part of catchable_signals."});
412  }
413 
414  // set up a signal handler per signal in the array
415  if (std::signal(catchable_signal, on_signal<TFrontendOptions>) == SIG_ERR)
416  {
417  QUILL_THROW(QuillError{"Failed to setup signal handler for signal: " + std::to_string(catchable_signal)});
418  }
419  }
420 
421  /* Register the alarm handler */
422  if (std::signal(SIGALRM, on_alarm) == SIG_ERR)
423  {
424  QUILL_THROW(QuillError{"Failed to setup signal handler for signal: SIGALRM"});
425  }
426 }
427 } // namespace detail
428 #endif
429 
430 QUILL_END_NAMESPACE
uint32_t timeout_seconds
Defines the timeout duration in seconds for the signal handler alarm.
Definition: SignalHandler.h:62
std::string logger
The logger instance that the signal handler will use to log errors when the application crashes...
Definition: SignalHandler.h:71
Setups a signal handler to handle fatal signals.
Definition: BackendManager.h:24
Thread safe logger.
Definition: Logger.h:48
Definition: SignalHandler.h:77
Definition: LoggerBase.h:36
custom exception
Definition: QuillError.h:45
std::vector< int > catchable_signals
List of signals that the backend should catch if with_signal_handler is enabled.
Definition: SignalHandler.h:53
Struct to hold options for the signal handler.
Definition: SignalHandler.h:48
QUILL_NODISCARD QUILL_EXPORT QUILL_ATTRIBUTE_USED uint32_t get_thread_id() noexcept
Returns the os assigned ID of the thread.
Definition: ThreadUtilities.h:193