Tools View & Crash Report & OOM Report
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import BinaryCodable
|
||||
import Foundation
|
||||
import Library
|
||||
|
||||
public enum ReportType: String, Codable {
|
||||
case crash
|
||||
case oom
|
||||
|
||||
public var directoryName: String {
|
||||
switch self {
|
||||
case .crash: return "crash_reports"
|
||||
case .oom: return "oom_reports"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ReportTransferMessageType: UInt8 {
|
||||
case error = 0
|
||||
case report = 1
|
||||
case complete = 2
|
||||
case ack = 3
|
||||
}
|
||||
|
||||
public struct ReportTransferPayload: Codable {
|
||||
public var reportType: ReportType
|
||||
public var timestamp: TimeInterval
|
||||
public var files: [ReportTransferFile]
|
||||
|
||||
public init(reportType: ReportType, timestamp: TimeInterval, files: [ReportTransferFile]) {
|
||||
self.reportType = reportType
|
||||
self.timestamp = timestamp
|
||||
self.files = files
|
||||
}
|
||||
}
|
||||
|
||||
public struct ReportTransferFile: Codable {
|
||||
public var name: String
|
||||
public var data: Data
|
||||
|
||||
public init(name: String, data: Data) {
|
||||
self.name = name
|
||||
self.data = data
|
||||
}
|
||||
}
|
||||
|
||||
public struct ReportTransferError: LocalizedError {
|
||||
public let errorDescription: String?
|
||||
|
||||
public init(_ message: String) {
|
||||
errorDescription = message
|
||||
}
|
||||
}
|
||||
|
||||
public enum ReportTransferService {
|
||||
public static let applicationServiceName = "sing-box:report-transfer"
|
||||
}
|
||||
|
||||
public enum ReportTransferMessage {
|
||||
public static func encodeReport(_ payload: ReportTransferPayload) throws -> Data {
|
||||
var data = Data([ReportTransferMessageType.report.rawValue])
|
||||
try data.append(BinaryEncoder().encode(payload))
|
||||
return data
|
||||
}
|
||||
|
||||
public static func encodeComplete() -> Data {
|
||||
Data([ReportTransferMessageType.complete.rawValue])
|
||||
}
|
||||
|
||||
public static func encodeAck() -> Data {
|
||||
Data([ReportTransferMessageType.ack.rawValue])
|
||||
}
|
||||
|
||||
public static func encodeError(_ message: String) -> Data {
|
||||
var data = Data([ReportTransferMessageType.error.rawValue])
|
||||
data.append(Data(message.utf8))
|
||||
return data
|
||||
}
|
||||
|
||||
public static func decodeType(_ data: Data) -> ReportTransferMessageType? {
|
||||
guard !data.isEmpty else { return nil }
|
||||
return ReportTransferMessageType(rawValue: data[0])
|
||||
}
|
||||
|
||||
public static func decodeReport(_ data: Data) throws -> ReportTransferPayload {
|
||||
try BinaryDecoder().decode(ReportTransferPayload.self, from: data.dropFirst())
|
||||
}
|
||||
|
||||
public static func decodeError(_ data: Data) -> String {
|
||||
String(data: data.dropFirst(), encoding: .utf8) ?? "Unknown error"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
#if os(iOS)
|
||||
|
||||
import Foundation
|
||||
import Library
|
||||
import Network
|
||||
import os
|
||||
import UIKit
|
||||
|
||||
private let logger = Logger(category: "ReportTransferServer")
|
||||
|
||||
public extension Notification.Name {
|
||||
static let reportReceived = Notification.Name("reportReceived")
|
||||
}
|
||||
|
||||
public class ReportTransferServer {
|
||||
private var listener: NWListener
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
public init() throws {
|
||||
listener = try NWListener(using: .applicationService)
|
||||
listener.service = NWListener.Service(applicationService: ReportTransferService.applicationServiceName)
|
||||
listener.newConnectionHandler = { connection in
|
||||
connection.stateUpdateHandler = { state in
|
||||
if state == .ready {
|
||||
Task.detached {
|
||||
try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 100)
|
||||
await ReportTransferConnection(connection).process()
|
||||
}
|
||||
}
|
||||
}
|
||||
connection.start(queue: .global())
|
||||
}
|
||||
}
|
||||
|
||||
public func start() {
|
||||
listener.start(queue: .global())
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
listener.cancel()
|
||||
}
|
||||
|
||||
class ReportTransferConnection {
|
||||
private let connection: NWSocket
|
||||
private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
|
||||
|
||||
init(_ connection: NWConnection) {
|
||||
self.connection = NWSocket(connection)
|
||||
}
|
||||
|
||||
func process() async {
|
||||
beginBackgroundTask()
|
||||
defer { endBackgroundTask() }
|
||||
|
||||
var receivedCount = 0
|
||||
var lastReportType: ReportType?
|
||||
do {
|
||||
while true {
|
||||
let message = try await connection.read()
|
||||
guard let type = ReportTransferMessage.decodeType(message) else {
|
||||
continue
|
||||
}
|
||||
switch type {
|
||||
case .report:
|
||||
let payload = try ReportTransferMessage.decodeReport(message)
|
||||
try importReport(payload)
|
||||
lastReportType = payload.reportType
|
||||
receivedCount += 1
|
||||
case .complete:
|
||||
logger.info("report transfer server: received \(receivedCount) report(s)")
|
||||
if receivedCount > 0 {
|
||||
let reportType = lastReportType
|
||||
await MainActor.run {
|
||||
NotificationCenter.default.post(name: .reportReceived, object: reportType)
|
||||
}
|
||||
}
|
||||
try await connection.write(ReportTransferMessage.encodeAck())
|
||||
return
|
||||
case .error:
|
||||
let errorMsg = ReportTransferMessage.decodeError(message)
|
||||
logger.warning("report transfer server: client error: \(errorMsg)")
|
||||
return
|
||||
case .ack:
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.warning("report transfer server: \(error.localizedDescription)")
|
||||
await writeError(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func importReport(_ payload: ReportTransferPayload) throws {
|
||||
let reportsDir = FilePath.workingDirectory.appendingPathComponent(payload.reportType.directoryName, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: reportsDir, withIntermediateDirectories: true)
|
||||
|
||||
let date = Date(timeIntervalSince1970: payload.timestamp)
|
||||
let artifactURL = ReportArchive.nextAvailableArtifactURL(in: reportsDir, for: date)
|
||||
try FileManager.default.createDirectory(at: artifactURL, withIntermediateDirectories: true)
|
||||
|
||||
for file in payload.files {
|
||||
let fileURL = artifactURL.appendingPathComponent(file.name)
|
||||
if file.name == ReportArchive.metadataFileName {
|
||||
try writeMetadataWithDeviceOrigin(file.data, to: fileURL)
|
||||
} else {
|
||||
try file.data.write(to: fileURL, options: .atomic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func writeMetadataWithDeviceOrigin(_ data: Data, to url: URL) throws {
|
||||
guard var json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
try data.write(to: url, options: .atomic)
|
||||
return
|
||||
}
|
||||
json["deviceOrigin"] = ReportArchive.tvOSDeviceOrigin
|
||||
let patched = try JSONSerialization.data(withJSONObject: json)
|
||||
try patched.write(to: url, options: .atomic)
|
||||
}
|
||||
|
||||
private func writeError(_ message: String) async {
|
||||
try? await connection.write(ReportTransferMessage.encodeError(message))
|
||||
}
|
||||
|
||||
private func beginBackgroundTask() {
|
||||
backgroundTaskID = UIApplication.shared.beginBackgroundTask { [weak self] in
|
||||
logger.warning("report transfer server: background task expiring")
|
||||
self?.connection.cancel()
|
||||
self?.endBackgroundTask()
|
||||
}
|
||||
}
|
||||
|
||||
private func endBackgroundTask() {
|
||||
guard backgroundTaskID != .invalid else { return }
|
||||
UIApplication.shared.endBackgroundTask(backgroundTaskID)
|
||||
backgroundTaskID = .invalid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user