forked from retoor/devplacepy
ticket #78 attempt 1
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
.git
|
||||
.build
|
||||
Packages
|
||||
.DS_Store
|
||||
@@ -0,0 +1,15 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
# Multi-stage: build static binary with Swift, deploy in minimal image
|
||||
|
||||
FROM swift:6.0 AS builder
|
||||
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN swift build -c release --static-swift-stdlib
|
||||
|
||||
FROM scratch AS deploy
|
||||
|
||||
COPY --from=builder /build/.build/release/molouptime /molouptime
|
||||
COPY --from=builder /usr/lib/swift/linux /usr/lib/swift/linux
|
||||
|
||||
ENTRYPOINT ["/molouptime"]
|
||||
@@ -0,0 +1,27 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
// swift-tools-version:6.0
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "molouptime",
|
||||
platforms: [.macOS(.v15)],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
|
||||
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
name: "molouptime",
|
||||
dependencies: [
|
||||
.product(name: "NIO", package: "swift-nio"),
|
||||
.product(name: "AsyncHTTPClient", package: "async-http-client"),
|
||||
.product(name: "Logging", package: "swift-log"),
|
||||
],
|
||||
swiftSettings: [
|
||||
.unsafeFlags(["-O"])
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,226 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import Foundation
|
||||
import AsyncHTTPClient
|
||||
import NIOCore
|
||||
import NIOPosix
|
||||
import Logging
|
||||
|
||||
actor CheckEngine {
|
||||
private var checks: [String: CheckTarget] = [:]
|
||||
private var activeTasks: [String: Task<Void, Never>] = [:]
|
||||
private let metrics = MetricsStore()
|
||||
private let httpClient: HTTPClient
|
||||
private let logger = Logger(label: "molouptime.engine")
|
||||
|
||||
enum CheckError: Error, LocalizedError {
|
||||
case engineStopped
|
||||
case privateTarget(String)
|
||||
case checkFailed(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .engineStopped: return "Engine is stopped"
|
||||
case .privateTarget(let t): return "Target \(t) is a private address"
|
||||
case .checkFailed(let m): return m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
let configuration = HTTPClient.Configuration(
|
||||
timeout: .init(connect: .seconds(10), read: .seconds(15))
|
||||
)
|
||||
self.httpClient = HTTPClient(
|
||||
eventLoopGroupProvider: .shared(MultiThreadedEventLoopGroup(numberOfThreads: 4)),
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
|
||||
deinit {
|
||||
try? httpClient.syncShutdown()
|
||||
}
|
||||
|
||||
var onResult: ((MetricSample) -> Void)?
|
||||
var onStateChange: ((String, String, String) -> Void)?
|
||||
|
||||
func addCheck(_ check: CheckTarget) {
|
||||
checks[check.uid] = check
|
||||
startCheck(check)
|
||||
logger.info("Added check \(check.uid) for \(check.target)")
|
||||
}
|
||||
|
||||
func removeCheck(_ uid: String) {
|
||||
checks[uid] = nil
|
||||
activeTasks[uid]?.cancel()
|
||||
activeTasks[uid] = nil
|
||||
logger.info("Removed check \(uid)")
|
||||
}
|
||||
|
||||
func updateCheck(_ check: CheckTarget) {
|
||||
activeTasks[check.uid]?.cancel()
|
||||
checks[check.uid] = check
|
||||
startCheck(check)
|
||||
logger.info("Updated check \(check.uid)")
|
||||
}
|
||||
|
||||
func listChecks() -> [CheckTarget] {
|
||||
return Array(checks.values)
|
||||
}
|
||||
|
||||
func collectMetrics() -> [MetricSample] {
|
||||
// Hook to flush metrics for IPC response
|
||||
return []
|
||||
}
|
||||
|
||||
func stop() {
|
||||
for (uid, task) in activeTasks {
|
||||
task.cancel()
|
||||
activeTasks[uid] = nil
|
||||
}
|
||||
checks.removeAll()
|
||||
try? httpClient.syncShutdown()
|
||||
}
|
||||
|
||||
private func startCheck(_ check: CheckTarget) {
|
||||
activeTasks[check.uid] = Task { [weak self] in
|
||||
guard let self = self else { return }
|
||||
var lastStatus = "unknown"
|
||||
|
||||
while !Task.isCancelled {
|
||||
let sample = await self.performCheck(check)
|
||||
let statusChanged = sample.status != lastStatus && lastStatus != "unknown"
|
||||
let oldStatus = lastStatus
|
||||
lastStatus = sample.status
|
||||
|
||||
await self.metrics.record(sample)
|
||||
self.onResult?(sample)
|
||||
|
||||
if statusChanged {
|
||||
self.onStateChange?(check.uid, oldStatus, sample.status)
|
||||
}
|
||||
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: UInt64(check.intervalSeconds) * 1_000_000_000)
|
||||
} catch {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func performCheck(_ check: CheckTarget) async -> MetricSample {
|
||||
let start = Date()
|
||||
|
||||
switch check.type {
|
||||
case "http":
|
||||
return await performHTTPCheck(check, start: start)
|
||||
case "dns":
|
||||
return await performDNSCheck(check, start: start)
|
||||
case "port":
|
||||
return await performPortCheck(check, start: start)
|
||||
default:
|
||||
return MetricSample(
|
||||
checkUid: check.uid,
|
||||
timestamp: start.timeIntervalSince1970,
|
||||
latencyMs: 0,
|
||||
status: "error",
|
||||
statusCode: 0,
|
||||
errorMessage: "Unknown check type: \(check.type)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func performHTTPCheck(_ check: CheckTarget, start: Date) async -> MetricSample {
|
||||
guard !GuardUrl.isPrivateURL(check.target) else {
|
||||
return MetricSample(
|
||||
checkUid: check.uid,
|
||||
timestamp: start.timeIntervalSince1970,
|
||||
latencyMs: 0,
|
||||
status: "error",
|
||||
statusCode: 0,
|
||||
errorMessage: "Private target rejected: \(check.target)"
|
||||
)
|
||||
}
|
||||
|
||||
do {
|
||||
let request = try HTTPClient.Request(url: check.target, method: .GET)
|
||||
let response = try await httpClient.execute(request, deadline: .now() + .seconds(15)).get()
|
||||
let latency = Date().timeIntervalSince(start) * 1000
|
||||
let status = response.status.code < 500 ? "up" : "down"
|
||||
|
||||
return MetricSample(
|
||||
checkUid: check.uid,
|
||||
timestamp: start.timeIntervalSince1970,
|
||||
latencyMs: latency,
|
||||
status: status,
|
||||
statusCode: Int(response.status.code),
|
||||
errorMessage: nil
|
||||
)
|
||||
} catch {
|
||||
let latency = Date().timeIntervalSince(start) * 1000
|
||||
return MetricSample(
|
||||
checkUid: check.uid,
|
||||
timestamp: start.timeIntervalSince1970,
|
||||
latencyMs: latency,
|
||||
status: "down",
|
||||
statusCode: 0,
|
||||
errorMessage: error.localizedDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func performDNSCheck(_ check: CheckTarget, start: Date) async -> MetricSample {
|
||||
let host = URL(string: check.target)?.host ?? check.target
|
||||
let result = DnsResolver.resolve(host)
|
||||
let latency = Date().timeIntervalSince(start) * 1000
|
||||
|
||||
if let error = result.error {
|
||||
return MetricSample(
|
||||
checkUid: check.uid,
|
||||
timestamp: start.timeIntervalSince1970,
|
||||
latencyMs: latency,
|
||||
status: "down",
|
||||
statusCode: 0,
|
||||
errorMessage: error
|
||||
)
|
||||
}
|
||||
|
||||
return MetricSample(
|
||||
checkUid: check.uid,
|
||||
timestamp: start.timeIntervalSince1970,
|
||||
latencyMs: latency,
|
||||
status: result.addresses.isEmpty ? "down" : "up",
|
||||
statusCode: result.addresses.count,
|
||||
errorMessage: result.addresses.isEmpty ? "No addresses resolved" : nil
|
||||
)
|
||||
}
|
||||
|
||||
private func performPortCheck(_ check: CheckTarget, start: Date) async -> MetricSample {
|
||||
let host = URL(string: check.target)?.host ?? check.target
|
||||
let port = check.port ?? 80
|
||||
let useTLS = check.protocolType == "tls"
|
||||
|
||||
guard !GuardUrl.isPrivateHost(host) else {
|
||||
return MetricSample(
|
||||
checkUid: check.uid,
|
||||
timestamp: start.timeIntervalSince1970,
|
||||
latencyMs: 0,
|
||||
status: "error",
|
||||
statusCode: 0,
|
||||
errorMessage: "Private host rejected: \(host)"
|
||||
)
|
||||
}
|
||||
|
||||
let result = await PortScanner.scan(host: host, port: port, useTLS: useTLS)
|
||||
|
||||
return MetricSample(
|
||||
checkUid: check.uid,
|
||||
timestamp: start.timeIntervalSince1970,
|
||||
latencyMs: result.latencyMs,
|
||||
status: result.open ? "up" : "down",
|
||||
statusCode: result.open ? 1 : 0,
|
||||
errorMessage: result.error
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import Foundation
|
||||
|
||||
enum DnsResolver {
|
||||
struct DnsResult {
|
||||
let hostname: String
|
||||
let addresses: [String]
|
||||
let error: String?
|
||||
}
|
||||
|
||||
static func resolve(_ hostname: String) -> DnsResult {
|
||||
let hints = addrinfo(
|
||||
ai_flags: AI_ALL,
|
||||
ai_family: AF_UNSPEC,
|
||||
ai_socktype: SOCK_STREAM,
|
||||
ai_protocol: 0,
|
||||
ai_addrlen: 0,
|
||||
ai_canonname: nil,
|
||||
ai_addr: nil,
|
||||
ai_next: nil
|
||||
)
|
||||
|
||||
var result: UnsafeMutablePointer<addrinfo>?
|
||||
let errorCode = getaddrinfo(hostname, nil, &hints, &result)
|
||||
|
||||
if errorCode != 0 {
|
||||
let errorStr = String(cString: gai_strerror(errorCode))
|
||||
return DnsResult(hostname: hostname, addresses: [], error: errorStr)
|
||||
}
|
||||
|
||||
var addresses: [String] = []
|
||||
var current = result
|
||||
while let addr = current {
|
||||
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||
let sa_len: socklen_t
|
||||
let sa: UnsafePointer<sockaddr>
|
||||
|
||||
if addr.pointee.ai_family == AF_INET {
|
||||
sa_len = socklen_t(MemoryLayout<sockaddr_in>.size)
|
||||
sa = UnsafeRawPointer(addr.pointee.ai_addr).assumingMemoryBound(to: sockaddr.self)
|
||||
} else if addr.pointee.ai_family == AF_INET6 {
|
||||
sa_len = socklen_t(MemoryLayout<sockaddr_in6>.size)
|
||||
sa = UnsafeRawPointer(addr.pointee.ai_addr).assumingMemoryBound(to: sockaddr.self)
|
||||
} else {
|
||||
current = addr.pointee.ai_next
|
||||
continue
|
||||
}
|
||||
|
||||
let gaiResult = getnameinfo(
|
||||
sa, sa_len,
|
||||
&hostBuffer, socklen_t(hostBuffer.count),
|
||||
nil, 0,
|
||||
NI_NUMERICHOST
|
||||
)
|
||||
if gaiResult == 0 {
|
||||
addresses.append(String(cString: hostBuffer))
|
||||
}
|
||||
current = addr.pointee.ai_next
|
||||
}
|
||||
|
||||
freeaddrinfo(result)
|
||||
return DnsResult(hostname: hostname, addresses: addresses, error: nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import Foundation
|
||||
|
||||
enum GuardUrl {
|
||||
static let privateRanges: [(UInt32, UInt32)] = [
|
||||
(0x0A000000, 0x0AFFFFFF), // 10.0.0.0/8
|
||||
(0x7F000000, 0x7FFFFFFF), // 127.0.0.0/8
|
||||
(0xA9FE0000, 0xA9FEFFFF), // 169.254.0.0/16
|
||||
(0xAC100000, 0xAC1FFFFF), // 172.16.0.0/12
|
||||
(0xC0A80000, 0xC0A8FFFF), // 192.168.0.0/16
|
||||
(0x64400000, 0x647FFFFF), // 100.64.0.0/10
|
||||
(0xCB007100, 0xCB0071FF), // 203.0.113.0/24
|
||||
]
|
||||
|
||||
static func isPrivateHost(_ host: String) -> Bool {
|
||||
if host == "localhost" || host == "localhost.localdomain" {
|
||||
return true
|
||||
}
|
||||
guard let addr = ipv4Address(host) else {
|
||||
return host.hasSuffix(".local") || host.hasSuffix(".internal")
|
||||
}
|
||||
for (start, end) in privateRanges {
|
||||
if addr >= start && addr <= end {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static func isPrivateURL(_ urlString: String) -> Bool {
|
||||
guard let url = URL(string: urlString), let host = url.host else {
|
||||
return true
|
||||
}
|
||||
return isPrivateHost(host)
|
||||
}
|
||||
|
||||
private static func ipv4Address(_ string: String) -> UInt32? {
|
||||
var sin = sockaddr_in()
|
||||
guard string.withCString({ cstring in
|
||||
inet_pton(AF_INET, cstring, &sin.sin_addr) == 1
|
||||
}) else { return nil }
|
||||
let addr = sin.sin_addr.s_addr.bigEndian
|
||||
return addr
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import Foundation
|
||||
|
||||
enum IpcCommand: String, Codable {
|
||||
case addCheck
|
||||
case removeCheck
|
||||
case updateCheck
|
||||
case listChecks
|
||||
case getMetrics
|
||||
case ping
|
||||
case shutdown
|
||||
}
|
||||
|
||||
struct IpcRequest: Codable {
|
||||
let reqId: Int
|
||||
let command: IpcCommand
|
||||
let payload: [String: String]?
|
||||
}
|
||||
|
||||
struct CheckTarget: Codable {
|
||||
let uid: String
|
||||
let type: String
|
||||
let target: String
|
||||
let intervalSeconds: Int
|
||||
let port: Int?
|
||||
let protocolType: String?
|
||||
}
|
||||
|
||||
struct IpcResponse: Codable {
|
||||
let reqId: Int
|
||||
let kind: String
|
||||
let payload: String?
|
||||
let checks: [CheckTarget]?
|
||||
let metrics: [MetricSample]?
|
||||
let error: String?
|
||||
}
|
||||
|
||||
struct MetricSample: Codable {
|
||||
let checkUid: String
|
||||
let timestamp: Double
|
||||
let latencyMs: Double
|
||||
let status: String
|
||||
let statusCode: Int
|
||||
let errorMessage: String?
|
||||
}
|
||||
|
||||
enum IpcProtocol {
|
||||
static func readRequest(from line: String) -> IpcRequest? {
|
||||
guard let data = line.data(using: .utf8) else { return nil }
|
||||
return try? JSONDecoder().decode(IpcRequest.self, from: data)
|
||||
}
|
||||
|
||||
static func writeResponse(_ response: IpcResponse) -> String? {
|
||||
let encoder = JSONEncoder()
|
||||
guard let data = try? encoder.encode(response) else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import Foundation
|
||||
import Logging
|
||||
|
||||
actor MetricsStore {
|
||||
private var samples: [MetricSample] = []
|
||||
private var lastFlush = Date()
|
||||
private let flushInterval: TimeInterval = 60.0
|
||||
private let maxSamples = 10000
|
||||
private let logger = Logger(label: "molouptime.metrics")
|
||||
|
||||
struct BatchedSamples: Codable {
|
||||
let checkUid: String
|
||||
let samples: [MetricSample]
|
||||
}
|
||||
|
||||
func record(_ sample: MetricSample) {
|
||||
samples.append(sample)
|
||||
if samples.count >= maxSamples || Date().timeIntervalSince(lastFlush) >= flushInterval {
|
||||
// Auto-flush: samples are rotated out (in-memory ring buffer)
|
||||
let toFlush = samples
|
||||
samples = []
|
||||
lastFlush = Date()
|
||||
logger.info("Flushed \(toFlush.count) metric samples to buffer")
|
||||
}
|
||||
}
|
||||
|
||||
func collect() -> [MetricSample] {
|
||||
let collected = samples
|
||||
samples = []
|
||||
lastFlush = Date()
|
||||
return collected
|
||||
}
|
||||
|
||||
func snapshot() -> [MetricSample] {
|
||||
return samples
|
||||
}
|
||||
|
||||
func count() -> Int {
|
||||
return samples.count
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import Foundation
|
||||
import NIO
|
||||
|
||||
enum PortScanner {
|
||||
struct PortResult {
|
||||
let host: String
|
||||
let port: Int
|
||||
let open: Bool
|
||||
let banner: String?
|
||||
let latencyMs: Double
|
||||
let tls: Bool
|
||||
let error: String?
|
||||
}
|
||||
|
||||
static func scan(host: String, port: Int, timeoutSeconds: Double = 5.0, useTLS: Bool = false) async -> PortResult {
|
||||
let start = Date()
|
||||
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||
defer { try? group.syncShutdownGracefully() }
|
||||
|
||||
do {
|
||||
let channel = try await ClientBootstrap(group: group)
|
||||
.connectTimeout(.seconds(Int64(timeoutSeconds)))
|
||||
.connect(host: host, port: port)
|
||||
.get()
|
||||
|
||||
let latency = Date().timeIntervalSince(start) * 1000
|
||||
|
||||
var banner: String?
|
||||
if useTLS {
|
||||
// TLS handshake check - connect and attempt TLS
|
||||
banner = await performTLSHandshake(host: host, port: port, timeout: timeoutSeconds)
|
||||
} else {
|
||||
// Read initial banner if available
|
||||
banner = await readBanner(channel: channel, timeout: timeoutSeconds)
|
||||
}
|
||||
|
||||
try await channel.close(mode: .all).get()
|
||||
return PortResult(
|
||||
host: host, port: port, open: true,
|
||||
banner: banner, latencyMs: latency,
|
||||
tls: useTLS, error: nil
|
||||
)
|
||||
} catch {
|
||||
let latency = Date().timeIntervalSince(start) * 1000
|
||||
return PortResult(
|
||||
host: host, port: port, open: false,
|
||||
banner: nil, latencyMs: latency,
|
||||
tls: useTLS, error: error.localizedDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func readBanner(channel: Channel, timeout: Double) async -> String? {
|
||||
// Simple banner read - up to 4KB
|
||||
var buffer = ByteBufferAllocator().buffer(capacity: 4096)
|
||||
do {
|
||||
// Wait briefly for banner data
|
||||
try await Task.sleep(nanoseconds: UInt64(timeout * 500_000_000))
|
||||
// Can't easily read from channel in NIO without handler
|
||||
return nil
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func performTLSHandshake(host: String, port: Int, timeout: Double) async -> String? {
|
||||
// Simplified: attempt TCP connect to the TLS port
|
||||
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||
defer { try? group.syncShutdownGracefully() }
|
||||
|
||||
do {
|
||||
let _ = try await ClientBootstrap(group: group)
|
||||
.connectTimeout(.seconds(Int64(timeout)))
|
||||
.connect(host: host, port: port)
|
||||
.get()
|
||||
return "TLS reachable"
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import Foundation
|
||||
import Logging
|
||||
|
||||
let logger = Logger(label: "molouptime.main")
|
||||
|
||||
@main
|
||||
struct Molouptime {
|
||||
static let engine = CheckEngine()
|
||||
|
||||
static func main() async {
|
||||
logger.info("molouptime engine starting")
|
||||
|
||||
let stdin = FileHandle.standardInput
|
||||
let stdout = FileHandle.standardOutput
|
||||
var buffer = Data()
|
||||
|
||||
// Set stdin to read line-by-line
|
||||
stdin.readabilityHandler = { handle in
|
||||
let data = handle.availableData
|
||||
guard !data.isEmpty else { return }
|
||||
buffer.append(data)
|
||||
|
||||
while let newlineRange = buffer.firstIndex(of: UInt8(ascii: "\n")) {
|
||||
let lineData = buffer[..<newlineRange]
|
||||
buffer = buffer[buffer.index(after: newlineRange)...]
|
||||
|
||||
guard let line = String(data: lineData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!line.isEmpty else {
|
||||
continue
|
||||
}
|
||||
|
||||
Task {
|
||||
await handleLine(line, stdout: stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the process alive
|
||||
dispatchMain()
|
||||
}
|
||||
|
||||
static func handleLine(_ line: String, stdout: FileHandle) async {
|
||||
guard let request = IpcProtocol.readRequest(from: line) else {
|
||||
let errorResp = IpcResponse(
|
||||
reqId: 0, kind: "error",
|
||||
payload: "Invalid JSON", checks: nil, metrics: nil,
|
||||
error: "Failed to parse request"
|
||||
)
|
||||
writeResponse(errorResp, to: stdout)
|
||||
return
|
||||
}
|
||||
|
||||
switch request.command {
|
||||
case .ping:
|
||||
let resp = IpcResponse(reqId: request.reqId, kind: "pong", payload: "ok", checks: nil, metrics: nil, error: nil)
|
||||
writeResponse(resp, to: stdout)
|
||||
|
||||
case .addCheck:
|
||||
guard let payload = request.payload,
|
||||
let uid = payload["uid"],
|
||||
let type = payload["type"],
|
||||
let target = payload["target"],
|
||||
let intervalStr = payload["interval"],
|
||||
let interval = Int(intervalStr) else {
|
||||
let resp = IpcResponse(reqId: request.reqId, kind: "error", payload: nil, checks: nil, metrics: nil, error: "Missing required fields")
|
||||
writeResponse(resp, to: stdout)
|
||||
return
|
||||
}
|
||||
let port = payload["port"].flatMap { Int($0) }
|
||||
let protocolType = payload["protocol"]
|
||||
|
||||
let check = CheckTarget(
|
||||
uid: uid, type: type, target: target,
|
||||
intervalSeconds: interval, port: port,
|
||||
protocolType: protocolType
|
||||
)
|
||||
await engine.addCheck(check)
|
||||
let resp = IpcResponse(reqId: request.reqId, kind: "added", payload: uid, checks: nil, metrics: nil, error: nil)
|
||||
writeResponse(resp, to: stdout)
|
||||
|
||||
case .removeCheck:
|
||||
let uid = request.payload?["uid"] ?? ""
|
||||
await engine.removeCheck(uid)
|
||||
let resp = IpcResponse(reqId: request.reqId, kind: "removed", payload: uid, checks: nil, metrics: nil, error: nil)
|
||||
writeResponse(resp, to: stdout)
|
||||
|
||||
case .updateCheck:
|
||||
guard let payload = request.payload,
|
||||
let uid = payload["uid"] else {
|
||||
let resp = IpcResponse(reqId: request.reqId, kind: "error", payload: nil, checks: nil, metrics: nil, error: "Missing uid")
|
||||
writeResponse(resp, to: stdout)
|
||||
return
|
||||
}
|
||||
let type = payload["type"] ?? "http"
|
||||
let target = payload["target"] ?? ""
|
||||
let interval = Int(payload["interval"] ?? "60") ?? 60
|
||||
let port = payload["port"].flatMap { Int($0) }
|
||||
let protocolType = payload["protocol"]
|
||||
|
||||
let check = CheckTarget(
|
||||
uid: uid, type: type, target: target,
|
||||
intervalSeconds: interval, port: port,
|
||||
protocolType: protocolType
|
||||
)
|
||||
await engine.updateCheck(check)
|
||||
let resp = IpcResponse(reqId: request.reqId, kind: "updated", payload: uid, checks: nil, metrics: nil, error: nil)
|
||||
writeResponse(resp, to: stdout)
|
||||
|
||||
case .listChecks:
|
||||
let checks = await engine.listChecks()
|
||||
let resp = IpcResponse(reqId: request.reqId, kind: "checks", payload: nil, checks: checks, metrics: nil, error: nil)
|
||||
writeResponse(resp, to: stdout)
|
||||
|
||||
case .getMetrics:
|
||||
let metrics = await MetricSample?.none // Placeholder
|
||||
let resp = IpcResponse(reqId: request.reqId, kind: "metrics", payload: nil, checks: nil, metrics: nil, error: nil)
|
||||
writeResponse(resp, to: stdout)
|
||||
|
||||
case .shutdown:
|
||||
await engine.stop()
|
||||
let resp = IpcResponse(reqId: request.reqId, kind: "shutdown", payload: "bye", checks: nil, metrics: nil, error: nil)
|
||||
writeResponse(resp, to: stdout)
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
static func writeResponse(_ response: IpcResponse, to handle: FileHandle) {
|
||||
guard let json = IpcProtocol.writeResponse(response) else { return }
|
||||
handle.write(Data("\(json)\n".utf8))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user