Handle SIGINT for openEMS and Python, with graceful exit support.

Currently, openEMS doesn't have any special code to handle SIGINT (which
is raised by pressing Control-C). By default, the program is terminated
without saving data. This worked okay in the past, but now its
limitations are becoming obvious.

1. When openEMS is used as a Python module, Control-C stops working
because SIGINT is now managed by Python in order to generate
KeyboardInterrupt exceptions, normally this isn't a problem, but if
we are running an external C++ (Cython) function such as openEMS, the
Python interpreter mainloop has no control until we return. As a
result, SIGINT is received but never handled. In Cython, programs are
expected to call PyErr_CheckSignals() in its blocking loop periodically
to temporally transfer control back to Python to handle signals. But
this introduces a dependency of Cython in the FDTD mainloop.

2. During a simulation, it's not possible to abort it gracefully by
pressing Control-C, this is a limitation of openEMS itself, it's
always a force exit. Currently the only supported method for graceful
exit is creating a file called "ABORT" in the simulation directory.
If we already need to implement a signal handler, adding a graceful
exit at the same time would be a good idea.

This commit installs SIGINT handlers during SetupFDTD() and RunFDTD().

1. In RunFDTD(), if SIGINT is received once, a status flag is set, which
is then checked in CheckAbortCond(), allowing a graceful exit with the
same effect of an "ABORT" file. If SIGINT is received twice, openEMS
force exit without saving data (just like the old default behavior).

2. In SetupFDTD(), if SIGINT is received, openEMS immediately force
exit without saving data, identical to the old behavior. In a huge
simulation, initializing and compressing operators may have a long
time. so we want an early exit before RunFDTD().

3. Before RunFDTD() and SetupFDTD() return, the original signal handler
for SIGINT is restored. This is important since when we're acting as
a shared library. When a program (such as the Python interpreter) calls
us, changing the SIGINT handler unilaterally may overwrite the original
handler and affect the functionality of the original program. For
example, Python would never be able to raise KeyboardInterrupt again.
Thus, we save the original handler and restore it later.

Signed-off-by: Yifeng Li <tomli@tomli.me>
pull/129/head
Yifeng Li 2023-05-07 09:59:13 +00:00 committed by Thorsten Liebig
parent ee3f2b7d80
commit 840c9755d5
4 changed files with 354 additions and 2 deletions

View File

