@@ -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