2022-01-23 20:49:06 +01:00
|
|
|
import Vapor
|
2023-01-31 19:10:57 +01:00
|
|
|
import Clairvoyant
|
2023-09-07 14:13:28 +02:00
|
|
|
import ClairvoyantVapor
|
|
|
|
import ClairvoyantBinaryCodable
|
2022-01-23 20:49:06 +01:00
|
|
|
|
2022-04-07 23:53:25 +02:00
|
|
|
var deviceManager: DeviceManager!
|
2022-01-24 17:17:06 +01:00
|
|
|
|
2023-09-07 14:13:28 +02:00
|
|
|
private var provider: VaporMetricProvider!
|
|
|
|
|
2022-05-01 13:12:16 +02:00
|
|
|
enum ServerError: Error {
|
|
|
|
case invalidAuthenticationFileContent
|
2022-05-01 13:28:06 +02:00
|
|
|
case invalidAuthenticationToken
|
2022-05-01 13:12:16 +02:00
|
|
|
}
|
|
|
|
|
2023-08-09 16:26:07 +02:00
|
|
|
private let dateFormatter: DateFormatter = {
|
|
|
|
let df = DateFormatter()
|
|
|
|
df.dateStyle = .short
|
|
|
|
df.timeStyle = .short
|
|
|
|
return df
|
|
|
|
}()
|
|
|
|
|
2022-01-23 20:49:06 +01:00
|
|
|
// configures your application
|
2023-02-17 00:09:51 +01:00
|
|
|
public func configure(_ app: Application) async throws {
|
2022-01-24 17:17:06 +01:00
|
|
|
let storageFolder = URL(fileURLWithPath: app.directory.resourcesDirectory)
|
2023-01-31 19:10:33 +01:00
|
|
|
let logFolder = storageFolder.appendingPathComponent("logs")
|
2023-09-07 15:23:44 +02:00
|
|
|
try await migrate(folder: logFolder)
|
|
|
|
fatalError("Done")
|
2023-01-31 19:10:33 +01:00
|
|
|
|
2023-09-07 14:13:28 +02:00
|
|
|
let monitor = MetricObserver(logFileFolder: logFolder, logMetricId: "sesame.log")
|
2023-01-31 19:10:57 +01:00
|
|
|
MetricObserver.standard = monitor
|
|
|
|
|
2023-02-17 00:09:51 +01:00
|
|
|
let status = try await Metric<ServerStatus>("sesame.status")
|
2023-09-07 14:13:28 +02:00
|
|
|
_ = try await status.update(.initializing)
|
2023-01-31 19:10:33 +01:00
|
|
|
|
|
|
|
let configUrl = storageFolder.appendingPathComponent("config.json")
|
|
|
|
let config = try Config(loadFrom: configUrl)
|
|
|
|
|
|
|
|
app.http.server.configuration.port = config.port
|
|
|
|
|
|
|
|
let keyFile = storageFolder.appendingPathComponent(config.keyFileName)
|
|
|
|
|
|
|
|
let (deviceKey, remoteKey) = try loadKeys(at: keyFile)
|
2023-02-17 00:09:51 +01:00
|
|
|
deviceManager = await DeviceManager(
|
|
|
|
deviceKey: deviceKey,
|
|
|
|
remoteKey: remoteKey,
|
|
|
|
deviceTimeout: config.deviceTimeout)
|
|
|
|
|
2023-01-31 19:10:33 +01:00
|
|
|
try routes(app)
|
2023-09-07 14:13:28 +02:00
|
|
|
|
|
|
|
provider = .init(observer: monitor, accessManager: config.authenticationTokens)
|
|
|
|
provider.registerRoutes(app)
|
2023-01-31 19:10:33 +01:00
|
|
|
|
|
|
|
// Gracefully shut down by closing potentially open socket
|
|
|
|
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + .seconds(5)) {
|
|
|
|
_ = app.server.onShutdown.always { _ in
|
|
|
|
deviceManager.removeDeviceConnection()
|
|
|
|
}
|
|
|
|
}
|
2023-01-31 19:10:57 +01:00
|
|
|
|
2023-09-07 14:13:28 +02:00
|
|
|
_ = try await status.update(.nominal)
|
2023-08-09 16:26:07 +02:00
|
|
|
print("[\(dateFormatter.string(from: Date()))] Server started")
|
2023-01-31 19:10:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private func loadKeys(at url: URL) throws -> (deviceKey: Data, remoteKey: Data) {
|
|
|
|
let authContent: [Data] = try String(contentsOf: url)
|
2022-01-24 17:17:06 +01:00
|
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
2022-05-01 13:12:16 +02:00
|
|
|
.components(separatedBy: "\n")
|
|
|
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
2022-05-01 13:28:06 +02:00
|
|
|
.map {
|
|
|
|
guard let key = Data(fromHexEncodedString: $0) else {
|
|
|
|
throw ServerError.invalidAuthenticationToken
|
|
|
|
}
|
|
|
|
guard key.count == SHA256.byteCount else {
|
|
|
|
throw ServerError.invalidAuthenticationToken
|
|
|
|
}
|
|
|
|
return key
|
|
|
|
}
|
2022-05-01 13:12:16 +02:00
|
|
|
guard authContent.count == 2 else {
|
|
|
|
throw ServerError.invalidAuthenticationFileContent
|
|
|
|
}
|
2023-01-31 19:10:33 +01:00
|
|
|
return (deviceKey: authContent[0], remoteKey: authContent[1])
|
2022-01-23 20:49:06 +01:00
|
|
|
}
|
2023-02-06 21:44:56 +01:00
|
|
|
|
|
|
|
func log(_ message: String) {
|
2023-02-17 00:09:51 +01:00
|
|
|
guard let observer = MetricObserver.standard else {
|
|
|
|
print(message)
|
|
|
|
return
|
|
|
|
}
|
2023-09-07 14:13:28 +02:00
|
|
|
observer.log(message)
|
2023-02-06 21:44:56 +01:00
|
|
|
}
|
2023-09-07 15:23:44 +02:00
|
|
|
|
|
|
|
import CBORCoding
|
|
|
|
|
|
|
|
private func migrate(folder: URL) async throws {
|
|
|
|
try await migrateMetric("sesame.log", containing: String.self, in: folder)
|
|
|
|
try await migrateMetric("sesame.status", containing: ServerStatus.self, in: folder)
|
|
|
|
try await migrateMetric("sesame.connected", containing: Bool.self, in: folder)
|
|
|
|
try await migrateMetric("sesame.messages", containing: Int.self, in: folder)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func migrateMetric<T>(_ id: String, containing type: T.Type, in folder: URL) async throws where T: MetricValue {
|
|
|
|
print("Processing metric \(id)")
|
|
|
|
let file = id.hashed()
|
|
|
|
let url = folder.appendingPathComponent(file)
|
|
|
|
|
|
|
|
let files = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
|
|
|
|
.filter { Int($0.lastPathComponent) != nil }
|
|
|
|
print("Found \(files.count) files for \(id)")
|
|
|
|
|
|
|
|
let all: [Timestamped<T>] = try files.map(readElements(from:))
|
|
|
|
.reduce([], +)
|
|
|
|
.sorted { $0.timestamp < $1.timestamp }
|
|
|
|
|
|
|
|
print("Found \(all.count) items for \(id)")
|
|
|
|
|
|
|
|
try FileManager.default.removeItem(at: url)
|
|
|
|
|
|
|
|
print("Removed log folder")
|
|
|
|
|
|
|
|
// TODO: Write values back to disk
|
|
|
|
let observer = MetricObserver(logFileFolder: folder, logMetricId: "sesame.migration")
|
|
|
|
let metric: Metric<T> = observer.addMetric(id: id)
|
|
|
|
try await metric.update(all)
|
|
|
|
|
|
|
|
print("Finished metric \(id)")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func readElements<T>(from url: URL) throws -> [Timestamped<T>] where T: MetricValue {
|
|
|
|
let data = try Data(contentsOf: url)
|
|
|
|
let file = url.lastPathComponent
|
|
|
|
print("File \(file): Loaded \(data.count) bytes")
|
|
|
|
|
|
|
|
let decoder = CBORDecoder()
|
|
|
|
let timestampLength = 9
|
|
|
|
let byteCountLength = 2
|
|
|
|
|
|
|
|
var result: [Timestamped<T>] = []
|
|
|
|
var currentIndex = data.startIndex
|
|
|
|
while currentIndex < data.endIndex {
|
|
|
|
let startIndexOfTimestamp = currentIndex + byteCountLength
|
|
|
|
guard startIndexOfTimestamp <= data.endIndex else {
|
|
|
|
print("File \(file): Only \(data.endIndex - currentIndex) bytes, needed \(byteCountLength) for byte count")
|
|
|
|
throw MetricError.logFileCorrupted
|
|
|
|
}
|
|
|
|
guard let byteCount = UInt16(fromData: data[currentIndex..<startIndexOfTimestamp]) else {
|
|
|
|
print("File \(file): Invalid byte count")
|
|
|
|
throw MetricError.logFileCorrupted
|
|
|
|
}
|
|
|
|
let nextIndex = startIndexOfTimestamp + Int(byteCount)
|
|
|
|
guard nextIndex <= data.endIndex else {
|
|
|
|
print("File \(file): Needed \(byteCountLength + Int(byteCount)) for timestamped value, has \(data.endIndex - startIndexOfTimestamp)")
|
|
|
|
throw MetricError.logFileCorrupted
|
|
|
|
}
|
|
|
|
guard byteCount >= timestampLength else {
|
|
|
|
print("File \(file): Only \(byteCount) bytes, needed \(timestampLength) for timestamp")
|
|
|
|
throw MetricError.logFileCorrupted
|
|
|
|
}
|
|
|
|
let timestampData = data[startIndexOfTimestamp..<startIndexOfTimestamp+timestampLength]
|
|
|
|
let timestamp = try decoder.decode(Double.self, from: timestampData)
|
|
|
|
let date = Date(timeIntervalSince1970: timestamp)
|
|
|
|
let elementData = data[currentIndex..<nextIndex]
|
|
|
|
let element: T = try decoder.decode(from: elementData)
|
|
|
|
result.append(.init(value: element, timestamp: date))
|
|
|
|
currentIndex = nextIndex
|
|
|
|
if result.count % 100 == 0 {
|
|
|
|
print("File \(file): \(result.count) entries loaded (\(currentIndex)/\(data.endIndex) bytes)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
print("Loaded \(result.count) data points")
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
extension UInt16 {
|
|
|
|
|
|
|
|
func toData() -> Data {
|
|
|
|
Data([UInt8(self >> 8 & 0xFF), UInt8(self & 0xFF)])
|
|
|
|
}
|
|
|
|
|
|
|
|
init?<T: DataProtocol>(fromData data: T) {
|
|
|
|
guard data.count == 2 else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
let bytes = Array(data)
|
|
|
|
self = UInt16(UInt32(bytes[0]) << 8 | UInt32(bytes[1]))
|
|
|
|
}
|
|
|
|
}
|