@ -20,6 +20,7 @@
#include <iostream>
#include <fstream>
#include "tools/array_ops.h"
#include "tools/signal.h"
#include "tools/useful.h"
#include "FDTD/operator_cylinder.h"
#include "FDTD/operator_cylindermultigrid.h"
@ -899,14 +900,18 @@ int openEMS::SetupFDTD()
timeval startTime;
gettimeofday(&startTime,NULL);
Signal::SetupHandlerForSIGINT(SIGNAL_EXIT_FORCE);
if (m_CSX==NULL)
{
cerr << "openEMS::SetupFDTD: Error: CSXCAD is not set!" << endl;
Signal::SetupHandlerForSIGINT(SIGNAL_ORIGINAL);
return 3;
}
if (m_CSX==NULL)
{
cerr << "openEMS::SetupFDTD: Error: CSXCAD is not set!" << endl;
Signal::SetupHandlerForSIGINT(SIGNAL_ORIGINAL);
return 3;
}
std::string ec = m_CSX->Update();
@ -927,7 +932,10 @@ int openEMS::SetupFDTD()
//*************** setup operator ************//
if (SetupOperator()==false)
{
Signal::SetupHandlerForSIGINT(SIGNAL_ORIGINAL);
return 2;
}
// default material averaging is quarter cell averaging
FDTD_Op->SetQuarterCellMaterialAvg();
@ -942,6 +950,7 @@ int openEMS::SetupFDTD()
if (m_Exc==NULL)
{
cerr << "openEMS::SetupFDTD: Error, excitation is not defined! Abort!" << endl;
Signal::SetupHandlerForSIGINT(SIGNAL_ORIGINAL);
return 3;
}
@ -950,7 +959,11 @@ int openEMS::SetupFDTD()
if (!CylinderCoords)
FDTD_Op->AddExtension(new Operator_Ext_TFSF(FDTD_Op));
if (FDTD_Op->SetGeometryCSX(m_CSX)==false) return(2);
if (FDTD_Op->SetGeometryCSX(m_CSX)==false)
{
Signal::SetupHandlerForSIGINT(SIGNAL_ORIGINAL);
return(2);
}
SetupBoundaryConditions();
@ -1065,6 +1078,7 @@ int openEMS::SetupFDTD()
if (m_no_simulation)
{
// simulation was disabled (to generate debug output only)
Signal::SetupHandlerForSIGINT(SIGNAL_ORIGINAL);
return 1;
}
@ -1079,7 +1093,10 @@ int openEMS::SetupFDTD()
//setup all processing classes
if (SetupProcessing()==false)
{
Signal::SetupHandlerForSIGINT(SIGNAL_ORIGINAL);
return 2;
}
// Cleanup all unused material storages...
FDTD_Op->CleanupMaterialStorage();
@ -1093,6 +1110,7 @@ int openEMS::SetupFDTD()
PA->DumpBoxes2File("box_dump_");
}
Signal::SetupHandlerForSIGINT(SIGNAL_ORIGINAL);
return 0;
}
@ -1118,12 +1136,19 @@ bool openEMS::CheckAbortCond()
if (m_Abort) //abort was set externally
return true;
//check whether SIGINT is received
if (Signal::ReceivedSIGINT())
{
cerr << "openEMS::CheckAbortCond(): Received SIGINT, aborting simulation gracefully..." << endl;
return true;
}
//check whether the file "ABORT" exist in current working directory
ifstream ifile("ABORT");
if (ifile)
{
ifile.close();
cerr << "openEMS::CheckAbortCond(): Found file \"ABORT\", aborting simulation..." << endl;
cerr << "openEMS::CheckAbortCond(): Found file \"ABORT\", aborting simulation gracefully..." << endl;
return true;
}
@ -1134,6 +1159,8 @@ void openEMS::RunFDTD()
{
cout << "Running FDTD engine... this may take a while... grab a cup of coffee?!?" << endl;
Signal::SetupHandlerForSIGINT(SIGNAL_EXIT_GRACEFUL);
//special handling of a field processing, needed to realize the end criteria...
ProcessFields* ProcField = new ProcessFields(NewEngineInterface());
PA->AddProcessing(ProcField);
@ -1232,6 +1259,8 @@ void openEMS::RunFDTD()
//*************** postproc ************//
PA->PostProcess();
Signal::SetupHandlerForSIGINT(SIGNAL_ORIGINAL);
}
bool openEMS::DumpStatistics(const string& filename, double time)

View File

