Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Register listener on the dbus interface to detect Avahi deamon restarts #970

Merged
merged 3 commits into from
Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/lib/Accessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,10 @@ export const enum MDNSAdvertiser {
AVAHI = "avahi",
/**
* Use systemd-resolved/D-Bus as advertiser.
*
* Note: The systemd-resolved D-Bus interface doesn't provide means to detect restarts of the service.
* Therefore, we can't detect if our advertisement might be lost due to a restart of the systemd-resolved daemon restart.
* Consequentially, treat this feature as an experimental feature.
*/
RESOLVED = "resolved",
}
Expand Down
96 changes: 80 additions & 16 deletions src/lib/Advertiser.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../@types/bonjour-hap.d.ts" />
import ciao, {
CiaoService,
MDNSServerOptions,
Responder,
ServiceEvent,
ServiceTxt,
ServiceType,
} from "@homebridge/ciao";
import ciao, { CiaoService, MDNSServerOptions, Responder, ServiceEvent, ServiceTxt, ServiceType } from "@homebridge/ciao";
import { InterfaceName, IPAddress } from "@homebridge/ciao/lib/NetworkManager";
import dbus, { DBusInterface, MessageBus } from "@homebridge/dbus-native";
import assert from "assert";
import bonjour, { BonjourHAP, BonjourHAPService, MulticastOptions } from "bonjour-hap";
import crypto from "crypto";
import createDebug from "debug";
import dbus, { MessageBus } from "@homebridge/dbus-native";
import { EventEmitter } from "events";
import { AccessoryInfo } from "./model/AccessoryInfo";
import { PromiseTimeout } from "./util/promise-utils";
Expand All @@ -40,6 +33,10 @@ export const enum PairingFeatureFlag {
}

export const enum AdvertiserEvent {
/**
* Emitted if the underlying mDNS advertisers signals, that the service name
* was automatically changed due to some naming conflicts on the network.
*/
UPDATED_NAME = "updated-name",
}

Expand Down Expand Up @@ -73,16 +70,19 @@ export interface ServiceNetworkOptions {
disabledIpv6?: boolean;
}

/**
* A generic Advertiser interface required for any MDNS Advertiser backend implementations.
*
* All implementations have to extend NodeJS' {@link EventEmitter} and emit the events defined in {@link AdvertiserEvent}.
*/
export interface Advertiser {

initPort(port: number): void;

startAdvertising(): Promise<void>;

updateAdvertisement(silent?: boolean): void;

destroy(): void;

}

/**
Expand All @@ -94,7 +94,6 @@ export interface Advertiser {
* mdns payload).
*/
export class CiaoAdvertiser extends EventEmitter implements Advertiser {

static protocolVersion = "1.1";
static protocolVersionService = "1.1.0";

Expand Down Expand Up @@ -182,14 +181,12 @@ export class CiaoAdvertiser extends EventEmitter implements Advertiser {
flags.forEach(flag => value |= flag);
return value;
}

}

