diff --git a/MacSymbolicator.xcodeproj/project.pbxproj b/MacSymbolicator.xcodeproj/project.pbxproj index a5c528b..32c1e3d 100644 --- a/MacSymbolicator.xcodeproj/project.pbxproj +++ b/MacSymbolicator.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ B2CA03D72B9CC15700AE3DFA /* FullDiskAccess in Frameworks */ = {isa = PBXBuildFile; productRef = B2CA03D62B9CC15700AE3DFA /* FullDiskAccess */; }; B2EC0F4126450D0B00E5473C /* DSYMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EC0F4026450D0B00E5473C /* DSYMTests.swift */; }; B2F6E9AF2B9E962500F2AEE2 /* FullDiskAccess in Frameworks */ = {isa = PBXBuildFile; productRef = B2F6E9AE2B9E962500F2AEE2 /* FullDiskAccess */; }; + B518E9CF2EA6D21900EB1AB2 /* SymbolStoreSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = B518E9CE2EA6D20B00EB1AB2 /* SymbolStoreSearch.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -148,6 +149,7 @@ B2C2292C1EF223C50015AB33 /* MacSymbolicator.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MacSymbolicator.entitlements; sourceTree = ""; }; B2EC0F4026450D0B00E5473C /* DSYMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DSYMTests.swift; sourceTree = ""; }; B2F64E012B9339F700D1410D /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; + B518E9CE2EA6D20B00EB1AB2 /* SymbolStoreSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolStoreSearch.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -228,6 +230,7 @@ B289E0A023F5F72A0019C28B /* DSYM Search */ = { isa = PBXGroup; children = ( + B518E9CE2EA6D20B00EB1AB2 /* SymbolStoreSearch.swift */, B289E0A123F5F7350019C28B /* DSYMSearch.swift */, B289E0A323F5F77C0019C28B /* SpotlightSearch.swift */, B21CA7E71EFCB61500AD9B75 /* FileSearch.swift */, @@ -529,6 +532,7 @@ B2B74D8F1EF8E9CC000BBFD6 /* StringExtensions.swift in Sources */, B2B74D8D1EF8E925000BBFD6 /* DSYMFile.swift in Sources */, B2B74D8B1EF8BF70000BBFD6 /* MainController.swift in Sources */, + B518E9CF2EA6D21900EB1AB2 /* SymbolStoreSearch.swift in Sources */, B295FFB3232DDCE8002791BF /* TextWindowController.swift in Sources */, B23646872618178400AB9486 /* BinaryImage.swift in Sources */, B24BB98628AFB42600610CF0 /* ReportProcess.swift in Sources */, diff --git a/MacSymbolicator/Controllers/InputCoordinator.swift b/MacSymbolicator/Controllers/InputCoordinator.swift index 37c8d16..c9abbc0 100644 --- a/MacSymbolicator/Controllers/InputCoordinator.swift +++ b/MacSymbolicator/Controllers/InputCoordinator.swift @@ -78,7 +78,7 @@ class InputCoordinator { DispatchQueue.main.async { results?.forEach { dsymResult in let dsymURL = URL(fileURLWithPath: dsymResult.path) - self?.dsymFilesDropZone.acceptFile(url: dsymURL) + self?.dsymFilesDropZone.acceptFile(url: dsymURL, validate: false) } if finished { diff --git a/MacSymbolicator/DSYM Search/DSYMSearch.swift b/MacSymbolicator/DSYM Search/DSYMSearch.swift index 8ae478c..0688e45 100644 --- a/MacSymbolicator/DSYM Search/DSYMSearch.swift +++ b/MacSymbolicator/DSYM Search/DSYMSearch.swift @@ -107,12 +107,29 @@ class DSYMSearch { processingResult = processSearchResults( recursiveFileSearchResults, expectedUUIDs: expectedUUIDs, - finished: true, + finished: false, logHandler: logMessage, callback: callback ) missingUUIDs = processingResult.missingUUIDs - logMessage("Missing UUIDs: \(missingUUIDs)") + + // Try downloading them using the user's symbol search + guard !missingUUIDs.isEmpty else { return } + + SymbolStoreSearch().search(forUUIDs: missingUUIDs, logHandler: logMessage) { results, finished in + if let results { + processingResult = processSearchResults( + results, + expectedUUIDs: expectedUUIDs, + finished: finished, + logHandler: logMessage, + callback: callback + ) + } else { + logMessage("Symbol server query failure.") + processingResult = ProcessingResult(missingUUIDs: expectedUUIDs) + } + } } } } diff --git a/MacSymbolicator/DSYM Search/SymbolStoreSearch.swift b/MacSymbolicator/DSYM Search/SymbolStoreSearch.swift new file mode 100644 index 0000000..d814eb9 --- /dev/null +++ b/MacSymbolicator/DSYM Search/SymbolStoreSearch.swift @@ -0,0 +1,160 @@ +// +// SymbolStoreSearch.swift +// MacSymbolicator +// + +import Foundation + +class SymbolStoreSearch { + + typealias CompletionHandler = ([SearchResult]?, Bool) -> Void + + private static let guidRegex = #"(....)(....)-(....)-(....)-(....)-(............)"# + + func search( + forUUIDs uuids: Set, + logHandler logMessage: @escaping LogHandler, + completion: @escaping CompletionHandler + ) { + DispatchQueue.global().async { + let missingUUIDs = self.mappedPathsSearch(forUUIDs: uuids, logHandler: logMessage, completion: completion) + self.shellCommandSearch(forUUIDs: missingUUIDs, logHandler: logMessage, completion: completion) + } + } + + private static func mappedPathList(suiteName: String?) -> [String] { + guard let defaults = UserDefaults(suiteName: suiteName) else { return [] } + + // This can be either a string or array of strings + guard let mappedPaths = defaults.stringArray(forKey: "DBGFileMappedPaths") else { + guard let mappedPathString = defaults.string(forKey: "DBGFileMappedPaths") else { return [] } + return [mappedPathString] + } + + return mappedPaths + } + + private static func shellCommandList(suiteName: String?) -> [String] { + guard let defaults = UserDefaults(suiteName: suiteName) else { return [] } + + // This can be either a string or array of strings + guard let mappedPaths = defaults.stringArray(forKey: "DBGShellCommands") else { + guard let mappedPathString = defaults.string(forKey: "DBGShellCommands") else { return [] } + return [mappedPathString] + } + + return mappedPaths + } + + private static func pathUUIDs(path: String) -> [String]? { + let command = "dwarfdump --uuid \"\(path)\"" + let commandResult = command.run() + + if let errorOutput = commandResult.error?.trimmed, !errorOutput.isEmpty { + // dwarfdump --uuid on /Users/x/Library/Developer/Xcode/Archives seems to output the dsym identifier + // correctly followed by an stderr message about not being able to open macho file due to + // "Too many levels of symbolic links". Seems safe to ignore. + if !errorOutput.contains("Too many levels of symbolic links") { + return nil + } + } + + guard let dwarfDumpOutput = commandResult.output?.trimmed else { return nil } + + let foundUUIDs = dwarfDumpOutput.scan(pattern: #"UUID: (.*) \("#).flatMap({ $0 }) + return foundUUIDs + } + + private func mappedPathsSearch( + forUUIDs uuids: Set, + logHandler logMessage: @escaping LogHandler, + completion: @escaping CompletionHandler + ) -> Set { + let mappedPaths = SymbolStoreSearch.mappedPathList(suiteName: "com.apple.DebugSymbols") + SymbolStoreSearch.mappedPathList(suiteName: nil) + + var results: [SearchResult] = [] + + for uuid in uuids { + // Need to convert UUID from AAAABBBB-CCCC-DDDD-EEEE-FFFFFFFFFFFF + // into AAAA/BBBB/CCCC/DDDD/EEEE/FFFFFFFFFFFF + let subPath = uuid.scan(pattern: SymbolStoreSearch.guidRegex)[0] + + for mappedPath in mappedPaths { + // There's gotta be a better way to do this + var path = URL(fileURLWithPath: mappedPath) + for sub in subPath { + path.appendPathComponent(sub) + } + + // If the file exists and is a dwarf file matching, accept it + if FileManager().fileExists(atPath: path.path) { + if let pathUUIDs = SymbolStoreSearch.pathUUIDs(path: path.path) { + if pathUUIDs.contains(uuid) { + results.append(SearchResult(path: path.path, matchedUUID: uuid)) + } + } + } + } + } + + completion(results, false) + + return uuids.subtracting(results.map({ result in result.matchedUUID })) + } + + private func shellCommandSearch( + forUUIDs uuids: Set, + logHandler logMessage: @escaping LogHandler, + completion: @escaping CompletionHandler + ) { + // Search through mapped paths + let shellCommands = SymbolStoreSearch.shellCommandList(suiteName: "com.apple.DebugSymbols") + SymbolStoreSearch.shellCommandList(suiteName: nil) + + var results: [SearchResult] = [] + + // TODO: parallelize + for uuid in uuids { + for command in shellCommands { + // $(command uuid) returns us an XML document with the path to the dYSM or with an error + /* + + + + + 4E793E15-4672-3387-9EA0-C1701F5C59CA + + DBGDSYMPath + /Users/x/Library/SymbolCache/dsyms/4E79/3E15/4672/3387/9EA0/C1701F5C59CA + + + + */ + + // Try to parse output + logMessage("Run script: \(command) \(uuid)") + guard let scriptOutput = "\(command) \(uuid)".run().output else { continue } + + logMessage("==> \(scriptOutput)") + + guard let data = scriptOutput.data(using: .utf8) else { continue } + guard let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] else { continue } + + // If the plist has our guid as a key then see what results it has + guard let value = plist[uuid] as? [String: Any] else { continue } + guard let dsymPath = value["DBGDSYMPath"] as? String else { continue } + + // If the file exists and is a dwarf file matching, accept it + if FileManager().fileExists(atPath: dsymPath) { + if let pathUUIDs = SymbolStoreSearch.pathUUIDs(path: dsymPath) { + if pathUUIDs.contains(uuid) { + results.append(SearchResult(path: dsymPath, matchedUUID: uuid)) + // Run the completion handler after each search so we can see results as they arrive + completion(results, false) + } + } + } + } + } + completion(results, true) + } +} diff --git a/MacSymbolicator/Views/DropZone.swift b/MacSymbolicator/Views/DropZone.swift index 0bed939..5021402 100644 --- a/MacSymbolicator/Views/DropZone.swift +++ b/MacSymbolicator/Views/DropZone.swift @@ -402,8 +402,10 @@ class DropZone: NSView { } @discardableResult - func acceptFile(url fileURL: URL) -> Bool { - guard validFileURL(fileURL) else { return false } + func acceptFile(url fileURL: URL, validate: Bool = true) -> Bool { + if validate { + guard validFileURL(fileURL) else { return false } + } let acceptedFileURLs = delegate?.receivedFiles(dropZone: self, fileURLs: [fileURL]) ?? [fileURL]