Skip to content

Commit bc0d6a7

Browse files
authored
store lock files in temporary directory (#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 4ac04c4 commit bc0d6a7

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
@@ -180,6 +180,9 @@ public protocol FileSystem: AnyObject {
180180
/// Get the caches directory of current user
181181
var cachesDirectory: AbsolutePath? { get }
182182

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

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

358+
var tempDirectory: AbsolutePath {
359+
let override = ProcessEnv.vars["TMPDIR"] ?? ProcessEnv.vars["TEMP"] ?? ProcessEnv.vars["TMP"]
360+
if let path = override.flatMap({ try? AbsolutePath(validating: $0) }) {
361+
return path
362+
}
363+
return AbsolutePath(NSTemporaryDirectory())
364+
}
365+
355366
func getDirectoryContents(_ path: AbsolutePath) throws -> [String] {
356-
#if canImport(Darwin)
367+
#if canImport(Darwin)
357368
return try FileManager.default.contentsOfDirectory(atPath: path.pathString)
358-
#else
369+
#else
359370
do {
360371
return try FileManager.default.contentsOfDirectory(atPath: path.pathString)
361372
} catch let error as NSError {
@@ -367,7 +378,7 @@ private class LocalFileSystem: FileSystem {
367378
}
368379
throw error
369380
}
370-
#endif
381+
#endif
371382
}
372383

373384
func createDirectory(_ path: AbsolutePath, recursive: Bool) throws {
@@ -478,10 +489,10 @@ private class LocalFileSystem: FileSystem {
478489
guard isDirectory(path) else { return }
479490

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

486497
if !options.contains(.recursive) {
487498
traverse.skipDescendants()
@@ -507,8 +518,7 @@ private class LocalFileSystem: FileSystem {
507518
}
508519

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

@@ -529,7 +539,7 @@ public class InMemoryFileSystem: FileSystem {
529539

530540
/// Creates deep copy of the object.
531541
func copy() -> Node {
532-
return Node(contents.copy())
542+
return Node(contents.copy())
533543
}
534544
}
535545

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

736+
public var tempDirectory: AbsolutePath {
737+
return AbsolutePath("/tmp")
738+
}
739+
726740
public func getDirectoryContents(_ path: AbsolutePath) throws -> [String] {
727741
return try lock.withLock {
728742
guard let node = try getNode(path) else {
@@ -872,9 +886,9 @@ public class InMemoryFileSystem: FileSystem {
872886
// Ignore root and get the parent node's content if its a directory.
873887
guard !path.isRoot,
874888
let parent = try? getNode(path.parentDirectory),
875-
case .directory(let contents) = parent.contents else {
876-
return
877-
}
889+
case .directory(let contents) = parent.contents else {
890+
return
891+
}
878892
// Set it to nil to release the contents.
879893
contents.entries[path.basename] = nil
880894
}
@@ -936,7 +950,7 @@ public class InMemoryFileSystem: FileSystem {
936950
public func withLock<T>(on path: AbsolutePath, type: FileLock.LockType = .exclusive, _ body: () throws -> T) throws -> T {
937951
let resolvedPath: AbsolutePath = try lock.withLock {
938952
if case let .symlink(destination) = try getNode(path)?.contents {
939-
return AbsolutePath(destination, relativeTo: path.parentDirectory)
953+
return AbsolutePath(destination, relativeTo: path.parentDirectory)
940954
} else {
941955
return path
942956
}
@@ -1029,6 +1043,10 @@ public class RerootedFileSystemView: FileSystem {
10291043
fatalError("cachesDirectory on RerootedFileSystemView is not supported.")
10301044
}
10311045

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

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)