Skip to content

Commit

Permalink
Add async overloads of withLock for FS I/O (#420)
Browse files Browse the repository at this point in the history
When converting parts of the SwiftPM codebase from callbacks to `async`/`await` I've stumbled upon uses of file system locking that have to work across an async closure call. This is an additive change and has no impact on the existing blocking non-async uses of `FileLock.withLock`.
  • Loading branch information
MaxDesiatov authored Jun 2, 2023
1 parent be6f396 commit 033ab45
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 9 deletions.
27 changes: 21 additions & 6 deletions Sources/TSCBasic/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@ public protocol FileSystem: Sendable {

/// Execute the given block while holding the lock.
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, _ body: () throws -> T) throws -> T

/// Execute the given block while holding the lock.
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, _ body: () async throws -> T) async throws -> T
}

/// Convenience implementations (default arguments aren't permitted in protocol
Expand Down Expand Up @@ -336,6 +339,10 @@ public extension FileSystem {
throw FileSystemError(.unsupported, path)
}

func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, _ body: () async throws -> T) async throws -> T {
throw FileSystemError(.unsupported, path)
}

func hasQuarantineAttribute(_ path: AbsolutePath) -> Bool { false }

func hasAttribute(_ name: FileSystemAttribute, _ path: AbsolutePath) -> Bool { false }
Expand Down Expand Up @@ -601,12 +608,20 @@ private struct LocalFileSystem: FileSystem {
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType = .exclusive, _ body: () throws -> T) throws -> T {
try FileLock.withLock(fileToLock: path, type: type, body: body)
}

func withLock<T>(
on path: AbsolutePath,
type: FileLock.LockType = .exclusive,
_ body: () async throws -> T
) async throws -> T {
try await FileLock.withLock(fileToLock: path, type: type, body: body)
}
}

/// Concrete FileSystem implementation which simulates an empty disk.
public final class InMemoryFileSystem: FileSystem {
/// Private internal representation of a file system node.
/// Not threadsafe.
/// Not thread-safe.
private class Node {
/// The actual node data.
let contents: NodeContents
Expand All @@ -622,7 +637,7 @@ public final class InMemoryFileSystem: FileSystem {
}

/// Private internal representation the contents of a file system node.
/// Not threadsafe.
/// Not thread-safe.
private enum NodeContents {
case file(ByteString)
case directory(DirectoryContents)
Expand All @@ -642,7 +657,7 @@ public final class InMemoryFileSystem: FileSystem {
}

/// Private internal representation the contents of a directory.
/// Not threadsafe.
/// Not thread-safe.
private final class DirectoryContents {
var entries: [String: Node]

Expand Down Expand Up @@ -697,7 +712,7 @@ public final class InMemoryFileSystem: FileSystem {
}

/// Private function to look up the node corresponding to a path.
/// Not threadsafe.
/// Not thread-safe.
private func getNode(_ path: AbsolutePath, followSymlink: Bool = true) throws -> Node? {
func getNodeInternal(_ path: AbsolutePath) throws -> Node? {
// If this is the root node, return it.
Expand Down Expand Up @@ -841,7 +856,7 @@ public final class InMemoryFileSystem: FileSystem {
}
}

/// Not threadsafe.
/// Not thread-safe.
private func _createDirectory(_ path: AbsolutePath, recursive: Bool) throws {
// Ignore if client passes root.
guard !path.isRoot else {
Expand Down Expand Up @@ -989,7 +1004,7 @@ public final class InMemoryFileSystem: FileSystem {
}

/// Private implementation of core copying function.
/// Not threadsafe.
/// Not thread-safe.
private func _copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
// Get the source node.
guard let source = try getNode(sourcePath) else {
Expand Down
36 changes: 33 additions & 3 deletions Sources/TSCBasic/Lock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,19 @@ public final class FileLock {
defer { unlock() }
return try body()
}

public static func withLock<T>(fileToLock: AbsolutePath, lockFilesDirectory: AbsolutePath? = nil, type: LockType = .exclusive, body: () throws -> T) throws -> T {

/// Execute the given block while holding the lock.
public func withLock<T>(type: LockType = .exclusive, _ body: () async throws -> T) async throws -> T {
try lock(type: type)
defer { unlock() }
return try await body()
}

private static func prepareLock(
fileToLock: AbsolutePath,
at lockFilesDirectory: AbsolutePath? = nil,
_ type: LockType = .exclusive
) throws -> FileLock {
// unless specified, we use the tempDirectory to store lock files
let lockFilesDirectory = try lockFilesDirectory ?? localFileSystem.tempDirectory
if !localFileSystem.exists(lockFilesDirectory) {
Expand Down Expand Up @@ -215,7 +226,26 @@ public final class FileLock {
#endif
let lockFilePath = lockFilesDirectory.appending(component: lockFileName)

let lock = FileLock(at: lockFilePath)
return FileLock(at: lockFilePath)
}

public static func withLock<T>(
fileToLock: AbsolutePath,
lockFilesDirectory: AbsolutePath? = nil,
type: LockType = .exclusive,
body: () throws -> T
) throws -> T {
let lock = try Self.prepareLock(fileToLock: fileToLock, at: lockFilesDirectory, type)
return try lock.withLock(type: type, body)
}

public static func withLock<T>(
fileToLock: AbsolutePath,
lockFilesDirectory: AbsolutePath? = nil,
type: LockType = .exclusive,
body: () async throws -> T
) async throws -> T {
let lock = try Self.prepareLock(fileToLock: fileToLock, at: lockFilesDirectory, type)
return try await lock.withLock(type: type, body)
}
}

0 comments on commit 033ab45

Please sign in to comment.