diff --git a/mnemo_cards_telegram_bot/Dockerfile b/mnemo_cards_telegram_bot/Dockerfile index a656904..882be1d 100644 --- a/mnemo_cards_telegram_bot/Dockerfile +++ b/mnemo_cards_telegram_bot/Dockerfile @@ -56,6 +56,9 @@ ENV BACKEND_URL=https://api.mnemo-cards.online \ # No EXPOSE needed - bot uses outbound connections only (polling) +# Signal for graceful shutdown +STOPSIGNAL SIGTERM + # Healthcheck - check if process is running HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \ CMD pgrep -f "/app/bot" > /dev/null || exit 1 diff --git a/mnemo_cards_telegram_bot/bin/main.dart b/mnemo_cards_telegram_bot/bin/main.dart index c7c9cc4..3c3c3c8 100644 --- a/mnemo_cards_telegram_bot/bin/main.dart +++ b/mnemo_cards_telegram_bot/bin/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:path/path.dart'; import 'package:mnemo_cards_telegram_bot/bot_config.dart'; @@ -10,10 +11,65 @@ import 'package:mnemo_cards_telegram_bot/backend_client.dart'; const String version = '0.0.1'; +// Global variables for graceful shutdown +BackendClient? _globalBackendClient; +TeleDart? _globalTeledart; +bool _isShuttingDown = false; +final Completer _shutdownCompleter = Completer(); + void main() async { + // Setup signal handlers for graceful shutdown + ProcessSignal.sigterm.watch().listen((signal) { + print('[BOT] Received SIGTERM, shutting down gracefully...'); + _shutdown(); + }); + + ProcessSignal.sigint.watch().listen((signal) { + print('[BOT] Received SIGINT, shutting down gracefully...'); + _shutdown(); + }); + return _run(); } +Future _shutdown() async { + if (_isShuttingDown) { + print('[BOT] Shutdown already in progress'); + return; + } + _isShuttingDown = true; + + print('[BOT] Starting graceful shutdown...'); + + try { + if (_globalTeledart != null) { + print('[BOT] Stopping TeleDart...'); + // TeleDart doesn't have explicit stop method, but we can dispose resources + // The process will exit naturally when main completes + } + } catch (e) { + print('[BOT] Error stopping TeleDart: $e'); + } + + try { + if (_globalBackendClient != null) { + print('[BOT] Disposing BackendClient...'); + _globalBackendClient!.dispose(); + } + } catch (e) { + print('[BOT] Error disposing BackendClient: $e'); + } + + print('[BOT] Shutdown complete'); + + // Complete the completer to allow main to exit + if (!_shutdownCompleter.isCompleted) { + _shutdownCompleter.complete(); + } + + exit(0); +} + String? _extractCodeFromText(String? rawText) { if (rawText == null) { return null; @@ -92,6 +148,11 @@ Future _handleCodeClaim({ } Future _run() async { + if (_isShuttingDown) { + print('[BOT] Shutdown in progress, not starting bot'); + return; + } + print('[BOT] Starting bot version $version'); BackendClient? backendClient; TeleDart? teledart; @@ -109,6 +170,9 @@ Future _run() async { final username = (await Telegram(config.botToken).getMe()).username; teledart = TeleDart(config.botToken, Event(username!)); + + // Store globally for shutdown handler + _globalTeledart = teledart; Future notifyAdmins(String message) async { for (final admin in adminIds) { @@ -126,6 +190,9 @@ Future _run() async { backendUrl: config.backendUrl, apiKey: config.apiKey, ); + + // Store globally for shutdown handler + _globalBackendClient = backendClient; // At this point all variables are initialized and non-null final teledartInstance = teledart; @@ -587,7 +654,16 @@ Future _run() async { teledartInstance.start(); print('[BOT] Bot started successfully'); + + // Wait for shutdown signal + // The bot will run until it receives SIGTERM/SIGINT + await _shutdownCompleter.future; } on Exception catch (e, s) { + if (_isShuttingDown) { + print('[BOT] Error during shutdown, exiting...'); + return; + } + print('[BOT] ERROR: Error: $e\n$s'); print('[BOT] Error while starting bot: $e'); print('Stack trace: $s'); @@ -598,16 +674,17 @@ Future _run() async { print(' - BACKEND_URL or MNEMO_BACKEND_URL (optional, default: https://api.mnemo-cards.online)'); print(' - BOT_SHARE_DAILY_LIMIT (optional, default: 1)'); - // Cleanup resources before restart + // Cleanup resources try { backendClient?.dispose(); } catch (_) { // Ignore cleanup errors } - print('Restarting in 5 seconds...'); - await Future.delayed(const Duration(seconds: 5)); - return _run(); + // Exit with error code instead of restarting + // Let Docker/Coolify handle restarts with restart policy + print('[BOT] Exiting with error code...'); + exit(1); } }