132 lines
5.2 KiB
Swift
132 lines
5.2 KiB
Swift
import Foundation
|
|
|
|
public struct DeviceService: Sendable {
|
|
private let client: any SimAdminClientProtocol
|
|
private let sessions: InstanceSessionStore
|
|
private let credentials: (any CredentialStore)?
|
|
|
|
public init(
|
|
client: any SimAdminClientProtocol,
|
|
sessions: InstanceSessionStore,
|
|
credentials: (any CredentialStore)? = nil
|
|
) {
|
|
self.client = client
|
|
self.sessions = sessions
|
|
self.credentials = credentials
|
|
}
|
|
|
|
public func snapshot(_ instance: Instance) async throws -> DeviceSnapshot {
|
|
let routes: [(id: String, title: String, route: UpstreamRoute)] = [
|
|
("device", "设备", .device),
|
|
("sim", "SIM", .sim),
|
|
("network", "蜂窝网络", .network),
|
|
("stats", "系统资源", .stats),
|
|
("cpu", "CPU", .statsCpu),
|
|
("connectivity", "连接状态", .connectivity)
|
|
]
|
|
|
|
var sections: [DeviceSection] = []
|
|
var successful = 0
|
|
var unauthorized = false
|
|
for item in routes {
|
|
do {
|
|
let response = try await request(item.route, instance)
|
|
let values = try Self.flatten(Self.dataObject(response.data))
|
|
sections.append(DeviceSection(id: item.id, title: item.title, values: values))
|
|
successful += 1
|
|
} catch UpstreamError.unauthorized, UpstreamError.forbidden {
|
|
unauthorized = true
|
|
sections.append(DeviceSection(id: item.id, title: item.title, values: [:]))
|
|
} catch {
|
|
sections.append(DeviceSection(id: item.id, title: item.title, values: [:]))
|
|
}
|
|
}
|
|
if successful == 0 {
|
|
throw unauthorized ? DeviceServiceError.unauthorized : DeviceServiceError.invalidResponse
|
|
}
|
|
return DeviceSnapshot(sections: sections)
|
|
}
|
|
|
|
private func request(_ route: UpstreamRoute, _ instance: Instance) async throws -> UpstreamResponse {
|
|
var cookie = await sessions.cookie(for: instance.id, origin: instance.origin)
|
|
do {
|
|
return try await client.send(route, baseURL: instance.origin, cookie: cookie)
|
|
} catch UpstreamError.unauthorized, UpstreamError.forbidden {
|
|
cookie = try await login(instance)
|
|
return try await client.send(route, baseURL: instance.origin, cookie: cookie)
|
|
}
|
|
}
|
|
|
|
private func login(_ instance: Instance) async throws -> String {
|
|
guard let credentials,
|
|
let password = try credentials.get(instanceID: instance.id),
|
|
!password.isEmpty
|
|
else {
|
|
throw DeviceServiceError.unauthorized
|
|
}
|
|
let sessionService = InstanceSessionService(store: sessions, client: client)
|
|
let result = try await sessionService.login(instance, password: password)
|
|
guard result.authenticated,
|
|
let cookie = await sessionService.cookie(for: instance)
|
|
else {
|
|
throw DeviceServiceError.unauthorized
|
|
}
|
|
return cookie
|
|
}
|
|
|
|
static func dataObject(_ data: Data) throws -> [String: Any] {
|
|
guard let root = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
throw DeviceServiceError.invalidResponse
|
|
}
|
|
if let payload = root["data"] as? [String: Any] { return payload }
|
|
if let response = root["response"] as? [String: Any],
|
|
let payload = response["data"] as? [String: Any] {
|
|
return payload
|
|
}
|
|
return root
|
|
}
|
|
|
|
static func flatten(_ source: [String: Any], maximum: Int = 40) -> [String: String] {
|
|
var output: [String: String] = [:]
|
|
flatten(source, prefix: "", into: &output, remaining: maximum)
|
|
return output
|
|
}
|
|
|
|
private static func flatten(
|
|
_ source: [String: Any],
|
|
prefix: String,
|
|
into output: inout [String: String],
|
|
remaining: Int
|
|
) {
|
|
for (key, value) in source.sorted(by: { $0.key < $1.key }) {
|
|
guard output.count < remaining else { return }
|
|
let fullKey = prefix.isEmpty ? key : "\(prefix)_\(key)"
|
|
switch value {
|
|
case let text as String:
|
|
output[fullKey] = text
|
|
case let flag as Bool:
|
|
output[fullKey] = flag ? "是" : "否"
|
|
case let number as Int:
|
|
output[fullKey] = String(number)
|
|
case let number as Double:
|
|
output[fullKey] = String(format: "%.1f", number)
|
|
case let array as [Any]:
|
|
let text = array.compactMap { entry -> String? in
|
|
if let record = entry as? [String: Any] {
|
|
let preferred = ["name", "zone", "mount_point", "interface", "model_name"]
|
|
.compactMap { record[$0] as? String }
|
|
.first
|
|
if let preferred { return preferred }
|
|
}
|
|
return entry as? String
|
|
}.joined(separator: "、")
|
|
output[fullKey] = text
|
|
case let nested as [String: Any]:
|
|
flatten(nested, prefix: fullKey, into: &output, remaining: remaining - output.count)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|