From 6a8fb5c6b165153ae43645b9e525dc6827d29ceb Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Tue, 27 Feb 2024 18:21:06 +0100 Subject: [PATCH 1/3] Asynchronously load script files Reading files from File Provider file systems such as iCloud using the normal blocking API will fail with EDEADLK 'Resource deadlock avoided.' if the file is not currently locally downloaded. Therefore, do file access asynchronously inside NSFileCoordinator coordinate(with:queue:byAccessor:) to avoid this. --- xcode/Ext-Safari/Functions.swift | 204 ++++++++++-------- .../SafariWebExtensionHandler.swift | 56 +++-- 2 files changed, 153 insertions(+), 107 deletions(-) diff --git a/xcode/Ext-Safari/Functions.swift b/xcode/Ext-Safari/Functions.swift index 477c8c75..8301eecb 100644 --- a/xcode/Ext-Safari/Functions.swift +++ b/xcode/Ext-Safari/Functions.swift @@ -267,12 +267,12 @@ func getManifest() -> Manifest { } } -func updateManifestMatches(_ optionalFilesArray: [[String: Any]] = []) -> Bool { +func updateManifestMatches(_ optionalFilesArray: [[String: Any]] = []) async -> Bool { logger?.info("\(#function, privacy: .public) - started") // only get all files if files were not provided var files = [[String: Any]]() if optionalFilesArray.isEmpty { - guard let getFiles = getAllFiles() else {return false} + guard let getFiles = await getAllFiles() else {return false} files = getFiles } else { files = optionalFilesArray @@ -385,12 +385,12 @@ func updatePatternDict(_ filename: String, _ filePatterns: [String], _ manifestK return returnDictionary } -func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) -> Bool { +func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) async -> Bool { logger?.info("\(#function, privacy: .public) - started") // only get all files if files were not provided var files = [[String: Any]]() if optionalFilesArray.isEmpty { - guard let getFiles = getAllFiles() else { + guard let getFiles = await getAllFiles() else { logger?.info("\(#function, privacy: .public) - count not get files") return false } @@ -436,11 +436,11 @@ func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) -> Bool return true } -func updateManifestDeclarativeNetRequests(_ optionalFilesArray: [[String: Any]] = []) -> Bool { +func updateManifestDeclarativeNetRequests(_ optionalFilesArray: [[String: Any]] = []) async -> Bool { logger?.info("\(#function, privacy: .public) - started") var files = [[String: Any]]() if optionalFilesArray.isEmpty { - guard let getFiles = getAllFiles() else { + guard let getFiles = await getAllFiles() else { logger?.error("\(#function, privacy: .public) - failed at (1)") return false } @@ -507,7 +507,7 @@ func updateManifestDeclarativeNetRequests(_ optionalFilesArray: [[String: Any]] return true } -func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { +func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) async -> Bool { logger?.info("\(#function, privacy: .public) - started") // purge all manifest keys of any stale entries var update = false, manifest = getManifest(), allSaveLocationFilenames = [String]() @@ -515,7 +515,7 @@ func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { var allFiles = [[String: Any]]() if optionalFilesArray.isEmpty { // if getAllFiles fails to return, ignore and pass an empty array - let getFiles = getAllFiles() ?? [] + let getFiles = await getAllFiles() ?? [] allFiles = getFiles } else { allFiles = optionalFilesArray @@ -677,8 +677,13 @@ func updateSettings(_ settings: [String: String]) -> Bool { } // files -func getAllFiles(includeCode: Bool = false) -> [[String: Any]]? { +func getAllFiles(includeCode: Bool = false) async -> [[String: Any]]? { logger?.info("\(#function, privacy: .public) - started") + + let queue = OperationQueue() + queue.underlyingQueue = .global() + let fc = NSFileCoordinator() + // returns all files of proper type with filenames, metadata & more var files = [[String: Any]]() let fm = FileManager.default @@ -697,51 +702,74 @@ func getAllFiles(includeCode: Bool = false) -> [[String: Any]]? { logger?.error("\(#function, privacy: .public) - failed at (2)") return nil } + for url in urls { - var fileData = [String: Any]() // only read contents for css & js files let filename = url.lastPathComponent if (!filename.hasSuffix(".css") && !filename.hasSuffix(".js")) { continue } - // file will be skipped if metablock is missing - guard - let content = try? String(contentsOf: url, encoding: .utf8), - let dateMod = try? fm.attributesOfItem(atPath: url.path)[.modificationDate] as? Date, - let parsed = parse(content), - let metadata = parsed["metadata"] as? [String: [String]], - let type = filename.split(separator: ".").last - else { - logger?.info("\(#function, privacy: .public) - ignoring \(filename, privacy: .public), file missing or metadata missing from file contents") - continue - } - fileData["canUpdate"] = false - fileData["content"] = content - fileData["disabled"] = manifest.disabled.contains(filename) - fileData["filename"] = filename - fileData["lastModified"] = dateToMilliseconds(dateMod) - fileData["metadata"] = metadata - // force unwrap name since parser ensure it exists - fileData["name"] = metadata["name"]![0] - fileData["type"] = "\(type)" - // add extra data if a request userscript - let runAt = metadata["run-at"]?[0] ?? "document-end" - if runAt == "request" { - fileData["request"] = true - } - if metadata["description"] != nil { - fileData["description"] = metadata["description"]![0] - } - if metadata["version"] != nil && metadata["updateURL"] != nil { - fileData["canUpdate"] = true + + let intent = NSFileAccessIntent.readingIntent(with: url); + + let fileData: [String: Any]? = await withCheckedContinuation { continuation in + fc.coordinate(with: [intent], queue: queue) { error in + if let error { + logger?.error("\(error)") + continuation.resume(returning: nil) + return + } + + let fm = FileManager.default + + // file will be skipped if metablock is missing + guard + let content = try? String(contentsOf: url, encoding: .utf8), + let dateMod = try? fm.attributesOfItem(atPath: url.path)[.modificationDate] as? Date, + let parsed = parse(content), + let metadata = parsed["metadata"] as? [String: [String]], + let type = filename.split(separator: ".").last + else { + logger?.info("\(#function, privacy: .public) - ignoring \(filename, privacy: .public), file missing or metadata missing from file contents") + continuation.resume(returning: nil) + return + } + + var fileData: [String: Any] = [:] + fileData["canUpdate"] = false + fileData["content"] = content + fileData["disabled"] = manifest.disabled.contains(filename) + fileData["filename"] = filename + fileData["lastModified"] = dateToMilliseconds(dateMod) + fileData["metadata"] = metadata + // force unwrap name since parser ensure it exists + fileData["name"] = metadata["name"]![0] + fileData["type"] = "\(type)" + // add extra data if a request userscript + let runAt = metadata["run-at"]?[0] ?? "document-end" + if runAt == "request" { + fileData["request"] = true + } + if metadata["description"] != nil { + fileData["description"] = metadata["description"]![0] + } + if metadata["version"] != nil && metadata["updateURL"] != nil { + fileData["canUpdate"] = true + } + fileData["noframes"] = metadata["noframes"] != nil ? true : false + // if asked, also return the code string + if (includeCode) { + // can force unwrap because always returned from parser + fileData["code"] = parsed["code"] as! String + } + + continuation.resume(returning: fileData) + }; } - fileData["noframes"] = metadata["noframes"] != nil ? true : false - // if asked, also return the code string - if (includeCode) { - // can force unwrap because always returned from parser - fileData["code"] = parsed["code"] as! String + + if let fileData { + files.append(fileData) } - files.append(fileData) } logger?.info("\(#function, privacy: .public) - completed") return files @@ -820,11 +848,11 @@ func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: Stri return true } -func checkForRemoteUpdates(_ optionalFilesArray: [[String: Any]] = []) -> [[String: String]]? { +func checkForRemoteUpdates(_ optionalFilesArray: [[String: Any]] = []) async -> [[String: String]]? { // only get all files if files were not provided var files = [[String: Any]]() if optionalFilesArray.isEmpty { - guard let getFiles = getAllFiles() else { + guard let getFiles = await getAllFiles() else { logger?.error("\(#function, privacy: .public) - failed at (1)") return nil } @@ -912,10 +940,10 @@ func getRemoteFileContents(_ url: String) -> String? { return contents } -func updateAllFiles(_ optionalFilesArray: [[String: Any]] = []) -> Bool { +func updateAllFiles(_ optionalFilesArray: [[String: Any]] = []) async -> Bool { // get names of all files with updates available guard - let filesWithUpdates = checkForRemoteUpdates(optionalFilesArray), + let filesWithUpdates = await checkForRemoteUpdates(optionalFilesArray), let saveLocation = getSaveLocation() else { logger?.error("\(#function, privacy: .public) - failed to update files (1)") @@ -1397,7 +1425,7 @@ func getInjectionFilenames(_ url: String) -> [String]? { return filenames } -func getRequestScripts() -> [[String: String]]? { +func getRequestScripts() async -> [[String: String]]? { var requestScripts = [[String: String]]() // check the manifest to see if injection is enabled let manifest = getManifest() @@ -1409,7 +1437,7 @@ func getRequestScripts() -> [[String: String]]? { if active != "true" { return requestScripts } - guard let files = getAllFiles(includeCode: true) else { + guard let files = await getAllFiles(includeCode: true) else { logger?.error("\(#function, privacy: .public) - failed at (2)") return nil } @@ -1431,7 +1459,7 @@ func getRequestScripts() -> [[String: String]]? { return requestScripts } -func getContextMenuScripts() -> [String: Any]? { +func getContextMenuScripts() async -> [String: Any]? { var menuFilenames = [String]() // check the manifest to see if injection is enabled let manifest = getManifest() @@ -1444,7 +1472,7 @@ func getContextMenuScripts() -> [String: Any]? { return ["files": ["menu": []]] } // get all files at save location - guard let files = getAllFiles() else { + guard let files = await getAllFiles() else { logger?.error("\(#function, privacy: .public) - failed at (2)") return nil } @@ -1473,7 +1501,7 @@ func getContextMenuScripts() -> [String: Any]? { } // popup -func getPopupMatches(_ url: String, _ subframeUrls: [String]) -> [[String: Any]]? { +func getPopupMatches(_ url: String, _ subframeUrls: [String]) async -> [[String: Any]]? { var matches = [[String: Any]]() // if the url doesn't start with http/s return empty array if !url.starts(with: "http://") && !url.starts(with: "https://") { @@ -1483,7 +1511,7 @@ func getPopupMatches(_ url: String, _ subframeUrls: [String]) -> [[String: Any]] let matched = getMatchedFiles(url, nil, false) // get all the files at the save location guard - let files = getAllFiles() + let files = await getAllFiles() else { logger?.error("\(#function, privacy: .public) - failed at (1)") return nil @@ -1527,26 +1555,26 @@ func getPopupMatches(_ url: String, _ subframeUrls: [String]) -> [[String: Any]] return matches } -func popupUpdateAll() -> Bool { +func popupUpdateAll() async -> Bool { guard - let files = getAllFiles(), - updateAllFiles(files), - updateManifestMatches(files), - updateManifestRequired(files), - purgeManifest(files) + let files = await getAllFiles(), + await updateAllFiles(files), + await updateManifestMatches(files), + await updateManifestRequired(files), + await purgeManifest(files) else { return false } return true } -func getPopupBadgeCount(_ url: String, _ subframeUrls: [String]) -> Int? { +func getPopupBadgeCount(_ url: String, _ subframeUrls: [String]) async -> Int? { if !url.starts(with: "http://") && !url.starts(with: "https://") { return 0 } let manifest = getManifest() guard - var matches = getPopupMatches(url, subframeUrls) + var matches = await getPopupMatches(url, subframeUrls) else { logger?.error("\(#function, privacy: .public) - failed at (1)") return nil @@ -1560,7 +1588,7 @@ func getPopupBadgeCount(_ url: String, _ subframeUrls: [String]) -> Int? { return matches.count } -func popupUpdateSingle(_ filename: String, _ url: String, _ subframeUrls: [String]) -> [[String: Any]]? { +func popupUpdateSingle(_ filename: String, _ url: String, _ subframeUrls: [String]) async -> [[String: Any]]? { guard let saveLocation = getSaveLocation() else { logger?.error("\(#function, privacy: .public) - failed at (1)") return nil @@ -1589,11 +1617,11 @@ func popupUpdateSingle(_ filename: String, _ url: String, _ subframeUrls: [Strin return nil } guard - let files = getAllFiles(), - updateManifestMatches(files), - updateManifestRequired(files), - purgeManifest(files), - let matches = getPopupMatches(url, subframeUrls) + let files = await getAllFiles(), + await updateManifestMatches(files), + await updateManifestRequired(files), + await purgeManifest(files), + let matches = await getPopupMatches(url, subframeUrls) else { logger?.error("\(#function, privacy: .public) - failed at (4)") return nil @@ -1623,7 +1651,7 @@ func getLegacyData() -> [String: Any]? { return data } -func saveFile(_ item: [String: Any],_ content: String) -> [String: Any] { +func saveFile(_ item: [String: Any],_ content: String) async -> [String: Any] { var response = [String: Any]() let newContent = content guard let saveLocation = getSaveLocation() else { @@ -1719,11 +1747,11 @@ func saveFile(_ item: [String: Any],_ content: String) -> [String: Any] { // update manifest for new file and purge anything from old file guard - let allFiles = getAllFiles(), - updateManifestMatches(allFiles), - updateManifestRequired(allFiles), - updateManifestDeclarativeNetRequests(allFiles), - purgeManifest(allFiles) + let allFiles = await getAllFiles(), + await updateManifestMatches(allFiles), + await updateManifestRequired(allFiles), + await updateManifestDeclarativeNetRequests(allFiles), + await purgeManifest(allFiles) else { logger?.error("\(#function, privacy: .public) - failed at (4)") return ["error": "file save but manifest couldn't be updated"] @@ -1753,7 +1781,7 @@ func saveFile(_ item: [String: Any],_ content: String) -> [String: Any] { return response } -func trashFile(_ item: [String: Any]) -> Bool { +func trashFile(_ item: [String: Any]) async -> Bool { guard let saveLocation = getSaveLocation(), let filename = item["filename"] as? String @@ -1777,7 +1805,7 @@ func trashFile(_ item: [String: Any]) -> Bool { } } // update manifest - guard updateManifestMatches(), updateManifestRequired(), purgeManifest() else { + guard await updateManifestMatches(), await updateManifestRequired(), await purgeManifest() else { logger?.error("\(#function, privacy: .public) - failed at (2)") return false } @@ -1838,7 +1866,7 @@ func getFileRemoteUpdate(_ content: String) -> [String: String] { } // background -func nativeChecks() -> [String: String] { +func nativeChecks() async -> [String: String] { logger?.info("\(#function, privacy: .public) - started") #if os(iOS) // check the save location is set @@ -1862,27 +1890,27 @@ func nativeChecks() -> [String: String] { return ["error": "Native checks error (2)"] } // get all files to pass as arguments to function below - guard let allFiles = getAllFiles() else { + guard let allFiles = await getAllFiles() else { logger?.error("\(#function, privacy: .public) - getAllFiles failed") return ["error": "Native checks error (3)"] } // purge the manifest of old records - guard purgeManifest(allFiles) else { + guard await purgeManifest(allFiles) else { logger?.error("\(#function, privacy: .public) - purgeManifest failed") return ["error": "Native checks error (4)"] } // update matches in manifest - guard updateManifestMatches(allFiles) else { + guard await updateManifestMatches(allFiles) else { logger?.error("\(#function, privacy: .public) - updateManifestMatches failed") return ["error": "Native checks error (5)"] } // update the required resources - guard updateManifestRequired(allFiles) else { + guard await updateManifestRequired(allFiles) else { logger?.error("\(#function, privacy: .public) - updateManifestRequired failed") return ["error": "Native checks error (6)"] } // update declarativeNetRequest - guard updateManifestDeclarativeNetRequests(allFiles) else { + guard await updateManifestDeclarativeNetRequests(allFiles) else { logger?.error("\(#function, privacy: .public) - updateManifestDeclarativeNetRequests failed") return ["error": "Native checks error (7)"] } @@ -1892,10 +1920,10 @@ func nativeChecks() -> [String: String] { } // userscript install -func installCheck(_ content: String) -> [String: Any] { +func installCheck(_ content: String) async -> [String: Any] { // this func checks a userscript's metadata to determine if it's already installed - guard let files = getAllFiles() else { + guard let files = await getAllFiles() else { logger?.error("\(#function, privacy: .public) - failed at (1)") return ["error": "installCheck failed at (1)"] } @@ -1942,7 +1970,7 @@ func installCheck(_ content: String) -> [String: Any] { ]; } -func installUserscript(_ url: String, _ type: String, _ content: String) -> [String: Any] { +func installUserscript(_ url: String, _ type: String, _ content: String) async -> [String: Any] { guard let parsed = parse(content), let metadata = parsed["metadata"] as? [String: [String]], @@ -1954,6 +1982,6 @@ func installUserscript(_ url: String, _ type: String, _ content: String) -> [Str let name = sanitize(n) let filename = "\(name).user.\(type)" - let saved = saveFile(["filename": filename, "type": type], content) + let saved = await saveFile(["filename": filename, "type": type], content) return saved } diff --git a/xcode/Ext-Safari/SafariWebExtensionHandler.swift b/xcode/Ext-Safari/SafariWebExtensionHandler.swift index 413d76fe..02cb8ac3 100644 --- a/xcode/Ext-Safari/SafariWebExtensionHandler.swift +++ b/xcode/Ext-Safari/SafariWebExtensionHandler.swift @@ -2,13 +2,30 @@ import SafariServices class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { func beginRequest(with context: NSExtensionContext) { + let lock = DispatchSemaphore(value: 0) + + Task { + guard + let request = context.inputItems[0] as? NSExtensionItem, + let response = await asyncBeginRequest(request: request) + else { + return + } + + context.completeRequest(returningItems: [response], completionHandler: nil) + lock.signal() + } + + lock.wait() + } + + func asyncBeginRequest(request item: NSExtensionItem) async -> NSExtensionItem? { let logger = USLogger(#fileID) - let item = context.inputItems[0] as? NSExtensionItem - let message = item?.userInfo?[SFExtensionMessageKey] as? [String: Any] + let message = item.userInfo?[SFExtensionMessageKey] as? [String: Any] // if message received without name, ignore guard let name = message?["name"] as? String else { logger?.error("\(#function, privacy: .public) - could not get message name from web extension") - return + return nil } logger?.info("\(#function, privacy: .public) - Got message with name: \(name, privacy: .public)") // got a valid message, construct response based on message received @@ -27,7 +44,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } } else if name == "NATIVE_CHECKS" { - let result = nativeChecks() + let result = await nativeChecks() response.userInfo = [SFExtensionMessageKey: result] } else if name == "REQ_PLATFORM" { @@ -49,14 +66,14 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } } else if name == "REQ_REQUESTS" { - if let requestScripts = getRequestScripts() { + if let requestScripts = await getRequestScripts() { response.userInfo = [SFExtensionMessageKey: requestScripts] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to get requestScripts"]] } } else if name == "REQ_CONTEXT_MENU_SCRIPTS" { - if let contextMenuScripts = getContextMenuScripts() { + if let contextMenuScripts = await getContextMenuScripts() { response.userInfo = [SFExtensionMessageKey: contextMenuScripts] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to get contextMenuScripts"]] @@ -64,7 +81,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } else if name == "POPUP_BADGE_COUNT" { if let url = message?["url"] as? String, let frameUrls = message?["frameUrls"] as? [String] { - if let matches = getPopupBadgeCount(url, frameUrls) { + if let matches = await getPopupBadgeCount(url, frameUrls) { response.userInfo = [SFExtensionMessageKey: ["count": matches]] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to update badge count"]] @@ -75,7 +92,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } else if name == "POPUP_MATCHES"{ if let url = message?["url"] as? String, let frameUrls = message?["frameUrls"] as? [String] { - if let matches = getPopupMatches(url, frameUrls) { + if let matches = await getPopupMatches(url, frameUrls) { response.userInfo = [SFExtensionMessageKey: ["matches": matches]] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to get matches"]] @@ -85,14 +102,14 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } } else if name == "POPUP_UPDATES" { - if let updates = checkForRemoteUpdates() { + if let updates = await checkForRemoteUpdates() { response.userInfo = [SFExtensionMessageKey: ["updates": updates]] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to get updates"]] } } else if name == "POPUP_UPDATE_ALL" { - if popupUpdateAll(), let updates = checkForRemoteUpdates() { + if await popupUpdateAll(), let updates = await checkForRemoteUpdates() { response.userInfo = [SFExtensionMessageKey: ["updates": updates]] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to run update sequence"]] @@ -104,7 +121,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { let url = message?["url"] as? String, let frameUrls = message?["frameUrls"] as? [String] { - if let matches = popupUpdateSingle(filename, url, frameUrls) { + if let matches = await popupUpdateSingle(filename, url, frameUrls) { response.userInfo = [SFExtensionMessageKey: ["items": matches]] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to update file"]] @@ -114,7 +131,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } } else if name == "POPUP_CHECK_UPDATES" { - if let updates = checkForRemoteUpdates() { + if let updates = await checkForRemoteUpdates() { response.userInfo = [SFExtensionMessageKey: ["updates": updates]] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to check for updates"]] @@ -156,7 +173,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { response.userInfo = [SFExtensionMessageKey: ["success": true]] } #elseif os(iOS) - if let files = getAllFiles() { + if let files = await getAllFiles() { response.userInfo = [SFExtensionMessageKey: ["items": files]] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to get all files"]] @@ -173,7 +190,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } else if name == "POPUP_INSTALL_CHECK" { if let content = message?["content"] as? String { - response.userInfo = [SFExtensionMessageKey: installCheck(content)] + response.userInfo = [SFExtensionMessageKey: await installCheck(content)] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to get script content"]] } @@ -184,7 +201,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { let type = message?["type"] as? String, let content = message?["content"] as? String { - response.userInfo = [SFExtensionMessageKey: installUserscript(url, type, content)] + response.userInfo = [SFExtensionMessageKey: await installUserscript(url, type, content)] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to get script content (2)"]] } @@ -207,7 +224,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } else if name == "PAGE_ALL_FILES" { #if os(macOS) - if let files = getAllFiles() { + if let files = await getAllFiles() { response.userInfo = [SFExtensionMessageKey: files] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to get all files"]] @@ -220,7 +237,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { let item = message?["item"] as? [String: Any], let content = message?["content"] as? String { - let saveResponse = saveFile(item, content) + let saveResponse = await saveFile(item, content) response.userInfo = [SFExtensionMessageKey: saveResponse] } else { inBoundError = true @@ -230,7 +247,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { else if name == "PAGE_TRASH" { #if os(macOS) if let item = message?["item"] as? [String: Any] { - if trashFile(item) { + if await trashFile(item) { response.userInfo = [SFExtensionMessageKey: ["success": true]] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to trash file"]] @@ -303,6 +320,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { if inBoundError { response.userInfo = [SFExtensionMessageKey: ["error": "Failed to parse inbound message"]] } - context.completeRequest(returningItems: [response], completionHandler: nil) + + return response } } From 3313202b7ab2bdd6d77a014833a661e1793b800b Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Tue, 27 Feb 2024 18:57:40 +0100 Subject: [PATCH 2/3] Parallelize script reading --- xcode/Ext-Safari/Functions.swift | 137 +++++++++++++++++-------------- 1 file changed, 77 insertions(+), 60 deletions(-) diff --git a/xcode/Ext-Safari/Functions.swift b/xcode/Ext-Safari/Functions.swift index 8301eecb..0d44f6c6 100644 --- a/xcode/Ext-Safari/Functions.swift +++ b/xcode/Ext-Safari/Functions.swift @@ -677,12 +677,81 @@ func updateSettings(_ settings: [String: String]) -> Bool { } // files +func loadScript(at url: URL, includeCode: Bool, manifest: Manifest, onQueue queue: OperationQueue) async -> [String: Any]? { + let fc = NSFileCoordinator() + let intent = NSFileAccessIntent.readingIntent(with: url) + + let filename = url.lastPathComponent + + guard let (content, dateMod): (String, Date?) = (await withCheckedContinuation { continuation in + fc.coordinate(with: [intent], queue: queue) { error in + if let error { + logger?.info("\(#function, privacy: .public) - failed to access file, ignoring: \(error)") + continuation.resume(returning: nil) + } + + do { + let content = try String(contentsOf: url, encoding: .utf8) + let dateMod = try FileManager.default.attributesOfItem(atPath: url.path)[.modificationDate] as? Date + continuation.resume(returning: (content, dateMod)) + } catch let error { + logger?.info("\(#function, privacy: .public) - failed to read file, ignoring: \(error)") + continuation.resume(returning: nil) + } + } + }) + else { + return nil + } + + // file will be skipped if metablock is missing + guard + let dateMod, + let parsed = parse(content), + let metadata = parsed["metadata"] as? [String: [String]], + let type = filename.split(separator: ".").last + else { + logger?.info("\(#function, privacy: .public) - ignoring \(filename, privacy: .public), file missing or metadata missing from file contents") + return nil + } + + var fileData: [String: Any] = [:] + fileData["canUpdate"] = false + fileData["content"] = content + fileData["disabled"] = manifest.disabled.contains(filename) + fileData["filename"] = filename + fileData["lastModified"] = dateToMilliseconds(dateMod) + fileData["metadata"] = metadata + // force unwrap name since parser ensure it exists + fileData["name"] = metadata["name"]![0] + fileData["type"] = "\(type)" + // add extra data if a request userscript + let runAt = metadata["run-at"]?[0] ?? "document-end" + if runAt == "request" { + fileData["request"] = true + } + if metadata["description"] != nil { + fileData["description"] = metadata["description"]![0] + } + if metadata["version"] != nil && metadata["updateURL"] != nil { + fileData["canUpdate"] = true + } + fileData["noframes"] = metadata["noframes"] != nil ? true : false + + // if asked, also return the code string + if (includeCode) { + // can force unwrap because always returned from parser + fileData["code"] = parsed["code"] as! String + } + + return fileData +} + func getAllFiles(includeCode: Bool = false) async -> [[String: Any]]? { logger?.info("\(#function, privacy: .public) - started") let queue = OperationQueue() queue.underlyingQueue = .global() - let fc = NSFileCoordinator() // returns all files of proper type with filenames, metadata & more var files = [[String: Any]]() @@ -703,74 +772,22 @@ func getAllFiles(includeCode: Bool = false) async -> [[String: Any]]? { return nil } - for url in urls { + let tasks: [Task<[String: Any]?, Never>] = urls.compactMap { url in Task { // only read contents for css & js files let filename = url.lastPathComponent if (!filename.hasSuffix(".css") && !filename.hasSuffix(".js")) { - continue + return nil } - let intent = NSFileAccessIntent.readingIntent(with: url); + return await loadScript(at: url, includeCode: includeCode, manifest: manifest, onQueue: queue) + }} - let fileData: [String: Any]? = await withCheckedContinuation { continuation in - fc.coordinate(with: [intent], queue: queue) { error in - if let error { - logger?.error("\(error)") - continuation.resume(returning: nil) - return - } - - let fm = FileManager.default - - // file will be skipped if metablock is missing - guard - let content = try? String(contentsOf: url, encoding: .utf8), - let dateMod = try? fm.attributesOfItem(atPath: url.path)[.modificationDate] as? Date, - let parsed = parse(content), - let metadata = parsed["metadata"] as? [String: [String]], - let type = filename.split(separator: ".").last - else { - logger?.info("\(#function, privacy: .public) - ignoring \(filename, privacy: .public), file missing or metadata missing from file contents") - continuation.resume(returning: nil) - return - } - - var fileData: [String: Any] = [:] - fileData["canUpdate"] = false - fileData["content"] = content - fileData["disabled"] = manifest.disabled.contains(filename) - fileData["filename"] = filename - fileData["lastModified"] = dateToMilliseconds(dateMod) - fileData["metadata"] = metadata - // force unwrap name since parser ensure it exists - fileData["name"] = metadata["name"]![0] - fileData["type"] = "\(type)" - // add extra data if a request userscript - let runAt = metadata["run-at"]?[0] ?? "document-end" - if runAt == "request" { - fileData["request"] = true - } - if metadata["description"] != nil { - fileData["description"] = metadata["description"]![0] - } - if metadata["version"] != nil && metadata["updateURL"] != nil { - fileData["canUpdate"] = true - } - fileData["noframes"] = metadata["noframes"] != nil ? true : false - // if asked, also return the code string - if (includeCode) { - // can force unwrap because always returned from parser - fileData["code"] = parsed["code"] as! String - } - - continuation.resume(returning: fileData) - }; - } - - if let fileData { + for task in tasks { + if let fileData = await task.value { files.append(fileData) } } + logger?.info("\(#function, privacy: .public) - completed") return files } From 66eb8dba310d9d68d75dd3ac567e99259ce427f1 Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Tue, 27 Feb 2024 20:05:40 +0100 Subject: [PATCH 3/3] Reduce getAllFiles code paths --- xcode/Ext-Safari/Functions.swift | 101 +++++------------- .../SafariWebExtensionHandler.swift | 6 +- 2 files changed, 31 insertions(+), 76 deletions(-) diff --git a/xcode/Ext-Safari/Functions.swift b/xcode/Ext-Safari/Functions.swift index 0d44f6c6..dfbac532 100644 --- a/xcode/Ext-Safari/Functions.swift +++ b/xcode/Ext-Safari/Functions.swift @@ -267,16 +267,8 @@ func getManifest() -> Manifest { } } -func updateManifestMatches(_ optionalFilesArray: [[String: Any]] = []) async -> Bool { +func updateManifestMatches(files: [[String: Any]]) -> Bool { logger?.info("\(#function, privacy: .public) - started") - // only get all files if files were not provided - var files = [[String: Any]]() - if optionalFilesArray.isEmpty { - guard let getFiles = await getAllFiles() else {return false} - files = getFiles - } else { - files = optionalFilesArray - } var manifest = getManifest() for file in files { // can be force unwrapped because getAllFiles didn't return nil @@ -385,19 +377,8 @@ func updatePatternDict(_ filename: String, _ filePatterns: [String], _ manifestK return returnDictionary } -func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) async -> Bool { +func updateManifestRequired(files: [[String: Any]]) -> Bool { logger?.info("\(#function, privacy: .public) - started") - // only get all files if files were not provided - var files = [[String: Any]]() - if optionalFilesArray.isEmpty { - guard let getFiles = await getAllFiles() else { - logger?.info("\(#function, privacy: .public) - count not get files") - return false - } - files = getFiles - } else { - files = optionalFilesArray - } logger?.info("\(#function, privacy: .public) - will loop through \(files.count, privacy: .public)") var manifest = getManifest() for file in files { @@ -436,18 +417,8 @@ func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) async -> return true } -func updateManifestDeclarativeNetRequests(_ optionalFilesArray: [[String: Any]] = []) async -> Bool { +func updateManifestDeclarativeNetRequests(files: [[String: Any]]) -> Bool { logger?.info("\(#function, privacy: .public) - started") - var files = [[String: Any]]() - if optionalFilesArray.isEmpty { - guard let getFiles = await getAllFiles() else { - logger?.error("\(#function, privacy: .public) - failed at (1)") - return false - } - files = getFiles - } else { - files = optionalFilesArray - } var manifest = getManifest() for file in files { // can be force unwrapped because getAllFiles didn't return nil @@ -507,19 +478,10 @@ func updateManifestDeclarativeNetRequests(_ optionalFilesArray: [[String: Any]] return true } -func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) async -> Bool { +func purgeManifest(files allFiles: [[String: Any]]) -> Bool { logger?.info("\(#function, privacy: .public) - started") // purge all manifest keys of any stale entries var update = false, manifest = getManifest(), allSaveLocationFilenames = [String]() - // only get all files if files were not provided - var allFiles = [[String: Any]]() - if optionalFilesArray.isEmpty { - // if getAllFiles fails to return, ignore and pass an empty array - let getFiles = await getAllFiles() ?? [] - allFiles = getFiles - } else { - allFiles = optionalFilesArray - } // populate array with filenames for file in allFiles { if let filename = file["filename"] as? String { @@ -865,19 +827,7 @@ func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: Stri return true } -func checkForRemoteUpdates(_ optionalFilesArray: [[String: Any]] = []) async -> [[String: String]]? { - // only get all files if files were not provided - var files = [[String: Any]]() - if optionalFilesArray.isEmpty { - guard let getFiles = await getAllFiles() else { - logger?.error("\(#function, privacy: .public) - failed at (1)") - return nil - } - files = getFiles - } else { - files = optionalFilesArray - } - +func checkForRemoteUpdates(files: [[String: Any]]) -> [[String: String]]? { var hasUpdates = [[String: String]]() for file in files { // can be force unwrapped because getAllFiles didn't return nil @@ -957,10 +907,10 @@ func getRemoteFileContents(_ url: String) -> String? { return contents } -func updateAllFiles(_ optionalFilesArray: [[String: Any]] = []) async -> Bool { +func updateAllFiles(files: [[String: Any]]) -> Bool { // get names of all files with updates available guard - let filesWithUpdates = await checkForRemoteUpdates(optionalFilesArray), + let filesWithUpdates = checkForRemoteUpdates(files: files), let saveLocation = getSaveLocation() else { logger?.error("\(#function, privacy: .public) - failed to update files (1)") @@ -1575,10 +1525,10 @@ func getPopupMatches(_ url: String, _ subframeUrls: [String]) async -> [[String: func popupUpdateAll() async -> Bool { guard let files = await getAllFiles(), - await updateAllFiles(files), - await updateManifestMatches(files), - await updateManifestRequired(files), - await purgeManifest(files) + updateAllFiles(files: files), + updateManifestMatches(files: files), + updateManifestRequired(files: files), + purgeManifest(files: files) else { return false } @@ -1635,9 +1585,9 @@ func popupUpdateSingle(_ filename: String, _ url: String, _ subframeUrls: [Strin } guard let files = await getAllFiles(), - await updateManifestMatches(files), - await updateManifestRequired(files), - await purgeManifest(files), + updateManifestMatches(files: files), + updateManifestRequired(files: files), + purgeManifest(files: files), let matches = await getPopupMatches(url, subframeUrls) else { logger?.error("\(#function, privacy: .public) - failed at (4)") @@ -1765,10 +1715,10 @@ func saveFile(_ item: [String: Any],_ content: String) async -> [String: Any] { // update manifest for new file and purge anything from old file guard let allFiles = await getAllFiles(), - await updateManifestMatches(allFiles), - await updateManifestRequired(allFiles), - await updateManifestDeclarativeNetRequests(allFiles), - await purgeManifest(allFiles) + updateManifestMatches(files: allFiles), + updateManifestRequired(files: allFiles), + updateManifestDeclarativeNetRequests(files: allFiles), + purgeManifest(files: allFiles) else { logger?.error("\(#function, privacy: .public) - failed at (4)") return ["error": "file save but manifest couldn't be updated"] @@ -1822,7 +1772,12 @@ func trashFile(_ item: [String: Any]) async -> Bool { } } // update manifest - guard await updateManifestMatches(), await updateManifestRequired(), await purgeManifest() else { + guard + let files = await getAllFiles(), + updateManifestMatches(files: files), + updateManifestRequired(files: files), + purgeManifest(files: files) + else { logger?.error("\(#function, privacy: .public) - failed at (2)") return false } @@ -1912,22 +1867,22 @@ func nativeChecks() async -> [String: String] { return ["error": "Native checks error (3)"] } // purge the manifest of old records - guard await purgeManifest(allFiles) else { + guard purgeManifest(files: allFiles) else { logger?.error("\(#function, privacy: .public) - purgeManifest failed") return ["error": "Native checks error (4)"] } // update matches in manifest - guard await updateManifestMatches(allFiles) else { + guard updateManifestMatches(files: allFiles) else { logger?.error("\(#function, privacy: .public) - updateManifestMatches failed") return ["error": "Native checks error (5)"] } // update the required resources - guard await updateManifestRequired(allFiles) else { + guard updateManifestRequired(files: allFiles) else { logger?.error("\(#function, privacy: .public) - updateManifestRequired failed") return ["error": "Native checks error (6)"] } // update declarativeNetRequest - guard await updateManifestDeclarativeNetRequests(allFiles) else { + guard updateManifestDeclarativeNetRequests(files: allFiles) else { logger?.error("\(#function, privacy: .public) - updateManifestDeclarativeNetRequests failed") return ["error": "Native checks error (7)"] } diff --git a/xcode/Ext-Safari/SafariWebExtensionHandler.swift b/xcode/Ext-Safari/SafariWebExtensionHandler.swift index 02cb8ac3..c554a964 100644 --- a/xcode/Ext-Safari/SafariWebExtensionHandler.swift +++ b/xcode/Ext-Safari/SafariWebExtensionHandler.swift @@ -102,14 +102,14 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } } else if name == "POPUP_UPDATES" { - if let updates = await checkForRemoteUpdates() { + if let updates = checkForRemoteUpdates(files: await getAllFiles() ?? []) { response.userInfo = [SFExtensionMessageKey: ["updates": updates]] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to get updates"]] } } else if name == "POPUP_UPDATE_ALL" { - if await popupUpdateAll(), let updates = await checkForRemoteUpdates() { + if await popupUpdateAll(), let updates = checkForRemoteUpdates(files: await getAllFiles() ?? []) { response.userInfo = [SFExtensionMessageKey: ["updates": updates]] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to run update sequence"]] @@ -131,7 +131,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } } else if name == "POPUP_CHECK_UPDATES" { - if let updates = await checkForRemoteUpdates() { + if let updates = checkForRemoteUpdates(files: await getAllFiles() ?? []) { response.userInfo = [SFExtensionMessageKey: ["updates": updates]] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to check for updates"]]