From e838c43f34853973e26aa4ec835bf656d8f86515 Mon Sep 17 00:00:00 2001 From: Boris Buegling Date: Wed, 17 Jan 2024 17:33:33 -0800 Subject: [PATCH] Lock scratch directory during tool execution Currently, concurrent execution of SwiftPM can easily break the state in .build irreparably. This is especially in conjunction with tools using file watching to trigger package resolution which is something that e.g. the Swift VS Code plugin does. We can resolve this by locking the .build directory for the duration of execution of any `SwiftTool` such that any following access will do a blocking wait until unlock. This is using the same infrastructure that we are already using for the shared repository cache. Unlike the shared cache, tool execution can take a long time, so we'll inform the user about the waiting process so that they can decide to cancel instead. rdar://113964179 --- .../FileSystem/FileSystem+Extensions.swift | 4 +-- Sources/CoreCommands/SwiftTool.swift | 26 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Sources/Basics/FileSystem/FileSystem+Extensions.swift b/Sources/Basics/FileSystem/FileSystem+Extensions.swift index c03178d3428..38c3557a586 100644 --- a/Sources/Basics/FileSystem/FileSystem+Extensions.swift +++ b/Sources/Basics/FileSystem/FileSystem+Extensions.swift @@ -196,8 +196,8 @@ extension FileSystem { } /// Execute the given block while holding the lock. - public func withLock(on path: AbsolutePath, type: FileLock.LockType, _ body: () throws -> T) throws -> T { - try self.withLock(on: path.underlying, type: type, body) + public func withLock(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool = true, _ body: () throws -> T) throws -> T { + try self.withLock(on: path.underlying, type: type, blocking: blocking, body) } /// Returns any known item replacement directories for a given path. These may be used by platform-specific diff --git a/Sources/CoreCommands/SwiftTool.swift b/Sources/CoreCommands/SwiftTool.swift index 0ed863b0098..1aa6d462602 100644 --- a/Sources/CoreCommands/SwiftTool.swift +++ b/Sources/CoreCommands/SwiftTool.swift @@ -41,6 +41,7 @@ import func TSCBasic.exec import protocol TSCBasic.OutputByteStream import class TSCBasic.Process import enum TSCBasic.ProcessEnv +import enum TSCBasic.ProcessLockError import var TSCBasic.stderrStream import class TSCBasic.TerminalController import class TSCBasic.ThreadSafeOutputByteStream @@ -108,14 +109,27 @@ extension SwiftCommand { workspaceLoaderProvider: self.workspaceLoaderProvider ) swiftTool.buildSystemProvider = try buildSystemProvider(swiftTool) - var toolError: Error? = .none + + // Try a non-blocking lock first so that we can inform the user about an already running SwiftPM. do { - try self.run(swiftTool) - if swiftTool.observabilityScope.errorsReported || swiftTool.executionStatus == .failure { - throw ExitCode.failure + try swiftTool.fileSystem.withLock(on: swiftTool.scratchDirectory, type: .exclusive, blocking: false) {} + } catch let ProcessLockError.unableToAquireLock(errno) { + if errno == EWOULDBLOCK { + swiftTool.outputStream.write("Another instance of SwiftPM is already running using '\(swiftTool.scratchDirectory)', waiting until that process has finished execution...".utf8) + swiftTool.outputStream.flush() + } + } + + var toolError: Error? = .none + try swiftTool.fileSystem.withLock(on: swiftTool.scratchDirectory, type: .exclusive) { + do { + try self.run(swiftTool) + if swiftTool.observabilityScope.errorsReported || swiftTool.executionStatus == .failure { + throw ExitCode.failure + } + } catch { + toolError = error } - } catch { - toolError = error } // wait for all observability items to process