Sources/CrashReporter2/LogParser.swift (232 lines of code) (raw):

// // Copyright 2023 aliyun-sls Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import Foundation import CommonCrypto let LINE_BLOCK_START: String = "Incident Identifier:" let LINE_BLOCK_MODULE_START: String = "" internal enum ParserState: Int { case notStart case start case blockStart case blockInProgress case idle } class BlockBuilder { let blockName: String var content: String init(blockName: String) { if blockName.hasPrefix("Memory Status(bytes):") { self.blockName = "mem_status" } else if blockName.hasPrefix("Exception Category:") { self.blockName = "exception_category" } else if blockName.hasPrefix("Thread ") && !blockName.hasSuffix("Thread State:") { self.blockName = "thread_list" } else if blockName.hasPrefix("Thread ") && blockName.hasSuffix("Thread State:") { self.blockName = "thread_state" } else if blockName.hasPrefix("Storage Status(bytes):") { self.blockName = "storage_status" } else if blockName.hasPrefix("Extra Information:") { self.blockName = "extra_information" } else { var name = blockName.trimmingCharacters(in: .whitespacesAndNewlines) if name.suffix(1) == ":" { name = String(name.prefix(name.count - 1)) } name = name.replacingOccurrences(of: " ", with: "_") self.blockName = name.lowercased() } self.content = "" } func append(line: String) { content.append(line) } func pack() { // Perform any necessary operations before packing the content } } internal class LogParser { static let shared = LogParser() var state: ParserState = .notStart var preState: ParserState = .notStart var inThreadListParsing = false var inStacktraceParsing = false var errorReason: String? = "" var errorFramework: String? var stackBlockBuilder: BlockBuilder? var errorId: String? var type: String? private init() { } func parser(filePath: String) -> [String: String]? { guard let content = try? String(contentsOfFile: filePath, encoding: .utf8) else { return nil } let mainBundlePath = Bundle.main.bundlePath var frameworks = [String]() var path = (mainBundlePath as NSString).lastPathComponent frameworks.append(String(path.prefix(upTo: path.range(of: ".")!.lowerBound))) for bundle in Bundle.allFrameworks { if bundle.bundlePath.contains(mainBundlePath) { path = (bundle.bundlePath as NSString).lastPathComponent frameworks.append(String(path.prefix(upTo: path.range(of: ".")!.lowerBound))) } } let lines = content.components(separatedBy: CharacterSet.newlines) var blockBuilders = [BlockBuilder]() var blockBuilder: BlockBuilder? preState = .notStart // state = .notStart for line in lines { // debug statement // print("debugggg, line: \(line)") // begin header info parse if state == .notStart && line.hasPrefix(LINE_BLOCK_START) { state = .start blockBuilder = BlockBuilder(blockName: "basic_info") parserBasicBlock(blockBuilder: blockBuilder, line: line) continue } // new block will parse if line == LINE_BLOCK_MODULE_START { preState = state state = .blockStart continue } // check should begin new block parse let shouldPackBlock = checkState(line: line) if shouldPackBlock { if let builder = blockBuilder { builder.pack() blockBuilders.append(builder) blockBuilder = nil } } // parse stack block if inStacktraceParsing { parserStacktraceBlock(builder: stackBlockBuilder, line: line, frameworks: frameworks) } if state == .start { parserBasicBlock(blockBuilder: blockBuilder, line: line) continue } if state == .blockStart { blockBuilder = BlockBuilder(blockName: line) parserBlock(blockBuilder: blockBuilder, line: line) state = .blockInProgress continue } if state == .blockInProgress { parserBlock(blockBuilder: blockBuilder, line: line) continue } } if let stackBlockBuilder = stackBlockBuilder { blockBuilders.append(stackBlockBuilder) } if let blockBuilder = blockBuilder { blockBuilders.append(blockBuilder) } var results = [String: String]() for block in blockBuilders { results[block.blockName] = block.content as String if block.blockName == "exception_category" { results["catId"] = md5(content: block.content) } } if let errorId = errorId { results["id"] = errorId } if let type = type { results["sub_type"] = type } if let errorFramework = errorFramework { var summary = [String: String]() summary["exception"] = type if let errorReason = errorReason { summary["reason"] = errorReason } summary["code"] = errorFramework if let jsonData = try? JSONSerialization.data(withJSONObject: summary, options: []) { if let jsonString = String(data: jsonData, encoding: .utf8) { results["summary"] = jsonString } } } return results } func parserBasicBlock(blockBuilder: BlockBuilder?, line: String) { blockBuilder?.append(line: line) blockBuilder?.append(line: "\n") if line.hasPrefix("Incident Identifier: ") { errorId = line.components(separatedBy: ":")[1].trimmingCharacters(in: .whitespacesAndNewlines) } } func parserBlock(blockBuilder: BlockBuilder?, line: String) { blockBuilder?.append(line: line) blockBuilder?.append(line: "\n") if "exception_category" == blockBuilder?.blockName { if line.hasPrefix("Exception Type") { let array = line.components(separatedBy: ":") if array.count == 2 { type = array[1].trimmingCharacters(in: .whitespacesAndNewlines) if let t = type { if t.contains("(") { type = t.prefix(upTo: t.range(of: "(")!.lowerBound).trimmingCharacters(in: .whitespacesAndNewlines) } } } } } else if "extra_information" == blockBuilder?.blockName { if line.hasPrefix("CrashDoctor Diagnosis:") || line.hasPrefix("Originated at") { errorReason?.append(line) } } } func checkState(line: String) -> Bool { if preState == .notStart { return false } if line.hasPrefix("Exception Category:") || (line.hasPrefix("Thread ") && line.hasSuffix("Thread State:")) || line.hasPrefix("Binary Images:") || line.hasPrefix("Memory Status(bytes):") || line.hasPrefix("Storage Status(bytes):") || line.hasPrefix("Extra Information:") || line.hasPrefix("User Action: ") || line.hasPrefix("User Info: ") || line.hasPrefix("Custom Crash Info: ") { preState = .notStart return true } if line.hasPrefix("Thread ") && !line.hasSuffix("Thread State:") { let shouldPack: Bool if !inThreadListParsing { inThreadListParsing = true state = .blockStart preState = .notStart shouldPack = true } else { state = .blockInProgress preState = .notStart shouldPack = false } inStacktraceParsing = line.hasSuffix(" Crashed:") return shouldPack } state = .blockInProgress preState = .notStart return false } func parserStacktraceBlock(builder: BlockBuilder?, line: String, frameworks: [String]) { if stackBlockBuilder == nil { stackBlockBuilder = BlockBuilder(blockName: "stacktrace") } stackBlockBuilder?.append(line: line) stackBlockBuilder?.append(line: "\n") if errorFramework != nil { return } for framework in frameworks { if line.contains(framework) { errorFramework = framework break } } } func md5(content: String) -> String { let cChar = (content as NSString).utf8String var result = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) CC_MD5(cChar, CC_LONG(strlen(cChar!)), &result) return result.reduce("", { $0 + String(format: "%02x", $1) }) } }