Skip to content

Commit a6f718f

Browse files
committed
fix: dangling Query and Advertisement promises are now awaited before MDNS is stopped
[ci-skip]
1 parent 9b7c95d commit a6f718f

File tree

2 files changed

+126
-13
lines changed

2 files changed

+126
-13
lines changed

src/MDNS.ts

+31-13
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ class MDNS {
7777

7878
protected queries: Map<string, PromiseCancellable<void>> = new Map();
7979
protected advertisements: Map<string, PromiseCancellable<void>> = new Map();
80+
/**
81+
* Represents the advertisements and queries that have been cancelled and are in the process of stopping
82+
*/
83+
protected stoppingTasks: Set<PromiseCancellable<unknown>> = new Set();
8084

8185
public constructor({
8286
getNetworkInterfaces = utils.getNetworkInterfaces,
@@ -465,11 +469,13 @@ class MDNS {
465469
advertisementKey: string,
466470
socket?: MulticastSocketInfo | Array<MulticastSocketInfo>,
467471
) {
472+
// If the advertisement already, exists cancel and restart it
468473
const advertisement = this.advertisements.get(advertisementKey);
469474
if (advertisement != null) {
470475
advertisement.cancel();
476+
this.stoppingTasks.add(advertisement);
471477
}
472-
478+
// Construct the PromiseCancellable
473479
const abortController = new AbortController();
474480
const promise = new PromiseCancellable<void>(async (resolve, reject) => {
475481
await this.sendMulticastPacket(packet, socket).catch(reject);
@@ -495,18 +501,18 @@ class MDNS {
495501
{ once: true },
496502
);
497503
}, abortController);
498-
499-
// Delete the advertisement key whether reject or resolve
504+
// Delete the advertisement key and delete it from stoppingTasks whether reject or resolve
500505
promise.then(
501506
() => {
507+
this.stoppingTasks.delete(promise);
502508
this.advertisements.delete(advertisementKey);
503509
},
504510
(reason) => {
505-
this.dispatchEvent(new events.EventMDNSError({ detail: reason }));
511+
this.stoppingTasks.delete(promise);
506512
this.advertisements.delete(advertisementKey);
513+
this.dispatchEvent(new events.EventMDNSError({ detail: reason }));
507514
},
508515
);
509-
510516
this.advertisements.set(advertisementKey, promise);
511517
}
512518

@@ -1082,13 +1088,19 @@ class MDNS {
10821088
this.logger.info(`Stop ${this.constructor.name}`);
10831089

10841090
// Cancel Queries and Advertisements
1091+
const stoppingTasks = [...this.stoppingTasks];
10851092
for (const query of this.queries.values()) {
10861093
query.cancel();
1094+
stoppingTasks.push(query);
10871095
}
10881096
for (const advertisement of this.advertisements.values()) {
10891097
advertisement.cancel();
1098+
stoppingTasks.push(advertisement);
10901099
}
1100+
// We don't care if any Queries or Advertisements have failed, just that they have stopped.
1101+
await Promise.allSettled(stoppingTasks);
10911102

1103+
const goodbyeProms: Array<Promise<unknown>> = [];
10921104
// Send the goodbye packet
10931105
const serviceResourceRecords = utils.toServiceResourceRecords(
10941106
[...this._localServices.values()],
@@ -1124,8 +1136,10 @@ class MDNS {
11241136
additionals: [],
11251137
authorities: [],
11261138
};
1127-
await this.sendMulticastPacket(advertisePacket, socketInfo);
1139+
goodbyeProms.push(this.sendMulticastPacket(advertisePacket, socketInfo));
11281140
}
1141+
// We don't care if any goodbye packets failed to send, just that they have completed
1142+
await Promise.allSettled(goodbyeProms);
11291143

11301144
// Clear Services and Cache
11311145
await this.localRecordCache.destroy();
@@ -1190,10 +1204,10 @@ class MDNS {
11901204
hosts: [],
11911205
};
11921206
const fqdn = utils.toFqdn(service);
1193-
1207+
// Update the service even if it is there
11941208
this._localServices.set(fqdn, service);
11951209
this.localRecordCacheDirty = true;
1196-
1210+
// Don't bother to advertise if the param is not enabled
11971211
if (!advertise) return;
11981212
const advertisePacket: Packet = {
11991213
id: this._id,
@@ -1232,10 +1246,10 @@ class MDNS {
12321246
type,
12331247
protocol,
12341248
});
1235-
1249+
// If there is no found service, don't bother
12361250
const foundService = this._localServices.get(fqdn);
12371251
if (foundService == null) return;
1238-
1252+
// Delete and advertise the service
12391253
this._localServices.delete(fqdn);
12401254
this.localRecordCacheDirty = true;
12411255
const advertisePacket: Packet = {
@@ -1287,6 +1301,7 @@ class MDNS {
12871301
const existingQuery = this.queries.get(serviceDomain);
12881302
if (existingQuery != null) {
12891303
existingQuery.cancel();
1304+
this.stoppingTasks.add(existingQuery);
12901305
this.queries.delete(serviceDomain);
12911306
}
12921307
const questionRecord: QuestionRecord = {
@@ -1307,7 +1322,7 @@ class MDNS {
13071322
additionals: [],
13081323
authorities: [],
13091324
};
1310-
1325+
// Create the PromiseCancellable
13111326
const abortController = new AbortController();
13121327
const promise = new PromiseCancellable<void>(async (resolve, reject) => {
13131328
await this.sendMulticastPacket(queryPacket).catch(reject);
@@ -1341,17 +1356,19 @@ class MDNS {
13411356
{ once: true },
13421357
);
13431358
}, abortController);
1344-
1359+
// After promise resolves/rejects
13451360
promise.then(
13461361
() => {
1362+
this.stoppingTasks.delete(promise);
13471363
this.queries.delete(serviceDomain);
13481364
},
13491365
(reason) => {
1366+
this.stoppingTasks.delete(promise);
13501367
this.queries.delete(serviceDomain);
13511368
this.dispatchEvent(new events.EventMDNSError({ detail: reason }));
13521369
},
13531370
);
1354-
1371+
// Set in queries map
13551372
this.queries.set(serviceDomain, promise);
13561373
}
13571374

@@ -1376,6 +1393,7 @@ class MDNS {
13761393
const existingQuery = this.queries.get(serviceDomain);
13771394
if (existingQuery != null) {
13781395
existingQuery.cancel();
1396+
this.stoppingTasks.add(existingQuery);
13791397
this.queries.delete(serviceDomain);
13801398
}
13811399
}

tests/MDNS.test.ts

+95
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,99 @@ describe(MDNS.name, () => {
9797
);
9898
});
9999
});
100+
describe('lifecycle', () => {
101+
test('starting and stopping a query', async () => {
102+
const mdns1Hostname = 'polykey1' as Hostname;
103+
await mdns1.start({
104+
hostname: mdns1Hostname,
105+
port: mdnsPort,
106+
groups: mdnsGroups,
107+
advertise: false,
108+
});
109+
const service = {
110+
name: 'test',
111+
port: mdnsPort,
112+
protocol: 'udp',
113+
type: 'polykey',
114+
advertise: false,
115+
} as Parameters<typeof MDNS.prototype.startQuery>[0];
116+
mdns1.startQuery(service);
117+
mdns1.stopQuery(service);
118+
// @ts-ignore: Kidnap protected property
119+
expect(mdns1.stoppingTasks.size).toBe(1);
120+
await mdns1.stop();
121+
// @ts-ignore: Kidnap protected property
122+
expect(mdns1.stoppingTasks.size).toBe(0);
123+
});
124+
test('starting and stopping multiple queries', async () => {
125+
const mdns1Hostname = 'polykey1' as Hostname;
126+
await mdns1.start({
127+
hostname: mdns1Hostname,
128+
port: mdnsPort,
129+
groups: mdnsGroups,
130+
advertise: false,
131+
});
132+
const service = {
133+
name: 'test',
134+
port: mdnsPort,
135+
protocol: 'udp',
136+
type: 'polykey',
137+
} as Parameters<typeof MDNS.prototype.startQuery>[0];
138+
mdns1.startQuery(service);
139+
mdns1.startQuery(service);
140+
mdns1.stopQuery(service);
141+
// @ts-ignore: Kidnap protected property
142+
expect(mdns1.stoppingTasks.size).toBe(2);
143+
await mdns1.stop();
144+
// @ts-ignore: Kidnap protected property
145+
expect(mdns1.stoppingTasks.size).toBe(0);
146+
});
147+
test('starting multiple advertisements', async () => {
148+
const mdns1Hostname = 'polykey1' as Hostname;
149+
await mdns1.start({
150+
hostname: mdns1Hostname,
151+
port: mdnsPort,
152+
groups: mdnsGroups,
153+
advertise: false,
154+
});
155+
const service = {
156+
name: 'test',
157+
port: mdnsPort,
158+
protocol: 'udp',
159+
type: 'polykey',
160+
} as Parameters<typeof MDNS.prototype.registerService>[0];
161+
mdns1.registerService(service);
162+
mdns1.registerService(service);
163+
// @ts-ignore: Kidnap protected property
164+
expect(mdns1.stoppingTasks.size).toBe(1);
165+
await mdns1.stop();
166+
// @ts-ignore: Kidnap protected property
167+
expect(mdns1.stoppingTasks.size).toBe(0);
168+
});
169+
test('starting and stopping multiple queries and advertisements', async () => {
170+
const mdns1Hostname = 'polykey1' as Hostname;
171+
await mdns1.start({
172+
hostname: mdns1Hostname,
173+
port: mdnsPort,
174+
groups: mdnsGroups,
175+
advertise: false,
176+
});
177+
const service = {
178+
name: 'test',
179+
port: mdnsPort,
180+
protocol: 'udp',
181+
type: 'polykey',
182+
} as Parameters<typeof MDNS.prototype.registerService>[0];
183+
mdns1.startQuery(service);
184+
mdns1.startQuery(service);
185+
mdns1.stopQuery(service);
186+
mdns1.registerService(service);
187+
mdns1.registerService(service);
188+
// @ts-ignore: Kidnap protected property
189+
expect(mdns1.stoppingTasks.size).toBe(3);
190+
await mdns1.stop();
191+
// @ts-ignore: Kidnap protected property
192+
expect(mdns1.stoppingTasks.size).toBe(0);
193+
});
194+
});
100195
});

0 commit comments

Comments
 (0)