Skip to content

Commit 3df612e

Browse files
committed
store lock files in temporary directory (swiftlang#273)
motivation: local file system store lock files next to the file being locked and never cleans them up changes: * add a tempDirectory to FileSystem protocol * update determineTempDirectory to use tempDirectory from localFileSystem * move logic to handle local file system locks to FileLock and call it from localFileSystem * expose an API on FileLock to create local file system locks, which by default uses the temp directory to store lock files, using the file-to-lock path and name to construct the lock file name * move local file system lock logic to use the new FileLock API * add tests rdar://86644116
1 parent 17b2fb6 commit 3df612e

File tree

4 files changed

+126
-39
lines changed

4 files changed

+126
-39
lines changed

Sources/TSCBasic/FileSystem.swift

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
See http://swift.org/LICENSE.txt for license information
88
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9-
*/
9+
*/
1010

1111
import TSCLibc
1212
import Foundation
@@ -179,6 +179,9 @@ public protocol FileSystem: AnyObject {
179179
/// Get the caches directory of current user
180180
var cachesDirectory: AbsolutePath? { get }
181181

182+
/// Get the temp directory
183+
var tempDirectory: AbsolutePath { get }
184+
182185
/// Create the given directory.
183186
func createDirectory(_ path: AbsolutePath) throws
184187

@@ -351,10 +354,18 @@ private class LocalFileSystem: FileSystem {
351354
return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first.flatMap { AbsolutePath($0.path) }
352355
}
353356

357+
var tempDirectory: AbsolutePath {
358+
let override = ProcessEnv.vars["TMPDIR"] ?? ProcessEnv.vars["TEMP"] ?? ProcessEnv.vars["TMP"]
359+
if let path = override.flatMap({ try? AbsolutePath(validating: $0) }) {
360+
return path
361+
}
362+
return AbsolutePath(NSTemporaryDirectory())
363+
}
364+
354365
func getDirectoryContents(_ path: AbsolutePath) throws -> [String] {
355-
#if canImport(Darwin)
366+
#if canImport(Darwin)
356367
return try FileManager.default.contentsOfDirectory(atPath: path.pathString)
357-
#else
368+
#else
358369
do {
359370
return try FileManager.default.contentsOfDirectory(atPath: path.pathString)
360371
} catch let error as NSError {
@@ -366,7 +377,7 @@ private class LocalFileSystem: FileSystem {
366377
}
367378
throw error
368379
}
369-
#endif
380+
#endif
370381
}
371382

372383
func createDirectory(_ path: AbsolutePath, recursive: Bool) throws {
@@ -477,10 +488,10 @@ private class LocalFileSystem: FileSystem {
477488
guard isDirectory(path) else { return }
478489

479490
guard let traverse = FileManager.default.enumerator(
480-
at: URL(fileURLWithPath: path.pathString),
481-
includingPropertiesForKeys: nil) else {
482-
throw FileSystemError(.noEntry, path)
483-
}
491+
at: URL(fileURLWithPath: path.pathString),
492+
includingPropertiesForKeys: nil) else {
493+
throw FileSystemError(.noEntry, path)
494+
}
484495

485496
if !options.contains(.recursive) {
486497
traverse.skipDescendants()
@@ -506,8 +517,7 @@ private class LocalFileSystem: FileSystem {
506517
}
507518

508519
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType = .exclusive, _ body: () throws -> T) throws -> T {
509-
let lock = FileLock(name: path.basename, cachePath: path.parentDirectory)
510-
return try lock.withLock(type: type, body)
520+
try FileLock.withLock(fileToLock: path, type: type, body: body)
511521
}
512522
}
513523

@@ -528,7 +538,7 @@ public class InMemoryFileSystem: FileSystem {
528538

529539
/// Creates deep copy of the object.
530540
func copy() -> Node {
531-
return Node(contents.copy())
541+
return Node(contents.copy())
532542
}
533543
}
534544

@@ -722,6 +732,10 @@ public class InMemoryFileSystem: FileSystem {
722732
return self.homeDirectory.appending(component: "caches")
723733
}
724734

735+
public var tempDirectory: AbsolutePath {
736+
return AbsolutePath("/tmp")
737+
}
738+
725739
public func getDirectoryContents(_ path: AbsolutePath) throws -> [String] {
726740
return try lock.withLock {
727741
guard let node = try getNode(path) else {
@@ -871,9 +885,9 @@ public class InMemoryFileSystem: FileSystem {
871885
// Ignore root and get the parent node's content if its a directory.
872886
guard !path.isRoot,
873887
let parent = try? getNode(path.parentDirectory),
874-
case .directory(let contents) = parent.contents else {
875-
return
876-
}
888+
case .directory(let contents) = parent.contents else {
889+
return
890+
}
877891
// Set it to nil to release the contents.
878892
contents.entries[path.basename] = nil
879893
}
@@ -935,7 +949,7 @@ public class InMemoryFileSystem: FileSystem {
935949
public func withLock<T>(on path: AbsolutePath, type: FileLock.LockType = .exclusive, _ body: () throws -> T) throws -> T {
936950
let resolvedPath: AbsolutePath = try lock.withLock {
937951
if case let .symlink(destination) = try getNode(path)?.contents {
938-
return AbsolutePath(destination, relativeTo: path.parentDirectory)
952+
return AbsolutePath(destination, relativeTo: path.parentDirectory)
939953
} else {
940954
return path
941955
}
@@ -1028,6 +1042,10 @@ public class RerootedFileSystemView: FileSystem {
10281042
fatalError("cachesDirectory on RerootedFileSystemView is not supported.")
10291043
}
10301044

1045+
public var tempDirectory: AbsolutePath {
1046+
fatalError("tempDirectory on RerootedFileSystemView is not supported.")
1047+
}
1048+
10311049
public func getDirectoryContents(_ path: AbsolutePath) throws -> [String] {
10321050
return try underlyingFileSystem.getDirectoryContents(formUnderlyingPath(path))
10331051
}

Sources/TSCBasic/Lock.swift

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ extension ProcessLockError: CustomNSError {
4444
return [NSLocalizedDescriptionKey: "\(self)"]
4545
}
4646
}
47-
/// Provides functionality to aquire a lock on a file via POSIX's flock() method.
47+
/// Provides functionality to acquire a lock on a file via POSIX's flock() method.
4848
/// It can be used for things like serializing concurrent mutations on a shared resource
49-
/// by mutiple instances of a process. The `FileLock` is not thread-safe.
49+
/// by multiple instances of a process. The `FileLock` is not thread-safe.
5050
public final class FileLock {
5151

5252
public enum LockType {
@@ -64,15 +64,19 @@ public final class FileLock {
6464
/// Path to the lock file.
6565
private let lockFile: AbsolutePath
6666

67-
/// Create an instance of process lock with a name and the path where
68-
/// the lock file can be created.
67+
/// Create an instance of FileLock at the path specified
6968
///
70-
/// Note: The cache path should be a valid directory.
71-
public init(name: String, cachePath: AbsolutePath) {
72-
self.lockFile = cachePath.appending(component: name + ".lock")
69+
/// Note: The parent directory path should be a valid directory.
70+
public init(at lockFile: AbsolutePath) {
71+
self.lockFile = lockFile
7372
}
7473

75-
/// Try to aquire a lock. This method will block until lock the already aquired by other process.
74+
@available(*, deprecated, message: "use init(at:) instead")
75+
public convenience init(name: String, cachePath: AbsolutePath) {
76+
self.init(at: cachePath.appending(component: name + ".lock"))
77+
}
78+
79+
/// Try to acquire a lock. This method will block until lock the already aquired by other process.
7680
///
7781
/// Note: This method can throw if underlying POSIX methods fail.
7882
public func lock(type: LockType = .exclusive) throws {
@@ -163,4 +167,24 @@ public final class FileLock {
163167
defer { unlock() }
164168
return try body()
165169
}
170+
171+
public static func withLock<T>(fileToLock: AbsolutePath, lockFilesDirectory: AbsolutePath? = nil, type: LockType = .exclusive, body: () throws -> T) throws -> T {
172+
// unless specified, we use the tempDirectory to store lock files
173+
let lockFilesDirectory = lockFilesDirectory ?? localFileSystem.tempDirectory
174+
if !localFileSystem.exists(lockFilesDirectory) {
175+
throw FileSystemError(.noEntry, lockFilesDirectory)
176+
}
177+
if !localFileSystem.isDirectory(lockFilesDirectory) {
178+
throw FileSystemError(.notDirectory, lockFilesDirectory)
179+
}
180+
// use the parent path to generate unique filename in temp
181+
var lockFileName = (resolveSymlinks(fileToLock.parentDirectory).appending(component: fileToLock.basename)).components.joined(separator: "_")
182+
if lockFileName.hasPrefix(AbsolutePath.root.pathString) {
183+
lockFileName = String(lockFileName.dropFirst(AbsolutePath.root.pathString.count))
184+
}
185+
let lockFilePath = lockFilesDirectory.appending(component: lockFileName + ".lock")
186+
187+
let lock = FileLock(at: lockFilePath)
188+
return try lock.withLock(type: type, body)
189+
}
166190
}

Sources/TSCBasic/TemporaryFile.swift

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,24 +53,13 @@ private extension TempFileError {
5353
///
5454
/// - Returns: Path to directory in which temporary file should be created.
5555
public func determineTempDirectory(_ dir: AbsolutePath? = nil) throws -> AbsolutePath {
56-
let tmpDir = dir ?? cachedTempDirectory
56+
let tmpDir = dir ?? localFileSystem.tempDirectory
5757
guard localFileSystem.isDirectory(tmpDir) else {
5858
throw TempFileError.couldNotFindTmpDir(tmpDir.pathString)
5959
}
6060
return tmpDir
6161
}
6262

63-
/// Returns temporary directory location by searching relevant env variables.
64-
///
65-
/// Evaluates once per execution.
66-
private var cachedTempDirectory: AbsolutePath = {
67-
let override = ProcessEnv.vars["TMPDIR"] ?? ProcessEnv.vars["TEMP"] ?? ProcessEnv.vars["TMP"]
68-
if let path = override.flatMap({ try? AbsolutePath(validating: $0) }) {
69-
return path
70-
}
71-
return AbsolutePath(NSTemporaryDirectory())
72-
}()
73-
7463
/// The closure argument of the `body` closue of `withTemporaryFile`.
7564
public struct TemporaryFile {
7665
/// If specified during init, the temporary file name begins with this prefix.

Tests/TSCBasicTests/LockTests.swift

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class LockTests: XCTestCase {
3838
let N = 10
3939
let threads = (1...N).map { idx in
4040
return Thread {
41-
let lock = FileLock(name: "TestLock", cachePath: tempDirPath)
41+
let lock = FileLock(at: tempDirPath.appending(component: "TestLock"))
4242
try! lock.withLock {
4343
// Get thr current contents of the file if any.
4444
let currentData: Int
@@ -71,7 +71,7 @@ class LockTests: XCTestCase {
7171

7272
let writerThreads = (0..<100).map { _ in
7373
return Thread {
74-
let lock = FileLock(name: "foo", cachePath: tempDir)
74+
let lock = FileLock(at: tempDir.appending(component: "foo"))
7575
try! lock.withLock(type: .exclusive) {
7676
// Get the current contents of the file if any.
7777
let valueA = Int(try localFileSystem.readFileContents(fileA).description)!
@@ -90,7 +90,7 @@ class LockTests: XCTestCase {
9090

9191
let readerThreads = (0..<20).map { _ in
9292
return Thread {
93-
let lock = FileLock(name: "foo", cachePath: tempDir)
93+
let lock = FileLock(at: tempDir.appending(component: "foo"))
9494
try! lock.withLock(type: .shared) {
9595
try XCTAssertEqual(localFileSystem.readFileContents(fileA), localFileSystem.readFileContents(fileB))
9696

@@ -110,5 +110,61 @@ class LockTests: XCTestCase {
110110
try XCTAssertEqual(localFileSystem.readFileContents(fileB), "100")
111111
}
112112
}
113-
113+
114+
func testFileLockLocation() throws {
115+
do {
116+
let fileName = UUID().uuidString
117+
let fileToLock = localFileSystem.homeDirectory.appending(component: fileName)
118+
try localFileSystem.withLock(on: fileToLock, type: .exclusive) {}
119+
120+
// lock file expected at temp when lockFilesDirectory set to nil
121+
// which is the case when going through localFileSystem
122+
let lockFile = try localFileSystem.getDirectoryContents(localFileSystem.tempDirectory)
123+
.first(where: { $0.contains(fileName) })
124+
XCTAssertNotNil(lockFile, "expected lock file at \(localFileSystem.tempDirectory)")
125+
}
126+
127+
do {
128+
let fileName = UUID().uuidString
129+
let fileToLock = localFileSystem.homeDirectory.appending(component: fileName)
130+
try FileLock.withLock(fileToLock: fileToLock, lockFilesDirectory: nil, body: {})
131+
132+
// lock file expected at temp when lockFilesDirectory set to nil
133+
let lockFile = try localFileSystem.getDirectoryContents(localFileSystem.tempDirectory)
134+
.first(where: { $0.contains(fileName) })
135+
XCTAssertNotNil(lockFile, "expected lock file at \(localFileSystem.tempDirectory)")
136+
}
137+
138+
do {
139+
try withTemporaryDirectory { tempDir in
140+
let fileName = UUID().uuidString
141+
let fileToLock = localFileSystem.homeDirectory.appending(component: fileName)
142+
try FileLock.withLock(fileToLock: fileToLock, lockFilesDirectory: tempDir, body: {})
143+
144+
// lock file expected at specified directory when lockFilesDirectory is set to a valid directory
145+
let lockFile = try localFileSystem.getDirectoryContents(tempDir)
146+
.first(where: { $0.contains(fileName) })
147+
XCTAssertNotNil(lockFile, "expected lock file at \(tempDir)")
148+
}
149+
}
150+
151+
do {
152+
let fileName = UUID().uuidString
153+
let fileToLock = localFileSystem.homeDirectory.appending(component: fileName)
154+
let lockFilesDirectory = localFileSystem.homeDirectory.appending(component: UUID().uuidString)
155+
XCTAssertThrows(FileSystemError(.noEntry, lockFilesDirectory)) {
156+
try FileLock.withLock(fileToLock: fileToLock, lockFilesDirectory: lockFilesDirectory, body: {})
157+
}
158+
}
159+
160+
do {
161+
let fileName = UUID().uuidString
162+
let fileToLock = localFileSystem.homeDirectory.appending(component: fileName)
163+
let lockFilesDirectory = localFileSystem.homeDirectory.appending(component: UUID().uuidString)
164+
try localFileSystem.writeFileContents(lockFilesDirectory, bytes: [])
165+
XCTAssertThrows(FileSystemError(.notDirectory, lockFilesDirectory)) {
166+
try FileLock.withLock(fileToLock: fileToLock, lockFilesDirectory: lockFilesDirectory, body: {})
167+
}
168+
}
169+
}
114170
}

0 commit comments

Comments
 (0)