/**
* Advertiser base on the legacy "bonjour-hap" library.
*/
export class BonjourHAPAdvertiser extends EventEmitter implements Advertiser {

private readonly accessoryInfo: AccessoryInfo;
private readonly setupHash: string;
private readonly serviceOptions?: ServiceNetworkOptions;
Expand Down Expand Up @@ -285,7 +282,13 @@ function messageBusConnectionResult(bus: MessageBus): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function dbusInvoke( bus: MessageBus, destination: string, path: string, dbusInterface: string, member: string, others?: any): Promise<any> {
return new Promise((resolve, reject) => {
const command = { destination, path, interface: dbusInterface, member, ...(others || {}) };
const command = {
destination,
path,
interface: dbusInterface,
member,
...(others || {}),
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
bus.invoke(command, (err: any, result: any) => {
Expand All @@ -299,9 +302,27 @@ function dbusInvoke( bus: MessageBus, destination: string, path: string, dbusInt
});
}


/**
* AvahiServerState.
*
* Refer to https://github.com/lathiat/avahi/blob/fd482a74625b8db8547b8cfca3ee3d3c6c721423/avahi-common/defs.h#L220-L227.
*/
const enum AvahiServerState {
// noinspection JSUnusedGlobalSymbols
INVALID = 0,
REGISTERING,
RUNNING,
COLLISION,
FAILURE
}

/**
* Advertiser based on the Avahi D-Bus library.
* For (very crappy) docs on the interface, see the XML files at: https://github.com/lathiat/avahi/tree/master/avahi-daemon.
*
* Refer to https://github.com/lathiat/avahi/blob/fd482a74625b8db8547b8cfca3ee3d3c6c721423/avahi-common/defs.h#L120-L155 for a
* rough API usage guide of Avahi.
*/
export class AvahiAdvertiser extends EventEmitter implements Advertiser {
private readonly accessoryInfo: AccessoryInfo;
Expand All @@ -310,16 +331,21 @@ export class AvahiAdvertiser extends EventEmitter implements Advertiser {
private port?: number;

private bus?: MessageBus;
private avahiServerInterface?: DBusInterface;
private path?: string;

private readonly stateChangeHandler: (state: AvahiServerState) => void;

constructor(accessoryInfo: AccessoryInfo) {
super();
this.accessoryInfo = accessoryInfo;
this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo);

debug(`Preparing Advertiser for '${this.accessoryInfo.displayName}' using Avahi backend!`);

this.bus = dbus.systemBus();

debug(`Preparing Advertiser for '${this.accessoryInfo.displayName}' using Avahi backend!`);
this.stateChangeHandler = this.handleStateChangedEvent.bind(this);
}

private createTxt(): Array<Buffer> {
Expand All @@ -342,6 +368,11 @@ export class AvahiAdvertiser extends EventEmitter implements Advertiser {

debug(`Starting to advertise '${this.accessoryInfo.displayName}' using Avahi backend!`);

if (!this.avahiServerInterface) {
this.avahiServerInterface = await AvahiAdvertiser.avahiInterface(this.bus, "Server");
this.avahiServerInterface.on("StateChanged", this.stateChangeHandler);
}

this.path = await AvahiAdvertiser.avahiInvoke(this.bus, "/", "Server", "EntryGroupNew") as string;
await AvahiAdvertiser.avahiInvoke(this.bus, this.path, "EntryGroup", "AddService", {
body: [
Expand All @@ -360,6 +391,20 @@ export class AvahiAdvertiser extends EventEmitter implements Advertiser {
await AvahiAdvertiser.avahiInvoke(this.bus, this.path, "EntryGroup", "Commit");
}

/**
* Event handler for the `StateChanged` event of the `org.freedesktop.Avahi.Server` DBus interface.
*
* This is called once the state of the running avahi-daemon changes its running state.
* @param state - The state the server changed into {@see AvahiServerState}.
*/
private handleStateChangedEvent(state: AvahiServerState): void {
if (state === AvahiServerState.RUNNING && this.path) {
debug("Found Avahi daemon to have restarted!");
this.startAdvertising()
.catch(reason => console.error("Could not (re-)create mDNS advertisement. The HAP-Server won't be discoverable: " + reason));
}
}

public async updateAdvertisement(silent?: boolean): Promise<void> {
if (!this.bus) {
throw new Error("Tried to update Avahi advertisement on a destroyed advertiser!");
Expand Down Expand Up @@ -396,6 +441,11 @@ export class AvahiAdvertiser extends EventEmitter implements Advertiser {
this.path = undefined;
}

if (this.avahiServerInterface) {
this.avahiServerInterface.removeListener("StateChanged", this.stateChangeHandler);
this.avahiServerInterface = undefined;
}

this.bus.connection.stream.destroy();
this.bus = undefined;
}
Expand Down Expand Up @@ -436,6 +486,20 @@ export class AvahiAdvertiser extends EventEmitter implements Advertiser {
others,
);
}

private static avahiInterface(bus: MessageBus, dbusInterface: string): Promise<DBusInterface> {
return new Promise((resolve, reject) => {
bus
.getService("org.freedesktop.Avahi")
.getInterface("/", "org.freedesktop.Avahi." + dbusInterface, (error, iface) => {
if (error || !iface) {
reject(error ?? new Error("Interface not present!"));
} else {
resolve(iface);
}
});
});
}
}

type ResolvedServiceTxt = Array<Array<string | Buffer>>;
Expand Down
25 changes: 25 additions & 0 deletions src/types/dbus-native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,34 @@ declare module "@homebridge/dbus-native" {

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any
public invoke(message: any, callback: any): void;

public getService(name: string): DBusService;
}

export class BusConnection extends EventEmitter {
public stream: Socket;
}

export class DBusService {
public name: string;
public bus: MessageBus;

// the dbus object has additional properties `proxy` and `nodes´ added to it!
public getObject(name: string, callback: (error: null | Error, obj?: DBusObject) => void): DBusObject;
public getInterface(objName: string, ifaceName: string, callback: (error: null | Error, iface?: DBusInterface) => void): void;
}

export class DBusObject {
public name: string;
public service: DBusService;

public as(name: string): DBusInterface;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class DBusInterface extends EventEmitter implements Record<string, any> {
public $parent: DBusObject;
public $name: string; // string interface name

}
}