diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d1bf7009..f885682e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -28,14 +28,14 @@ jobs: # Test dependencies yum install -y procps fi - linux_build_command: 'swift test && swift test --disable-default-traits' + linux_build_command: 'swift-format lint -s -r --configuration ./.swift-format . && swift test && swift test --disable-default-traits' windows_swift_versions: '["6.1", "nightly-main"]' windows_build_command: | Invoke-Program swift test Invoke-Program swift test --disable-default-traits enable_macos_checks: true macos_xcode_versions: '["16.3"]' - macos_build_command: 'xcrun swift test && xcrun swift test --disable-default-traits' + macos_build_command: 'xcrun swift-format lint -s -r --configuration ./.swift-format . && xcrun swift test && xcrun swift test --disable-default-traits' enable_linux_static_sdk_build: true linux_static_sdk_versions: '["6.1", "nightly-6.2"]' linux_static_sdk_build_command: | diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000..84d3719d --- /dev/null +++ b/.swift-format @@ -0,0 +1,74 @@ +{ + "fileScopedDeclarationPrivacy": { + "accessLevel": "private" + }, + "indentConditionalCompilationBlocks": false, + "indentSwitchCaseLabels": false, + "indentation": { + "spaces": 4 + }, + "lineBreakAroundMultilineExpressionChainComponents": false, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": false, + "lineBreakBeforeEachGenericRequirement": false, + "lineBreakBetweenDeclarationAttributes": false, + "lineLength": 9999, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "noAssignmentInExpressions": { + "allowedFunctions": [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether": false, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": false, + "AlwaysUseLowerCamelCase": false, + "AmbiguousTrailingClosureOverload": false, + "AvoidRetroactiveConformances": false, + "BeginDocumentationCommentWithOneLineSummary": false, + "DoNotUseSemicolons": false, + "DontRepeatTypeInStaticProperties": false, + "FileScopedDeclarationPrivacy": false, + "FullyIndirectEnum": false, + "GroupNumericLiterals": false, + "IdentifiersMustBeASCII": false, + "NeverForceUnwrap": false, + "NeverUseForceTry": false, + "NeverUseImplicitlyUnwrappedOptionals": false, + "NoAccessLevelOnExtensionDeclaration": false, + "NoAssignmentInExpressions": false, + "NoBlockComments": false, + "NoCasesWithOnlyFallthrough": false, + "NoEmptyLinesOpeningClosingBraces": false, + "NoEmptyTrailingClosureParentheses": false, + "NoLabelsInCasePatterns": false, + "NoLeadingUnderscores": false, + "NoParensAroundConditions": false, + "NoPlaygroundLiterals": false, + "NoVoidReturnOnFunctionSignature": false, + "OmitExplicitReturns": false, + "OneCasePerLine": false, + "OneVariableDeclarationPerLine": false, + "OnlyOneTrailingClosureArgument": false, + "OrderedImports": false, + "ReplaceForEachWithForLoop": false, + "ReturnVoidInsteadOfEmptyTuple": false, + "TypeNamesShouldBeCapitalized": false, + "UseEarlyExits": false, + "UseExplicitNilCheckInConditions": false, + "UseLetInEveryBoundCaseVariable": false, + "UseShorthandTypeNames": false, + "UseSingleLinePropertyGetter": false, + "UseSynthesizedInitializer": false, + "UseTripleSlashForDocumentationComments": false, + "UseWhereClausesInForLoops": false, + "ValidateDocumentationComments": false + }, + "spacesAroundRangeFormationOperators": false, + "spacesBeforeEndOfLineComments": 1, + "tabWidth": 4, + "version": 1 +} diff --git a/Package.swift b/Package.swift index ebf38b22..935b93ae 100644 --- a/Package.swift +++ b/Package.swift @@ -56,7 +56,7 @@ let package = Package( .product(name: "SystemPackage", package: "swift-system"), ], path: "Sources/Subprocess", - exclude: [ "CMakeLists.txt" ], + exclude: ["CMakeLists.txt"], swiftSettings: [ .enableExperimentalFeature("StrictConcurrency"), .enableExperimentalFeature("NonescapableTypes"), @@ -73,7 +73,7 @@ let package = Package( .product(name: "SystemPackage", package: "swift-system"), ], swiftSettings: [ - .enableExperimentalFeature("Span"), + .enableExperimentalFeature("Span") ] + packageSwiftSettings ), @@ -91,7 +91,7 @@ let package = Package( .target( name: "_SubprocessCShims", path: "Sources/_SubprocessCShims", - exclude: [ "CMakeLists.txt" ] + exclude: ["CMakeLists.txt"] ), ] ) diff --git a/Sources/Subprocess/API.swift b/Sources/Subprocess/API.swift index ee7ac0f5..62553091 100644 --- a/Sources/Subprocess/API.swift +++ b/Sources/Subprocess/API.swift @@ -133,8 +133,7 @@ public func run< standardError: result.value.standardError ) } -#endif // SubprocessSpan - +#endif // SubprocessSpan // MARK: - Custom Execution Body diff --git a/Sources/Subprocess/AsyncBufferSequence.swift b/Sources/Subprocess/AsyncBufferSequence.swift index 8fe78f1f..f365f718 100644 --- a/Sources/Subprocess/AsyncBufferSequence.swift +++ b/Sources/Subprocess/AsyncBufferSequence.swift @@ -192,10 +192,10 @@ extension AsyncBufferSequence { } // https://en.wikipedia.org/wiki/Newline#Unicode - let lineFeed = Encoding.CodeUnit(0x0A) + let lineFeed = Encoding.CodeUnit(0x0A) /// let verticalTab = Encoding.CodeUnit(0x0B) /// let formFeed = Encoding.CodeUnit(0x0C) - let carriageReturn = Encoding.CodeUnit(0x0D) + let carriageReturn = Encoding.CodeUnit(0x0D) // carriageReturn + lineFeed let newLine1: Encoding.CodeUnit let newLine2: Encoding.CodeUnit @@ -207,24 +207,24 @@ extension AsyncBufferSequence { let paragraphSeparator3: Encoding.CodeUnit switch Encoding.CodeUnit.self { case is UInt8.Type: - newLine1 = Encoding.CodeUnit(0xC2) - newLine2 = Encoding.CodeUnit(0x85) + newLine1 = Encoding.CodeUnit(0xC2) + newLine2 = Encoding.CodeUnit(0x85) - lineSeparator1 = Encoding.CodeUnit(0xE2) - lineSeparator2 = Encoding.CodeUnit(0x80) - lineSeparator3 = Encoding.CodeUnit(0xA8) + lineSeparator1 = Encoding.CodeUnit(0xE2) + lineSeparator2 = Encoding.CodeUnit(0x80) + lineSeparator3 = Encoding.CodeUnit(0xA8) paragraphSeparator1 = Encoding.CodeUnit(0xE2) paragraphSeparator2 = Encoding.CodeUnit(0x80) paragraphSeparator3 = Encoding.CodeUnit(0xA9) case is UInt16.Type, is UInt32.Type: // UTF16 and UTF32 use one byte for all - newLine1 = Encoding.CodeUnit(0x0085) - newLine2 = Encoding.CodeUnit(0x0085) + newLine1 = Encoding.CodeUnit(0x0085) + newLine2 = Encoding.CodeUnit(0x0085) - lineSeparator1 = Encoding.CodeUnit(0x2028) - lineSeparator2 = Encoding.CodeUnit(0x2028) - lineSeparator3 = Encoding.CodeUnit(0x2028) + lineSeparator1 = Encoding.CodeUnit(0x2028) + lineSeparator2 = Encoding.CodeUnit(0x2028) + lineSeparator3 = Encoding.CodeUnit(0x2028) paragraphSeparator1 = Encoding.CodeUnit(0x2029) paragraphSeparator2 = Encoding.CodeUnit(0x2029) @@ -285,7 +285,7 @@ extension AsyncBufferSequence { continue } return yield() - case lineFeed ..< carriageReturn, newLine1, lineSeparator1, paragraphSeparator1: + case lineFeed.. UnsafeMutablePointer { switch self { case .string(let string): -#if os(Windows) + #if os(Windows) return _strdup(string) -#else + #else return strdup(string) -#endif + #endif case .rawBytes(let rawBytes): -#if os(Windows) + #if os(Windows) return _strdup(rawBytes) -#else + #else return strdup(rawBytes) -#endif + #endif } } @@ -1026,11 +1026,10 @@ internal struct _OrderedSet: Hashable, Sendable { } } -extension _OrderedSet : Sequence { +extension _OrderedSet: Sequence { typealias Iterator = Array.Iterator internal func makeIterator() -> Iterator { return self.elements.makeIterator() } } - diff --git a/Sources/Subprocess/IO/AsyncIO+Dispatch.swift b/Sources/Subprocess/IO/AsyncIO+Dispatch.swift index 280fee91..3bebe122 100644 --- a/Sources/Subprocess/IO/AsyncIO+Dispatch.swift +++ b/Sources/Subprocess/IO/AsyncIO+Dispatch.swift @@ -27,7 +27,7 @@ final class AsyncIO: Sendable { internal init() {} - internal func shutdown() { /* noop on Darwin */ } + internal func shutdown() { /* noop on Darwin */ } internal func read( from diskIO: borrowing IOChannel, @@ -104,7 +104,7 @@ final class AsyncIO: Sendable { } } } - #endif // SubprocessSpan + #endif // SubprocessSpan internal func write( _ array: [UInt8], @@ -167,7 +167,7 @@ final class AsyncIO: Sendable { } #if !canImport(Darwin) -extension DispatchData: @retroactive @unchecked Sendable { } +extension DispatchData: @retroactive @unchecked Sendable {} #endif #endif diff --git a/Sources/Subprocess/IO/AsyncIO+Linux.swift b/Sources/Subprocess/IO/AsyncIO+Linux.swift index b04b747e..1f870412 100644 --- a/Sources/Subprocess/IO/AsyncIO+Linux.swift +++ b/Sources/Subprocess/IO/AsyncIO+Linux.swift @@ -33,9 +33,10 @@ import Synchronization private typealias SignalStream = AsyncThrowingStream private let _epollEventSize = 256 -private let _registration: Mutex< - [PlatformFileDescriptor : SignalStream.Continuation] -> = Mutex([:]) +private let _registration: + Mutex< + [PlatformFileDescriptor: SignalStream.Continuation] + > = Mutex([:]) final class AsyncIO: Sendable { @@ -105,8 +106,9 @@ final class AsyncIO: Sendable { ) guard rc == 0 else { let error = SubprocessError( - code: .init(.asyncIOFailed( - "failed to add shutdown fd \(shutdownFileDescriptor) to epoll list") + code: .init( + .asyncIOFailed( + "failed to add shutdown fd \(shutdownFileDescriptor) to epoll list") ), underlyingError: .init(rawValue: errno) ) @@ -149,8 +151,9 @@ final class AsyncIO: Sendable { } // Report other errors let error = SubprocessError( - code: .init(.asyncIOFailed( - "epoll_wait failed") + code: .init( + .asyncIOFailed( + "epoll_wait failed") ), underlyingError: .init(rawValue: errno) ) @@ -158,7 +161,7 @@ final class AsyncIO: Sendable { break monitorLoop } - for index in 0 ..< Int(eventCount) { + for index in 0.. Bool { @@ -571,6 +578,6 @@ extension AsyncIO { } } -extension Array : AsyncIO._ContiguousBytes where Element == UInt8 {} +extension Array: AsyncIO._ContiguousBytes where Element == UInt8 {} #endif // canImport(Glibc) || canImport(Android) || canImport(Musl) diff --git a/Sources/Subprocess/IO/AsyncIO+Windows.swift b/Sources/Subprocess/IO/AsyncIO+Windows.swift index 031126b3..4e794ee1 100644 --- a/Sources/Subprocess/IO/AsyncIO+Windows.swift +++ b/Sources/Subprocess/IO/AsyncIO+Windows.swift @@ -26,9 +26,10 @@ internal import Dispatch private typealias SignalStream = AsyncThrowingStream private let shutdownPort: UInt64 = .max -private let _registration: Mutex< - [UInt64 : SignalStream.Continuation] -> = Mutex([:]) +private let _registration: + Mutex< + [UInt64: SignalStream.Continuation] + > = Mutex([:]) final class AsyncIO: @unchecked Sendable { @@ -36,8 +37,10 @@ final class AsyncIO: @unchecked Sendable { var count: Int { get } func withUnsafeBytes( - _ body: (UnsafeRawBufferPointer - ) throws -> ResultType) rethrows -> ResultType + _ body: ( + UnsafeRawBufferPointer + ) throws -> ResultType + ) rethrows -> ResultType } private final class MonitorThreadContext { @@ -57,9 +60,11 @@ final class AsyncIO: @unchecked Sendable { internal init() { var maybeSetupError: SubprocessError? = nil // Create the the completion port - guard let port = CreateIoCompletionPort( - INVALID_HANDLE_VALUE, nil, 0, 0 - ), port != INVALID_HANDLE_VALUE else { + guard + let port = CreateIoCompletionPort( + INVALID_HANDLE_VALUE, nil, 0, 0 + ), port != INVALID_HANDLE_VALUE + else { let error = SubprocessError( code: .init(.asyncIOFailed("CreateIoCompletionPort failed")), underlyingError: .init(rawValue: GetLastError()) @@ -76,76 +81,79 @@ final class AsyncIO: @unchecked Sendable { /// > A thread in an executable that calls the C run-time library (CRT) /// > should use the _beginthreadex and _endthreadex functions for /// > thread management rather than CreateThread and ExitThread - let threadHandleValue = _beginthreadex(nil, 0, { args in - func reportError(_ error: SubprocessError) { - let continuations = _registration.withLock { store in - return store.values - } - for continuation in continuations { - continuation.finish(throwing: error) + let threadHandleValue = _beginthreadex( + nil, 0, + { args in + func reportError(_ error: SubprocessError) { + let continuations = _registration.withLock { store in + return store.values + } + for continuation in continuations { + continuation.finish(throwing: error) + } } - } - let unmanaged = Unmanaged.fromOpaque(args!) - let context = unmanaged.takeRetainedValue() - - // Monitor loop - while true { - var bytesTransferred: DWORD = 0 - var targetFileDescriptor: UInt64 = 0 - var overlapped: LPOVERLAPPED? = nil - - let monitorResult = GetQueuedCompletionStatus( - context.ioCompletionPort, - &bytesTransferred, - &targetFileDescriptor, - &overlapped, - INFINITE - ) - if !monitorResult { - let lastError = GetLastError() - if lastError == ERROR_BROKEN_PIPE { - // We finished reading the handle. Signal EOF by - // finishing the stream. - // NOTE: here we deliberately leave now unused continuation - // in the store. Windows does not offer an API to remove a - // HANDLE from an IOCP port, therefore we leave the registration - // to signify the HANDLE has already been resisted. - let continuation = _registration.withLock { store -> SignalStream.Continuation? in - if let continuation = store[targetFileDescriptor] { - return continuation + let unmanaged = Unmanaged.fromOpaque(args!) + let context = unmanaged.takeRetainedValue() + + // Monitor loop + while true { + var bytesTransferred: DWORD = 0 + var targetFileDescriptor: UInt64 = 0 + var overlapped: LPOVERLAPPED? = nil + + let monitorResult = GetQueuedCompletionStatus( + context.ioCompletionPort, + &bytesTransferred, + &targetFileDescriptor, + &overlapped, + INFINITE + ) + if !monitorResult { + let lastError = GetLastError() + if lastError == ERROR_BROKEN_PIPE { + // We finished reading the handle. Signal EOF by + // finishing the stream. + // NOTE: here we deliberately leave now unused continuation + // in the store. Windows does not offer an API to remove a + // HANDLE from an IOCP port, therefore we leave the registration + // to signify the HANDLE has already been resisted. + let continuation = _registration.withLock { store -> SignalStream.Continuation? in + if let continuation = store[targetFileDescriptor] { + return continuation + } + return nil } - return nil + continuation?.finish() + continue + } else { + let error = SubprocessError( + code: .init(.asyncIOFailed("GetQueuedCompletionStatus failed")), + underlyingError: .init(rawValue: lastError) + ) + reportError(error) + break } - continuation?.finish() - continue - } else { - let error = SubprocessError( - code: .init(.asyncIOFailed("GetQueuedCompletionStatus failed")), - underlyingError: .init(rawValue: lastError) - ) - reportError(error) - break } - } - // Breakout the monitor loop if we received shutdown from the shutdownFD - if targetFileDescriptor == shutdownPort { - break - } - // Notify the continuations - let continuation = _registration.withLock { store -> SignalStream.Continuation? in - if let continuation = store[targetFileDescriptor] { - return continuation + // Breakout the monitor loop if we received shutdown from the shutdownFD + if targetFileDescriptor == shutdownPort { + break } - return nil + // Notify the continuations + let continuation = _registration.withLock { store -> SignalStream.Continuation? in + if let continuation = store[targetFileDescriptor] { + return continuation + } + return nil + } + continuation?.yield(bytesTransferred) } - continuation?.yield(bytesTransferred) - } - return 0 - }, threadContextPtr.toOpaque(), 0, nil) + return 0 + }, threadContextPtr.toOpaque(), 0, nil) guard threadHandleValue > 0, - let threadHandle = HANDLE(bitPattern: threadHandleValue) else { + let threadHandle = HANDLE(bitPattern: threadHandleValue) + else { // _beginthreadex uses errno instead of GetLastError() let capturedError = _subprocess_windows_get_errno() let error = SubprocessError( @@ -164,7 +172,8 @@ final class AsyncIO: @unchecked Sendable { internal func shutdown() { guard case .success(let ioPort) = ioCompletionPort, - case .success(let monitorThreadHandle) = monitorThread else { + case .success(let monitorThreadHandle) = monitorThread + else { return } // Make sure we don't shutdown the same instance twice @@ -173,11 +182,12 @@ final class AsyncIO: @unchecked Sendable { return } // Post status to shutdown HANDLE + // swift-format-ignore PostQueuedCompletionStatus( - ioPort, // CompletionPort - 0, // Number of bytes transferred. - shutdownPort, // Completion key to post status - nil // Overlapped + ioPort, // CompletionPort + 0, // Number of bytes transferred. + shutdownPort, // Completion key to post status + nil // Overlapped ) // Wait for monitor thread to exit WaitForSingleObject(monitorThreadHandle, INFINITE) @@ -215,9 +225,11 @@ final class AsyncIO: @unchecked Sendable { // Windows Documentation: The function returns the handle // of the existing I/O completion port if successful - guard CreateIoCompletionPort( - handle, ioPort, completionKey, 0 - ) == ioPort else { + guard + CreateIoCompletionPort( + handle, ioPort, completionKey, 0 + ) == ioPort + else { let error = SubprocessError( code: .init(.asyncIOFailed("CreateIoCompletionPort failed")), underlyingError: .init(rawValue: GetLastError()) @@ -347,7 +359,7 @@ final class AsyncIO: @unchecked Sendable { return try await self._write(array, to: diskIO) } -#if SubprocessSpan + #if SubprocessSpan func write( _ span: borrowing RawSpan, to diskIO: borrowing IOChannel @@ -401,7 +413,7 @@ final class AsyncIO: @unchecked Sendable { } } } -#endif // SubprocessSpan + #endif // SubprocessSpan func _write( _ bytes: Bytes, @@ -469,7 +481,6 @@ final class AsyncIO: @unchecked Sendable { } } -extension Array : AsyncIO._ContiguousBytes where Element == UInt8 {} +extension Array: AsyncIO._ContiguousBytes where Element == UInt8 {} #endif - diff --git a/Sources/Subprocess/IO/Input.swift b/Sources/Subprocess/IO/Input.swift index 55db67d9..72ae534d 100644 --- a/Sources/Subprocess/IO/Input.swift +++ b/Sources/Subprocess/IO/Input.swift @@ -29,7 +29,7 @@ import Foundation import FoundationEssentials #endif -#endif // SubprocessFoundation +#endif // SubprocessFoundation // MARK: - Input diff --git a/Sources/Subprocess/IO/Output.swift b/Sources/Subprocess/IO/Output.swift index 131d2840..9c7e3bbe 100644 --- a/Sources/Subprocess/IO/Output.swift +++ b/Sources/Subprocess/IO/Output.swift @@ -242,7 +242,6 @@ extension OutputProtocol { } } - extension OutputProtocol where Self == BytesOutput { /// Create a `Subprocess` output that collects output as /// `Buffer` with a buffer limit in bytes. Subprocess throws @@ -265,7 +264,6 @@ extension OutputProtocol { } #endif - // MARK: - Default Implementations extension OutputProtocol { @_disfavoredOverload diff --git a/Sources/Subprocess/Platforms/Subprocess+BSD.swift b/Sources/Subprocess/Platforms/Subprocess+BSD.swift index c17dab74..0b3ea8ba 100644 --- a/Sources/Subprocess/Platforms/Subprocess+BSD.swift +++ b/Sources/Subprocess/Platforms/Subprocess+BSD.swift @@ -43,17 +43,18 @@ internal func monitorProcessTermination( ) source.setEventHandler { source.cancel() - continuation.resume(with: Result(catching: { () throws(SubprocessError.UnderlyingError) -> TerminationStatus in - // NOTE_EXIT may be delivered slightly before the process becomes reapable, - // so we must call waitid without WNOHANG to avoid a narrow possibility of a race condition. - // If waitid does block, it won't do so for very long at all. - try processIdentifier.blockingReap() - }).mapError { underlyingError in - SubprocessError( - code: .init(.failedToMonitorProcess), - underlyingError: underlyingError - ) - }) + continuation.resume( + with: Result(catching: { () throws(SubprocessError.UnderlyingError) -> TerminationStatus in + // NOTE_EXIT may be delivered slightly before the process becomes reapable, + // so we must call waitid without WNOHANG to avoid a narrow possibility of a race condition. + // If waitid does block, it won't do so for very long at all. + try processIdentifier.blockingReap() + }).mapError { underlyingError in + SubprocessError( + code: .init(.failedToMonitorProcess), + underlyingError: underlyingError + ) + }) } source.resume() } diff --git a/Sources/Subprocess/Platforms/Subprocess+Darwin.swift b/Sources/Subprocess/Platforms/Subprocess+Darwin.swift index cfaa6149..c1aa76c4 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Darwin.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Darwin.swift @@ -31,7 +31,7 @@ import Foundation import FoundationEssentials #endif -#endif // SubprocessFoundation +#endif // SubprocessFoundation // MARK: - PlatformOptions @@ -488,7 +488,7 @@ public struct ProcessIdentifier: Sendable, Hashable { self.value = value } - internal func close() { /* No-op on Darwin */ } + internal func close() { /* No-op on Darwin */ } } extension ProcessIdentifier: CustomStringConvertible, CustomDebugStringConvertible { @@ -497,4 +497,4 @@ extension ProcessIdentifier: CustomStringConvertible, CustomDebugStringConvertib public var debugDescription: String { "\(self.value)" } } -#endif // canImport(Darwin) +#endif // canImport(Darwin) diff --git a/Sources/Subprocess/Platforms/Subprocess+Linux.swift b/Sources/Subprocess/Platforms/Subprocess+Linux.swift index 444a4c01..aee34f87 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Linux.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Linux.swift @@ -153,7 +153,7 @@ internal func monitorProcessTermination( } #if canImport(Musl) -extension pthread_t: @retroactive @unchecked Sendable { } +extension pthread_t: @retroactive @unchecked Sendable {} #endif private enum ProcessMonitorState { @@ -161,7 +161,7 @@ private enum ProcessMonitorState { let epollFileDescriptor: CInt let shutdownFileDescriptor: CInt let monitorThread: pthread_t - var continuations: [PlatformFileDescriptor : CheckedContinuation] + var continuations: [PlatformFileDescriptor: CheckedContinuation] } case notStarted @@ -206,7 +206,6 @@ private func shutdown() { pthread_join(storage.monitorThread, nil) } - /// See the following page for the complete list of `async-signal-safe` functions /// https://man7.org/linux/man-pages/man7/signal-safety.7.html /// Only these functions can be used in the signal handler below @@ -265,7 +264,7 @@ private func monitorThreadFunc(context: MonitorThreadContext) { break monitorLoop } - for index in 0 ..< Int(eventCount) { + for index in 0.. CheckedContinuation? in guard case .started(let storage) = state, - let continuation = storage.continuations[pidfd] else { + let continuation = storage.continuations[pidfd] + else { return nil } // Remove registration @@ -466,7 +467,7 @@ private func _reapAllKnownChildProcesses(_ signalFd: CInt, context: MonitorThrea // Drain the signalFd var buffer: UInt8 = 0 - while _subprocess_read(signalFd, &buffer, 1) > 0 { /* noop, drain the pipe */ } + while _subprocess_read(signalFd, &buffer, 1) > 0 { /* noop, drain the pipe */ } let resumingContinuations: [ResultContinuation] = _processMonitorState.withLock { state in guard case .started(let storage) = state else { @@ -528,4 +529,4 @@ internal func _isWaitprocessDescriptorSupported() -> Bool { return errno == ECHILD } -#endif // canImport(Glibc) || canImport(Android) || canImport(Musl) +#endif // canImport(Glibc) || canImport(Android) || canImport(Musl) diff --git a/Sources/Subprocess/Platforms/Subprocess+Unix.swift b/Sources/Subprocess/Platforms/Subprocess+Unix.swift index b513dbc3..17d5b32b 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Unix.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Unix.swift @@ -699,7 +699,7 @@ internal func pthread_create(_ body: @Sendable @escaping () -> ()) throws(Subpro #endif } -#endif // !canImport(Darwin) +#endif // !canImport(Darwin) extension ProcessIdentifier { /// Reaps the zombie for the exited process. This function may block. @@ -780,4 +780,4 @@ internal extension siginfo_t { } #endif -#endif // canImport(Darwin) || canImport(Glibc) || canImport(Android) || canImport(Musl) +#endif // canImport(Darwin) || canImport(Glibc) || canImport(Android) || canImport(Musl) diff --git a/Sources/Subprocess/Platforms/Subprocess+Windows.swift b/Sources/Subprocess/Platforms/Subprocess+Windows.swift index 38827835..b3526989 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Windows.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Windows.swift @@ -132,9 +132,9 @@ extension Configuration { CreateProcessW( applicationNameW, UnsafeMutablePointer(mutating: commandAndArgsW), - nil, // lpProcessAttributes - nil, // lpThreadAttributes - true, // bInheritHandles + nil, // lpProcessAttributes + nil, // lpThreadAttributes + true, // bInheritHandles createProcessFlags, UnsafeMutableRawPointer(mutating: environmentW), intendedWorkingDirW, @@ -279,10 +279,12 @@ extension Configuration { intendedWorkingDir ): (String?, String, String, String?) do { - (applicationName, - commandAndArgs, - environment, - intendedWorkingDir) = try self.preSpawn(withPossibleExecutablePath: executablePath) + ( + applicationName, + commandAndArgs, + environment, + intendedWorkingDir + ) = try self.preSpawn(withPossibleExecutablePath: executablePath) } catch { try self.safelyCloseMultiple( inputRead: inputReadFileDescriptor, @@ -351,7 +353,6 @@ extension Configuration { } } - guard created else { let windowsError = GetLastError() @@ -992,7 +993,7 @@ extension Configuration { // On Windows, the PATH is required in order to locate dlls needed by // the process so we should also pass that to the child if env.pathValue() == nil, - let parentPath = Environment.currentEnvironmentValues().pathValue() + let parentPath = Environment.currentEnvironmentValues().pathValue() { env["Path"] = parentPath } @@ -1369,7 +1370,8 @@ extension String { base[0] == UInt16(UInt8._backslash), base[1] == UInt16(UInt8._backslash), base[2] == UInt16(UInt8._period), - base[3] == UInt16(UInt8._backslash) { + base[3] == UInt16(UInt8._backslash) + { return try body(base) } @@ -1450,20 +1452,23 @@ internal func fillNullTerminatedWideStringBuffer( ) throws -> String { var bufferCount = max(1, min(initialSize, maxSize)) while bufferCount <= maxSize { - if let result = try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(bufferCount), { buffer in - let count = try body(buffer) - switch count { - case 0: - throw SubprocessError.UnderlyingError(rawValue: GetLastError()) - case 1..&2", ] ) -#else + #else let setup = TestSetup( executable: .path("/bin/sh"), arguments: ["-c", "cat 1>&2"] ) -#endif + #endif let expected: Data = try Data( contentsOf: URL(filePath: theMysteriousIsland.string) ) @@ -1485,7 +1480,7 @@ extension SubprocessIntegrationTests { ) #endif - for _ in 0 ..< 128 { + for _ in 0..<128 { let result = try await _run( setup, input: .none, @@ -1511,7 +1506,7 @@ extension SubprocessIntegrationTests { ) #endif - for _ in 0 ..< 128 { + for _ in 0..<128 { let result = try await _run( setup, input: .none, @@ -1556,7 +1551,7 @@ extension SubprocessIntegrationTests { $data = New-Object byte[] $size [Console]::OpenStandardOutput().Write($data, 0, $data.Length) [Console]::OpenStandardError().Write($data, 0, $data.Length) - """ + """, ] ) #else @@ -1564,7 +1559,7 @@ extension SubprocessIntegrationTests { executable: .path("/bin/sh"), arguments: [ "-c", - "/bin/dd if=/dev/zero bs=\(1024*1024) count=1; /bin/dd >&2 if=/dev/zero bs=\(1024*1024) count=1;" + "/bin/dd if=/dev/zero bs=\(1024*1024) count=1; /bin/dd >&2 if=/dev/zero bs=\(1024*1024) count=1;", ] ) #endif @@ -1643,39 +1638,39 @@ extension SubprocessIntegrationTests { @Test func testLineSequence() async throws { typealias TestCase = (value: String, count: Int, newLine: String) enum TestCaseSize: CaseIterable { - case large // (1.0 ~ 2.0) * buffer size - case medium // (0.2 ~ 1.0) * buffer size - case small // Less than 16 characters + case large // (1.0 ~ 2.0) * buffer size + case medium // (0.2 ~ 1.0) * buffer size + case small // Less than 16 characters } let newLineCharacters: [[UInt8]] = [ - [0x0A], // Line feed - [0x0B], // Vertical tab - [0x0C], // Form feed - [0x0D], // Carriage return - [0x0D, 0x0A], // Carriage return + Line feed - [0xC2, 0x85], // New line + [0x0A], // Line feed + [0x0B], // Vertical tab + [0x0C], // Form feed + [0x0D], // Carriage return + [0x0D, 0x0A], // Carriage return + Line feed + [0xC2, 0x85], // New line [0xE2, 0x80, 0xA8], // Line Separator - [0xE2, 0x80, 0xA9] // Paragraph separator + [0xE2, 0x80, 0xA9], // Paragraph separator ] // Generate test cases func generateString(size: TestCaseSize) -> [UInt8] { // Basic Latin has the range U+0020 ... U+007E - let range: ClosedRange = 0x20 ... 0x7E + let range: ClosedRange = 0x20...0x7E let length: Int switch size { case .large: - length = Int(Double.random(in: 1.0 ..< 2.0) * Double(readBufferSize)) + 1 + length = Int(Double.random(in: 1.0..<2.0) * Double(readBufferSize)) + 1 case .medium: - length = Int(Double.random(in: 0.2 ..< 1.0) * Double(readBufferSize)) + 1 + length = Int(Double.random(in: 0.2..<1.0) * Double(readBufferSize)) + 1 case .small: - length = Int.random(in: 1 ..< 16) + length = Int.random(in: 1..<16) } var buffer: [UInt8] = Array(repeating: 0, count: length) - for index in 0 ..< length { + for index in 0.. Bool { struct SubprocessLintingTest { @Test( .enabled( - if: false/* enableLintingTest() */, + if: false /* enableLintingTest() */, "Skipped until we decide on the rules" ) ) diff --git a/Tests/SubprocessTests/LinuxTests.swift b/Tests/SubprocessTests/LinuxTests.swift index 98f975bf..f0b0f7d7 100644 --- a/Tests/SubprocessTests/LinuxTests.swift +++ b/Tests/SubprocessTests/LinuxTests.swift @@ -103,11 +103,11 @@ struct SubprocessLinuxTests { if subprocess.processIdentifier.processDescriptor > 0 { var statinfo = stat() try #require(fstat(subprocess.processIdentifier.processDescriptor, &statinfo) == 0) - + // In kernel 6.9+, st_ino globally uniquely identifies the process #expect(statinfo.st_ino > 0) } } } } -#endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl) +#endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl) diff --git a/Tests/SubprocessTests/PlatformConformance.swift b/Tests/SubprocessTests/PlatformConformance.swift index 04bd2705..a7268f62 100644 --- a/Tests/SubprocessTests/PlatformConformance.swift +++ b/Tests/SubprocessTests/PlatformConformance.swift @@ -46,4 +46,4 @@ protocol ProcessIdentifierProtocol: Sendable, Hashable, CustomStringConvertible, #endif } -extension ProcessIdentifier : ProcessIdentifierProtocol {} +extension ProcessIdentifier: ProcessIdentifierProtocol {} diff --git a/Tests/SubprocessTests/ProcessMonitoringTests.swift b/Tests/SubprocessTests/ProcessMonitoringTests.swift index 1fab4ebe..e0a869c2 100644 --- a/Tests/SubprocessTests/ProcessMonitoringTests.swift +++ b/Tests/SubprocessTests/ProcessMonitoringTests.swift @@ -273,11 +273,11 @@ extension SubprocessProcessMonitoringTests { @Test func testCanMonitorProcessConcurrently() async throws { let testCount = 100 try await withThrowingTaskGroup { group in - for _ in 0 ..< testCount { + for _ in 0.. String { internal func randomData(count: Int) -> [UInt8] { return Array(unsafeUninitializedCapacity: count) { buffer, initializedCount in - for i in 0 ..< count { + for i in 0.. /dev/null & - child_pid=$! # Retrieve the grand child yes pid - # When SIGINT is sent to the script, kill grand child now - trap "echo >&2 'child: received signal, killing grand child ($child_pid)'; kill -s KILL $child_pid; exit 0" INT - echo "$child_pid" # communicate the child pid to our parent - echo "child: waiting for grand child, pid: $child_pid" >&2 - wait $child_pid # wait for runaway child to exit - """ + """ + set -e + # The following /usr/bin/yes is the runaway grand child. + # It runs in the background forever until this script kills it + /usr/bin/yes "Runaway process from \(#function), please file a SwiftSubprocess bug." > /dev/null & + child_pid=$! # Retrieve the grand child yes pid + # When SIGINT is sent to the script, kill grand child now + trap "echo >&2 'child: received signal, killing grand child ($child_pid)'; kill -s KILL $child_pid; exit 0" INT + echo "$child_pid" # communicate the child pid to our parent + echo "child: waiting for grand child, pid: $child_pid" >&2 + wait $child_pid # wait for runaway child to exit + """, ], platformOptions: platformOptions, output: .string(limit: .max), @@ -279,7 +279,7 @@ extension SubprocessUnixTests { // Make sure the grand child `/usr/bin/yes` actually exited // This is unfortunately racy because the pid isn't immediately invalided // once `kill` returns. Allow a few failures and delay to counter this - for _ in 0 ..< 10 { + for _ in 0..<10 { let rc = kill(grandChildPid, 0) if rc == 0 { // Wait for a small delay @@ -394,7 +394,7 @@ extension SubprocessUnixTests { """ echo this string should be discarded >&\(pipe.writeEnd.rawValue); echo wrote into \(pipe.writeEnd.rawValue), echo exit code $?; - """ + """, ], input: .none, output: .string(limit: 64), @@ -410,8 +410,7 @@ extension SubprocessUnixTests { } #expect(readCount == 0) #expect( - result.standardOutput?.trimmingNewLineAndQuotes() == - "wrote into \(pipe.writeEnd.rawValue), echo exit code 1" + result.standardOutput?.trimmingNewLineAndQuotes() == "wrote into \(pipe.writeEnd.rawValue), echo exit code 1" ) } } @@ -440,8 +439,8 @@ extension SubprocessUnixTests { internal func assertNewSessionCreated( with result: CollectedResult< - StringOutput, - Output + StringOutput, + Output > ) throws { try assertNewSessionCreated( @@ -526,4 +525,4 @@ extension SubprocessUnixTests { #endif } -#endif // canImport(Darwin) || canImport(Glibc) +#endif // canImport(Darwin) || canImport(Glibc) diff --git a/Tests/SubprocessTests/WindowsTests.swift b/Tests/SubprocessTests/WindowsTests.swift index 36578502..76bee2bd 100644 --- a/Tests/SubprocessTests/WindowsTests.swift +++ b/Tests/SubprocessTests/WindowsTests.swift @@ -284,7 +284,7 @@ extension SubprocessWindowsTests { defer { #expect(CloseHandle(hJob)) } var info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION() info.BasicLimitInformation.LimitFlags = DWORD(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE) - #expect(SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &info, DWORD(MemoryLayout.size))) + #expect(SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &info, DWORD(MemoryLayout.size))) var platformOptions = PlatformOptions() platformOptions.preSpawnProcessConfigurator = { (createProcessFlags, startupInfo) in @@ -375,7 +375,7 @@ extension SubprocessWindowsTests { guard AllocateAndInitializeSid( &netAuthority, - 2, // nSubAuthorityCount + 2, // nSubAuthorityCount DWORD(SECURITY_BUILTIN_DOMAIN_RID), DWORD(DOMAIN_ALIAS_RID_ADMINS), 0, @@ -470,4 +470,4 @@ extension FileDescriptor { } } -#endif // canImport(WinSDK) +#endif // canImport(WinSDK)