Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class ImportExtension: CSImportExtension {
#warning("TODO: Make this support save states, cheats, and other related objects")

if RealmConfiguration.supportsAppGroups {
guard let md5Hash = FileManager.default.md5ForFile(atPath: forFileAt.path, fromOffset: 0) else {
guard let md5Hash = FileManager.default.md5ForFile(at: forFileAt, fromOffset: 0) else {
WLOG("No MD5 hash found for file")
return
}
Expand Down
2 changes: 1 addition & 1 deletion PVHashing/Sources/PVHashing/MD5Provider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import Foundation

/// Protocol for MD5 computation
public protocol MD5Provider {
func md5ForFile(atPath path: String, fromOffset offset: UInt) -> String?
func md5ForFile(at url: URL, fromOffset offset: UInt) -> String?
}
11 changes: 1 addition & 10 deletions PVHashing/Sources/PVHashing/NSFileManager+Hashing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,10 @@ import Foundation
import PVLogging

extension FileManager: MD5Provider {
public func md5ForFile(atPath path: String, fromOffset offset: UInt = 0) -> String? {
public func md5ForFile(at url: URL, fromOffset offset: UInt = 0) -> String? {
#if LEGACY_MD5
guard let url = URL(string: path) else {
ELOG("File path URL invalid")
return nil
}
return url.checksum(algorithm: .md5, fromOffset: offset)
#else
guard let url = URL(string: path) else {
print("Error: File path URL invalid")
return nil
}

do {
let md5Hash = try calculateMD5Synchronously(of: url, startingAt: UInt64(offset))
VLOG("MD5 Hash: \(md5Hash)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public final class RealmConfiguration {
return
}

if let md5 = FileManager.default.md5ForFile(atPath: fullPath.path, fromOffset: UInt(offset)), !md5.isEmpty {
if let md5 = FileManager.default.md5ForFile(at: fullPath, fromOffset: UInt(offset)), !md5.isEmpty {
newObject!["md5Hash"] = md5
counter += 1
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,36 @@ import RealmSwift
private init() {}
}

/// Actor to prevent crash when mutating and another thread is accessing
actor FileOperationTasks {
private var fileOperationTasks = Set<Task<Void, Never>>()

/// inserts item into set
/// - Parameter item: item to insert
func insert(_ item: Task<Void, Never>) {
fileOperationTasks.insert(item)
}

/// removes item from set
/// - Parameter item: item to remove
func remove(_ item: Task<Void, Never>) {
fileOperationTasks.remove(item)
}

/// clears set
func removeAll() {
fileOperationTasks.removeAll()
}

/// Cancels all ongoing file operation tasks and clears set after
func cancelAllFileOperations() {
for task in fileOperationTasks {
task.cancel()
}
fileOperationTasks.removeAll()
}
}

@Perceptible
public final class BIOSWatcher: ObservableObject {
public static let shared = BIOSWatcher()
Expand All @@ -29,7 +59,7 @@ public final class BIOSWatcher: ObservableObject {
private var newBIOSFilesContinuation: AsyncStream<[URL]>.Continuation?

/// Task group for managing concurrent file operations
private var fileOperationTasks = Set<Task<Void, Never>>()
private var fileOperationTasks = FileOperationTasks()

/// Serial queue for file operations that need to be sequential
private let fileOperationQueue = DispatchQueue(label: "com.provenance.biosWatcher.fileOperations", qos: .utility)
Expand Down Expand Up @@ -63,8 +93,10 @@ public final class BIOSWatcher: ObservableObject {
directoryWatcher = nil
}

// Cancel any ongoing file operation tasks
cancelAllFileOperations()
Task {
// Cancel any ongoing file operation tasks
await fileOperationTasks.cancelAllFileOperations()
}

// Watch BIOS directory and its subdirectories, but exclude sibling directories
let options = DirectoryWatcherOptions(
Expand Down Expand Up @@ -122,14 +154,6 @@ public final class BIOSWatcher: ObservableObject {
}
}

/// Cancels all ongoing file operation tasks
private func cancelAllFileOperations() {
for task in fileOperationTasks {
task.cancel()
}
fileOperationTasks.removeAll()
}

/// Scans for BIOS files and updates database entries
public func scanForBIOSFiles() async {
ILOG("Starting BIOS file scan")
Expand Down Expand Up @@ -324,12 +348,12 @@ public final class BIOSWatcher: ObservableObject {
}

// Store the task for potential cancellation
fileOperationTasks.insert(task)
await fileOperationTasks.insert(task)

// Set up cleanup when task completes
Task {
await task.value
self.fileOperationTasks.remove(task)
await self.fileOperationTasks.remove(task)
}
}
}
Expand Down Expand Up @@ -383,7 +407,9 @@ public final class BIOSWatcher: ObservableObject {
directoryWatcher = nil
directoryWatchingTask?.cancel()
directoryWatchingTask = nil
cancelAllFileOperations()
Task {
await fileOperationTasks.cancelAllFileOperations()
}
setupDirectoryWatcher()
}

Expand Down Expand Up @@ -483,12 +509,12 @@ public final class BIOSWatcher: ObservableObject {
}

// Store the task for potential cancellation
fileOperationTasks.insert(task)
await fileOperationTasks.insert(task)

// Set up cleanup when task completes
Task {
await task.value
self.fileOperationTasks.remove(task)
await self.fileOperationTasks.remove(task)
}
}

Expand All @@ -515,22 +541,25 @@ public final class BIOSWatcher: ObservableObject {
let task = Task.detached(priority: .utility) {
await self.processBIOSFiles([fileURL])
}

// Store the task for potential cancellation
fileOperationTasks.insert(task)

// Set up cleanup when task completes
Task {
await task.value
self.fileOperationTasks.remove(task)
// Store the task for potential cancellation
await fileOperationTasks.insert(task)

// Set up cleanup when task completes
await Task {
await task.value
await fileOperationTasks.remove(task)
}
}
}

deinit {
// Clean up resources
directoryWatcher?.stopMonitoring()
directoryWatchingTask?.cancel()
cancelAllFileOperations()
NotificationCenter.default.removeObserver(self)
Task {
await fileOperationTasks.cancelAllFileOperations()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public class ImportQueueItem: Identifiable, ObservableObject {
if let cached = cache.md5 {
return cached
} else {
let computed = md5Provider.md5ForFile(atPath: url.path, fromOffset: 0)
let computed = md5Provider.md5ForFile(at: url, fromOffset: 0)
cache.md5 = computed
return computed
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing {
// @MainActor
internal func importToDatabaseROM(forItem queueItem: ImportQueueItem, system: SystemIdentifier, relatedFiles: [URL]?) async throws {

guard let _ = queueItem.destinationUrl else {
guard let destinationUrl = queueItem.destinationUrl else {
//how did we get here, throw?
throw GameImporterError.incorrectDestinationURL
}
Expand All @@ -136,7 +136,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing {
throw GameImporterError.noSystemMatched
}

let file = PVFile(withURL: queueItem.destinationUrl!)
let file = PVFile(withURL: destinationUrl, relativeRoot: .iCloud)
let game = PVGame(withFile: file, system: system)
game.romPath = partialPath
game.title = title
Expand Down Expand Up @@ -206,7 +206,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing {
}
if PVMediaCache.fileExists(forKey: url) {
if let localURL = PVMediaCache.filePath(forKey: url) {
let file = PVImageFile(withURL: localURL, relativeRoot: .iCloud)
let file = PVImageFile(withURL: localURL, relativeRoot: .documents)
game.originalArtworkFile = file
return game
}
Expand All @@ -230,15 +230,15 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing {
if let artwork = NSImage(data: data) {
do {
let localURL = try PVMediaCache.writeImage(toDisk: artwork, withKey: url)
let file = PVImageFile(withURL: localURL, relativeRoot: .iCloud)
let file = PVImageFile(withURL: localURL, relativeRoot: .documents)
game.originalArtworkFile = file
} catch { ELOG("\(error.localizedDescription)") }
}
#elseif !os(watchOS)
if let artwork = UIImage(data: data) {
do {
let localURL = try PVMediaCache.writeImage(toDisk: artwork, withKey: url)
let file = PVImageFile(withURL: localURL, relativeRoot: .iCloud)
let file = PVImageFile(withURL: localURL, relativeRoot: .documents)
game.originalArtworkFile = file
} catch { ELOG("\(error.localizedDescription)") }
}
Expand Down Expand Up @@ -480,7 +480,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing {
ELOG("Cannot find file at path: \(romPath)")
return nil
}
return fm.md5ForFile(atPath: romPath.path, fromOffset: offset)
return fm.md5ForFile(at: romPath, fromOffset: offset)
}

return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,8 @@ public extension PVFile {
return md5
}
guard let url = url else { return nil }
let path = url.path
// Lazy make MD5
guard let calculatedMD5 = FileManager.default.md5ForFile(atPath: path, fromOffset: 0) else {
guard let calculatedMD5 = FileManager.default.md5ForFile(at: url, fromOffset: 0) else {
ELOG("calculatedMD5 nil")
return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import PVHashing
class MockMD5Provider: MD5Provider {
var mockMD5: String?

func md5ForFile(atPath path: String, fromOffset offset: UInt = 0) -> String? {
func md5ForFile(at url: URL, fromOffset offset: UInt = 0) -> String? {
return mockMD5
}
}
Expand Down
2 changes: 1 addition & 1 deletion PVPrimitives/Sources/PVPrimitives/LocalFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public struct LocalFile: LocalFileProvider, Codable, Equatable, Sendable {
if let md5Cache = md5Cache {
return md5Cache.uppercased()
} else {
let md5 = await FileManager.default.md5ForFile(atPath: url.path, fromOffset: 0)
let md5 = await FileManager.default.md5ForFile(at: url, fromOffset: 0)
md5Cache = md5
return md5
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public extension LocalFileInfoProvider {
}

// Lazy make MD5
guard let calculatedMD5 = FileManager.default.md5ForFile(atPath: url.path, fromOffset: 0) else {
guard let calculatedMD5 = FileManager.default.md5ForFile(at: url, fromOffset: 0) else {
return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public final class PVRetroArchCoreManager {

/// Gets the MD5 hash of a file at a given URL
public func md5Hash(for url: URL) async -> String? {
return FileManager.default.md5ForFile(atPath: url.path, fromOffset: 0)
return FileManager.default.md5ForFile(at: url, fromOffset: 0)
}

/// Returns the active config file URL in Documents directory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public extension GameLaunchingViewController {
Task {
try await downloadFileIfNeeded(fullBIOSFileURL)
}
if let hash = FileManager.default.md5ForFile(atPath: fullBIOSFileURL.path, fromOffset: 0), !hash.isEmpty {
if let hash = FileManager.default.md5ForFile(at: fullBIOSFileURL, fromOffset: 0), !hash.isEmpty {
// Make mutable
var hashDictionary = hashDictionary
hashDictionary[hash] = filename
Expand Down Expand Up @@ -183,9 +183,9 @@ public extension GameLaunchingViewController {
} else {
// Not as important, but log if MD5 is mismatched.
// Cores care about filenames for some reason, not MD5s
let path = system.biosDirectory.appendingPathComponent(expectedFilename, isDirectory: false).path
let url = system.biosDirectory.appendingPathComponent(expectedFilename, isDirectory: false)
Task.detached(priority: .low) {
let fileMD5 = FileManager.default.md5ForFile(atPath: path, fromOffset: 0) ?? ""
let fileMD5 = FileManager.default.md5ForFile(at: url, fromOffset: 0) ?? ""
let expectedMD5 = currentEntry.expectedMD5.lowercased()
if fileMD5 != expectedMD5 {
WLOG("MD5 hash for \(expectedFilename) didn't match the expected value.\nGot {\(fileMD5)} expected {\(expectedMD5)}")
Expand Down
Loading