diff --git a/Sources/SWBBuildSystem/BuildOperation.swift b/Sources/SWBBuildSystem/BuildOperation.swift index 5542c89e..f68b8e57 100644 --- a/Sources/SWBBuildSystem/BuildOperation.swift +++ b/Sources/SWBBuildSystem/BuildOperation.swift @@ -1850,15 +1850,19 @@ internal final class OperationSystemAdaptor: SWBLLBuild.BuildSystemDelegate, Act return } + guard let outputDelegate = (queue.blocking_sync { self.commandOutputDelegates.removeValue(forKey: command) }) else { + // If there's no outputDelegate, the command never started (i.e. it was skipped by shouldCommandStart(). + return + } + + // We can call this here because we're on an llbuild worker thread. This shouldn't be used while on `self.queue` because we have Swift async work elsewhere which blocks on that queue. + let sandboxViolations = task.isSandboxed && result == .failed ? task.extractSandboxViolationMessages_ASYNC_UNSAFE(startTime: outputDelegate.startTime) : [] + queue.async { - // Find the output delegate, and remove it from the active set. - guard let outputDelegate = self.commandOutputDelegates.removeValue(forKey: command) else { - // If there's no outputDelegate, the command never started (i.e. it was skipped by shouldCommandStart(). - return + for message in sandboxViolations { + outputDelegate.emit(Diagnostic(behavior: .error, location: .unknown, data: DiagnosticData(message))) } - outputDelegate.emitSandboxingViolations(task: task, commandResult: result) - // This may be updated by commandProcessFinished if it was an // ExternalCommand, so only update the exit status in output delegate if // it is nil. However, always update the status if the result is failed, diff --git a/Sources/SWBBuildSystem/SandboxViolations.swift b/Sources/SWBBuildSystem/SandboxViolations.swift index de6df768..be69d624 100644 --- a/Sources/SWBBuildSystem/SandboxViolations.swift +++ b/Sources/SWBBuildSystem/SandboxViolations.swift @@ -10,38 +10,33 @@ // //===----------------------------------------------------------------------===// -import Foundation +package import Foundation import SWBUtil package import SWBCore -package import SWBTaskExecution #if os(macOS) import OSLog #endif -extension TaskOutputDelegate { - package func emitSandboxingViolations(task: any ExecutableTask, commandResult result: CommandResult) { - guard task.isSandboxed && result == .failed else { - return - } - - for message in task.extractSandboxViolationMessages(startTime: startTime) { - emit(Diagnostic(behavior: .error, location: .unknown, data: DiagnosticData(message))) - } - } -} - extension ExecutableTask { /// Whether or not this task is being executed within a sandbox which restricts filesystem access to declared inputs and outputs. /// /// - note: Currently this will be true for any task whose executable is `sandbox-exec`, not only tasks which were created using _Swift Builds_ sandboxing mechanism and therefore whose diagnostics contain the message sentinel that we look for. However, this shouldn't really matter in practice at this time as if this property is true for a task not sandboxed _by Swift Build_, we'll simply not extract any diagnostics for it. The only way to get into this situation is to create such tasks in the PIF, which end-users can't do. - fileprivate var isSandboxed: Bool { + package var isSandboxed: Bool { return commandLine.first?.asByteString == ByteString(encodingAsUTF8: "/usr/bin/sandbox-exec") } - fileprivate func extractSandboxViolationMessages(startTime: Date) -> [String] { + /// This must be called from threads which aren't Swift async worker threads. This func uses OSLog which kicks off async work and waits for it on a semaphore, causing deadlocks when invoked from Swift Concurrency worker threads. + @available(*, noasync) + package func extractSandboxViolationMessages_ASYNC_UNSAFE(startTime: Date) -> [String] { var res: [String] = [] #if os(macOS) + withUnsafeCurrentTask { task in + if task != nil { + preconditionFailure("This function should not be invoked from the Swift Concurrency thread pool as it may lead to deadlock via thread starvation.") + } + } + if let store = try? OSLogStore.local() { let query = String("((processID == 0 AND senderImagePath CONTAINS[c] \"/Sandbox\") OR (process == \"sandboxd\" AND subsystem == \"com.apple.sandbox.reporting\")) AND (eventMessage CONTAINS[c] %@)") let endTime = Date()