Files
sing-box-for-apple/ApplicationLibrary/Service/ReportTransferServer.swift
T

142 lines
5.8 KiB
Swift

#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