diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PosixSignal.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PosixSignal.cs new file mode 100644 index 0000000000000..b42b5138ec622 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PosixSignal.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_SetPosixSignalHandler")] + [SuppressGCTransition] + internal static extern unsafe void SetPosixSignalHandler(delegate* unmanaged handler); + + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_EnablePosixSignalHandling", SetLastError = true)] + internal static extern bool EnablePosixSignalHandling(int signal); + + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_DisablePosixSignalHandling")] + internal static extern void DisablePosixSignalHandling(int signal); + + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_HandleNonCanceledPosixSignal")] + internal static extern bool HandleNonCanceledPosixSignal(int signal, int handlersDisposed); + + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetPlatformSignalNumber")] + [SuppressGCTransition] + internal static extern int GetPlatformSignalNumber(PosixSignal signal); + } +} diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.RegisterForCtrlC.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.RegisterForCtrlC.cs deleted file mode 100644 index cb06c67651c0c..0000000000000 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.RegisterForCtrlC.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.InteropServices; - -internal static partial class Interop -{ - internal static partial class Sys - { - internal enum CtrlCode - { - Interrupt = 0, - Break = 1 - } - - [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_RegisterForCtrl")] - [SuppressGCTransition] - internal static extern unsafe void RegisterForCtrl(delegate* unmanaged handler); - - [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_UnregisterForCtrl")] - [SuppressGCTransition] - internal static extern void UnregisterForCtrl(); - } -} diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.RegisterForSigChld.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.RegisterForSigChld.cs index 3082c661cbaec..e11fcfaf0e65c 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.RegisterForSigChld.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.RegisterForSigChld.cs @@ -8,6 +8,6 @@ internal static partial class Interop internal static partial class Sys { [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_RegisterForSigChld")] - internal static extern unsafe void RegisterForSigChld(delegate* unmanaged handler); + internal static extern unsafe void RegisterForSigChld(delegate* unmanaged handler); } } diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.RestoreAndHandleCtrl.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SetDelayedSigChildConsoleConfigurationHandler.cs similarity index 61% rename from src/libraries/Common/src/Interop/Unix/System.Native/Interop.RestoreAndHandleCtrl.cs rename to src/libraries/Common/src/Interop/Unix/System.Native/Interop.SetDelayedSigChildConsoleConfigurationHandler.cs index 927c2d2706833..bd09aa12abc00 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.RestoreAndHandleCtrl.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SetDelayedSigChildConsoleConfigurationHandler.cs @@ -7,7 +7,8 @@ internal static partial class Interop { internal static partial class Sys { - [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_RestoreAndHandleCtrl")] - internal static extern void RestoreAndHandleCtrl(CtrlCode ctrlCode); + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_SetDelayedSigChildConsoleConfigurationHandler")] + [SuppressGCTransition] + internal static extern unsafe void SetDelayedSigChildConsoleConfigurationHandler(delegate* unmanaged callback); } } diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SetTerminalInvalidationHandler.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SetTerminalInvalidationHandler.cs index a12194e173755..3082cb96b7183 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SetTerminalInvalidationHandler.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SetTerminalInvalidationHandler.cs @@ -8,7 +8,6 @@ internal static partial class Interop internal static partial class Sys { [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_SetTerminalInvalidationHandler")] - [SuppressGCTransition] internal static extern unsafe void SetTerminalInvalidationHandler(delegate* unmanaged handler); } } diff --git a/src/libraries/Native/Unix/System.Native/entrypoints.c b/src/libraries/Native/Unix/System.Native/entrypoints.c index cf6485ecf8430..b1b5a92e5f35b 100644 --- a/src/libraries/Native/Unix/System.Native/entrypoints.c +++ b/src/libraries/Native/Unix/System.Native/entrypoints.c @@ -216,10 +216,8 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_GetOSArchitecture) DllImportEntry(SystemNative_GetProcessArchitecture) DllImportEntry(SystemNative_SearchPath) - DllImportEntry(SystemNative_RegisterForCtrl) - DllImportEntry(SystemNative_UnregisterForCtrl) DllImportEntry(SystemNative_RegisterForSigChld) - DllImportEntry(SystemNative_RestoreAndHandleCtrl) + DllImportEntry(SystemNative_SetDelayedSigChildConsoleConfigurationHandler) DllImportEntry(SystemNative_SetTerminalInvalidationHandler) DllImportEntry(SystemNative_InitializeTerminalAndSignalHandling) DllImportEntry(SystemNative_SNPrintF) @@ -251,6 +249,11 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_PWrite) DllImportEntry(SystemNative_PReadV) DllImportEntry(SystemNative_PWriteV) + DllImportEntry(SystemNative_EnablePosixSignalHandling) + DllImportEntry(SystemNative_DisablePosixSignalHandling) + DllImportEntry(SystemNative_HandleNonCanceledPosixSignal) + DllImportEntry(SystemNative_SetPosixSignalHandler) + DllImportEntry(SystemNative_GetPlatformSignalNumber) }; EXTERN_C const void* SystemResolveDllImport(const char* name); diff --git a/src/libraries/Native/Unix/System.Native/pal_console.c b/src/libraries/Native/Unix/System.Native/pal_console.c index c18e3f82620f6..1fd55aa3d0430 100644 --- a/src/libraries/Native/Unix/System.Native/pal_console.c +++ b/src/libraries/Native/Unix/System.Native/pal_console.c @@ -97,23 +97,11 @@ static bool g_hasTty = false; // cache we are not a tty static volatile bool g_receivedSigTtou = false; -static void ttou_handler(int signo) +static void ttou_handler() { - (void)signo; g_receivedSigTtou = true; } -static void InstallTTOUHandler(void (*handler)(int), int flags) -{ - struct sigaction action; - memset(&action, 0, sizeof(action)); - action.sa_handler = handler; - action.sa_flags = flags; - int rvSigaction = sigaction(SIGTTOU, &action, NULL); - assert(rvSigaction == 0); - (void)rvSigaction; -} - static bool TcSetAttr(struct termios* termios, bool blockIfBackground) { if (g_terminalUninitialized) @@ -131,7 +119,7 @@ static bool TcSetAttr(struct termios* termios, bool blockIfBackground) // stdout. We set SA_RESETHAND to avoid that handler's write loops infinitly // on EINTR when the process is running in background and the terminal // configured with TOSTOP. - InstallTTOUHandler(ttou_handler, (int)SA_RESETHAND); + InstallTTOUHandlerForConsole(ttou_handler); g_receivedSigTtou = false; } @@ -147,8 +135,7 @@ static bool TcSetAttr(struct termios* termios, bool blockIfBackground) rv = true; } - // Restore default SIGTTOU handler. - InstallTTOUHandler(SIG_DFL, 0); + UninstallTTOUHandlerForConsole(); } // On success, update the cached value. @@ -205,8 +192,6 @@ static bool ConfigureTerminal(bool signalForBreak, bool forChild, uint8_t minCha void UninitializeTerminal() { - assert(g_hasTty); - // This method is called on SIGQUIT/SIGINT from the signal dispatching thread // and on atexit. @@ -473,7 +458,7 @@ int32_t SystemNative_InitializeTerminalAndSignalHandling() { static int32_t initialized = 0; - // Both the Process and Console class call this method for initialization. + // The Process, Console and PosixSignalRegistration classes call this method for initialization. if (pthread_mutex_lock(&g_lock) == 0) { if (initialized == 0) diff --git a/src/libraries/Native/Unix/System.Native/pal_signal.c b/src/libraries/Native/Unix/System.Native/pal_signal.c index dfe30adb9e89f..4c4017f654746 100644 --- a/src/libraries/Native/Unix/System.Native/pal_signal.c +++ b/src/libraries/Native/Unix/System.Native/pal_signal.c @@ -17,33 +17,212 @@ #include static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; -static struct sigaction g_origSigIntHandler, g_origSigQuitHandler; // saved signal handlers for ctrl handling -static struct sigaction g_origSigContHandler, g_origSigChldHandler; // saved signal handlers for reinitialization -static struct sigaction g_origSigWinchHandler; // saved signal handlers for SIGWINCH -static volatile CtrlCallback g_ctrlCallback = NULL; // Callback invoked for SIGINT/SIGQUIT -static volatile TerminalInvalidationCallback g_terminalInvalidationCallback = NULL; // Callback invoked for SIGCHLD/SIGCONT/SIGWINCH -static volatile SigChldCallback g_sigChldCallback = NULL; // Callback invoked for SIGCHLD + +// Saved signal handlers +static struct sigaction* g_origSigHandler; +static bool* g_handlerIsInstalled; + +// Callback invoked for SIGCHLD/SIGCONT/SIGWINCH +static volatile TerminalInvalidationCallback g_terminalInvalidationCallback = NULL; +// Callback invoked for SIGCHLD +static volatile SigChldCallback g_sigChldCallback = NULL; +static volatile bool g_sigChldConsoleConfigurationDelayed; +static void (*g_sigChldConsoleConfigurationCallback)(void); +// Callback invoked for for SIGTTOU while terminal settings are changed. +static volatile ConsoleSigTtouHandler g_consoleTtouHandler; + +// Callback invoked for PosixSignal handling. +static PosixSignalHandler g_posixSignalHandler = NULL; +// Tracks whether there are PosixSignal handlers registered. +static volatile bool* g_hasPosixSignalRegistrations; + static int g_signalPipe[2] = {-1, -1}; // Pipe used between signal handler and worker -static struct sigaction* OrigActionFor(int sig) +static int GetSignalMax() // Returns the highest usable signal number. +{ +#ifdef SIGRTMAX + return SIGRTMAX; +#else + return NSIG; +#endif +} + +static bool IsCancelableTerminationSignal(int sig) { - switch (sig) + return sig == SIGINT || + sig == SIGQUIT || + sig == SIGTERM; +} + +static bool IsSaSigInfo(struct sigaction* action) +{ + assert(action); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wsign-conversion" // sa_flags is unsigned on Android. + return (action->sa_flags & SA_SIGINFO) != 0; +#pragma clang diagnostic pop +} + +static bool IsSigIgn(struct sigaction* action) +{ + assert(action); + return !IsSaSigInfo(action) && action->sa_handler == SIG_IGN; +} + +static bool IsSigDfl(struct sigaction* action) +{ + assert(action); + return !IsSaSigInfo(action) && action->sa_handler == SIG_DFL; +} + +static bool TryConvertSignalCodeToPosixSignal(int signalCode, PosixSignal* posixSignal) +{ + assert(posixSignal != NULL); + + switch (signalCode) { - case SIGINT: return &g_origSigIntHandler; - case SIGQUIT: return &g_origSigQuitHandler; - case SIGCONT: return &g_origSigContHandler; - case SIGCHLD: return &g_origSigChldHandler; - case SIGWINCH: return &g_origSigWinchHandler; + case SIGHUP: + *posixSignal = PosixSignalSIGHUP; + return true; + + case SIGINT: + *posixSignal = PosixSignalSIGINT; + return true; + + case SIGQUIT: + *posixSignal = PosixSignalSIGQUIT; + return true; + + case SIGTERM: + *posixSignal = PosixSignalSIGTERM; + return true; + + case SIGCHLD: + *posixSignal = PosixSignalSIGCHLD; + return true; + + case SIGWINCH: + *posixSignal = PosixSignalSIGWINCH; + return true; + + case SIGCONT: + *posixSignal = PosixSignalSIGCONT; + return true; + + case SIGTTIN: + *posixSignal = PosixSignalSIGTTIN; + return true; + + case SIGTTOU: + *posixSignal = PosixSignalSIGTTOU; + return true; + + case SIGTSTP: + *posixSignal = PosixSignalSIGTSTP; + return true; + + default: + *posixSignal = signalCode; + return false; + } +} + +int32_t SystemNative_GetPlatformSignalNumber(PosixSignal signal) +{ + switch (signal) + { + case PosixSignalSIGHUP: + return SIGHUP; + + case PosixSignalSIGINT: + return SIGINT; + + case PosixSignalSIGQUIT: + return SIGQUIT; + + case PosixSignalSIGTERM: + return SIGTERM; + + case PosixSignalSIGCHLD: + return SIGCHLD; + + case PosixSignalSIGWINCH: + return SIGWINCH; + + case PosixSignalSIGCONT: + return SIGCONT; + + case PosixSignalSIGTTIN: + return SIGTTIN; + + case PosixSignalSIGTTOU: + return SIGTTOU; + + case PosixSignalSIGTSTP: + return SIGTSTP; + + case PosixSignalInvalid: + break; } - assert(false); - return NULL; + if (signal > 0 && signal <= GetSignalMax()) + { + return signal; + } + + return 0; +} + +void SystemNative_SetPosixSignalHandler(PosixSignalHandler signalHandler) +{ + assert(signalHandler); + assert(g_posixSignalHandler == NULL || g_posixSignalHandler == signalHandler); + + g_posixSignalHandler = signalHandler; +} + +static struct sigaction* OrigActionFor(int sig) +{ + return &g_origSigHandler[sig - 1]; +} + +static void RestoreSignalHandler(int sig) +{ + g_handlerIsInstalled[sig - 1] = false; + sigaction(sig, OrigActionFor(sig), NULL); } static void SignalHandler(int sig, siginfo_t* siginfo, void* context) { - // Signal handler for signals where we want our background thread to do the real processing. - // It simply writes the signal code to a pipe that's read by the thread. + if (sig == SIGCONT) + { + ConsoleSigTtouHandler consoleTtouHandler = g_consoleTtouHandler; + if (consoleTtouHandler != NULL) + { + consoleTtouHandler(); + } + } + + // For these signals, the runtime original sa_sigaction/sa_handler will terminate the app. + // This termination can be canceled using the PosixSignal API. + // For other signals, we immediately invoke the original handler. + if (!IsCancelableTerminationSignal(sig)) + { + struct sigaction* origHandler = OrigActionFor(sig); + if (IsSaSigInfo(origHandler)) + { + assert(origHandler->sa_sigaction); + origHandler->sa_sigaction(sig, siginfo, context); + } + else if (origHandler->sa_handler != SIG_IGN && + origHandler->sa_handler != SIG_DFL) + { + origHandler->sa_handler(sig); + } + } + + // Perform further processing on background thread. + // Write the signal code to a pipe that's read by the thread. uint8_t signalCodeByte = (uint8_t)sig; ssize_t writtenBytes; while ((writtenBytes = write(g_signalPipe[1], &signalCodeByte, 1)) < 0 && errno == EINTR); @@ -52,19 +231,69 @@ static void SignalHandler(int sig, siginfo_t* siginfo, void* context) { abort(); // fatal error } +} - // Delegate to any saved handler we may have - // We assume the original SIGCHLD handler will not reap our children. - if (sig == SIGCONT || sig == SIGCHLD || sig == SIGWINCH) +int32_t SystemNative_HandleNonCanceledPosixSignal(int32_t signalCode, int32_t handlersDisposed) +{ + switch (signalCode) { - struct sigaction* origHandler = OrigActionFor(sig); - if (origHandler->sa_sigaction != NULL && - (void*)origHandler->sa_sigaction != (void*)SIG_DFL && - (void*)origHandler->sa_sigaction != (void*)SIG_IGN) - { - origHandler->sa_sigaction(sig, siginfo, context); - } + case SIGCONT: + // Default disposition is Continue. +#ifdef HAS_CONSOLE_SIGNALS + ReinitializeTerminal(); +#endif + break; + case SIGTSTP: + case SIGTTIN: + case SIGTTOU: + // Default disposition is Stop. + // no-op. + break; + case SIGCHLD: + // Default disposition is Ignore. + if (g_sigChldConsoleConfigurationDelayed) + { + g_sigChldConsoleConfigurationDelayed = false; + + assert(g_sigChldConsoleConfigurationCallback); + g_sigChldConsoleConfigurationCallback(); + } + break; + case SIGURG: + case SIGWINCH: + // Default disposition is Ignore. + // no-op. + break; + default: + // Default disposition is Terminate. + if (!IsCancelableTerminationSignal(signalCode) && !IsSigDfl(OrigActionFor(signalCode))) + { + // We've already called the original handler in SignalHandler. + break; + } + if (IsSigIgn(OrigActionFor(signalCode))) + { + // Original handler doesn't do anything. + break; + } + if (handlersDisposed && g_hasPosixSignalRegistrations[signalCode - 1]) + { + // New handlers got registered. + return 0; + } + // Restore and invoke the original handler. + pthread_mutex_lock(&lock); + { + RestoreSignalHandler(signalCode); + } + pthread_mutex_unlock(&lock); +#ifdef HAS_CONSOLE_SIGNALS + UninitializeTerminal(); +#endif + kill(getpid(), signalCode); + break; } + return 1; } // Entrypoint for the thread that handles signals where our handling @@ -106,25 +335,12 @@ static void* SignalHandlerLoop(void* arg) } } - if (signalCode == SIGQUIT || signalCode == SIGINT) - { - // We're now handling SIGQUIT and SIGINT. Invoke the callback, if we have one. - CtrlCallback callback = g_ctrlCallback; - CtrlCode ctrlCode = signalCode == SIGQUIT ? Break : Interrupt; - if (callback != NULL) - { - callback(ctrlCode); - } - else - { - SystemNative_RestoreAndHandleCtrl(ctrlCode); - } - } - else if (signalCode == SIGCHLD) + bool usePosixSignalHandler = g_hasPosixSignalRegistrations[signalCode - 1]; + if (signalCode == SIGCHLD) { // When the original disposition is SIG_IGN, children that terminated did not become zombies. // Since we overwrote the disposition, we have become responsible for reaping those processes. - bool reapAll = (void*)OrigActionFor(signalCode)->sa_sigaction == (void*)SIG_IGN; + bool reapAll = IsSigIgn(OrigActionFor(signalCode)); SigChldCallback callback = g_sigChldCallback; // double-checked locking @@ -149,18 +365,27 @@ static void* SignalHandlerLoop(void* arg) if (callback != NULL) { - callback(reapAll ? 1 : 0); + if (callback(reapAll ? 1 : 0, usePosixSignalHandler ? 0 : 1 /* configureConsole */)) + { + g_sigChldConsoleConfigurationDelayed = true; + } } } - else if (signalCode == SIGCONT) + + if (usePosixSignalHandler) { -#ifdef HAS_CONSOLE_SIGNALS - ReinitializeTerminal(); -#endif + assert(g_posixSignalHandler != NULL); + PosixSignal signal; + if (!TryConvertSignalCodeToPosixSignal(signalCode, &signal)) + { + signal = PosixSignalInvalid; + } + usePosixSignalHandler = g_posixSignalHandler(signalCode, signal) != 0; } - else if (signalCode != SIGWINCH) + + if (!usePosixSignalHandler) { - assert_msg(false, "invalid signalCode", (int)signalCode); + SystemNative_HandleNonCanceledPosixSignal(signalCode, 0); } } } @@ -175,72 +400,102 @@ static void CloseSignalHandlingPipe() g_signalPipe[1] = -1; } -void SystemNative_RegisterForCtrl(CtrlCallback callback) +static bool InstallSignalHandler(int sig, int flags) { - assert(callback != NULL); - assert(g_ctrlCallback == NULL); - g_ctrlCallback = callback; -} + int rv; + struct sigaction* orig = OrigActionFor(sig); + bool* isInstalled = &g_handlerIsInstalled[sig - 1]; -void SystemNative_UnregisterForCtrl() -{ - assert(g_ctrlCallback != NULL); - g_ctrlCallback = NULL; -} + if (*isInstalled) + { + // Already installed. + return true; + } -void SystemNative_RestoreAndHandleCtrl(CtrlCode ctrlCode) -{ - int signalCode = ctrlCode == Break ? SIGQUIT : SIGINT; -#ifdef HAS_CONSOLE_SIGNALS - UninitializeTerminal(); -#endif - sigaction(signalCode, OrigActionFor(signalCode), NULL); - kill(getpid(), signalCode); + // We respect ignored signals. + // Setting up a handler for them causes child processes to reset to the + // default handler on exec, which means they will terminate on some signals + // which were set to ignore. + rv = sigaction(sig, NULL, orig); + if (rv != 0) + { + return false; + } + if (IsSigIgn(orig)) + { + *isInstalled = true; + return true; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wsign-conversion" // sa_flags is unsigned on Android. + struct sigaction newAction; + if (!IsSigDfl(orig)) + { + // Maintain flags and mask of original handler. + newAction = *orig; + newAction.sa_flags = orig->sa_flags & ~(SA_RESTART | SA_RESETHAND); + } + else + { + memset(&newAction, 0, sizeof(struct sigaction)); + } + newAction.sa_flags |= flags | SA_SIGINFO; +#pragma clang diagnostic pop + newAction.sa_sigaction = &SignalHandler; + + rv = sigaction(sig, &newAction, orig); + if (rv != 0) + { + return false; + } + *isInstalled = true; + return true; } void SystemNative_SetTerminalInvalidationHandler(TerminalInvalidationCallback callback) { assert(callback != NULL); assert(g_terminalInvalidationCallback == NULL); - g_terminalInvalidationCallback = callback; + bool installed; + (void)installed; // only used for assert + + pthread_mutex_lock(&lock); + { + g_terminalInvalidationCallback = callback; + + installed = InstallSignalHandler(SIGCONT, SA_RESTART); + assert(installed); + installed = InstallSignalHandler(SIGCHLD, SA_RESTART); + assert(installed); + installed = InstallSignalHandler(SIGWINCH, SA_RESTART); + assert(installed); + } + pthread_mutex_unlock(&lock); } void SystemNative_RegisterForSigChld(SigChldCallback callback) { assert(callback != NULL); assert(g_sigChldCallback == NULL); + bool installed; + (void)installed; // only used for assert pthread_mutex_lock(&lock); { g_sigChldCallback = callback; + + installed = InstallSignalHandler(SIGCHLD, SA_RESTART); + assert(installed); } pthread_mutex_unlock(&lock); } -static void InstallSignalHandler(int sig, bool skipWhenSigIgn) +void SystemNative_SetDelayedSigChildConsoleConfigurationHandler(void (*callback)(void)) { - int rv; - (void)rv; // only used for assert - struct sigaction* orig = OrigActionFor(sig); - - if (skipWhenSigIgn) - { - rv = sigaction(sig, NULL, orig); - assert(rv == 0); - if ((void*)orig->sa_sigaction == (void*)SIG_IGN) - { - return; - } - } - - struct sigaction newAction; - memset(&newAction, 0, sizeof(struct sigaction)); - newAction.sa_flags = SA_RESTART | SA_SIGINFO; - sigemptyset(&newAction.sa_mask); - newAction.sa_sigaction = &SignalHandler; + assert(g_sigChldConsoleConfigurationCallback == NULL); - rv = sigaction(sig, &newAction, orig); - assert(rv == 0); + g_sigChldConsoleConfigurationCallback = callback; } static bool CreateSignalHandlerThread(int* readFdPtr) @@ -276,6 +531,24 @@ static bool CreateSignalHandlerThread(int* readFdPtr) int32_t InitializeSignalHandlingCore() { + size_t signalMax = (size_t)GetSignalMax(); + g_origSigHandler = (struct sigaction*)calloc(sizeof(struct sigaction), signalMax); + g_handlerIsInstalled = (bool*)calloc(sizeof(bool), signalMax); + g_hasPosixSignalRegistrations = (bool*)calloc(sizeof(bool), signalMax); + if (g_origSigHandler == NULL || + g_handlerIsInstalled == NULL || + g_hasPosixSignalRegistrations == NULL) + { + free(g_origSigHandler); + free(g_handlerIsInstalled); + free((void*)(size_t)g_hasPosixSignalRegistrations); + g_origSigHandler = NULL; + g_handlerIsInstalled = NULL; + g_hasPosixSignalRegistrations = NULL; + errno = ENOMEM; + return 0; + } + // Create a pipe we'll use to communicate with our worker // thread. We can't do anything interesting in the signal handler, // so we instead send a message to another thread that'll do @@ -308,23 +581,104 @@ int32_t InitializeSignalHandlingCore() return 0; } - // Finally, register our signal handlers - // We don't handle ignored SIGINT/SIGQUIT signals. If we'd setup a handler, our child - // processes would reset to the default on exec causing them to terminate on these signals. - InstallSignalHandler(SIGINT , /* skipWhenSigIgn */ true); - InstallSignalHandler(SIGQUIT, /* skipWhenSigIgn */ true); - InstallSignalHandler(SIGCONT, /* skipWhenSigIgn */ false); - InstallSignalHandler(SIGCHLD, /* skipWhenSigIgn */ false); - InstallSignalHandler(SIGWINCH, /* skipWhenSigIgn */ false); - return 1; } +int32_t SystemNative_EnablePosixSignalHandling(int signalCode) +{ + assert(g_posixSignalHandler != NULL); + assert(signalCode > 0 && signalCode <= GetSignalMax()); + + bool installed; + pthread_mutex_lock(&lock); + { + installed = InstallSignalHandler(signalCode, SA_RESTART); + + g_hasPosixSignalRegistrations[signalCode - 1] = installed; + } + pthread_mutex_unlock(&lock); + + return installed ? 1 : 0; +} + +void SystemNative_DisablePosixSignalHandling(int signalCode) +{ + assert(signalCode > 0 && signalCode <= GetSignalMax()); + + pthread_mutex_lock(&lock); + { + g_hasPosixSignalRegistrations[signalCode - 1] = false; + + if (!(g_consoleTtouHandler && signalCode == SIGTTOU) && + !(g_sigChldCallback && signalCode == SIGCHLD) && + !(g_terminalInvalidationCallback && (signalCode == SIGCONT || + signalCode == SIGCHLD || + signalCode == SIGWINCH))) + { + RestoreSignalHandler(signalCode); + } + } + pthread_mutex_unlock(&lock); +} + +void InstallTTOUHandlerForConsole(ConsoleSigTtouHandler handler) +{ + bool installed; + + pthread_mutex_lock(&lock); + { + assert(g_consoleTtouHandler == NULL); + g_consoleTtouHandler = handler; + + // When the process is running in background, changing terminal settings + // will stop it (default SIGTTOU action). + // We change SIGTTOU's disposition to get EINTR instead. + // This thread may be used to run a signal handler, which may write to + // stdout. We set SA_RESETHAND to avoid that handler's write loops infinitly + // on EINTR when the process is running in background and the terminal + // configured with TOSTOP. + RestoreSignalHandler(SIGTTOU); + installed = InstallSignalHandler(SIGTTOU, (int)SA_RESETHAND); + assert(installed); + } + pthread_mutex_unlock(&lock); +} + +void UninstallTTOUHandlerForConsole(void) +{ + bool installed; + (void)installed; // only used for assert + pthread_mutex_lock(&lock); + { + g_consoleTtouHandler = NULL; + + RestoreSignalHandler(SIGTTOU); + if (g_hasPosixSignalRegistrations[SIGTTOU - 1]) + { + installed = InstallSignalHandler(SIGTTOU, SA_RESTART); + assert(installed); + } + } + pthread_mutex_unlock(&lock); +} + #ifndef HAS_CONSOLE_SIGNALS int32_t SystemNative_InitializeTerminalAndSignalHandling() { - return 0; + static int32_t initialized = 0; + + // The Process, Console and PosixSignalRegistration classes call this method for initialization. + if (pthread_mutex_lock(&lock) == 0) + { + if (initialized == 0) + { + initialized = InitializeSignalHandlingCore(); + } + pthread_mutex_unlock(&lock); + } + + return initialized; } #endif diff --git a/src/libraries/Native/Unix/System.Native/pal_signal.h b/src/libraries/Native/Unix/System.Native/pal_signal.h index 6b0882c0413e1..138f37835a8ca 100644 --- a/src/libraries/Native/Unix/System.Native/pal_signal.h +++ b/src/libraries/Native/Unix/System.Native/pal_signal.h @@ -13,52 +13,81 @@ */ int32_t InitializeSignalHandlingCore(void); +typedef int32_t (*SigChldCallback)(int32_t reapAll, int32_t configureConsole); + /** - * Hooks up the specified callback for notifications when SIGINT or SIGQUIT is received. - * - * Not thread safe. Caller must provide its owns synchronization to ensure RegisterForCtrl - * is not called concurrently with itself or with UnregisterForCtrl. + * Hooks up the specified callback for notifications when SIGCHLD is received. * * Should only be called when a callback is not currently registered. */ -PALEXPORT void SystemNative_RegisterForCtrl(CtrlCallback callback); +PALEXPORT void SystemNative_RegisterForSigChld(SigChldCallback callback); + +typedef void (*TerminalInvalidationCallback)(void); + +PALEXPORT void SystemNative_SetDelayedSigChildConsoleConfigurationHandler(void (*callback)(void)); /** - * Unregisters the previously registered ctrlCCallback. - * - * Not thread safe. Caller must provide its owns synchronization to ensure UnregisterForCtrl - * is not called concurrently with itself or with RegisterForCtrl. - * - * Should only be called when a callback is currently registered. The pointer - * previously registered must remain valid until all ctrl handling activity - * has quiesced. + * Hooks up the specified callback for notifications when SIGCHLD, SIGCONT, SIGWINCH are received. + * */ -PALEXPORT void SystemNative_UnregisterForCtrl(void); +PALEXPORT void SystemNative_SetTerminalInvalidationHandler(TerminalInvalidationCallback callback); -typedef void (*SigChldCallback)(int reapAll); +typedef enum +{ + PosixSignalInvalid = 0, + PosixSignalSIGHUP = -1, + PosixSignalSIGINT = -2, + PosixSignalSIGQUIT = -3, + PosixSignalSIGTERM = -4, + PosixSignalSIGCHLD = -5, + PosixSignalSIGWINCH = -6, + PosixSignalSIGCONT = -7, + PosixSignalSIGTTIN = -8, + PosixSignalSIGTTOU = -9, + PosixSignalSIGTSTP = -10 +} PosixSignal; + +typedef int32_t (*PosixSignalHandler)(int32_t signalCode, PosixSignal signal); /** - * Hooks up the specified callback for notifications when SIGCHLD is received. + * Hooks up the specified callback for handling PosixSignalRegistrations. * * Should only be called when a callback is not currently registered. */ -PALEXPORT void SystemNative_RegisterForSigChld(SigChldCallback callback); +PALEXPORT void SystemNative_SetPosixSignalHandler(PosixSignalHandler signalHandler); /** - * Remove our handler and reissue the signal to be picked up by the previously registered handler. - * - * In the most common case, this will be the default handler, causing the process to be torn down. - * It could also be a custom handler registered by other code before us. + * Converts a PosixSignal value to the platform signal number. + * When the signal is out of range, the function returns zero. */ -PALEXPORT void SystemNative_RestoreAndHandleCtrl(CtrlCode ctrlCode); +PALEXPORT int32_t SystemNative_GetPlatformSignalNumber(PosixSignal signal); -typedef void (*TerminalInvalidationCallback)(void); +/** + * Enables calling the PosixSignalHandler for the specified signal. + */ +PALEXPORT int32_t SystemNative_EnablePosixSignalHandling(int signalCode); /** - * Hooks up the specified callback for notifications when SIGCHLD, SIGCONT, SIGWINCH are received. - * + * Disables calling the PosixSignalHandler for the specified signal. */ -PALEXPORT void SystemNative_SetTerminalInvalidationHandler(TerminalInvalidationCallback callback); +PALEXPORT void SystemNative_DisablePosixSignalHandling(int signalCode); + +/** + * Performs the default runtime action for a non-canceled PosixSignal. + */ +PALEXPORT int32_t SystemNative_HandleNonCanceledPosixSignal(int32_t signalCode, int32_t handlersDisposed); + +typedef void (*ConsoleSigTtouHandler)(void); + +/** + * Hooks up callback to be called from the signal handler directly on SIGTTOU. + */ +void InstallTTOUHandlerForConsole(ConsoleSigTtouHandler handler); + +/** + * Uninstalls the SIGTTOU handler. + */ +void UninstallTTOUHandlerForConsole(void); #ifndef HAS_CONSOLE_SIGNALS diff --git a/src/libraries/System.Console/src/System.Console.csproj b/src/libraries/System.Console/src/System.Console.csproj index b4b5eb0bcb247..87a0fc8b164bc 100644 --- a/src/libraries/System.Console/src/System.Console.csproj +++ b/src/libraries/System.Console/src/System.Console.csproj @@ -196,10 +196,6 @@ Link="Common\Interop\Unix\Interop.GetEUid.cs" /> - - - - - diff --git a/src/libraries/System.Console/src/System/Console.cs b/src/libraries/System.Console/src/System/Console.cs index 6bbcb9f0dea41..097e4ac9b80a5 100644 --- a/src/libraries/System.Console/src/System/Console.cs +++ b/src/libraries/System.Console/src/System/Console.cs @@ -960,7 +960,7 @@ public static void Write(string? value) Out.Write(value); } - internal static bool HandleBreakEvent(ConsoleSpecialKey controlKey) + internal static bool HandleBreakEvent(ConsoleSpecialKey controlKey, bool cancel = false) { ConsoleCancelEventHandler? handler = s_cancelCallbacks; if (handler == null) @@ -969,6 +969,7 @@ internal static bool HandleBreakEvent(ConsoleSpecialKey controlKey) } var args = new ConsoleCancelEventArgs(controlKey); + args.Cancel = cancel; handler(null, args); return args.Cancel; } diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs index d581ae795ce5b..c438413c801cd 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs @@ -1444,50 +1444,32 @@ public override void Flush() internal sealed class ControlCHandlerRegistrar { - private bool _handlerRegistered; + private PosixSignalRegistration? _sigIntRegistration; + private PosixSignalRegistration? _sigQuitRegistration; internal unsafe void Register() { - Debug.Assert(s_initialized); // by CancelKeyPress add. + Debug.Assert(_sigIntRegistration is null); - Debug.Assert(!_handlerRegistered); - Interop.Sys.RegisterForCtrl(&OnBreakEvent); - _handlerRegistered = true; + _sigIntRegistration = PosixSignalRegistration.Create(PosixSignal.SIGINT, HandlePosixSignal); + _sigQuitRegistration = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, HandlePosixSignal); } internal void Unregister() { - Debug.Assert(_handlerRegistered); - _handlerRegistered = false; - Interop.Sys.UnregisterForCtrl(); - } + Debug.Assert(_sigIntRegistration is not null); - [UnmanagedCallersOnly] - private static void OnBreakEvent(Interop.Sys.CtrlCode ctrlCode) - { - // This is called on the native signal handling thread. We need to move to another thread so - // signal handling is not blocked. Otherwise we may get deadlocked when the handler depends - // on work triggered from the signal handling thread. - // We use a new thread rather than queueing to the ThreadPool in order to prioritize handling - // in case the ThreadPool is saturated. - Thread handlerThread = new Thread(HandleBreakEvent) - { - IsBackground = true, - Name = ".NET Console Break" - }; - handlerThread.Start(ctrlCode); + _sigIntRegistration?.Dispose(); + _sigQuitRegistration?.Dispose(); } - private static void HandleBreakEvent(object? state) + private static void HandlePosixSignal(PosixSignalContext ctx) { - Debug.Assert(state != null); - var ctrlCode = (Interop.Sys.CtrlCode)state; - ConsoleSpecialKey controlKey = (ctrlCode == Interop.Sys.CtrlCode.Break ? ConsoleSpecialKey.ControlBreak : ConsoleSpecialKey.ControlC); - bool cancel = Console.HandleBreakEvent(controlKey); - if (!cancel) - { - Interop.Sys.RestoreAndHandleCtrl(ctrlCode); - } + Debug.Assert(ctx.Signal == PosixSignal.SIGINT || ctx.Signal == PosixSignal.SIGQUIT); + + ConsoleSpecialKey controlKey = ctx.Signal == PosixSignal.SIGINT ? ConsoleSpecialKey.ControlC : ConsoleSpecialKey.ControlBreak; + bool cancel = Console.HandleBreakEvent(controlKey, ctx.Cancel); + ctx.Cancel = cancel; } } diff --git a/src/libraries/System.Console/tests/ManualTests/ManualTests.cs b/src/libraries/System.Console/tests/ManualTests/ManualTests.cs index cf0ea897254d3..6c4b0195fc568 100644 --- a/src/libraries/System.Console/tests/ManualTests/ManualTests.cs +++ b/src/libraries/System.Console/tests/ManualTests/ManualTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; using System.Threading.Tasks; using System.IO; using Xunit; @@ -243,6 +244,33 @@ public static void CursorPositionAndArrowKeys() AssertUserExpectedResults("the arrow keys move around the screen as expected with no other bad artifacts"); } + [ConditionalFact(nameof(ManualTestsEnabled))] + [PlatformSpecific(TestPlatforms.AnyUnix)] // .NET echo handling is Unix specific. + public static void EchoWorksDuringAndAfterProcessThatUsesTerminal() + { + Console.WriteLine($"Please type \"test\" without the quotes and press Enter."); + string line = Console.ReadLine(); + Assert.Equal("test", line); + AssertUserExpectedResults("the characters you typed properly echoed as you typed"); + + Console.WriteLine($"Now type \"test\" without the quotes and press Ctrl+D twice."); + using Process p = Process.Start(new ProcessStartInfo + { + FileName = "cat", + RedirectStandardOutput = true, + }); + string stdout = p.StandardOutput.ReadToEnd(); + p.WaitForExit(); + Assert.Equal("test", stdout); + Console.WriteLine(); + AssertUserExpectedResults("the characters you typed properly echoed as you typed"); + + Console.WriteLine($"Please type \"test\" without the quotes and press Enter."); + line = Console.ReadLine(); + Assert.Equal("test", line); + AssertUserExpectedResults("the characters you typed properly echoed as you typed"); + } + [ConditionalFact(nameof(ManualTestsEnabled))] public static void EncodingTest() { diff --git a/src/libraries/System.Console/tests/ManualTests/System.Console.Manual.Tests.csproj b/src/libraries/System.Console/tests/ManualTests/System.Console.Manual.Tests.csproj index e1daf6a964ea9..ddd698d0d0200 100644 --- a/src/libraries/System.Console/tests/ManualTests/System.Console.Manual.Tests.csproj +++ b/src/libraries/System.Console/tests/ManualTests/System.Console.Manual.Tests.csproj @@ -6,4 +6,7 @@ + + + \ No newline at end of file diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index cdd65c22c4431..424a7a87b31c6 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -257,6 +257,8 @@ Link="Common\Interop\Unix\Interop.ReadLink.cs" /> + - + + + + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs new file mode 100644 index 0000000000000..b51957fb881bf --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Runtime.InteropServices; + +namespace System.Diagnostics +{ + public partial class Process + { + private static int s_childrenUsingTerminalCount; + + internal static void ConfigureTerminalForChildProcesses(int increment, bool configureConsole = true) + { + Debug.Assert(increment != 0); + + int childrenUsingTerminalRemaining = Interlocked.Add(ref s_childrenUsingTerminalCount, increment); + if (increment > 0) + { + Debug.Assert(s_processStartLock.IsReadLockHeld); + Debug.Assert(configureConsole); + + // At least one child is using the terminal. + Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: true); + } + else + { + Debug.Assert(s_processStartLock.IsWriteLockHeld); + + if (childrenUsingTerminalRemaining == 0 && configureConsole) + { + // No more children are using the terminal. + Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: false); + } + } + } + + private static unsafe void SetDelayedSigChildConsoleConfigurationHandler() + { + Interop.Sys.SetDelayedSigChildConsoleConfigurationHandler(&DelayedSigChildConsoleConfiguration); + } + + [UnmanagedCallersOnly] + private static void DelayedSigChildConsoleConfiguration() + { + // Lock to avoid races with Process.Start + s_processStartLock.EnterWriteLock(); + try + { + if (s_childrenUsingTerminalCount == 0) + { + // No more children are using the terminal. + Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: false); + } + } + finally + { + s_processStartLock.ExitWriteLock(); + } + } + + private static bool AreChildrenUsingTerminal => s_childrenUsingTerminalCount > 0; + } +} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.iOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.iOS.cs new file mode 100644 index 0000000000000..3d9f85f7a4301 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.iOS.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; + +namespace System.Diagnostics +{ + public partial class Process + { + /// These methods are used on other Unix systems to track how many children use the terminal, + /// and update the terminal configuration when necessary. + + internal static void ConfigureTerminalForChildProcesses(int increment, bool configureConsole = true) + { } + + private static unsafe void SetDelayedSigChildConsoleConfigurationHandler() + { } + + private static bool AreChildrenUsingTerminal => false; + } +} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.ConfigureTerminalForChildProcesses.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.ConfigureTerminalForChildProcesses.cs deleted file mode 100644 index 6f84319b0c096..0000000000000 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.ConfigureTerminalForChildProcesses.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Threading; - -namespace System.Diagnostics -{ - public partial class Process - { - private static int s_childrenUsingTerminalCount; - - static partial void ConfigureTerminalForChildProcessesInner(int increment) - { - Debug.Assert(increment != 0); - - int childrenUsingTerminalRemaining = Interlocked.Add(ref s_childrenUsingTerminalCount, increment); - if (increment > 0) - { - Debug.Assert(s_processStartLock.IsReadLockHeld); - - // At least one child is using the terminal. - Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: true); - } - else - { - Debug.Assert(s_processStartLock.IsWriteLockHeld); - - if (childrenUsingTerminalRemaining == 0) - { - // No more children are using the terminal. - Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: false); - } - } - } - } -} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs index 1b52e4d141ebb..cce32e3760109 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs @@ -1023,6 +1023,7 @@ private static unsafe void EnsureInitialized() // Register our callback. Interop.Sys.RegisterForSigChld(&OnSigChild); + SetDelayedSigChildConsoleConfigurationHandler(); s_initialized = true; } @@ -1030,29 +1031,29 @@ private static unsafe void EnsureInitialized() } [UnmanagedCallersOnly] - private static void OnSigChild(int reapAll) + private static int OnSigChild(int reapAll, int configureConsole) { + // configureConsole is non zero when there are PosixSignalRegistrations that + // may Cancel the terminal configuration that happens when there are no more + // children using the terminal. + // When the registrations don't cancel the terminal configuration, + // DelayedSigChildConsoleConfiguration will be called. + // Lock to avoid races with Process.Start s_processStartLock.EnterWriteLock(); try { - ProcessWaitState.CheckChildren(reapAll != 0); + bool childrenUsingTerminalPre = AreChildrenUsingTerminal; + ProcessWaitState.CheckChildren(reapAll != 0, configureConsole != 0); + bool childrenUsingTerminalPost = AreChildrenUsingTerminal; + + // return whether console configuration was skipped. + return childrenUsingTerminalPre && !childrenUsingTerminalPost && configureConsole == 0 ? 1 : 0; } finally { s_processStartLock.ExitWriteLock(); } } - - /// - /// This method is called when the number of child processes that are using the terminal changes. - /// It updates the terminal configuration if necessary. - /// - internal static void ConfigureTerminalForChildProcesses(int increment) - { - ConfigureTerminalForChildProcessesInner(increment); - } - - static partial void ConfigureTerminalForChildProcessesInner(int increment); } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs index 03e93a725c4b7..482a627918fe4 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs @@ -552,7 +552,7 @@ private Task WaitForExitAsync(CancellationToken cancellationToken = default) }, cancellationToken); } - private bool TryReapChild() + private bool TryReapChild(bool configureConsole) { lock (_gate) { @@ -572,7 +572,7 @@ private bool TryReapChild() if (_usesTerminal) { // Update terminal settings before calling SetExited. - Process.ConfigureTerminalForChildProcesses(-1); + Process.ConfigureTerminalForChildProcesses(-1, configureConsole); } SetExited(); @@ -593,7 +593,7 @@ private bool TryReapChild() } } - internal static void CheckChildren(bool reapAll) + internal static void CheckChildren(bool reapAll, bool configureConsole) { // This is called on SIGCHLD from a native thread. // A lock in Process ensures no new processes are spawned while we are checking. @@ -612,7 +612,7 @@ internal static void CheckChildren(bool reapAll) if (s_childProcessWaitStates.TryGetValue(pid, out ProcessWaitState? pws)) { // Known Process. - if (pws.TryReapChild()) + if (pws.TryReapChild(configureConsole)) { pws.ReleaseRef(); } @@ -645,7 +645,7 @@ internal static void CheckChildren(bool reapAll) foreach (KeyValuePair kv in s_childProcessWaitStates) { ProcessWaitState pws = kv.Value; - if (pws.TryReapChild()) + if (pws.TryReapChild(configureConsole)) { if (firstToRemove == null) { diff --git a/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs b/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs index 02e739ad73173..8be17302389c1 100644 --- a/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs +++ b/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs @@ -796,6 +796,32 @@ public sealed partial class OptionalAttribute : System.Attribute { public OptionalAttribute() { } } + public enum PosixSignal + { + SIGTSTP = -10, + SIGTTOU = -9, + SIGTTIN = -8, + SIGWINCH = -7, + SIGCONT = -6, + SIGCHLD = -5, + SIGTERM = -4, + SIGQUIT = -3, + SIGINT = -2, + SIGHUP = -1, + } + public sealed partial class PosixSignalContext + { + public PosixSignalContext(System.Runtime.InteropServices.PosixSignal signal) { } + public bool Cancel { get { throw null; } set { } } + public System.Runtime.InteropServices.PosixSignal Signal { get { throw null; } } + } + public sealed partial class PosixSignalRegistration : System.IDisposable + { + internal PosixSignalRegistration() { } + public static System.Runtime.InteropServices.PosixSignalRegistration Create(System.Runtime.InteropServices.PosixSignal signal, System.Action handler) { throw null; } + public void Dispose() { } + ~PosixSignalRegistration() { } + } [System.AttributeUsageAttribute(System.AttributeTargets.Method, Inherited=false)] public sealed partial class PreserveSigAttribute : System.Attribute { diff --git a/src/libraries/System.Runtime.InteropServices/src/Resources/Strings.resx b/src/libraries/System.Runtime.InteropServices/src/Resources/Strings.resx index b65b6b5d5b1a2..7834b952cc9a9 100644 --- a/src/libraries/System.Runtime.InteropServices/src/Resources/Strings.resx +++ b/src/libraries/System.Runtime.InteropServices/src/Resources/Strings.resx @@ -75,4 +75,40 @@ Event invocation for COM objects requires event to be attributed with DispIdAttribute. + + The file '{0}' already exists. + + + Unable to find the specified file. + + + Could not find file '{0}'. + + + Could not find a part of the path. + + + Could not find a part of the path '{0}'. + + + The specified file name or path is too long, or a component of the specified path is too long. + + + The process cannot access the file '{0}' because it is being used by another process. + + + The process cannot access the file because it is being used by another process. + + + The path '{0}' is too long, or a component of the specified path is too long. + + + Access to the path '{0}' is denied. + + + Access to the path is denied. + + + Specified file length was too large for the file system. + \ No newline at end of file diff --git a/src/libraries/System.Runtime.InteropServices/src/System.Runtime.InteropServices.csproj b/src/libraries/System.Runtime.InteropServices/src/System.Runtime.InteropServices.csproj index 0565ac47a77c1..c6ba2fbe496fd 100644 --- a/src/libraries/System.Runtime.InteropServices/src/System.Runtime.InteropServices.csproj +++ b/src/libraries/System.Runtime.InteropServices/src/System.Runtime.InteropServices.csproj @@ -2,7 +2,7 @@ true enable - $(NetCoreAppCurrent) + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-Browser @@ -33,6 +33,8 @@ + + @@ -48,6 +50,25 @@ + + + + + + + + + + + + + + diff --git a/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignal.cs b/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignal.cs new file mode 100644 index 0000000000000..f1cb32a362b8d --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignal.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.InteropServices +{ + public enum PosixSignal + { + SIGHUP = -1, + SIGINT = -2, + SIGQUIT = -3, + SIGTERM = -4, + SIGCHLD = -5, + SIGCONT = -6, + SIGWINCH = -7, + SIGTTIN = -8, + SIGTTOU = -9, + SIGTSTP = -10 + } +} diff --git a/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignalContext.cs b/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignalContext.cs new file mode 100644 index 0000000000000..19ba4f9686849 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignalContext.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.InteropServices +{ + /// + /// Provides data for a event. + /// + public sealed class PosixSignalContext + { + /// + /// Initializes a new instance of the class. + /// + public PosixSignalContext(PosixSignal signal) + { + Signal = signal; + } + + /// + /// Gets the signal that occurred. + /// + public PosixSignal Signal + { + get; + internal set; + } + + /// + /// Gets or sets a value that indicates whether to cancel the default handling of the signal. The default is . + /// + public bool Cancel + { + get; + set; + } + + internal PosixSignalContext() + { } + } +} diff --git a/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignalRegistration.Browser.cs b/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignalRegistration.Browser.cs new file mode 100644 index 0000000000000..7c88a92cae944 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignalRegistration.Browser.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.Runtime.InteropServices +{ + public sealed class PosixSignalRegistration : IDisposable + { + private PosixSignalRegistration() { } + + public static PosixSignalRegistration Create(PosixSignal signal, Action handler) + => throw new PlatformNotSupportedException(); + + public void Dispose() + => throw new PlatformNotSupportedException(); + } +} diff --git a/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignalRegistration.Unix.cs b/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignalRegistration.Unix.cs new file mode 100644 index 0000000000000..9a3144f5e8163 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignalRegistration.Unix.cs @@ -0,0 +1,292 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; + +namespace System.Runtime.InteropServices +{ + /// + /// Handles a . + /// + public sealed class PosixSignalRegistration : IDisposable + { + private static volatile bool s_initialized; + private static readonly Dictionary?>> s_registrations = new(); + + private readonly Action _handler; + private readonly PosixSignal _signal; + private readonly int _signo; + private bool _registered; + private readonly object _gate = new object(); + + private PosixSignalRegistration(PosixSignal signal, int signo, Action handler) + { + _signal = signal; + _signo = signo; + _handler = handler; + } + + /// + /// Registers a that is invoked when the occurs. + /// + /// The signal to register for. + /// The handler that gets invoked. + /// A instance that can be disposed to unregister. + /// is . + /// is out the range of expected values for the platform. + /// An error occurred while setting up the signal handling or while installing the handler for the specified signal. + /// + /// Raw values can be provided for by casting them to . + /// + /// Default handling of the signal can be canceled through . + /// For SIGTERM, SIGINT, and SIGQUIT process termination can be canceled. + /// For SIGCHLD, and SIGCONT terminal configuration can be canceled. + /// + public static PosixSignalRegistration Create(PosixSignal signal, Action handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + int signo = Interop.Sys.GetPlatformSignalNumber(signal); + if (signo == 0) + { + throw new ArgumentOutOfRangeException(nameof(signal)); + } + PosixSignalRegistration registration = new PosixSignalRegistration(signal, signo, handler); + registration.Register(); + return registration; + } + + /// + /// Unregister the handler. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private unsafe void Register() + { + if (!s_initialized) + { + if (!Interop.Sys.InitializeTerminalAndSignalHandling()) + { + // We can't use Win32Exception because that causes a cycle with + // Microsoft.Win32.Primitives. + Interop.CheckIo(-1); + } + + Interop.Sys.SetPosixSignalHandler(&OnPosixSignal); + s_initialized = true; + } + lock (s_registrations) + { + if (!s_registrations.TryGetValue(_signo, out List?>? signalRegistrations)) + { + signalRegistrations = new List?>(); + s_registrations.Add(_signo, signalRegistrations); + } + + if (signalRegistrations.Count == 0) + { + if (!Interop.Sys.EnablePosixSignalHandling(_signo)) + { + // We can't use Win32Exception because that causes a cycle with + // Microsoft.Win32.Primitives. + Interop.CheckIo(-1); + } + } + + signalRegistrations.Add(new WeakReference(this)); + } + _registered = true; + } + + private bool CallHandler(PosixSignalContext context) + { + lock (_gate) + { + if (_registered) + { + _handler(context); + return true; + } + return false; + } + } + + [UnmanagedCallersOnly] + private static int OnPosixSignal(int signo, PosixSignal signal) + { + PosixSignalRegistration?[]? registrations = GetRegistrations(signo); + if (registrations != null) + { + // This is called on the native signal handling thread. We need to move to another thread so + // signal handling is not blocked. Otherwise we may get deadlocked when the handler depends + // on work triggered from the signal handling thread. + + // For terminate/interrupt signals we use a dedicated Thread + // in case the ThreadPool is saturated. + bool useDedicatedThread = signal == PosixSignal.SIGINT || + signal == PosixSignal.SIGQUIT || + signal == PosixSignal.SIGTERM; + if (useDedicatedThread) + { + Thread handlerThread = new Thread(HandleSignal) + { + IsBackground = true, + Name = ".NET Signal Handler" + }; + handlerThread.UnsafeStart((signo, registrations)); + } + else + { + ThreadPool.UnsafeQueueUserWorkItem(HandleSignal, (signo, registrations)); + } + return 1; + } + return 0; + } + + private static PosixSignalRegistration?[]? GetRegistrations(int signo) + { + lock (s_registrations) + { + if (s_registrations.TryGetValue(signo, out List?>? signalRegistrations)) + { + if (signalRegistrations.Count != 0) + { + var registrations = new PosixSignalRegistration?[signalRegistrations.Count]; + bool hasRegistrations = false; + bool pruneWeakReferences = false; + for (int i = 0; i < signalRegistrations.Count; i++) + { + if (signalRegistrations[i]!.TryGetTarget(out PosixSignalRegistration? registration)) + { + registrations[i] = registration; + hasRegistrations = true; + } + else + { + // WeakReference no longer holds an object. PosixSignalRegistration got finalized. + signalRegistrations[i] = null; + pruneWeakReferences = true; + } + } + + if (pruneWeakReferences) + { + signalRegistrations.RemoveAll(item => item is null); + } + + if (hasRegistrations) + { + return registrations; + } + else + { + Interop.Sys.DisablePosixSignalHandling(signo); + } + } + } + return null; + } + } + + private static void HandleSignal(object? state) + { + HandleSignal(((int, PosixSignalRegistration?[]))state!); + } + + private static void HandleSignal((int signo, PosixSignalRegistration?[]? registrations) state) + { + do + { + bool handlersCalled = false; + if (state.registrations != null) + { + PosixSignalContext ctx = new(); + foreach (PosixSignalRegistration? registration in state.registrations) + { + if (registration != null) + { + // Different values for PosixSignal map to the same signo. + // Match the PosixSignal value used when registering. + ctx.Signal = registration._signal; + if (registration.CallHandler(ctx)) + { + handlersCalled = true; + } + } + } + + if (ctx.Cancel) + { + return; + } + } + + if (Interop.Sys.HandleNonCanceledPosixSignal(state.signo, handlersCalled ? 0 : 1)) + { + return; + } + + // HandleNonCanceledPosixSignal returns false when handlers got registered. + state.registrations = GetRegistrations(state.signo); + } while (true); + } + + ~PosixSignalRegistration() + => Dispose(false); + + private void Dispose(bool disposing) + { + if (_registered) + { + lock (s_registrations) + { + List?> signalRegistrations = s_registrations[_signo]; + bool pruneWeakReferences = false; + for (int i = 0; i < signalRegistrations.Count; i++) + { + if (signalRegistrations[i]!.TryGetTarget(out PosixSignalRegistration? registration)) + { + if (object.ReferenceEquals(this, registration)) + { + signalRegistrations.RemoveAt(i); + break; + } + } + else + { + // WeakReference no longer holds an object. PosixSignalRegistration got finalized. + signalRegistrations[i] = null; + pruneWeakReferences = true; + } + } + + if (pruneWeakReferences) + { + signalRegistrations.RemoveAll(item => item is null); + } + + if (signalRegistrations.Count == 0) + { + Interop.Sys.DisablePosixSignalHandling(_signo); + } + } + + // Synchronize with _handler invocations. + lock (_gate) + { + _registered = false; + } + } + } + } +} diff --git a/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs b/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs new file mode 100644 index 0000000000000..7c88a92cae944 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.Runtime.InteropServices +{ + public sealed class PosixSignalRegistration : IDisposable + { + private PosixSignalRegistration() { } + + public static PosixSignalRegistration Create(PosixSignal signal, Action handler) + => throw new PlatformNotSupportedException(); + + public void Dispose() + => throw new PlatformNotSupportedException(); + } +} diff --git a/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.Tests.csproj b/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.Tests.csproj index 9ed8c6ff8a776..4bcb75b0b810c 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.Tests.csproj +++ b/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.Tests.csproj @@ -1,7 +1,7 @@ true - $(NetCoreAppCurrent);$(NetCoreAppCurrent)-windows + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-Browser true true @@ -150,6 +150,7 @@ + @@ -183,4 +184,9 @@ + + + + diff --git a/src/libraries/System.Runtime.InteropServices/tests/System/Runtime/InteropServices/PosixSignalContextTests.cs b/src/libraries/System.Runtime.InteropServices/tests/System/Runtime/InteropServices/PosixSignalContextTests.cs new file mode 100644 index 0000000000000..3d8ca635ac20f --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/tests/System/Runtime/InteropServices/PosixSignalContextTests.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.Runtime.InteropServices; + +namespace System.Tests +{ + public class PosixSignalContextTests + { + [Theory] + [InlineData(0)] + [InlineData(3)] + [InlineData(-1000)] + [InlineData(1000)] + public void Constructor(int value) + { + var ctx = new PosixSignalContext((PosixSignal)value); + Assert.Equal(value, (int)ctx.Signal); + } + } +} diff --git a/src/libraries/System.Runtime.InteropServices/tests/System/Runtime/InteropServices/PosixSignalRegistrationTests.Unix.cs b/src/libraries/System.Runtime.InteropServices/tests/System/Runtime/InteropServices/PosixSignalRegistrationTests.Unix.cs new file mode 100644 index 0000000000000..b006af50150d3 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/tests/System/Runtime/InteropServices/PosixSignalRegistrationTests.Unix.cs @@ -0,0 +1,236 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using Microsoft.DotNet.RemoteExecutor; + +namespace System.Tests +{ + public class PosixSignalRegistrationTests + { + private static TimeSpan Timeout => TimeSpan.FromSeconds(30); + + [Fact] + public void HandlerNullThrows() + { + Assert.Throws(() => PosixSignalRegistration.Create(PosixSignal.SIGCONT, null)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1000)] + [InlineData(1000)] + public void InvalidSignalValueThrows(int value) + { + Assert.Throws(() => PosixSignalRegistration.Create((PosixSignal)value, ctx => { })); + } + + [Theory] + [InlineData((PosixSignal)9)] // SIGKILL + public void UninstallableSignalsThrow(PosixSignal signal) + { + Assert.Throws(() => PosixSignalRegistration.Create(signal, ctx => { })); + } + + [Theory] + [MemberData(nameof(PosixSignalValues))] + public void CanRegisterForKnownValues(PosixSignal signal) + { + using var _ = PosixSignalRegistration.Create(signal, ctx => { }); + } + + [Theory] + [MemberData(nameof(PosixSignalValues))] + public void SignalHandlerCalledForKnownSignals(PosixSignal s) + { + RemoteExecutor.Invoke((signalStr) => { + PosixSignal signal = Enum.Parse(signalStr); + using SemaphoreSlim semaphore = new(0); + using var _ = PosixSignalRegistration.Create(signal, ctx => + { + Assert.Equal(signal, ctx.Signal); + + // Ensure signal doesn't cause the process to terminate. + ctx.Cancel = true; + + semaphore.Release(); + }); + kill(signal); + bool entered = semaphore.Wait(Timeout); + Assert.True(entered); + }, s.ToString()).Dispose(); + } + + [Theory] + [MemberData(nameof(PosixSignalAsRawValues))] + public void SignalHandlerCalledForRawSignals(PosixSignal s) + { + RemoteExecutor.Invoke((signalStr) => { + PosixSignal signal = Enum.Parse(signalStr); + using SemaphoreSlim semaphore = new(0); + using var _ = PosixSignalRegistration.Create(signal, ctx => + { + Assert.Equal(signal, ctx.Signal); + + // Ensure signal doesn't cause the process to terminate. + ctx.Cancel = true; + + semaphore.Release(); + }); + kill(signal); + bool entered = semaphore.Wait(Timeout); + Assert.True(entered); + }, s.ToString()).Dispose(); + } + + [Fact] + public void SignalHandlerWorksForSecondRegistration() + { + PosixSignal signal = PosixSignal.SIGCONT; + + for (int i = 0; i < 2; i++) + { + using SemaphoreSlim semaphore = new(0); + using var _ = PosixSignalRegistration.Create(signal, ctx => + { + Assert.Equal(signal, ctx.Signal); + + // Ensure signal doesn't cause the process to terminate. + ctx.Cancel = true; + + semaphore.Release(); + }); + kill(signal); + bool entered = semaphore.Wait(Timeout); + Assert.True(entered); + } + } + + [Fact] + public void SignalHandlerNotCalledWhenDisposed() + { + PosixSignal signal = PosixSignal.SIGCONT; + + using var registration = PosixSignalRegistration.Create(signal, ctx => + { + Assert.False(true, "Signal handler was called."); + }); + registration.Dispose(); + + kill(signal); + Thread.Sleep(100); + } + + [Fact] + public void SignalHandlerNotCalledWhenFinalized() + { + PosixSignal signal = PosixSignal.SIGCONT; + + CreateDanglingRegistration(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + kill(signal); + Thread.Sleep(100); + + [MethodImpl(MethodImplOptions.NoInlining)] + void CreateDanglingRegistration() + { + PosixSignalRegistration.Create(signal, ctx => + { + Assert.False(true, "Signal handler was called."); + }); + } + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(PosixSignal.SIGINT, true, 0)] + [InlineData(PosixSignal.SIGINT, false, 130)] + [InlineData(PosixSignal.SIGTERM, true, 0)] + [InlineData(PosixSignal.SIGTERM, false, 143)] + [InlineData(PosixSignal.SIGQUIT, true, 0)] + [InlineData(PosixSignal.SIGQUIT, false, 131)] + public void SignalCanCancelTermination(PosixSignal signal, bool cancel, int expectedExitCode) + { + // Mono doesn't restore and call SIG_DFL on SIGQUIT. + if (PlatformDetection.IsMonoRuntime && signal == PosixSignal.SIGQUIT && cancel == false) + { + expectedExitCode = 0; + } + + RemoteExecutor.Invoke((signalStr, cancelStr, expectedStr) => + { + PosixSignal signalArg = Enum.Parse(signalStr); + bool cancelArg = bool.Parse(cancelStr); + int expected = int.Parse(expectedStr); + + using SemaphoreSlim semaphore = new(0); + using var _ = PosixSignalRegistration.Create(signalArg, ctx => + { + ctx.Cancel = cancelArg; + + semaphore.Release(); + }); + + kill(signalArg); + + bool entered = semaphore.Wait(Timeout); + Assert.True(entered); + + // Give the default signal handler a chance to run. + Thread.Sleep(expected == 0 ? TimeSpan.FromSeconds(2) : TimeSpan.FromMinutes(10)); + + return 0; + }, signal.ToString(), cancel.ToString(), expectedExitCode.ToString(), + new RemoteInvokeOptions() { ExpectedExitCode = expectedExitCode, TimeOut = 10 * 60 * 1000 }).Dispose(); + } + + public static TheoryData PosixSignalValues + { + get + { + var data = new TheoryData(); + foreach (var value in Enum.GetValues(typeof(PosixSignal))) + { + data.Add((PosixSignal)value); + } + return data; + } + } + + public static TheoryData PosixSignalAsRawValues + { + get + { + var data = new TheoryData(); + foreach (var value in Enum.GetValues(typeof(PosixSignal))) + { + int signo = GetPlatformSignalNumber((PosixSignal)value); + Assert.True(signo > 0, "Expected raw signal number to be greater than 0."); + data.Add((PosixSignal)signo); + } + return data; + } + } + + [DllImport("libc", SetLastError = true)] + private static extern int kill(int pid, int sig); + + private static void kill(PosixSignal sig) + { + int signo = GetPlatformSignalNumber(sig); + Assert.NotEqual(0, signo); + int rv = kill(Environment.ProcessId, signo); + Assert.Equal(0, rv); + } + + [DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetPlatformSignalNumber")] + [SuppressGCTransition] + private static extern int GetPlatformSignalNumber(PosixSignal signal); + } +}