113 lines
5.2 KiB
Swift
113 lines
5.2 KiB
Swift
import Foundation
|
|
import Combine
|
|
import Darwin
|
|
|
|
struct DiscoveredComputer: Identifiable, Equatable {
|
|
let name: String
|
|
let endpoint: String
|
|
let fingerprint: String
|
|
var id: String { fingerprint + endpoint }
|
|
|
|
static func parse(name: String, host: String, port: Int, txt: [String: Data]) -> Self? {
|
|
guard txt["v"] == Data("1".utf8), let data = txt["id"],
|
|
let pin = String(data: data, encoding: .utf8),
|
|
let connection = try? Connection(endpoint: "https://\(host):\(port)", fingerprint: pin) else { return nil }
|
|
return Self(name: String(name.prefix(80)), endpoint: connection.endpoint.absoluteString,
|
|
fingerprint: connection.fingerprint)
|
|
}
|
|
|
|
// An advertisement is a routing hint, never a replacement for saved trust.
|
|
func reconnect(_ saved: Connection) -> Connection? {
|
|
guard fingerprint == saved.fingerprint else { return nil }
|
|
return try? Connection(endpoint: endpoint, fingerprint: saved.fingerprint, token: saved.token)
|
|
}
|
|
}
|
|
|
|
// Started on the main run loop; Foundation delivers browser/service delegates there.
|
|
final class DiscoveryStore: NSObject, ObservableObject, NetServiceBrowserDelegate, NetServiceDelegate {
|
|
@Published private(set) var computers: [DiscoveredComputer] = []
|
|
@Published private(set) var message = "Поиск ПК не запущен"
|
|
private var browser: NetServiceBrowser?
|
|
private var services: [String: NetService] = [:]
|
|
private var records: [String: [DiscoveredComputer]] = [:]
|
|
private var refresh: Timer?
|
|
|
|
func start() {
|
|
guard browser == nil else { return }
|
|
message = "Ищем MusicBridge в локальной сети…"
|
|
let browser = NetServiceBrowser()
|
|
self.browser = browser
|
|
browser.delegate = self
|
|
browser.searchForServices(ofType: "_musicbridge._tcp.", inDomain: "local.")
|
|
refresh = Timer.scheduledTimer(withTimeInterval: 15, repeats: true) { [weak self] _ in
|
|
guard let self else { return }
|
|
for service in self.services.values {
|
|
service.stop()
|
|
service.resolve(withTimeout: 5)
|
|
}
|
|
}
|
|
}
|
|
|
|
func stop() {
|
|
refresh?.invalidate(); refresh = nil
|
|
browser?.delegate = nil; browser?.stop(); browser = nil
|
|
for service in services.values { service.delegate = nil; service.stop() }
|
|
services.removeAll(); records.removeAll(); computers = []
|
|
message = "Поиск ПК приостановлен"
|
|
}
|
|
|
|
func restart() { stop(); start() }
|
|
private func key(_ service: NetService) -> String { service.domain + service.type + service.name }
|
|
|
|
private func publish() {
|
|
computers = Array(Set(records.values.flatMap { $0 }.map(\.id))).sorted().compactMap { id in
|
|
records.values.lazy.flatMap { $0 }.first { $0.id == id }
|
|
}
|
|
message = computers.isEmpty ? "ПК пока не найден. Проверьте Wi-Fi и LAN-режим агента." : "Найдено адресов: \(computers.count)"
|
|
}
|
|
|
|
func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
|
|
guard self.browser === browser, services.count < 64, services[key(service)] == nil else { return }
|
|
services[key(service)] = service
|
|
service.delegate = self
|
|
service.resolve(withTimeout: 5)
|
|
}
|
|
|
|
func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
|
|
guard self.browser === browser else { return }
|
|
let old = services.removeValue(forKey: key(service))
|
|
old?.delegate = nil; old?.stop()
|
|
records.removeValue(forKey: key(service)); publish()
|
|
}
|
|
|
|
func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String: NSNumber]) {
|
|
guard self.browser === browser else { return }
|
|
stop()
|
|
message = "Поиск недоступен. Разрешите MusicBridge доступ к локальной сети в Настройках и повторите поиск."
|
|
}
|
|
|
|
func netServiceDidResolveAddress(_ sender: NetService) {
|
|
guard services[key(sender)] === sender else { return }
|
|
let txt = sender.txtRecordData().map(NetService.dictionary(fromTXTRecord:)) ?? [:]
|
|
records[key(sender)] = (sender.addresses ?? []).prefix(16).compactMap { data in
|
|
guard let host = Self.ipv4(data) else { return nil }
|
|
return DiscoveredComputer.parse(name: sender.name, host: host, port: sender.port, txt: txt)
|
|
}
|
|
publish()
|
|
}
|
|
|
|
func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) {
|
|
guard services[key(sender)] === sender else { return }
|
|
records.removeValue(forKey: key(sender)); publish()
|
|
}
|
|
|
|
static func ipv4(_ data: Data) -> String? {
|
|
guard data.count >= MemoryLayout<sockaddr_in>.size else { return nil }
|
|
var address = data.withUnsafeBytes { $0.loadUnaligned(as: sockaddr_in.self) }
|
|
guard address.sin_family == sa_family_t(AF_INET) else { return nil }
|
|
var buffer = [CChar](repeating: 0, count: Int(INET_ADDRSTRLEN))
|
|
guard inet_ntop(AF_INET, &address.sin_addr, &buffer, socklen_t(INET_ADDRSTRLEN)) != nil else { return nil }
|
|
return String(cString: buffer)
|
|
}
|
|
}
|