Skip to content

Commit 62c167c

Browse files
committed
fix(macos): close mwebd on app close
1 parent 6e4af03 commit 62c167c

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

lib/main.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,38 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
615615
// }
616616
break;
617617
case AppLifecycleState.detached:
618+
// Clean shutdown of mwebd service with aggressive timeout for macOS.
619+
if (Platform.isMacOS) {
620+
// On macOS, use very short timeout since native StopServer is skipped.
621+
try {
622+
debugPrint("App detached on macOS, quick shutdown...");
623+
await ref.read(pMwebService).shutdown().timeout(
624+
const Duration(seconds: 2),
625+
onTimeout: () {
626+
debugPrint("MwebdService shutdown timed out after 2 seconds on macOS");
627+
exit(0);
628+
},
629+
);
630+
debugPrint("MwebdService shutdown completed on macOS");
631+
} catch (e, s) {
632+
debugPrint("Error during MwebdService shutdown on macOS: $e\n$s");
633+
exit(0);
634+
}
635+
} else {
636+
// Non-macOS platforms can use longer timeout.
637+
try {
638+
debugPrint("App detached, shutting down MwebdService...");
639+
await ref.read(pMwebService).shutdown().timeout(
640+
const Duration(seconds: 5),
641+
onTimeout: () {
642+
debugPrint("MwebdService shutdown timed out after 5 seconds");
643+
},
644+
);
645+
debugPrint("MwebdService shutdown completed successfully");
646+
} catch (e, s) {
647+
debugPrint("Error during MwebdService shutdown: $e\n$s");
648+
}
649+
}
618650
break;
619651
case AppLifecycleState.hidden:
620652
break;

lib/services/mwebd_service.dart

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ final class MwebdService {
3434

3535
final Mutex _torConnectingLock = Mutex();
3636

37+
// Track active log stream controllers for cleanup during shutdown.
38+
final Set<StreamController<String>> _activeLogControllers = {};
39+
3740
static final instance = MwebdService._();
3841

3942
MwebdService._() {
@@ -204,6 +207,9 @@ final class MwebdService {
204207
String leftover = '';
205208
Timer? timer;
206209

210+
// Track this controller for cleanup during shutdown.
211+
_activeLogControllers.add(controller);
212+
207213
final path =
208214
"${(await StackFileSystem.applicationMwebdDirectory(net == CryptoCurrencyNetwork.main ? "mainnet" : "testnet")).path}"
209215
"${Platform.pathSeparator}logs"
@@ -237,11 +243,141 @@ final class MwebdService {
237243

238244
controller.onCancel = () {
239245
timer?.cancel();
246+
_activeLogControllers.remove(controller);
240247
controller.close();
241248
};
242249

243250
return controller.stream;
244251
}
252+
253+
/// Shutdown all mwebd servers and clean up resources.
254+
///
255+
/// This method should be called when the app is terminating to prevent hanging.
256+
Future<void> shutdown() async {
257+
final stopwatch = Stopwatch()..start();
258+
Logging.instance.i("MwebdService shutdown() started");
259+
260+
await _updateLock.protect(() async {
261+
// Cancel stream subscriptions to prevent further events.
262+
try {
263+
await _torStatusListener.cancel();
264+
Logging.instance.i("Canceled tor status listener");
265+
} catch (e, s) {
266+
Logging.instance.w(
267+
"Error canceling tor status listener",
268+
error: e,
269+
stackTrace: s,
270+
);
271+
}
272+
273+
try {
274+
await _torPreferenceListener.cancel();
275+
Logging.instance.i("Canceled tor preference listener");
276+
} catch (e, s) {
277+
Logging.instance.w(
278+
"Error canceling tor preference listener",
279+
error: e,
280+
stackTrace: s,
281+
);
282+
}
283+
284+
// Cancel all active log stream controllers and their timers.
285+
final logControllers = List.from(_activeLogControllers);
286+
for (final controller in logControllers) {
287+
try {
288+
await controller.close();
289+
Logging.instance.i("Closed log stream controller");
290+
} catch (e, s) {
291+
Logging.instance.w(
292+
"Error closing log stream controller",
293+
error: e,
294+
stackTrace: s,
295+
);
296+
}
297+
}
298+
_activeLogControllers.clear();
299+
300+
// Stop all servers and clean up clients with timeout protection.
301+
final stopFutures = <Future>[];
302+
for (final entry in _map.values) {
303+
stopFutures.add(_shutdownServerSafely(entry));
304+
}
305+
306+
// Wait for all shutdowns with overall timeout.
307+
try {
308+
await Future.wait(stopFutures).timeout(
309+
const Duration(seconds: 10),
310+
onTimeout: () {
311+
Logging.instance.w("Timeout waiting for mwebd servers to stop");
312+
return []; // Return a dummy list.
313+
},
314+
);
315+
} catch (e, s) {
316+
Logging.instance.w(
317+
"Error during mwebd servers shutdown",
318+
error: e,
319+
stackTrace: s,
320+
);
321+
}
322+
323+
_map.clear();
324+
325+
final elapsedMs = stopwatch.elapsedMilliseconds;
326+
Logging.instance.i("MwebdService shutdown() completed in ${elapsedMs}ms");
327+
328+
// Warn if shutdown took too long (could indicate hanging).
329+
if (elapsedMs > 3000) {
330+
Logging.instance.w("MwebdService shutdown took ${elapsedMs}ms - longer than expected");
331+
}
332+
});
333+
}
334+
335+
/// Safely shutdown a server/client pair with timeout protection.
336+
Future<void> _shutdownServerSafely(
337+
({MwebdServer server, MwebClient client}) entry,
338+
) async {
339+
final serverStopwatch = Stopwatch()..start();
340+
Logging.instance.i("Starting shutdown of mwebd server/client pair");
341+
342+
try {
343+
// Clean up client first.
344+
final clientStopwatch = Stopwatch()..start();
345+
await entry.client.cleanup().timeout(
346+
const Duration(seconds: 3),
347+
onTimeout: () {
348+
Logging.instance.w("Timeout cleaning up mweb client after 3s");
349+
},
350+
);
351+
Logging.instance.i("Client cleanup completed in ${clientStopwatch.elapsedMilliseconds}ms");
352+
} catch (e, s) {
353+
Logging.instance.w(
354+
"Error cleaning up mweb client",
355+
error: e,
356+
stackTrace: s,
357+
);
358+
}
359+
360+
try {
361+
// Stop server with timeout protection.
362+
final serverShutdownStopwatch = Stopwatch()..start();
363+
await entry.server.stopServer().timeout(
364+
const Duration(seconds: 5),
365+
onTimeout: () {
366+
Logging.instance.w("Timeout stopping mwebd server after 5s");
367+
},
368+
);
369+
Logging.instance.i("Server stop completed in ${serverShutdownStopwatch.elapsedMilliseconds}ms");
370+
} catch (e, s) {
371+
Logging.instance.w(
372+
"Error stopping mwebd server",
373+
error: e,
374+
stackTrace: s,
375+
);
376+
}
377+
378+
final totalMs = serverStopwatch.elapsedMilliseconds;
379+
Logging.instance.i("Server/client pair shutdown completed in ${totalMs}ms");
380+
}
245381
}
246382

247383
// ============================================================================

0 commit comments

Comments
 (0)