Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CtrlCHandler helper class (Swift) #3469

Merged
merged 13 commits into from
Feb 1, 2025
22 changes: 14 additions & 8 deletions cpp/include/Ice/CtrlCHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,31 @@ namespace Ice
/// \headerfile Ice/Ice.h
using CtrlCHandlerCallback = std::function<void(int sig)>;

/// Provides a portable way to handle Ctrl-C and Ctrl-C like signals. On Linux and macOS, the CtrlCHandler handles
/// SIGHUP, SIGINT and SIGTERM. On Windows, it is essentially a wrapper for SetConsoleCtrlHandler.
/// Provides a portable way to handle Ctrl+C and Ctrl+C like signals. On Linux and macOS, the CtrlCHandler handles
/// SIGHUP, SIGINT and SIGTERM. On Windows, it is a wrapper for SetConsoleCtrlHandler.
/// \headerfile Ice/Ice.h
class ICE_API CtrlCHandler
{
public:
/// Constructs a CtrlCHandler.
/// Constructs a CtrlCHandler with a null callback, meaning the callback does nothing.
/// @see #CtrlCHandler(CtrlCHandlerCallback)
CtrlCHandler();

/// Constructs a CtrlCHandler. Only one instance of this class can exist in a program at any point in time.
/// On Linux and macOS, this constructor masks the SIGHUP, SIGINT and SIGTERM signals and then creates a thread
/// that waits for these signals using sigwait. On Windows, this constructor calls SetConsoleCtrlCHandler to
/// register a handler routine that calls the supplied callback function.
/// Only a single CtrlCHandler object can exist in a process at a give time.
/// @param cb The callback function to invoke when a signal is received. The default (nullptr) means do nothing.
explicit CtrlCHandler(CtrlCHandlerCallback cb = nullptr);
/// @param cb The callback function to invoke when a signal is received.
explicit CtrlCHandler(CtrlCHandlerCallback cb);

CtrlCHandler(const CtrlCHandler&) = delete;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not having these made the class rather bug-prone.

CtrlCHandler& operator=(const CtrlCHandler&) = delete;

/// Unregisters the callback function.
/// On Linux and macOS, this destructor joins and terminates the thread created by the constructor but does not
/// "unmask" SIGHUP, SIGINT and SIGTERM. As a result, these signals are ignored after this destructor completes.
/// unmask SIGHUP, SIGINT and SIGTERM. As a result, these signals are ignored after this destructor completes.
/// On Windows, this destructor unregisters the SetConsoleCtrlHandler handler routine, and as a result a
/// Ctrl-C or similar signal will terminate the application after this destructor completes.
/// Ctrl+C or similar signal will terminate the application after this destructor completes.
~CtrlCHandler();

/// Replaces the signal callback.
Expand Down
85 changes: 37 additions & 48 deletions cpp/src/Ice/CtrlCHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@

#ifdef _WIN32
# include <windows.h>
#else
# include <csignal>
#endif

#include <cassert>
#include <future>
#include <mutex>

#ifndef _WIN32
# include <csignal>
#endif

using namespace Ice;
using namespace std;

Expand All @@ -27,6 +25,8 @@ namespace
mutex globalMutex;
}

CtrlCHandler::CtrlCHandler() : CtrlCHandler(nullptr) {}

CtrlCHandlerCallback
CtrlCHandler::setCallback(CtrlCHandlerCallback callback)
{
Expand All @@ -43,6 +43,27 @@ CtrlCHandler::getCallback() const
return _callback;
}

int
CtrlCHandler::wait()
{
promise<int> promise;

CtrlCHandlerCallback oldCallback = setCallback(
[&promise, this](int sig)
{
setCallback(nullptr); // ignore further signals
promise.set_value(sig);
});

if (oldCallback)
{
setCallback(oldCallback);
throw Ice::LocalException{__FILE__, __LINE__, "do not call wait on a CtrlCHandler with a registered callback"};
}

return promise.get_future().get();
}

#ifdef _WIN32

