|
| 1 | +// |
| 2 | +// SymbolStoreSearch.swift |
| 3 | +// MacSymbolicator |
| 4 | +// |
| 5 | + |
| 6 | +import Foundation |
| 7 | + |
| 8 | +class SymbolStoreSearch { |
| 9 | + |
| 10 | + typealias CompletionHandler = ([SearchResult]?, Bool) -> Void |
| 11 | + |
| 12 | + private static let guidRegex = #"(....)(....)-(....)-(....)-(....)-(............)"# |
| 13 | + |
| 14 | + func search( |
| 15 | + forUUIDs uuids: Set<String>, |
| 16 | + logHandler logMessage: @escaping LogHandler, |
| 17 | + completion: @escaping CompletionHandler |
| 18 | + ) { |
| 19 | + DispatchQueue.global().async { |
| 20 | + let missingUUIDs = self.mappedPathsSearch(forUUIDs: uuids, logHandler: logMessage, completion: completion) |
| 21 | + self.shellCommandSearch(forUUIDs: missingUUIDs, logHandler: logMessage, completion: completion) |
| 22 | + } |
| 23 | + } |
| 24 | + |
| 25 | + private static func mappedPathList(suiteName: String?) -> [String] { |
| 26 | + guard let defaults = UserDefaults(suiteName: suiteName) else { return [] } |
| 27 | + |
| 28 | + // This can be either a string or array of strings |
| 29 | + guard let mappedPaths = defaults.stringArray(forKey: "DBGFileMappedPaths") else { |
| 30 | + guard let mappedPathString = defaults.string(forKey: "DBGFileMappedPaths") else { return [] } |
| 31 | + return [mappedPathString] |
| 32 | + } |
| 33 | + |
| 34 | + return mappedPaths |
| 35 | + } |
| 36 | + |
| 37 | + private static func shellCommandList(suiteName: String?) -> [String] { |
| 38 | + guard let defaults = UserDefaults(suiteName: suiteName) else { return [] } |
| 39 | + |
| 40 | + // This can be either a string or array of strings |
| 41 | + guard let mappedPaths = defaults.stringArray(forKey: "DBGShellCommands") else { |
| 42 | + guard let mappedPathString = defaults.string(forKey: "DBGShellCommands") else { return [] } |
| 43 | + return [mappedPathString] |
| 44 | + } |
| 45 | + |
| 46 | + return mappedPaths |
| 47 | + } |
| 48 | + |
| 49 | + private static func pathUUIDs(path: String) -> [String]? { |
| 50 | + let command = "dwarfdump --uuid \"\(path)\"" |
| 51 | + let commandResult = command.run() |
| 52 | + |
| 53 | + if let errorOutput = commandResult.error?.trimmed, !errorOutput.isEmpty { |
| 54 | + // dwarfdump --uuid on /Users/x/Library/Developer/Xcode/Archives seems to output the dsym identifier |
| 55 | + // correctly followed by an stderr message about not being able to open macho file due to |
| 56 | + // "Too many levels of symbolic links". Seems safe to ignore. |
| 57 | + if !errorOutput.contains("Too many levels of symbolic links") { |
| 58 | + return nil |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + guard let dwarfDumpOutput = commandResult.output?.trimmed else { return nil } |
| 63 | + |
| 64 | + let foundUUIDs = dwarfDumpOutput.scan(pattern: #"UUID: (.*) \("#).flatMap({ $0 }) |
| 65 | + return foundUUIDs |
| 66 | + } |
| 67 | + |
| 68 | + private func mappedPathsSearch( |
| 69 | + forUUIDs uuids: Set<String>, |
| 70 | + logHandler logMessage: @escaping LogHandler, |
| 71 | + completion: @escaping CompletionHandler |
| 72 | + ) -> Set<String> { |
| 73 | + let mappedPaths = SymbolStoreSearch.mappedPathList(suiteName: "com.apple.DebugSymbols") + SymbolStoreSearch.mappedPathList(suiteName: nil) |
| 74 | + |
| 75 | + var results: [SearchResult] = [] |
| 76 | + |
| 77 | + for uuid in uuids { |
| 78 | + // Need to convert UUID from AAAABBBB-CCCC-DDDD-EEEE-FFFFFFFFFFFF |
| 79 | + // into AAAA/BBBB/CCCC/DDDD/EEEE/FFFFFFFFFFFF |
| 80 | + let subPath = uuid.scan(pattern: SymbolStoreSearch.guidRegex)[0] |
| 81 | + |
| 82 | + for mappedPath in mappedPaths { |
| 83 | + // There's gotta be a better way to do this |
| 84 | + var path = URL(fileURLWithPath: mappedPath) |
| 85 | + for sub in subPath { |
| 86 | + path.appendPathComponent(sub) |
| 87 | + } |
| 88 | + |
| 89 | + // If the file exists and is a dwarf file matching, accept it |
| 90 | + if FileManager().fileExists(atPath: path.path) { |
| 91 | + if let pathUUIDs = SymbolStoreSearch.pathUUIDs(path: path.path) { |
| 92 | + if pathUUIDs.contains(uuid) { |
| 93 | + results.append(SearchResult(path: path.path, matchedUUID: uuid)) |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + completion(results, false) |
| 101 | + |
| 102 | + return uuids.subtracting(results.map({ result in result.matchedUUID })) |
| 103 | + } |
| 104 | + |
| 105 | + private func shellCommandSearch( |
| 106 | + forUUIDs uuids: Set<String>, |
| 107 | + logHandler logMessage: @escaping LogHandler, |
| 108 | + completion: @escaping CompletionHandler |
| 109 | + ) { |
| 110 | + // Search through mapped paths |
| 111 | + let shellCommands = SymbolStoreSearch.shellCommandList(suiteName: "com.apple.DebugSymbols") + SymbolStoreSearch.shellCommandList(suiteName: nil) |
| 112 | + |
| 113 | + var results: [SearchResult] = [] |
| 114 | + |
| 115 | + // TODO: parallelize |
| 116 | + for uuid in uuids { |
| 117 | + for command in shellCommands { |
| 118 | + // $(command uuid) returns us an XML document with the path to the dYSM or with an error |
| 119 | + /* |
| 120 | + <?xml version="1.0" encoding="UTF-8"?> |
| 121 | + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 122 | + <plist version="1.0"> |
| 123 | + <dict> |
| 124 | + <key>4E793E15-4672-3387-9EA0-C1701F5C59CA</key> |
| 125 | + <dict> |
| 126 | + <key>DBGDSYMPath</key> |
| 127 | + <string>/Users/x/Library/SymbolCache/dsyms/4E79/3E15/4672/3387/9EA0/C1701F5C59CA</string> |
| 128 | + </dict> |
| 129 | + </dict> |
| 130 | + </plist> |
| 131 | + */ |
| 132 | + |
| 133 | + // Try to parse output |
| 134 | + logMessage("Run script: \(command) \(uuid)") |
| 135 | + guard let scriptOutput = "\(command) \(uuid)".run().output else { continue } |
| 136 | + |
| 137 | + logMessage("==> \(scriptOutput)") |
| 138 | + |
| 139 | + guard let data = scriptOutput.data(using: .utf8) else { continue } |
| 140 | + guard let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] else { continue } |
| 141 | + |
| 142 | + // If the plist has our guid as a key then see what results it has |
| 143 | + guard let value = plist[uuid] as? [String: Any] else { continue } |
| 144 | + guard let dsymPath = value["DBGDSYMPath"] as? String else { continue } |
| 145 | + |
| 146 | + // If the file exists and is a dwarf file matching, accept it |
| 147 | + if FileManager().fileExists(atPath: dsymPath) { |
| 148 | + if let pathUUIDs = SymbolStoreSearch.pathUUIDs(path: dsymPath) { |
| 149 | + if pathUUIDs.contains(uuid) { |
| 150 | + results.append(SearchResult(path: dsymPath, matchedUUID: uuid)) |
| 151 | + // Run the completion handler after each search so we can see results as they arrive |
| 152 | + completion(results, false) |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | + } |
| 158 | + completion(results, true) |
| 159 | + } |
| 160 | +} |
0 commit comments