-
Notifications
You must be signed in to change notification settings - Fork 49
Adding FileLock #361
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
Merged
Merged
Adding FileLock #361
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
3569a1b
Issue #142 Implement non-blocking file lock
roulpriya 1d67cb7
Reduced critical section under lock
roulpriya 1572a30
Add lock in uninstall
roulpriya 1fd0dd2
Use atomic create for lock
roulpriya cdf027a
Adding FileLock Tests
roulpriya File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Sources/SwiftlyCore/ProcessInfo.swift |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import Foundation | ||
import SystemPackage | ||
|
||
enum FileLockError: Error, LocalizedError { | ||
case cannotAcquireLock(FilePath) | ||
case lockedByPID(FilePath, String) | ||
|
||
var errorDescription: String? { | ||
switch self { | ||
case let .cannotAcquireLock(path): | ||
return "Cannot acquire lock at \(path). Another process may be holding the lock. If you are sure no other processes are running, you can manually remove the lock file at \(path)." | ||
case let .lockedByPID(path, pid): | ||
return | ||
"Lock at \(path) is held by process ID \(pid). Wait for the process to complete or manually remove the lock file if the process is no longer running." | ||
} | ||
} | ||
} | ||
|
||
/// A non-blocking file lock implementation using file creation as locking mechanism. | ||
/// Use case: When installing multiple Swiftly instances on the same machine, | ||
/// one should acquire the lock while others poll until it becomes available. | ||
public struct FileLock { | ||
let filePath: FilePath | ||
|
||
public static let defaultPollingInterval: TimeInterval = 1 | ||
public static let defaultTimeout: TimeInterval = 300.0 | ||
|
||
public init(at path: FilePath) throws { | ||
self.filePath = path | ||
do { | ||
let fileURL = URL(fileURLWithPath: self.filePath.string) | ||
let contents = Foundation.ProcessInfo.processInfo.processIdentifier.description.data(using: .utf8) | ||
?? Data() | ||
try contents.write(to: fileURL, options: .withoutOverwriting) | ||
} catch CocoaError.fileWriteFileExists { | ||
// Read the PID from the existing lock file | ||
let fileURL = URL(fileURLWithPath: self.filePath.string) | ||
if let data = try? Data(contentsOf: fileURL), | ||
let pidString = String(data: data, encoding: .utf8)?.trimmingCharacters( | ||
in: .whitespacesAndNewlines), | ||
!pidString.isEmpty | ||
{ | ||
throw FileLockError.lockedByPID(self.filePath, pidString) | ||
} else { | ||
throw FileLockError.cannotAcquireLock(self.filePath) | ||
} | ||
} | ||
} | ||
|
||
public static func waitForLock( | ||
_ path: FilePath, | ||
timeout: TimeInterval = FileLock.defaultTimeout, | ||
pollingInterval: TimeInterval = FileLock.defaultPollingInterval | ||
) async throws -> FileLock { | ||
let start = Date() | ||
var lastError: Error? | ||
|
||
while Date().timeIntervalSince(start) < timeout { | ||
let result = Result { try FileLock(at: path) } | ||
|
||
switch result { | ||
case let .success(lock): | ||
return lock | ||
case let .failure(error): | ||
lastError = error | ||
try? await Task.sleep(for: .seconds(pollingInterval) + .milliseconds(Int.random(in: 0...200))) | ||
} | ||
} | ||
|
||
// Timeout reached, throw the last error from the loop | ||
if let lastError = lastError { | ||
throw lastError | ||
} else { | ||
throw FileLockError.cannotAcquireLock(path) | ||
} | ||
} | ||
|
||
public func unlock() async throws { | ||
try await FileSystem.remove(atPath: self.filePath) | ||
} | ||
} | ||
|
||
public func withLock<T>( | ||
_ lockFile: FilePath, | ||
timeout: TimeInterval = FileLock.defaultTimeout, | ||
pollingInterval: TimeInterval = FileLock.defaultPollingInterval, | ||
action: @escaping () async throws -> T | ||
) async throws -> T { | ||
let lock: FileLock | ||
do { | ||
lock = try await FileLock.waitForLock( | ||
lockFile, | ||
timeout: timeout, | ||
pollingInterval: pollingInterval | ||
) | ||
} catch { | ||
throw SwiftlyError(message: "Failed to acquire file lock at \(lockFile): \(error.localizedDescription)") | ||
} | ||
|
||
do { | ||
let result = try await action() | ||
try await lock.unlock() | ||
return result | ||
} catch { | ||
try await lock.unlock() | ||
throw error | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import Foundation | ||
|
||
#if os(Windows) | ||
import WinSDK | ||
#endif | ||
|
||
enum ProcessCheckError: Error { | ||
case invalidPID | ||
case checkFailed | ||
} | ||
|
||
/// Checks if a process is still running by process ID | ||
/// - Parameter pidString: The process ID | ||
/// - Returns: true if the process is running, false if it's not running or doesn't exist | ||
/// - Throws: ProcessCheckError if the check fails or PID is invalid | ||
public func isProcessRunning(pidString: String) throws -> Bool { | ||
guard let pid = Int32(pidString.trimmingCharacters(in: .whitespaces)) else { | ||
throw ProcessCheckError.invalidPID | ||
} | ||
|
||
return try isProcessRunning(pid: pid) | ||
} | ||
|
||
public func isProcessRunning(pid: Int32) throws -> Bool { | ||
#if os(macOS) || os(Linux) | ||
let result = kill(pid, 0) | ||
if result == 0 { | ||
return true | ||
} else if errno == ESRCH { // No such process | ||
return false | ||
} else if errno == EPERM { // Operation not permitted, but process exists | ||
return true | ||
} else { | ||
throw ProcessCheckError.checkFailed | ||
} | ||
|
||
#elseif os(Windows) | ||
// On Windows, use OpenProcess to check if process exists | ||
let handle = OpenProcess(DWORD(PROCESS_QUERY_LIMITED_INFORMATION), false, DWORD(pid)) | ||
if handle != nil { | ||
CloseHandle(handle) | ||
return true | ||
} else { | ||
let error = GetLastError() | ||
if error == ERROR_INVALID_PARAMETER || error == ERROR_NOT_FOUND { | ||
return false // Process not found | ||
} else { | ||
throw ProcessCheckError.checkFailed | ||
} | ||
} | ||
|
||
#else | ||
#error("Platform is not supported") | ||
#endif | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: The path is mentioned twice in the error message when it is probably only needed once.