Skip to content

Commit

Permalink
Watchdog: handle sporadic resolver issues, refs #29
Browse files Browse the repository at this point in the history
  • Loading branch information
tlk committed Jan 30, 2022
1 parent 9abe782 commit 1325e53
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 106 deletions.
5 changes: 2 additions & 3 deletions Sources/BeoplayRemoteGUI/DeviceBrowserDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ struct DeviceCommand {
}

class DeviceBrowserDelegate : NSObject, NetServiceBrowserDelegate {
private let q = DispatchQueue(label: "beoplay-device-manager")
private var pendingUpdates = [DeviceCommand]()
private var deviceMenuController: DeviceMenuController?

Expand All @@ -27,7 +26,7 @@ class DeviceBrowserDelegate : NSObject, NetServiceBrowserDelegate {
}

func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
self.q.async {
DispatchQueue.main.async {
self.pendingUpdates.append(DeviceCommand(type: DeviceAction.Remove, device: service))

NSLog("didRemove: \(service.name), moreComing: \(moreComing)")
Expand All @@ -39,7 +38,7 @@ class DeviceBrowserDelegate : NSObject, NetServiceBrowserDelegate {
}

func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
self.q.async {
DispatchQueue.main.async {
self.pendingUpdates.append(DeviceCommand(type: DeviceAction.Add, device: service))

NSLog("didFind: \(service.name), moreComing: \(moreComing)")
Expand Down
233 changes: 132 additions & 101 deletions Sources/BeoplayRemoteGUI/DeviceMenuController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,41 @@
import Cocoa
import RemoteCore

class DeviceMenuController : NSObject, NetServiceDelegate {
private let queue = DispatchQueue.init(label: "serialized-device-connection")
class DeviceDelegate : NSObject, NetServiceDelegate {
private let deviceMenuController: DeviceMenuController

public init(deviceMenuController: DeviceMenuController) {
self.deviceMenuController = deviceMenuController
}

func netServiceDidResolveAddress(_ device: NetService) {
DispatchQueue.main.async {
NSLog("resolved: \(device.name) -> http://\(device.hostName!):\(device.port)")

guard let menuItem = self.deviceMenuController.getMenuItem(device) else {
NSLog("resolved: unexpected error")
return
}

menuItem.isEnabled = true

if let deviceName = UserDefaults.standard.string(forKey: "devices.lastConnected") {
self.deviceMenuController.tryAutoConnect(deviceName)
}
}
}
}

class DeviceMenuController {
private let remoteControl: RemoteControl
private let statusMenu: NSMenu
private let mainMenuController: MainMenuController
private let volumeLevelViewController: VolumeLevelViewController
private let deviceSeparatorMenuItem: NSMenuItem
private let sourcesMenuController: SourcesMenuController?

private var deviceDelegate: DeviceDelegate?

public init(remoteControl: RemoteControl, statusMenu: NSMenu, mainMenuController: MainMenuController, volumeLevelViewController: VolumeLevelViewController, deviceSeparatorMenuItem: NSMenuItem, sourcesMenuController: SourcesMenuController?) {
self.remoteControl = remoteControl
self.statusMenu = statusMenu
Expand All @@ -26,35 +52,39 @@ class DeviceMenuController : NSObject, NetServiceDelegate {
self.sourcesMenuController = sourcesMenuController
}

// The device menu item has a StateValue that reflects
// the current connection state for the device.
//
// symbol | StateValue | meaning
// --------|-------------|------------
// ― | .mixed | connecting
// ✓ | .on | connected
// blank | .off | not connected
//
public func onConnectionChange(_ data: NotificationBridge.DataConnectionNotification) {
let selectedItems = self.getDeviceMenuItems().filter {
$0.isEnabled &&
$0.state != NSControl.StateValue.off
}
// This is called from DeviceBrowserDelegate when a device is
// to be added or removed according to DNS-SD (Bonjour).
func devicePresenceChanged(_ updates: [DeviceCommand]) {
DispatchQueue.main.async {
for update in updates {
switch update.type {
case DeviceAction.Add:
NSLog("addDevice: \(update.device.name)")

precondition(selectedItems.count == 0 || selectedItems.count == 1)
guard self.getMenuItem(update.device) == nil else {
NSLog("found an existing menu item for this device(!)")
return
}

if let item = selectedItems.first {
if data.state == NotificationSession.ConnectionState.online {
self.setConnected(item: item)
} else {
self.setConnecting(item: item)
}
}
self.addMenuItem(device: update.device)
self.resolveDevices()

if let message = data.message {
NSLog("connection state: \(data.state): \(message)")
} else {
NSLog("connection state: \(data.state)")
case DeviceAction.Remove:
NSLog("removeDevice: \(update.device.name)")

guard let item = self.getMenuItem(update.device) else {
NSLog("no menu item found for this device(!)")
return
}

if item.state != NSControl.StateValue.off {
self.disconnect()
}

update.device.stop()
self.statusMenu.removeItem(item)
}
}
}
}

Expand All @@ -76,24 +106,54 @@ class DeviceMenuController : NSObject, NetServiceDelegate {
self.statusMenu.insertItem(item, at: getInsertLocation(item))
}

func netServiceDidResolveAddress(_ device: NetService) {
DispatchQueue.main.async {
guard let menuItem = self.getMenuItem(device), !menuItem.isEnabled else {
// When the resolve process runs with an indefinite duration it
// may call this method repeatedly. However, a single update is
// all that is needed so let us keep this method idempotent.
return
}
func getDeviceMenuItems() -> [NSMenuItem] {
self.statusMenu.items.filter { $0.representedObject is NetService }
}

NSLog("resolved: \(device.name) -> http://\(device.hostName!):\(device.port)")
func getMenuItem(_ device: NetService) -> NSMenuItem? {
let location = self.statusMenu.indexOfItem(withRepresentedObject: device)

menuItem.isEnabled = true
guard location > -1 else {
return nil
}

if let deviceName = UserDefaults.standard.string(forKey: "devices.lastConnected") {
self.tryAutoConnect(deviceName)
return self.statusMenu.item(at: location)
}

// Resolve any devices that have not yet been resolved.
func resolveDevices() {
let remaining = getDeviceMenuItems().filter { !$0.isEnabled }

for item in remaining {
guard let device = item.representedObject as? NetService else {
NSLog("resolveDevices: unexpected error")
continue
}

resolve(device)
}
}

// The device address and port must be resolved before
// attempting to connect to it.
//
// NSNetService.resolve starts a process on the main thread
// that will call DeviceDelegate.netServiceDidResolveAddress
// zero or many times when the service address is resolved.
//
// DeviceDelegate.netServiceDidResolveAddress is
// responsible for enabling the menu item.
func resolve(_ device: NetService) {
NSLog("resolveDevice: \(device.name)")

if self.deviceDelegate == nil {
self.deviceDelegate = DeviceDelegate(deviceMenuController: self)
}

device.stop()
device.delegate = self.deviceDelegate
device.resolve(withTimeout: 1.0)
}

func tryAutoConnect(_ deviceName: String) {
let menuItems = self.getDeviceMenuItems()
Expand All @@ -107,10 +167,13 @@ class DeviceMenuController : NSObject, NetServiceDelegate {
$0.title == deviceName
}

guard selectedItems.count == 0,
let item = enabledItems.first,
guard selectedItems.count == 0 else {
NSLog("auto connect: already connected")
return
}
guard let item = enabledItems.first,
let device = item.representedObject as? NetService else {
// Already connected to a device or device not found
NSLog("auto connect: no device")
return
}

Expand All @@ -119,20 +182,6 @@ class DeviceMenuController : NSObject, NetServiceDelegate {
self.connect(device: device)
}

func getDeviceMenuItems() -> [NSMenuItem] {
self.statusMenu.items.filter { $0.representedObject is NetService }
}

func getMenuItem(_ device: NetService) -> NSMenuItem? {
let location = self.statusMenu.indexOfItem(withRepresentedObject: device)

guard location > -1 else {
return nil
}

return self.statusMenu.item(at: location)
}

func setConnecting(item selectedItem: NSMenuItem) {
let items = self.getDeviceMenuItems()
for item in items {
Expand Down Expand Up @@ -189,54 +238,36 @@ class DeviceMenuController : NSObject, NetServiceDelegate {
}
}

func devicePresenceChanged(_ updates: [DeviceCommand]) {
DispatchQueue.main.async {
for update in updates {
switch update.type {
case DeviceAction.Add:
NSLog("addDevice: \(update.device.name)")

guard self.getMenuItem(update.device) == nil else {
NSLog("found an existing menu item for this device(!)")
return
}

// The device IP address must be resolved before the menu item is
// enabled and before attempting to connect to it.
//
// The call to update.device.resolve will start a resolve process
// that will call NSNetServiceDelegate.netServiceDidResolveAddress
// zero or many times when the service address is resolved.
//
// The delegate netServiceDidResolveAddress() method is
// responsible for enabling the menu item.
//
// From NSNetService.resolve(withTimeout):
// The maximum number of seconds to attempt a resolve.
// A value of 0.0 indicates no timeout and a resolve
// process of indefinite duration.

let indefinite = 0.0
self.addMenuItem(device: update.device)
update.device.delegate = self
update.device.resolve(withTimeout: indefinite)

case DeviceAction.Remove:
NSLog("removeDevice: \(update.device.name)")

guard let item = self.getMenuItem(update.device) else {
NSLog("no menu item found for this device(!)")
return
}
// The device menu item has a StateValue that reflects
// the current connection state for the device.
//
// symbol | StateValue | meaning
// --------|-------------|------------
// ― | .mixed | connecting
// ✓ | .on | connected
// blank | .off | not connected
//
public func onConnectionChange(_ data: NotificationBridge.DataConnectionNotification) {
let selectedItems = self.getDeviceMenuItems().filter {
$0.isEnabled &&
$0.state != NSControl.StateValue.off
}

if item.state != NSControl.StateValue.off {
self.disconnect()
}
precondition(selectedItems.count == 0 || selectedItems.count == 1)

self.statusMenu.removeItem(item)
}
if let item = selectedItems.first {
if data.state == NotificationSession.ConnectionState.online {
self.setConnected(item: item)
} else {
self.setConnecting(item: item)
}
}

if let message = data.message {
NSLog("connection state: \(data.state): \(message)")
} else {
NSLog("connection state: \(data.state)")
}
}

@IBAction func deviceClicked(_ sender: NSMenuItem) {
Expand Down
15 changes: 13 additions & 2 deletions Sources/BeoplayRemoteGUI/MainMenuController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ class MainMenuController: NSObject {

private let menuBar = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
private let remoteControl = RemoteControl()
private let browser = NetServiceBrowser()

private var browserDelegate: DeviceBrowserDelegate?
private var deviceMenuController: DeviceMenuController?
private var volumeLevelViewController: VolumeLevelViewController?
private var hotkeysController: HotkeysController?
Expand Down Expand Up @@ -78,8 +80,17 @@ class MainMenuController: NSObject {

addObservers()

let deviceBrowserDelegate = DeviceBrowserDelegate(deviceMenuController: deviceMenuController!)
remoteControl.startDiscovery(delegate: deviceBrowserDelegate)
browserDelegate = DeviceBrowserDelegate(deviceMenuController: deviceMenuController!)
browser.delegate = browserDelegate
browser.schedule(in: .main, forMode: .default)
browser.searchForServices(ofType: "_beoremote._tcp.", inDomain: "local.")

// Watchdog: handle sporadic resolver issues, refs #29
Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { timer in
DispatchQueue.main.async {
self.deviceMenuController?.resolveDevices()
}
}
}

func addObservers() {
Expand Down

0 comments on commit 1325e53

Please sign in to comment.