@ -4,6 +4,7 @@ set(SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/AdrOp.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ErrorMsg.cpp
${CMAKE_CURRENT_SOURCE_DIR}/array_ops.cpp
${CMAKE_CURRENT_SOURCE_DIR}/signal.cpp
${CMAKE_CURRENT_SOURCE_DIR}/global.cpp
${CMAKE_CURRENT_SOURCE_DIR}/hdf5_file_reader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/hdf5_file_writer.cpp

263
tools/signal.cpp Normal file
View File

@ -0,0 +1,263 @@
/*
* Copyright (C) 2023 Yifeng Li <tomli@tomli.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include "signal.h"
void Signal::SetupHandlerForSIGINT(int type)
{
m_sigintAbort = 0;
#ifndef WIN32
UnixSetupHandlerForSIGINT(type);
#else
Win32SetupHandlerForConsoleCtrl(type);
#endif
}
#ifndef WIN32
void Signal::UnixSetupHandlerForSIGINT(int type)
{
if (type == SIGNAL_ORIGINAL && m_sigHandlerOriginal)
{
// If we're acting as a shared library and a program (such as
// the Python interpreter) calls us, changing the SIGINT handler
// unilaterally may overwrite the original handler and affect
// the functionality of the original program. Thus, we save the
// original handler and restore it after the end of SetupFDTD()
// or RunFDTD() to minimize the disruption.
auto retval = std::signal(SIGINT, m_sigHandlerOriginal);
if (retval == SIG_ERR)
{
fprintf(stderr, "Signal::UnixSetupHandlerForSIGINT(): "
"Failed to restore signal handler!\n");
}
m_sigHandlerOriginal = NULL;
}
else if (type == SIGNAL_EXIT_GRACEFUL)
{
m_sigHandlerOriginal = std::signal(SIGINT, UnixGracefulExitHandler);
if (m_sigHandlerOriginal == SIG_ERR)
{
fprintf(stderr, "Signal::UnixSetupHandlerForSIGINT(): "
"Failed to set UnixGracefulExitHandler!\n");
m_sigHandlerOriginal = NULL;
}
}
else if (type == SIGNAL_EXIT_FORCE)
{
m_sigHandlerOriginal = std::signal(SIGINT, UnixForceExitHandler);
if (m_sigHandlerOriginal == SIG_ERR)
{
fprintf(stderr, "Signal::UnixSetupHandlerForSIGINT(): "
"Failed to set UnixForceExitHandler!\n");
m_sigHandlerOriginal = NULL;
}
}
}
void Signal::UnixGracefulExitHandler(int signal)
{
m_sigintAbort = 1;
// C standard only guarantees that a sig_atomic_t variable is safe
// to read or write, but it's not necessarily safe to increment by
// one, and also not safe to set one sig_atomic_t depending on the
// result of another sig_atomic_t.
//
// Thus, we switch the signal handler itself instead of recording
// the number of times SIGINT is raised.
auto retval = std::signal(SIGINT, UnixForceExitHandler);
if (retval == SIG_ERR)
{
SafeStderrWrite("\nSignal::UnixGracefulExitHandler(): "
"Failed to set UnixForceExitHandler!");
}
else
{
SafeStderrWrite("\nSignal::UnixGracefulExitHandler(): "
"Gracefully aborting simulation "
"now, this may take a few seconds...\n"
"Signal::UnixGracefulExitHandler(): "
"To force-exit, send Ctrl-C again, "
"but simulation results may be lost.\n");
}
}
void Signal::UnixForceExitHandler(int signal)
{
SafeStderrWrite("\nSignal::UnixForceExitHandler(): "
"Force-exit simulation process now!\n");
// By convention, if a program is (uncleanly) aborted due to
// an external signal, preferably it should return 128 + signal.
// For SIGINT, it's 130.
std::_Exit(128 + signal);
}
#else
void Signal::Win32SetupHandlerForConsoleCtrl(int type)
{
if (type == SIGNAL_ORIGINAL || m_sigHandlerRegistered)
{
// On Windows, SetConsoleCtrlHandler appends a new ConsoleCtrlHandler
// in addition to the existing handlers. Thus, we need to record
// the ConsoleCtrlHandler installed by us (instead of getting the
// pre-existing handlers on Unix). Then, before we install a new
// signal handler, we need to use the argument "Add == FALSE" to
// remove the handler we previously installed.
//
// We also need to do the same in case that we're restoring the
// ConsoleCtrlHandler to the original state (note how on Unix, the
// if expression uses "AND", but on Windows, the if expression uses
// "OR".
BOOL success = SetConsoleCtrlHandler(m_sigHandlerRegistered, FALSE);
m_sigHandlerRegistered = NULL;
if (!success)
{
fprintf(stderr, "Signal::Win32SetupHandlerForConsoleCtrl(): "
"Failed to unregister ConsoleCtrlHandler!\n");
return;
}
}
// Assume m_sigHandlerRegistered has already been unregistered.
if (type == SIGNAL_EXIT_GRACEFUL)
{
m_sigHandlerRegistered = (PHANDLER_ROUTINE) Win32GracefulExitHandler;
BOOL success = SetConsoleCtrlHandler(m_sigHandlerRegistered, TRUE);
if (!success)
{
fprintf(stderr, "Signal::Win32SetupHandlerForConsoleCtrl(): "
"Failed to register Win32GracefulExitHandler!\n");
}
}
else if (type == SIGNAL_EXIT_FORCE)
{
m_sigHandlerRegistered = (PHANDLER_ROUTINE) Win32ForceExitHandler;
BOOL success = SetConsoleCtrlHandler(m_sigHandlerRegistered, TRUE);
if (!success)
{
fprintf(stderr, "Signal::Win32SetupHandlerForConsoleCtrl(): "
"Failed to register Win32ForceExitHandler!\n");
}
}
}
BOOL Signal::Win32GracefulExitHandler(DWORD fdwCtrlType)
{
m_sigintAbort = 1;
// unregister the current handler
BOOL success = SetConsoleCtrlHandler(m_sigHandlerRegistered, FALSE);
if (!success)
{
SafeStderrWrite("Signal::Win32GracefulExitHandler(): "
"Failed to unregister Win32GracefulExitHandler!\n");
return true;
}
// install a new handler
m_sigHandlerRegistered = (PHANDLER_ROUTINE) Win32ForceExitHandler;
success = SetConsoleCtrlHandler(m_sigHandlerRegistered, TRUE);
if (!success)
{
SafeStderrWrite("Signal::Win32GracefulExitHandler(): "
"Failed to register Win32ForceExitHandler!\n");
}
else
{
SafeStderrWrite("\nSignal::Win32GracefulExitHandler(): "
"Gracefully aborting simulation "
"now, this may take a few seconds...\n"
"Signal::Win32GracefulExitHandler(): "
"To force-exit, send Ctrl-C again, "
"but simulation results may be lost.\n");
}
return true;
}
BOOL Signal::Win32ForceExitHandler(DWORD fdwCtrlType)
{
SafeStderrWrite("\nSignal::Win32ForceExitHandler(): "
"Force-exit simulation process now!\n");
// On Windows, the exit code for SIGINT is always 3.
std::_Exit(3);
// unreachable
return true;
}
#endif
bool Signal::ReceivedSIGINT(void)
{
if (m_sigintAbort)
return true;
else
return false;
}
void Signal::SafeStderrWrite(const char *buf)
{
#ifdef WIN32
// On Windows, using any kind of system calls in a ANSI C signal
// handler is prohibited, in this case, this function should return
// immediately without doing anything. But, when the official way
// SetConsoleCtrlHandler() is used (instead of using ANSI C signals),
// there's no such restriction.
fprintf(stderr, "%s", buf);
fflush(stderr);
return;
#else
// On Unix, in a signal handler, it's unsafe to use normal I/O
// functions such as iostream, puts(), printf(), fprintf(). The
// only safe option is the system call write().
size_t buf_len = strlen(buf);
ssize_t bytes = 0;
while (buf_len > 0)
{
bytes = write(STDERR_FILENO, buf, buf_len);
if (bytes < 0)
{
// write failure, nothing we can do.
return;
}
if ((size_t) bytes > buf_len)
{
// Assertion: This should never happen. bytes is
// always less or equal to buf_len, and buf_len
// will never underflow under any circumstances
// (unless the write system call is broken).
return;
}
buf += bytes; // advance buffer position
buf_len -= (size_t) bytes; // decrement limiter
}
return; // write completed.
#endif
}

59
tools/signal.h Normal file
View File

@ -0,0 +1,59 @@
/*
* Copyright (C) 2023 Yifeng Li <tomli@tomli.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SIGNAL_H
#define SIGNAL_H
#include <csignal>
#ifndef WIN32
#include <unistd.h>
#else
#include <windows.h>
#endif
enum
{
SIGNAL_ORIGINAL,
SIGNAL_EXIT_GRACEFUL,
SIGNAL_EXIT_FORCE,
};
class Signal
{
public:
static void SetupHandlerForSIGINT(int type);
static bool ReceivedSIGINT(void);
private:
inline static volatile std::sig_atomic_t m_sigintAbort = 0;
static void SafeStderrWrite(const char *buf);
#ifndef WIN32
inline static void (*m_sigHandlerOriginal)(int) = NULL;
static void UnixSetupHandlerForSIGINT(int type);
static void UnixGracefulExitHandler(int signal);
static void UnixForceExitHandler(int signal);
#else
inline static PHANDLER_ROUTINE m_sigHandlerRegistered = NULL;
static void Win32SetupHandlerForConsoleCtrl(int type);
static BOOL Win32GracefulExitHandler(DWORD fdwCtrlType);
static BOOL Win32ForceExitHandler(DWORD fdwCtrlType);
#endif
};
#endif