Skip to content

Commit 0a49bb9

Browse files
authored
protect from concurrency issues when extracting binary target archives (#3798)
motivation: protect from edge cases when archives download twice at the same time changes: * make the temp extraction directory name more unique * move the temp extraction directory creation closer to extraction, to minimize the window for another process interrupting * add file lock on the binary artifact * add test to make sure binary artifacts are not duplicated due to transitive dependencies (they are not) rdar://83516383
1 parent 2c1bc5f commit 0a49bb9

File tree

4 files changed

+350
-209
lines changed

4 files changed

+350
-209
lines changed

Sources/Basics/FileSystem+Extensions.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,13 @@ extension FileSystem {
134134
return try self.writeFileContents(path, string: provider())
135135
}
136136
}
137+
138+
extension FileSystem {
139+
public func forceCreateDirectory(at path: AbsolutePath) throws {
140+
try self.createDirectory(path.parentDirectory, recursive: true)
141+
if self.exists(path) {
142+
try self.removeFileTree(path)
143+
}
144+
try self.createDirectory(path, recursive: true)
145+
}
146+
}

Sources/SPMTestSupport/MockWorkspace.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ public final class MockWorkspace {
546546
XCTFail("\(packageIdentity).\(targetName) does not exists", file: file, line: line)
547547
return
548548
}
549-
XCTAssertEqual(artifact.path, path)
549+
XCTAssertEqual(artifact.path, path, file: file, line: line)
550550
switch (artifact.source, source) {
551551
case (.remote(let lhsURL, let lhsChecksum), .remote(let rhsURL, let rhsChecksum)):
552552
XCTAssertEqual(lhsURL, rhsURL, file: file, line: line)

Sources/Workspace/Workspace.swift

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1899,9 +1899,9 @@ extension Workspace {
18991899
// Download the artifacts
19001900
let downloadedArtifacts = try self.download(artifactsToDownload, diagnostics: diagnostics)
19011901
artifactsToAdd.append(contentsOf: downloadedArtifacts)
1902-
1902+
19031903
// Extract the local archived artifacts
1904-
let extractedLocalArtifacts = self.extract(artifactsToExtract, diagnostics: diagnostics)
1904+
let extractedLocalArtifacts = try self.extract(artifactsToExtract, diagnostics: diagnostics)
19051905
artifactsToAdd.append(contentsOf: extractedLocalArtifacts)
19061906

19071907
// Add the new artifacts
@@ -2028,16 +2028,7 @@ extension Workspace {
20282028
defer { group.leave() }
20292029

20302030
let parentDirectory = self.location.artifactsDirectory.appending(component: artifact.packageRef.name)
2031-
let tempExtractionDirectory = self.location.artifactsDirectory.appending(components: "extract", artifact.targetName)
2032-
2033-
do {
2034-
try fileSystem.createDirectory(parentDirectory, recursive: true)
2035-
if fileSystem.exists(tempExtractionDirectory) {
2036-
try fileSystem.removeFileTree(tempExtractionDirectory)
2037-
}
2038-
try fileSystem.createDirectory(tempExtractionDirectory, recursive: true)
2039-
} catch {
2040-
tempDiagnostics.emit(error)
2031+
guard tempDiagnostics.wrap ({ try fileSystem.createDirectory(parentDirectory, recursive: true) }) else {
20412032
continue
20422033
}
20432034

@@ -2064,12 +2055,19 @@ extension Workspace {
20642055
case .success:
20652056
let archiveChecksum = self.checksum(forBinaryArtifactAt: archivePath, diagnostics: tempDiagnostics )
20662057
guard archiveChecksum == artifact.checksum else {
2067-
tempDiagnostics.emit(
2068-
.artifactInvalidChecksum(targetName: artifact.targetName, expectedChecksum: artifact.checksum, actualChecksum: archiveChecksum))
2058+
tempDiagnostics.emit(.artifactInvalidChecksum(targetName: artifact.targetName, expectedChecksum: artifact.checksum, actualChecksum: archiveChecksum))
20692059
tempDiagnostics.wrap { try self.fileSystem.removeFileTree(archivePath) }
20702060
return
20712061
}
20722062

2063+
guard let tempExtractionDirectory = tempDiagnostics.wrap({ () -> AbsolutePath in
2064+
let path = self.location.artifactsDirectory.appending(components: "extract", artifact.packageRef.name, artifact.targetName, UUID().uuidString)
2065+
try self.fileSystem.forceCreateDirectory(at: path)
2066+
return path
2067+
}) else {
2068+
return
2069+
}
2070+
20732071
// TODO: Use the same extraction logic for both remote and local archived artifacts.
20742072
group.enter()
20752073
self.archiver.extract(from: archivePath, to: tempExtractionDirectory, completion: { extractResult in
@@ -2079,17 +2077,19 @@ extension Workspace {
20792077
case .success:
20802078
var artifactPath: AbsolutePath? = nil
20812079
tempDiagnostics.wrap {
2082-
// copy from temp location to actual location
2083-
let content = try self.fileSystem.getDirectoryContents(tempExtractionDirectory)
2084-
for file in content {
2085-
let source = tempExtractionDirectory.appending(component: file)
2086-
let destination = parentDirectory.appending(component: file)
2087-
if self.fileSystem.exists(destination) {
2088-
try self.fileSystem.removeFileTree(destination)
2089-
}
2090-
try self.fileSystem.copy(from: source, to: destination)
2091-
if destination.basenameWithoutExt == artifact.targetName {
2092-
artifactPath = destination
2080+
try self.fileSystem.withLock(on: parentDirectory, type: .exclusive) {
2081+
// copy from temp location to actual location
2082+
let content = try self.fileSystem.getDirectoryContents(tempExtractionDirectory)
2083+
for file in content {
2084+
let source = tempExtractionDirectory.appending(component: file)
2085+
let destination = parentDirectory.appending(component: file)
2086+
if self.fileSystem.exists(destination) {
2087+
try self.fileSystem.removeFileTree(destination)
2088+
}
2089+
try self.fileSystem.copy(from: source, to: destination)
2090+
if destination.basenameWithoutExt == artifact.targetName {
2091+
artifactPath = destination
2092+
}
20932093
}
20942094
}
20952095
// remove temp location
@@ -2134,23 +2134,16 @@ extension Workspace {
21342134
return result.map{ $0 }
21352135
}
21362136

2137-
private func extract(_ artifacts: [ManagedArtifact], diagnostics: DiagnosticsEngine) -> [ManagedArtifact] {
2137+
private func extract(_ artifacts: [ManagedArtifact], diagnostics: DiagnosticsEngine) throws -> [ManagedArtifact] {
21382138
let result = ThreadSafeArrayStore<ManagedArtifact>()
21392139
let group = DispatchGroup()
21402140

21412141
for artifact in artifacts {
2142-
let tempExtractionDirectory = self.location.artifactsDirectory.appending(components: "extract", artifact.targetName)
21432142
let destinationDirectory = self.location.artifactsDirectory.appending(component: artifact.packageRef.name)
2143+
try fileSystem.createDirectory(destinationDirectory, recursive: true)
21442144

2145-
do {
2146-
try fileSystem.createDirectory(destinationDirectory, recursive: true)
2147-
if fileSystem.exists(tempExtractionDirectory) {
2148-
try fileSystem.removeFileTree(tempExtractionDirectory)
2149-
}
2150-
try fileSystem.createDirectory(tempExtractionDirectory, recursive: true)
2151-
} catch {
2152-
diagnostics.emit(error)
2153-
}
2145+
let tempExtractionDirectory = self.location.artifactsDirectory.appending(components: "extract", artifact.packageRef.name, artifact.targetName, UUID().uuidString)
2146+
try self.fileSystem.forceCreateDirectory(at: tempExtractionDirectory)
21542147

21552148
group.enter()
21562149
self.archiver.extract(from: artifact.path, to: tempExtractionDirectory, completion: { extractResult in

0 commit comments

Comments
 (0)