static BOOL WINAPI
Expand All @@ -67,9 +88,9 @@ handlerRoutine(DWORD dwCtrlType)
CtrlCHandler::CtrlCHandler(CtrlCHandlerCallback callback)
{
unique_lock lock(globalMutex);
bool handler = _handler != nullptr;
const bool handlerAlive = _handler != nullptr;

if (handler)
if (handlerAlive)
bernardnormier marked this conversation as resolved.
Show resolved Hide resolved
{
throw Ice::LocalException{__FILE__, __LINE__, "another CtrlCHandler was already created"};
}
Expand All @@ -86,11 +107,9 @@ CtrlCHandler::CtrlCHandler(CtrlCHandlerCallback callback)
CtrlCHandler::~CtrlCHandler()
{
SetConsoleCtrlHandler(handlerRoutine, FALSE);
{
lock_guard lock(globalMutex);
_handler = 0;
_callback = nullptr;
}
lock_guard lock(globalMutex);
_handler = nullptr;
_callback = nullptr;
}

#else
Expand All @@ -105,19 +124,15 @@ extern "C"
sigaddset(&ctrlCLikeSignals, SIGINT);
sigaddset(&ctrlCLikeSignals, SIGTERM);

//
// Run until the handler is destroyed (_handler == 0)
//
// Run until the handler is destroyed (!_handler)
for (;;)
{
int signal = 0;
int rc = sigwait(&ctrlCLikeSignals, &signal);
if (rc == EINTR)
{
//
// Some sigwait() implementations incorrectly return EINTR
// when interrupted by an unblocked caught signal
//
// Some sigwait() implementations incorrectly return EINTR when interrupted by an unblocked caught
// signal.
continue;
}
assert(rc == 0);
Expand Down Expand Up @@ -149,9 +164,9 @@ namespace
CtrlCHandler::CtrlCHandler(CtrlCHandlerCallback callback)
{
unique_lock lock(globalMutex);
bool handler = _handler != nullptr;
const bool handlerAlive = _handler != nullptr;

if (handler)
if (handlerAlive)
bernardnormier marked this conversation as resolved.
Show resolved Hide resolved
{
throw Ice::LocalException{__FILE__, __LINE__, "another CtrlCHandler was already created"};
}
Expand All @@ -162,9 +177,8 @@ CtrlCHandler::CtrlCHandler(CtrlCHandlerCallback callback)

lock.unlock();

// We block these CTRL+C like signals in the main thread,
// and by default all other threads will inherit this signal
// mask.
// We block these CTRL+C like signals in the main thread, and by default all other threads will inherit this
// signal mask.

sigset_t ctrlCLikeSignals;
sigemptyset(&ctrlCLikeSignals);
Expand All @@ -183,18 +197,14 @@ CtrlCHandler::CtrlCHandler(CtrlCHandlerCallback callback)

CtrlCHandler::~CtrlCHandler()
{
//
// Clear the handler, the sigwaitThread will exit if _handler is null
//
{
lock_guard lock(globalMutex);
_handler = nullptr;
_callback = nullptr;
}

//
// Signal the sigwaitThread and join it.
//
void* status = nullptr;
[[maybe_unused]] int rc = pthread_kill(_tid, SIGTERM); // NOLINT(cert-pos44-c)
assert(rc == 0);
Expand All @@ -203,24 +213,3 @@ CtrlCHandler::~CtrlCHandler()
}

#endif

int
CtrlCHandler::wait()
{
promise<int> promise;

CtrlCHandlerCallback oldCallback = setCallback(
[&promise, this](int sig)
{
setCallback(nullptr); // ignore further signals
promise.set_value(sig);
});

if (oldCallback)
{
setCallback(oldCallback);
throw Ice::LocalException{__FILE__, __LINE__, "do not call wait on a CtrlCHandler with a registered callback"};
}

return promise.get_future().get();
}
24 changes: 24 additions & 0 deletions swift/src/Ice/CtrlCHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) ZeroC, Inc.

import IceImpl

#if os(macOS)
/// Helps applications handle Ctrl+C (SIGINT) and similar signals (SIGHUP and SIGTERM). Only available on macOS.
public final class CtrlCHandler {
private let handle = ICECtrlCHandler()

/// Creates a CtrlCHandler. Only one instance of this class can exist in a program at any point in time.
/// This instance must be created before starting any thread.
public init() {}

/// Waits until this handler catches a Ctrl+C or similar signal.
/// - Returns: The signal number.
public func catchSignal() async -> Int32 {
return await withCheckedContinuation { continuation in
self.handle.catchSignal { signal in
continuation.resume(returning: signal)
}
}
}
}
#endif
28 changes: 28 additions & 0 deletions swift/src/IceImpl/CtlCHandler.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) ZeroC, Inc.

#import "include/CtrlCHandler.h"

#if TARGET_OS_OSX != 0
@implementation ICECtrlCHandler

- (void)catchSignal:(void (^)(int))callback
{
[[maybe_unused]] Ice::CtrlCHandlerCallback previousCallback = self->cppObject.setCallback(
[callback, self](int signal)
{
// This callback executes in the C++ CtrlCHandler thread.

// We need an autorelease pool in case the callback creates autorelease objects.
@autoreleasepool
{
callback(signal);
}

// Then remove callback
self->cppObject.setCallback(nullptr);
});

assert(previousCallback == nullptr);
}
@end
#endif
24 changes: 24 additions & 0 deletions swift/src/IceImpl/include/CtrlCHandler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) ZeroC, Inc.

#import "Config.h"

#if TARGET_OS_OSX != 0
NS_ASSUME_NONNULL_BEGIN

// The implementation of Ice.CtrlCHandler, which wraps the C++ class Ice::CtrlCHandler.
ICEIMPL_API @interface ICECtrlCHandler : NSObject
- (void)catchSignal:(void (^)(int))callback;
@end

# ifdef __cplusplus

@interface ICECtrlCHandler ()
{
Ice::CtrlCHandler cppObject;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's essential to use a field and not a property here, since a property wants to return a copy of the C++ object (which is not copyable).

}
@end

# endif

NS_ASSUME_NONNULL_END
#endif