You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Musubi/Packages/MusubiKit/Sources/Remote/NodeServer.swift

114 lines
4.0 KiB

import Foundation
import MusubiCore
import Network
import Observation
import OSLog
/// Node side: advertises itself via Bonjour on the technical interface and accepts one control at a time.
@MainActor @Observable
public final class NodeServer {
public private(set) var controlName: String?
public private(set) var error: String?
public private(set) var isListening = false
/// Called for `apply`, `show` and `stop` from the control.
public var onCommand: ((RemoteMessage) -> Void)?
private let name: String
private var listener: NWListener?
private var link: Link?
private let queue = DispatchQueue(label: "studio.zielke.musubi.remote.node")
private let log = Logger(subsystem: "studio.zielke.musubi", category: "remote")
public init(name: String) {
self.name = name
}
public func start(interfaceName: String) async {
stop()
guard let interface = await NetworkInterfaces.interface(named: interfaceName) else {
error = "Technik-Schnittstelle \(interfaceName) nicht gefunden."
log.error("Node: interface \(interfaceName, privacy: .public) not found")
return
}
do {
let listener = try NWListener(using: .musubi(interface: interface))
listener.service = NWListener.Service(name: name, type: RemoteProtocol.serviceType)
listener.newConnectionHandler = { [weak self] connection in
Task { @MainActor in self?.accept(connection) }
}
listener.stateUpdateHandler = { [weak self] state in
Task { @MainActor in self?.listenerChanged(state) }
}
listener.start(queue: queue)
self.listener = listener
error = nil
log.info("Node: advertising \(self.name, privacy: .public) on \(interfaceName, privacy: .public)")
} catch {
self.error = "Fernsteuerung lässt sich nicht starten."
log.error("Node: listener failed: \(error.localizedDescription, privacy: .public)")
}
}
public func stop() {
link?.cancel()
link = nil
listener?.cancel()
listener = nil
isListening = false
controlName = nil
}
public func send(_ status: NodeStatus) {
guard controlName != nil else { return }
link?.send(.status(status))
}
private func listenerChanged(_ state: NWListener.State) {
switch state {
case .ready: isListening = true
case .failed(let failure):
isListening = false
error = "Fernsteuerung gestört: \(failure.localizedDescription)"
log.error("Node: listener failed: \(failure.localizedDescription, privacy: .public)")
case .cancelled: isListening = false
default: break
}
}
/// The newest control wins; the previous connection is closed.
private func accept(_ connection: NWConnection) {
link?.cancel()
controlName = nil
let link = Link(connection: connection, queue: queue)
link.onMessage = { [weak self] in self?.received($0, from: link) }
link.onState = { [weak self] state in
guard let self, self.link === link else { return }
switch state {
case .ready:
link.send(.hello(name: self.name, role: .node, version: RemoteProtocol.version))
case .failed, .cancelled:
log.notice("Node: control \(self.controlName ?? "?", privacy: .public) disconnected")
self.controlName = nil
self.link = nil
default: break
}
}
self.link = link
link.start()
}
private func received(_ message: RemoteMessage, from link: Link) {
guard self.link === link else { return }
switch message {
case .hello(let name, _, _):
controlName = name
log.info("Node: controlled by \(name, privacy: .public)")
case .apply, .show, .stop:
onCommand?(message)
case .status:
break
}
}
}

Powered by TurnKey Linux.