Skip to content

filelock Workspace #2037

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 64 additions & 19 deletions Sources/Basic/Lock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,49 +31,85 @@ enum ProcessLockError: Swift.Error {
case unableToAquireLock(errno: Int32)
}

/// Provides functionality to aquire a lock on a file via POSIX's flock() method.
/// Provides functionality to aquire an exclusive lock on a file via POSIX's flock() method.
/// It can be used for things like serializing concurrent mutations on a shared resource
/// by mutiple instances of a process. The `FileLock` is not thread-safe.
/// by mutiple instances of a process.
public final class FileLock {
private typealias FileDescriptor = CInt

/// File descriptor to the lock file.
private var fd: CInt?
private var fd: FileDescriptor?

private let _lock = NSLock()

/// Path to the lock file.
private let lockFile: AbsolutePath
public let path: AbsolutePath

/// Create an instance of process lock with a name and the path where
/// the lock file can be created.
///
/// Note: The cache path should be a valid directory.
public init(name: String, cachePath: AbsolutePath) {
self.lockFile = cachePath.appending(component: name + ".lock")
public init(name: String, in directory: AbsolutePath) {
self.path = directory.appending(component: name)
}

/// Attempts to acquire a lock and immediately returns a Boolean value
/// that indicates whether the attempt was successful.
///
/// - Returns: `true` if the lock was acquired, otherwise `false`.
public func `try`() -> Bool {
_lock.lock()
defer { _lock.unlock() }

// Open the lock file.
if self.fd == nil, let fd = try? openFile(at: path) {
self.fd = fd
}

guard let fd = self.fd else { return false }

// Aquire lock on the file.
while flock(fd, LOCK_EX | LOCK_NB) != 0 {
switch errno {
case EWOULDBLOCK: // non-blocking lock not available
return false
case EINTR: // Retry if interrupted.
continue
default:
return false
}
}

return true
}

/// Try to aquire a lock. This method will block until lock the already aquired by other process.
///
/// Note: This method can throw if underlying POSIX methods fail.
public func lock() throws {
_lock.lock()
defer { _lock.unlock() }

// Open the lock file.
if fd == nil {
let fd = SPMLibc.open(lockFile.pathString, O_WRONLY | O_CREAT | O_CLOEXEC, 0o666)
if fd == -1 {
throw FileSystemError(errno: errno)
}
self.fd = fd
if self.fd == nil {
self.fd = try openFile(at: path)
}

// Aquire lock on the file.
while true {
if flock(fd!, LOCK_EX) == 0 {
break
}
while flock(self.fd!, LOCK_EX) != 0 {
// Retry if interrupted.
if errno == EINTR { continue }
if errno == EINTR {
continue
}
throw ProcessLockError.unableToAquireLock(errno: errno)
}
}

/// Unlock the held lock.
public func unlock() {
_lock.lock()
defer { _lock.unlock() }

guard let fd = fd else { return }
flock(fd, LOCK_UN)
}
Expand All @@ -85,8 +121,17 @@ public final class FileLock {

/// Execute the given block while holding the lock.
public func withLock<T>(_ body: () throws -> T) throws -> T {
try lock()
defer { unlock() }
try self.lock()
defer { self.unlock() }
return try body()
}

private func openFile(at path: AbsolutePath) throws -> FileDescriptor {
// Open the lock file.
let fd = SPMLibc.open(path.pathString, O_WRONLY | O_CREAT | O_CLOEXEC, 0o666)
if fd == -1 {
throw FileSystemError(errno: errno)
}
return fd
}
}
4 changes: 4 additions & 0 deletions Sources/Commands/SwiftBuildTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ public class SwiftBuildTool: SwiftTool<BuildToolOptions> {

// Create the build plan and build.
let plan = try BuildPlan(buildParameters: buildParameters(), graph: loadPackageGraph(), diagnostics: diagnostics)

let workspace = try getActiveWorkspace()
workspace.lockWorkspace()
try build(plan: plan, subset: subset)
workspace.unlockWorkspace()

case .binPath:
try print(buildParameters().buildPath.description)
Expand Down
4 changes: 0 additions & 4 deletions Sources/Commands/SwiftTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,6 @@ public class SwiftTool<Options: ToolOptions> {
func loadPackageGraph(
createREPLProduct: Bool = false
) throws -> PackageGraph {
do {
let workspace = try getActiveWorkspace()

// Fetch and load the package graph.
Expand All @@ -573,9 +572,6 @@ public class SwiftTool<Options: ToolOptions> {
throw Diagnostics.fatalError
}
return graph
} catch {
throw error
}
}

/// Returns the user toolchain to compile the actual product.
Expand Down
8 changes: 4 additions & 4 deletions Sources/TestSupport/TestWorkspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,22 +250,22 @@ public final class TestWorkspace {
roots: [String] = [],
deps: [TestWorkspace.PackageDependency],
_ result: (PackageGraph, DiagnosticsEngine) -> ()
) {
) throws {
let dependencies = deps.map({ $0.convert(packagesDir) })
checkPackageGraph(roots: roots, dependencies: dependencies, result)
try checkPackageGraph(roots: roots, dependencies: dependencies, result)
}

public func checkPackageGraph(
roots: [String] = [],
dependencies: [PackageGraphRootInput.PackageDependency] = [],
forceResolvedVersions: Bool = false,
_ result: (PackageGraph, DiagnosticsEngine) -> ()
) {
) throws {
let diagnostics = DiagnosticsEngine()
let workspace = createWorkspace()
let rootInput = PackageGraphRootInput(
packages: rootPaths(for: roots), dependencies: dependencies)
let graph = workspace.loadPackageGraph(
let graph = try workspace.loadPackageGraph(
root: rootInput, forceResolvedVersions: forceResolvedVersions, diagnostics: diagnostics)
result(graph, diagnostics)
}
Expand Down
81 changes: 56 additions & 25 deletions Sources/TestSupportExecutable/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,28 @@ import SPMUtility
/// - cacheDir: The cache directory where lock file should be created.
/// - path: Path to a file which will be mutated.
/// - content: Integer that should be added in that file.
func fileLockTest(cacheDir: AbsolutePath, path: AbsolutePath, content: Int) throws {
let lock = FileLock(name: "TestLock", cachePath: cacheDir)
func fileLockTest(name: String, lockDirectory: AbsolutePath, contentPath: AbsolutePath, content: Int) throws {
let lock = FileLock(name: name, in: lockDirectory)
try lock.withLock {
// Get thr current contents of the file if any.
let currentData: Int
if localFileSystem.exists(path) {
currentData = Int(try localFileSystem.readFileContents(path).description) ?? 0
if localFileSystem.exists(contentPath) {
currentData = Int(try localFileSystem.readFileContents(contentPath).description) ?? 0
} else {
currentData = 0
}
// Sum and write back to file.
try localFileSystem.writeFileContents(path, bytes: ByteString(encodingAsUTF8: String(currentData + content)))
try localFileSystem.writeFileContents(contentPath, bytes: ByteString(encodingAsUTF8: String(currentData + content)))
}
}

func fileLockLock(name: String, in directory: AbsolutePath, duration sleepValue: Int) throws {
let lock = FileLock(name: name, in: directory)
try lock.lock()
SPMLibc.sleep(UInt32(sleepValue))
lock.unlock()
}

class HandlerTest {
let interruptHandler: InterruptHandler

Expand All @@ -48,18 +55,20 @@ class HandlerTest {

enum Mode: String {
case fileLockTest
case fileLockLock
case interruptHandlerTest
case pathArgumentTest
case help
}

struct Options {
struct FileLockOptions {
let cacheDir: AbsolutePath
let path: AbsolutePath
let content: Int
var name: String?
var lockDirectory: AbsolutePath?
var contentPath: AbsolutePath?
var value: Int?
}
var fileLockOptions: FileLockOptions?
var fileLockOptions = FileLockOptions()
var temporaryFile: AbsolutePath?
var absolutePath: AbsolutePath?
var mode = Mode.help
Expand All @@ -72,18 +81,30 @@ do {
usage: "subcommand",
overview: "Test support executable")

let fileLockParser = parser.add(subparser: Mode.fileLockTest.rawValue, overview: "Execute the file lock test")

binder.bindPositional(
fileLockParser.add(positional: "cache directory", kind: String.self, usage: "Path to cache directory"),
fileLockParser.add(positional: "file path", kind: String.self, usage: "Path of the file to mutate"),
fileLockParser.add(positional: "contents", kind: Int.self, usage: "Contents to write in the file"),
to: {
$0.fileLockOptions = Options.FileLockOptions(
cacheDir: AbsolutePath($1),
path: AbsolutePath($2),
content: $3)
})
let fileLockTestParser = parser.add(subparser: Mode.fileLockTest.rawValue, overview: "Execute the file lock test")
binder.bind(positional: fileLockTestParser.add(positional: "lock name", kind: String.self, usage: "File lock name"), to: { (options, value) in
options.fileLockOptions.name = value
})
binder.bind(positional: fileLockTestParser.add(positional: "lock directory", kind: String.self, usage: "Path to lock directory"), to: { (options, value) in
options.fileLockOptions.lockDirectory = AbsolutePath(value)
})
binder.bind(positional: fileLockTestParser.add(positional: "file path", kind: String.self, usage: "Path of the file to mutate"), to: { (options, value) in
options.fileLockOptions.contentPath = AbsolutePath(value)
})
binder.bind(positional: fileLockTestParser.add(positional: "content", kind: Int.self, usage: "Contents to write in the file"), to: { (options, value) in
options.fileLockOptions.value = value
})

let fileLockLockParser = parser.add(subparser: Mode.fileLockLock.rawValue, overview: "Execute the file lock test")
binder.bind(positional: fileLockLockParser.add(positional: "lock name", kind: String.self, usage: "File lock name"), to: { (options, value) in
options.fileLockOptions.name = value
})
binder.bind(positional: fileLockLockParser.add(positional: "lock directory", kind: String.self, usage: "Path to lock directory"), to: { (options, value) in
options.fileLockOptions.lockDirectory = AbsolutePath(value)
})
binder.bind(positional: fileLockLockParser.add(positional: "duration", kind: Int.self, usage: "Lock duration"), to: { (options, value) in
options.fileLockOptions.value = value
})

let intHandlerParser = parser.add(
subparser: Mode.interruptHandlerTest.rawValue,
Expand Down Expand Up @@ -112,11 +133,21 @@ do {

switch options.mode {
case .fileLockTest:
guard let fileLockOptions = options.fileLockOptions else { break }
let fileLockOptions = options.fileLockOptions
guard let name = fileLockOptions.name, let lockDirectory = fileLockOptions.lockDirectory,
let contentPath = fileLockOptions.contentPath, let value = fileLockOptions.value else { break }
try fileLockTest(
cacheDir: fileLockOptions.cacheDir,
path: fileLockOptions.path,
content: fileLockOptions.content)
name: name,
lockDirectory: lockDirectory,
contentPath: contentPath,
content: value)
case .fileLockLock:
let fileLockOptions = options.fileLockOptions
guard let name = fileLockOptions.name, let lockDirectory = fileLockOptions.lockDirectory,
let value = fileLockOptions.value else { break }
try fileLockLock(name: name,
in: lockDirectory,
duration: value)
case .interruptHandlerTest:
let handlerTest = try HandlerTest(options.temporaryFile!)
handlerTest.run()
Expand Down
Loading