From 6cc1da640c995ced6339cd475910a71346789341 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Mon, 3 Nov 2025 15:48:11 +0100 Subject: [PATCH 1/2] Support ucrt-based signal handling on Windows --- Sources/UnixSignals/UnixSignal.swift | 37 ++++++++++++------- Sources/UnixSignals/UnixSignalsSequence.swift | 3 ++ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/Sources/UnixSignals/UnixSignal.swift b/Sources/UnixSignals/UnixSignal.swift index 9a0f9cb..c58cb6c 100644 --- a/Sources/UnixSignals/UnixSignal.swift +++ b/Sources/UnixSignals/UnixSignal.swift @@ -20,6 +20,8 @@ import Glibc import Musl #elseif canImport(Android) import Android +#elseif canImport(ucrt) +import ucrt #endif import Dispatch @@ -29,11 +31,13 @@ import Dispatch public struct UnixSignal: Hashable, Sendable, CustomStringConvertible { internal enum Wrapped { case sigabrt - case sighup case sigill case sigint case sigsegv case sigterm + #if !os(Windows) + case sighup + case sigquit case sigusr1 case sigusr2 case sigalrm @@ -41,6 +45,7 @@ public struct UnixSignal: Hashable, Sendable, CustomStringConvertible { case sigwinch case sigcont case sigpipe + #endif } private let wrapped: Wrapped @@ -58,18 +63,19 @@ public struct UnixSignal: Hashable, Sendable, CustomStringConvertible { /// Usually generated by the abort() function. Useful for cleanup prior to termination. public static let sigabrt = Self(.sigabrt) - /// Hang up detected on controlling terminal or death of controlling process. # ignore-unacceptable-language - public static let sighup = Self(.sighup) /// Issued if the user attempts to execute an illegal, malformed, or privileged instruction. public static let sigill = Self(.sigill) /// Issued if the user sends an interrupt signal. public static let sigint = Self(.sigint) - /// Issued if the user sends a quit signal. - public static let sigquit = Self(.sigquit) /// Issued if the user makes an invalid memory reference, such as dereferencing a null or invalid pointer. public static let sigsegv = Self(.sigsegv) /// Software termination signal. public static let sigterm = Self(.sigterm) + #if !os(Windows) + /// Hang up detected on controlling terminal or death of controlling process. # ignore-unacceptable-language + public static let sighup = Self(.sighup) + /// Issued if the user sends a quit signal. + public static let sigquit = Self(.sigquit) public static let sigusr1 = Self(.sigusr1) public static let sigusr2 = Self(.sigusr2) public static let sigalrm = Self(.sigalrm) @@ -78,6 +84,7 @@ public struct UnixSignal: Hashable, Sendable, CustomStringConvertible { public static let sigcont = Self(.sigcont) /// Signal when a write is performed on a closed fd public static let sigpipe = Self(.sigpipe) + #endif } extension UnixSignal.Wrapped: Hashable {} @@ -88,18 +95,19 @@ extension UnixSignal.Wrapped: CustomStringConvertible { switch self { case .sigabrt: return "SIGABRT" - case .sighup: - return "SIGHUP" case .sigill: return "SIGILL" case .sigint: return "SIGINT" - case .sigquit: - return "SIGQUIT" case .sigsegv: return "SIGSEGV" case .sigterm: return "SIGTERM" + #if !os(Windows) + case .sighup: + return "SIGHUP" + case .sigquit: + return "SIGQUIT" case .sigusr1: return "SIGUSR1" case .sigusr2: @@ -112,6 +120,7 @@ extension UnixSignal.Wrapped: CustomStringConvertible { return "SIGCONT" case .sigpipe: return "SIGPIPE" + #endif } } } @@ -121,18 +130,19 @@ extension UnixSignal.Wrapped { switch self { case .sigabrt: return SIGABRT - case .sighup: - return SIGHUP case .sigill: return SIGILL case .sigint: return SIGINT - case .sigquit: - return SIGQUIT case .sigsegv: return SIGSEGV case .sigterm: return SIGTERM + #if !os(Windows) + case .sighup: + return SIGHUP + case .sigquit: + return SIGQUIT case .sigusr1: return SIGUSR1 case .sigusr2: @@ -145,6 +155,7 @@ extension UnixSignal.Wrapped { return SIGCONT case .sigpipe: return SIGPIPE + #endif } } } diff --git a/Sources/UnixSignals/UnixSignalsSequence.swift b/Sources/UnixSignals/UnixSignalsSequence.swift index 4627792..637c1dd 100644 --- a/Sources/UnixSignals/UnixSignalsSequence.swift +++ b/Sources/UnixSignals/UnixSignalsSequence.swift @@ -24,6 +24,9 @@ import Musl #elseif canImport(Android) @preconcurrency import Dispatch import Android +#elseif canImport(ucrt) +@preconcurrency import ucrt +@preconcurrency import Dispatch #endif import ConcurrencyHelpers From 6627ca5a8821dbd7296ce5c8d1687c1cfd482b98 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Mon, 3 Nov 2025 16:00:18 +0100 Subject: [PATCH 2/2] Compile on windows using ucrt, disable tests that use signals not supported on Windows --- .../ServiceGroupAddServiceTests.swift | 8 ++++++++ Tests/ServiceLifecycleTests/ServiceGroupTests.swift | 6 +++++- Tests/UnixSignalsTests/UnixSignalTests.swift | 10 ++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Tests/ServiceLifecycleTests/ServiceGroupAddServiceTests.swift b/Tests/ServiceLifecycleTests/ServiceGroupAddServiceTests.swift index 9bed364..9436383 100644 --- a/Tests/ServiceLifecycleTests/ServiceGroupAddServiceTests.swift +++ b/Tests/ServiceLifecycleTests/ServiceGroupAddServiceTests.swift @@ -17,6 +17,10 @@ import ServiceLifecycle import UnixSignals import XCTest +#if canImport(ucrt) +import ucrt +#endif + private struct ExampleError: Error, Hashable {} @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -121,6 +125,7 @@ final class ServiceGroupAddServiceTests: XCTestCase { await XCTAsyncAssertEqual(await mockService2.hasRun, false) } + #if !os(Windows) func testRun_whenAddedServiceExitsEarly_andIgnore() async throws { let service1 = MockService(description: "Service1") let service2 = MockService(description: "Service2") @@ -153,6 +158,7 @@ final class ServiceGroupAddServiceTests: XCTestCase { try await XCTAsyncAssertNoThrow(await group.next()) } } + #endif func testRun_whenAddedServiceExitsEarly_andShutdownGracefully() async throws { let service1 = MockService(description: "Service1") @@ -207,6 +213,7 @@ final class ServiceGroupAddServiceTests: XCTestCase { } } + #if !os(Windows) func testRun_whenAddedServiceThrows() async throws { let service1 = MockService(description: "Service1") let service2 = MockService(description: "Service2") @@ -369,6 +376,7 @@ final class ServiceGroupAddServiceTests: XCTestCase { await service2.resumeRunContinuation(with: .success(())) } } + #endif // MARK: - Helpers diff --git a/Tests/ServiceLifecycleTests/ServiceGroupTests.swift b/Tests/ServiceLifecycleTests/ServiceGroupTests.swift index a936e9d..ff31004 100644 --- a/Tests/ServiceLifecycleTests/ServiceGroupTests.swift +++ b/Tests/ServiceLifecycleTests/ServiceGroupTests.swift @@ -81,6 +81,7 @@ final class ServiceGroupTests: XCTestCase { } } + #if !os(Windows) func test_whenRun_ShutdownGracefully() async throws { let mockService = MockService(description: "Service1") let serviceGroup = self.makeServiceGroup( @@ -694,7 +695,7 @@ final class ServiceGroupTests: XCTestCase { } } } - + func testGracefulShutdownOrdering_whenServiceThrows_andServiceGracefullyShutsdown() async throws { let service1 = MockService(description: "Service1") let service2 = MockService(description: "Service2") @@ -955,6 +956,7 @@ final class ServiceGroupTests: XCTestCase { await service1.resumeRunContinuation(with: .success(())) } } + #endif func testTriggerGracefulShutdown() async throws { let service1 = MockService(description: "Service1") @@ -1014,6 +1016,7 @@ final class ServiceGroupTests: XCTestCase { } } + #if !os(Windows) func testGracefulShutdownEscalation() async throws { let mockService = MockService(description: "Service1") let serviceGroup = self.makeServiceGroup( @@ -1130,6 +1133,7 @@ final class ServiceGroupTests: XCTestCase { try await XCTAsyncAssertNoThrow(await group.next()) } } + #endif func testTriggerGracefulShutdown_serviceThrows_inOrder_gracefullyShutdownGroup() async throws { let service1 = MockService(description: "Service1") diff --git a/Tests/UnixSignalsTests/UnixSignalTests.swift b/Tests/UnixSignalsTests/UnixSignalTests.swift index abea2da..70542ca 100644 --- a/Tests/UnixSignalsTests/UnixSignalTests.swift +++ b/Tests/UnixSignalsTests/UnixSignalTests.swift @@ -22,9 +22,12 @@ import Glibc import Musl #elseif canImport(Android) import Android +#elseif canImport(ucrt) +import ucrt #endif final class UnixSignalTests: XCTestCase { + #if !os(Windows) func testSingleSignal() async throws { let signal = UnixSignal.sigalrm let signals = await UnixSignalsSequence(trapping: signal) @@ -89,6 +92,7 @@ final class UnixSignalTests: XCTestCase { XCTAssertEqual(second, .cancelled) } } + #endif func testEmptySequence() async throws { let signals = await UnixSignalsSequence(trapping: []) @@ -97,6 +101,7 @@ final class UnixSignalTests: XCTestCase { } } + #if !os(Windows) func testCorrectSignalIsGiven() async throws { let signals = await UnixSignalsSequence(trapping: .sigterm, .sigusr1, .sigusr2, .sighup, .sigint, .sigalrm) var signalIterator = signals.makeAsyncIterator() @@ -110,7 +115,7 @@ final class UnixSignalTests: XCTestCase { XCTAssertEqual(trapped, signal) } } - + func testSignalRawValue() { func assert(_ signal: UnixSignal, rawValue: Int32) { XCTAssertEqual(signal.rawValue, rawValue) @@ -124,7 +129,7 @@ final class UnixSignalTests: XCTestCase { assert(.sigusr2, rawValue: SIGUSR2) assert(.sigterm, rawValue: SIGTERM) } - + func testSignalCustomStringConvertible() { func assert(_ signal: UnixSignal, description: String) { XCTAssertEqual(String(describing: signal), description) @@ -139,6 +144,7 @@ final class UnixSignalTests: XCTestCase { assert(.sigterm, description: "SIGTERM") assert(.sigwinch, description: "SIGWINCH") } + #endif func testCancelledTask() async throws { let task = Task {