From 781e52aae02e3c10491fa9af8ac30a3d3778536a Mon Sep 17 00:00:00 2001 From: Faiz Hashmi Date: Thu, 18 Dec 2025 14:28:11 +0300 Subject: [PATCH] Improved Logging --- lib/config/dependency_injection.dart | 5 +- lib/constants/app_constants.dart | 2 + lib/main.dart | 38 +++++++-- lib/services/crash_handler_service.dart | 58 +++++++++++++ lib/services/logger_service.dart | 76 +++++++++++++++-- lib/view_models/queuing_view_model.dart | 101 ++++++++++++----------- lib/views/common_widgets/app_header.dart | 5 +- pubspec.lock | 8 -- pubspec.yaml | 2 +- 9 files changed, 219 insertions(+), 76 deletions(-) create mode 100644 lib/services/crash_handler_service.dart diff --git a/lib/config/dependency_injection.dart b/lib/config/dependency_injection.dart index d9f3c6f..c46de0a 100644 --- a/lib/config/dependency_injection.dart +++ b/lib/config/dependency_injection.dart @@ -2,7 +2,6 @@ // import 'package:flutter/material.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_tts/flutter_tts.dart'; import 'package:get_it/get_it.dart'; import 'package:hmg_qline/api/api_client.dart'; @@ -13,6 +12,7 @@ import 'package:hmg_qline/services/cache_service.dart'; import 'package:hmg_qline/services/connectivity_service.dart'; import 'package:hmg_qline/services/logger_service.dart'; import 'package:hmg_qline/services/text_to_speech_service.dart'; +import 'package:hmg_qline/services/crash_handler_service.dart'; import 'package:hmg_qline/utilities/native_method_handler.dart'; import 'package:hmg_qline/view_models/queuing_view_model.dart'; import 'package:hmg_qline/view_models/screen_config_view_model.dart'; @@ -35,6 +35,9 @@ class AppDependencies { //api client getIt.registerSingleton(LoggerServiceImp(logger: logger)); + // register crash handler after logger so it can resolve logger + getIt.registerSingleton(CrashHandlerServiceImp(loggerService: getIt.get())); + getIt.registerSingleton(ApiClientImp(loggerService: getIt.get())); //repos diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart index 473bda8..7523999 100644 --- a/lib/constants/app_constants.dart +++ b/lib/constants/app_constants.dart @@ -169,6 +169,8 @@ class AppConstants { static int thresholdForListUI = 5; static double currentBuildVersion = 9.1; static double clearLogsHoursThreshold = 48; + // Maximum log file size in bytes before rotation/clearing. Default 2 MB. + static int maxLogFileSizeBytes = 2 * 1024 * 1024; } class ApiConstants { diff --git a/lib/main.dart b/lib/main.dart index e7ed40f..56adf5e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -10,16 +11,35 @@ import 'package:hmg_qline/constants/app_constants.dart'; import 'package:hmg_qline/view_models/queuing_view_model.dart'; import 'package:hmg_qline/views/view_helpers/size_config.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:hmg_qline/services/crash_handler_service.dart'; -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - try { - await AppDependencies.addDependencies(); - await WakelockPlus.enable(); - } catch (e) { - log('Initialization error: $e'); - } - runApp(const MyApp()); +void main() { + // Run all initialization inside the same Zone used by runApp to avoid zone mismatch warnings. + runZonedGuarded(() async { + WidgetsFlutterBinding.ensureInitialized(); + try { + await AppDependencies.addDependencies(); + // initialize crash handler after DI setup + if (getIt.isRegistered()) { + getIt.get().setupGlobalErrorHandlers(); + } + await WakelockPlus.enable(); + } catch (e) { + log('Initialization error: $e'); + } + + runApp(const MyApp()); + }, (error, stack) async { + try { + if (getIt.isRegistered()) { + await getIt.get().handleCrash(error: error, stackTrace: stack, context: 'runZonedGuarded'); + } else { + log('Uncaught error before DI initialized: $error'); + } + } catch (e) { + log('Error in runZonedGuarded handler: $e'); + } + }); } class MyApp extends StatelessWidget { diff --git a/lib/services/crash_handler_service.dart b/lib/services/crash_handler_service.dart new file mode 100644 index 0000000..71ac5dd --- /dev/null +++ b/lib/services/crash_handler_service.dart @@ -0,0 +1,58 @@ +// filepath: /Volumes/Data/Projects/Flutter/HMG_QLine/lib/services/crash_handler_service.dart +import 'dart:async'; +import 'dart:developer'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:hmg_qline/services/logger_service.dart'; + +// Abstract interface for crash handling (matches style of ConnectivityService) +abstract class CrashHandlerService { + /// Install global error handlers for Flutter and platform errors. + void setupGlobalErrorHandlers(); + + /// Central crash handler for logging/reporting crashes. + Future handleCrash({required Object error, StackTrace? stackTrace, String? context}); +} + +class CrashHandlerServiceImp implements CrashHandlerService { + final LoggerService loggerService; + + CrashHandlerServiceImp({required this.loggerService}); + + /// Sets up global error handlers for Flutter framework errors and platform errors. + @override + void setupGlobalErrorHandlers() { + // Flutter framework errors + FlutterError.onError = (FlutterErrorDetails details) async { + try { + final error = details.exception; + final stack = details.stack; + await handleCrash(error: error, stackTrace: stack, context: 'FlutterError.onError'); + } catch (e, st) { + // include stack trace in the log to avoid unused variable warning + log('Error while handling FlutterError.onError: $e\nStack: $st'); + loggerService.logError('Error while handling FlutterError.onError: $e\nStack: $st'); + } + }; + + // PlatformDispatcher (engine) errors + PlatformDispatcher.instance.onError = (Object error, StackTrace stack) { + handleCrash(error: error, stackTrace: stack, context: 'PlatformDispatcher.onError'); + return true; // we handled it + }; + + // note: Zone errors are handled by runZonedGuarded where runApp is invoked + } + + /// Central crash handler. Logs crash info and can be extended to report to remote services. + @override + Future handleCrash({required Object error, StackTrace? stackTrace, String? context}) async { + try { + await loggerService.logCrash(error: error, stackTrace: stackTrace, context: context); + } catch (e) { + // ensure we don't throw while handling crashes + log('CrashHandlerService failed to log crash: $e'); + loggerService.logError('CrashHandlerService failed to log crash: $e'); + } + } +} diff --git a/lib/services/logger_service.dart b/lib/services/logger_service.dart index 1d7a85c..8571192 100644 --- a/lib/services/logger_service.dart +++ b/lib/services/logger_service.dart @@ -14,6 +14,8 @@ abstract class LoggerService { void logError(String message); void logInfo(String message); + + Future logCrash({required Object error, StackTrace? stackTrace, String? context, String source = "CRASH"}); } class LoggerServiceImp implements LoggerService { @@ -21,6 +23,39 @@ class LoggerServiceImp implements LoggerService { LoggerServiceImp({required this.logger}); + Future _ensureLogFileClearedIfNeeded({required File file, required DateTime now, required DateTime? lastLogsClearedAt, required String source, required String typeName}) async { + try { + CacheService cacheService = getIt.get(); + + // Clear by time threshold + if (lastLogsClearedAt != null && now.difference(lastLogsClearedAt).inHours >= AppConstants.clearLogsHoursThreshold) { + final clearingMessage = "[${DateFormat('yyyy-MM-dd hh:mm:ss a').format(now)}] [SOURCE: $source] $typeName: LOGS HAVE BEEN CLEARED\n"; + await file.writeAsString(clearingMessage, mode: FileMode.write); + // persist + final updated = DateTime.now(); + getIt.get().lastTimeLogsCleared = updated; + await cacheService.setLastTimeLogsCleared(lastTimeCleared: updated.millisecondsSinceEpoch); + return; + } + + // Clear by size threshold + if (await file.exists()) { + final length = await file.length(); + if (length >= AppConstants.maxLogFileSizeBytes) { + final clearingMessage = "[${DateFormat('yyyy-MM-dd hh:mm:ss a').format(now)}] [SOURCE: $source] $typeName: LOGS ROTATED DUE_TO_SIZE\n"; + // Overwrite file with clearing message + await file.writeAsString(clearingMessage, mode: FileMode.write); + final updated = DateTime.now(); + getIt.get().lastTimeLogsCleared = updated; + await cacheService.setLastTimeLogsCleared(lastTimeCleared: updated.millisecondsSinceEpoch); + return; + } + } + } catch (e) { + logger.e('Failed to check/clear log file: $e'); + } + } + @override Future logToFile({required String message, required LogTypeEnum type, required String source}) async { try { @@ -46,23 +81,46 @@ class LoggerServiceImp implements LoggerService { final file = File('${logDir.path}/${screenConfigViewModel.currentScreenIP}_$logFileName'); - CacheService cacheService = getIt.get(); - - final lastLogsClearedAt = screenConfigViewModel.lastTimeLogsCleared; final now = DateTime.now(); + await _ensureLogFileClearedIfNeeded(file: file, now: now, lastLogsClearedAt: screenConfigViewModel.lastTimeLogsCleared, source: source, typeName: type.name.toUpperCase()); - if (lastLogsClearedAt != null && now.difference(lastLogsClearedAt).inHours >= AppConstants.clearLogsHoursThreshold) { - final clearingMessage = "[$timestamp] [SOURCE: $source] ${type.name.toUpperCase()}: LOGS HAVE BEEN CLEARED\n"; - await file.writeAsString(clearingMessage); - screenConfigViewModel.lastTimeLogsCleared = now; - cacheService.setLastTimeLogsCleared(lastTimeCleared: now.millisecondsSinceEpoch); - } await file.writeAsString("$formattedMessage\n", mode: FileMode.append); } catch (e) { logger.e('Logging failed: $e'); } } + @override + Future logCrash({required Object error, StackTrace? stackTrace, String? context, String source = "CRASH"}) async { + try { + ScreenConfigViewModel screenConfigViewModel = getIt.get(); + final timestamp = DateFormat('yyyy-MM-dd hh:mm:ss a').format(DateTime.now()); + final sep = '==================== CRASH ===================='; + final contextStr = context ?? 'No context provided'; + final errorMsg = error.toString(); + final stack = stackTrace?.toString() ?? 'No stack trace available'; + + final crashMessage = + '$sep\n[$timestamp]\nCONTEXT: $contextStr\nERROR: $errorMsg\nSTACKTRACE:\n$stack\n$sep\n'; + + final baseDir = await getExternalStorageDirectory(); + final logDir = Directory('${baseDir!.path}/logs'); + if (!await logDir.exists()) { + await logDir.create(recursive: true); + } + + final file = File('${logDir.path}/${screenConfigViewModel.currentScreenIP}_${AppStrings.errorLogsFileName}'); + + final now = DateTime.now(); + await _ensureLogFileClearedIfNeeded(file: file, now: now, lastLogsClearedAt: screenConfigViewModel.lastTimeLogsCleared, source: source, typeName: 'ERROR'); + + await file.writeAsString(crashMessage, mode: FileMode.append); + logger.e('Crash logged: $errorMsg'); + } catch (e) { + logger.e('Crash logging failed: $e'); + } + } + @override void logError(String message) { logger.e(message); diff --git a/lib/view_models/queuing_view_model.dart b/lib/view_models/queuing_view_model.dart index 6671ca4..d37f396 100644 --- a/lib/view_models/queuing_view_model.dart +++ b/lib/view_models/queuing_view_model.dart @@ -11,6 +11,7 @@ import 'package:hmg_qline/services/audio_service.dart'; import 'package:hmg_qline/services/cache_service.dart'; import 'package:hmg_qline/services/logger_service.dart'; import 'package:hmg_qline/services/text_to_speech_service.dart'; +import 'package:hmg_qline/services/crash_handler_service.dart'; import 'package:hmg_qline/utilities/enums.dart'; import 'package:hmg_qline/view_models/screen_config_view_model.dart'; @@ -32,21 +33,30 @@ class QueuingViewModel extends ChangeNotifier { }); Future initializeQueueingVM() async { - await startHubConnection(); - initializeAudioPlayer(); - initializeTextToSpeech(); + try { + await startHubConnection(); + initializeAudioPlayer(); + initializeTextToSpeech(); + } catch (e, st) { + await getIt.get().handleCrash(error: e, stackTrace: st, context: 'initializeQueueingVM -> queuing_view_model.dart'); + } } Future startHubConnection() async { - ScreenConfigViewModel screenConfigViewModel = getIt.get(); - return await signalrRepo.startHubConnection( - deviceIp: screenConfigViewModel.currentScreenIP, - onHubTicketCall: onHubTicketCall, - onHubConfigCall: onHubConfigCall, - onHubReconnected: onHubReconnected, - onHubDisconnected: onHubDisconnected, - onHubConnecting: onHubConnecting, - ); + try { + ScreenConfigViewModel screenConfigViewModel = getIt.get(); + return await signalrRepo.startHubConnection( + deviceIp: screenConfigViewModel.currentScreenIP, + onHubTicketCall: onHubTicketCall, + onHubConfigCall: onHubConfigCall, + onHubReconnected: onHubReconnected, + onHubDisconnected: onHubDisconnected, + onHubConnecting: onHubConnecting, + ); + } catch (e, st) { + await getIt.get().handleCrash(error: e, stackTrace: st, context: 'startHubConnection -> queuing_view_model.dart'); + return false; + } } Future stopHubConnection() async { @@ -80,14 +90,18 @@ class QueuingViewModel extends ChangeNotifier { } Future onHubTicketCall(List? response) async { - loggerService.logToFile(message: response.toString(), source: "onHubTicketCall -> queueing_view_model.dart ", type: LogTypeEnum.data); + try { + loggerService.logToFile(message: response.toString(), source: "onHubTicketCall -> queueing_view_model.dart ", type: LogTypeEnum.data); - log("onHubTicketCall: $response"); + log("onHubTicketCall: $response"); - log("isCallingInProgress: $isCallingInProgress"); - if (response != null && response.isNotEmpty) { - TicketDetailsModel ticketDetailsModel = TicketDetailsModel.fromJson(response.first as Map); - addNewTicket(ticketDetailsModel); + log("isCallingInProgress: $isCallingInProgress"); + if (response != null && response.isNotEmpty) { + TicketDetailsModel ticketDetailsModel = TicketDetailsModel.fromJson(response.first as Map); + addNewTicket(ticketDetailsModel); + } + } catch (e, st) { + await getIt.get().handleCrash(error: e, stackTrace: st, context: 'onHubTicketCall -> queuing_view_model.dart'); } } @@ -103,7 +117,6 @@ class QueuingViewModel extends ChangeNotifier { Future onHubDisconnected(var response) async { log("onHubDisconnected: $response"); loggerService.logToFile(message: response.toString(), source: "onHubDisconnected -> queueing_view_model.dart ", type: LogTypeEnum.connectivity); - ScreenConfigViewModel screenConfigViewModel = getIt.get(); screenConfigViewModel.updateIsHubConnected(false); screenConfigViewModel.notifyListeners(); @@ -209,31 +222,27 @@ class QueuingViewModel extends ChangeNotifier { isCallingInProgress = true; await audioService.playTone(path: AppAssets.callTone, isMute: !(globalConfigurationsModel.isToneReq)); } -} -// Future callTicketOnScreen({required TicketData? ticketData}) async { -// if (ticketData == null) return; -// ScreenConfigViewModel screenConfigViewModel = getIt.get(); -// GlobalConfigurationsModel globalConfigurationsModel = screenConfigViewModel.globalConfigurationsModel; -// if (globalConfigurationsModel.qTypeEnum == QTypeEnum.appointment) { -// screenConfigViewModel.acknowledgeTicketForAppointmentOnly( -// ticketQueueID: ticketData.id ?? 0, -// ipAddress: screenConfigViewModel.currentScreenIP, -// callTypeEnum: ticketData.callTypeEnum, -// ); -// } else { -// screenConfigViewModel.acknowledgeTicket(ticketQueueID: ticketData.id ?? 0); -// } -// if (globalConfigurationsModel.isToneReq) { -// isCallingInProgress = true; -// await audioService.playTone(path: AppAssets.callTone, isMute: !(globalConfigurationsModel.isToneReq)); -// } else if (globalConfigurationsModel.isVoiceReq) { -// isCallingInProgress = true; -// await textToSpeechService.speechText(globalConfigurationsModel: globalConfigurationsModel, ticket: currentTickets.first, isMute: !(globalConfigurationsModel.isVoiceReq)); -// } else { -// waitAndCallNextTicketIfAvailable(); -// } -// } - -// jQ -//ucq + List> memoryHog = []; + + void triggerOOM() { + while (true) { + // Repeatedly add large chunks (e.g., 10 million integers) to a list + // This prevents the Garbage Collector (GC) from reclaiming the memory + memoryHog.add(List.filled(10000000, 9)); + } + } + + /// Throws an exception from an async context (await) to test runZonedGuarded handling. + Future triggerAsyncCrash() async { + await Future.delayed(const Duration(milliseconds: 50)); + throw Exception('Test asynchronous crash from QueuingViewModel.triggerAsyncCrash'); + } + + /// Creates an unawaited Future that throws to simulate an unhandled async error. + void triggerUnhandledFutureCrash() { + Future.delayed(const Duration(milliseconds: 50), () { + throw Exception('Test unhandled future crash from QueuingViewModel.triggerUnhandledFutureCrash'); + }); + } +} diff --git a/lib/views/common_widgets/app_header.dart b/lib/views/common_widgets/app_header.dart index 66d7ebc..be01996 100644 --- a/lib/views/common_widgets/app_header.dart +++ b/lib/views/common_widgets/app_header.dart @@ -6,6 +6,7 @@ import 'package:hmg_qline/config/dependency_injection.dart'; import 'package:hmg_qline/models/global_config_model.dart'; import 'package:hmg_qline/utilities/enums.dart'; import 'package:hmg_qline/utilities/native_method_handler.dart'; +import 'package:hmg_qline/view_models/queuing_view_model.dart'; import 'package:hmg_qline/view_models/screen_config_view_model.dart'; import 'package:hmg_qline/views/common_widgets/app_general_widgets.dart'; import 'package:provider/provider.dart'; @@ -33,8 +34,8 @@ class AppHeader extends StatelessWidget implements PreferredSizeWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ InkWell( - onTap: () async { - await getIt.get().smartRestart(); + onTap: () { + // getIt.get().triggerOOM(); }, child: engArabicTextWithSeparatorWidget( englishText: globalConfigurationsModel.currentServeTextEng ?? "", diff --git a/pubspec.lock b/pubspec.lock index e25a83f..6553eb6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,14 +1,6 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - animated_flip_counter: - dependency: "direct main" - description: - name: animated_flip_counter - sha256: "73f852d84c461c3e4c1ddf320bee334dde8dba89441922ab11a8013be0b2fad1" - url: "https://pub.dev" - source: hosted - version: "0.3.4" args: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index be645d2..ae909e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,7 +56,7 @@ dependencies: flutter_foreground_task: ^9.1.0 # restart_app: ^1.3.2 zo_animated_border: ^1.0.1 - animated_flip_counter: ^0.3.4 +# animated_flip_counter: ^0.3.4 # smooth_corner: ^1.1.1