From 2f73a21a418defdc7a7c22dfedd98353f77ce209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Sat, 19 Oct 2024 17:16:34 +0200 Subject: [PATCH] Added raw-failure logs to flutter app (to debug init errors) --- flutter/lib/main.dart | 11 +++ flutter/lib/pages/debug/debug_actions.dart | 14 +++ .../lib/pages/debug/debug_persistence.dart | 27 ++++++ .../debug_persistence_failurelogfile.dart | 64 ++++++++++++++ .../debug/debug_persistence_failurelogs.dart | 86 +++++++++++++++++++ flutter/lib/state/application_log.dart | 60 +++++++++++++ flutter/lib/state/globals.dart | 10 +++ 7 files changed, 272 insertions(+) create mode 100644 flutter/lib/pages/debug/debug_persistence_failurelogfile.dart create mode 100644 flutter/lib/pages/debug/debug_persistence_failurelogs.dart diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 650f7fd..ba61687 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -59,6 +59,7 @@ void main() async { Hive.deleteBoxFromDisk('scn-logs'); await Hive.openBox('scn-logs'); ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace); + ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-logs', {'error': exc.toString(), 'trace': trace}); } print('[INIT] Load Hive...'); @@ -69,6 +70,7 @@ void main() async { Hive.deleteBoxFromDisk('scn-requests'); await Hive.openBox('scn-requests'); ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace); + ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-requests', {'error': exc.toString(), 'trace': trace}); } print('[INIT] Load Hive...'); @@ -79,6 +81,7 @@ void main() async { Hive.deleteBoxFromDisk('scn-message-cache'); await Hive.openBox('scn-message-cache'); ApplicationLog.error('Failed to open Hive-Box: scn-message-cache' + exc.toString(), trace: trace); + ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-message-cache', {'error': exc.toString(), 'trace': trace}); } print('[INIT] Load Hive...'); @@ -89,6 +92,7 @@ void main() async { Hive.deleteBoxFromDisk('scn-channel-cache'); await Hive.openBox('scn-channel-cache'); ApplicationLog.error('Failed to open Hive-Box: scn-channel-cache' + exc.toString(), trace: trace); + ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-channel-cache', {'error': exc.toString(), 'trace': trace}); } print('[INIT] Load Hive...'); @@ -99,6 +103,7 @@ void main() async { Hive.deleteBoxFromDisk('scn-fb-messages'); await Hive.openBox('scn-fb-messages'); ApplicationLog.error('Failed to open Hive-Box: scn-fb-messages' + exc.toString(), trace: trace); + ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-fb-messages', {'error': exc.toString(), 'trace': trace}); } print('[INIT] Load AppAuth...'); @@ -112,11 +117,13 @@ void main() async { await appAuth.loadUser(); } catch (exc, trace) { ApplicationLog.error('Failed to load user (background load on startup): ' + exc.toString(), trace: trace); + ApplicationLog.writeRawFailure('Failed to load user (background load on startup)', {'error': exc.toString(), 'trace': trace}); } try { await appAuth.loadClient(); } catch (exc, trace) { ApplicationLog.error('Failed to load user (background load on startup): ' + exc.toString(), trace: trace); + ApplicationLog.writeRawFailure('Failed to load user (background load on startup)', {'error': exc.toString(), 'trace': trace}); } }(); } @@ -321,6 +328,7 @@ Future _receiveMessage(RemoteMessage message, bool foreground) async { await Hive.openBox('scn-message-cache'); await Hive.openBox('scn-requests'); } catch (exc, trace) { + ApplicationLog.writeRawFailure('Failed to init hive', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground}); ApplicationLog.error('Failed to init hive:' + exc.toString(), trace: trace); Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (init failed)", null); return; @@ -341,6 +349,7 @@ Future _receiveMessage(RemoteMessage message, bool foreground) async { Notifier.showLocalNotification(scn_msg_id, channel_id, channel, 'Channel: ${channel}', title, body, timestamp); } catch (exc, trace) { + ApplicationLog.writeRawFailure('Failed to decode received FB message', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground}); ApplicationLog.error('Failed to decode received FB message' + exc.toString(), trace: trace); Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (decode failed)", null); return; @@ -349,6 +358,7 @@ Future _receiveMessage(RemoteMessage message, bool foreground) async { try { FBMessageLog.insert(message); } catch (exc, trace) { + ApplicationLog.writeRawFailure('Failed to persist received FB message', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground}); ApplicationLog.error('Failed to persist received FB message' + exc.toString(), trace: trace); Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (persist failed)", null); return; @@ -359,6 +369,7 @@ Future _receiveMessage(RemoteMessage message, bool foreground) async { SCNDataCache().addToMessageCache([msg]); if (foreground) AppEvents().notifyMessageReceivedListeners(msg); } catch (exc, trace) { + ApplicationLog.writeRawFailure('Failed to query+persist message', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground}); ApplicationLog.error('Failed to query+persist message: ' + exc.toString(), trace: trace); return; } diff --git a/flutter/lib/pages/debug/debug_actions.dart b/flutter/lib/pages/debug/debug_actions.dart index aaef251..a997cc1 100644 --- a/flutter/lib/pages/debug/debug_actions.dart +++ b/flutter/lib/pages/debug/debug_actions.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/utils/notifier.dart'; import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/ui.dart'; @@ -59,6 +60,19 @@ class _DebugActionsPageState extends State { onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null), text: 'Show local notification', ), + SizedBox(height: 20), + UI.button( + big: false, + onPressed: () => ApplicationLog.writeRawFailure('test', { + 'text': "hello world", + 'object': { + 1: 2, + 4: 5, + 6: [7, 8, 9] + }, + 'trace': StackTrace.current + }), + text: 'asdf'), ], ), ), diff --git a/flutter/lib/pages/debug/debug_persistence.dart b/flutter/lib/pages/debug/debug_persistence.dart index 1daaec9..0a51a45 100644 --- a/flutter/lib/pages/debug/debug_persistence.dart +++ b/flutter/lib/pages/debug/debug_persistence.dart @@ -1,15 +1,20 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/scn_message.dart'; +import 'package:simplecloudnotifier/pages/debug/debug_persistence_failurelogs.dart'; import 'package:simplecloudnotifier/pages/debug/debug_persistence_hive.dart'; import 'package:simplecloudnotifier/pages/debug/debug_persistence_sharedprefs.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/fb_message.dart'; +import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/interfaces.dart'; import 'package:simplecloudnotifier/state/request_log.dart'; import 'package:simplecloudnotifier/utils/navi.dart'; +import 'package:path/path.dart' as path; class DebugPersistencePage extends StatefulWidget { @override @@ -39,6 +44,7 @@ class _DebugPersistencePageState extends State { _buildHiveCard(context, () => Hive.box('scn-message-cache'), 'scn-message-cache'), _buildHiveCard(context, () => Hive.box('scn-channel-cache'), 'scn-channel-cache'), _buildHiveCard(context, () => Hive.box('scn-fb-messages'), 'scn-fb-messages'), + _buildFailureLogCard(context, Globals().rawFailureLogsDir), ], ), ); @@ -85,4 +91,25 @@ class _DebugPersistencePageState extends State { ), ); } + + Widget _buildFailureLogCard(BuildContext context, Directory dir) { + return Card.outlined( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: GestureDetector( + onTap: () { + Navi.push(context, () => DebugFailureLogsPage(dir: dir.path)); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 30, child: Text('')), + Expanded(child: Text('Failure [/${path.basename(dir.path)}/]', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center)), + SizedBox(width: 40, child: Text("${dir.listSync().length}", textAlign: TextAlign.end)), + ], + ), + ), + ), + ); + } } diff --git a/flutter/lib/pages/debug/debug_persistence_failurelogfile.dart b/flutter/lib/pages/debug/debug_persistence_failurelogfile.dart new file mode 100644 index 0000000..1706307 --- /dev/null +++ b/flutter/lib/pages/debug/debug_persistence_failurelogfile.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:simplecloudnotifier/components/layout/scaffold.dart'; +import 'package:simplecloudnotifier/types/immediate_future.dart'; + +class DebugFailureLogFilePage extends StatefulWidget { + final String path; + + DebugFailureLogFilePage({required this.path}) {} + + @override + State createState() => _DebugFailureLogFilePageState(); +} + +class _DebugFailureLogFilePageState extends State { + ImmediateFuture? _futureContent; + + @override + void initState() { + super.initState(); + + _futureContent = ImmediateFuture.ofFuture(new File(this.widget.path).readAsString()); + } + + @override + Widget build(BuildContext context) { + return SCNScaffold( + title: 'FailureLog', + showSearch: false, + child: () { + if (_futureContent == null) { + return Center(child: CircularProgressIndicator()); + } + + return FutureBuilder( + future: _futureContent!.future, + builder: ((context, snapshot) { + if (_futureContent?.value != null) { + return _buildContent(context, _futureContent!.value!); + } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { + return Text('Error: ${snapshot.error}'); //TODO better error display + } else if (snapshot.connectionState == ConnectionState.done) { + return _buildContent(context, snapshot.data!); + } else { + return Center(child: CircularProgressIndicator()); + } + }), + ); + }(), + ); + } + + Widget _buildContent(BuildContext context, String value) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(value, style: TextStyle(fontFamily: "monospace")), + ), + ); + } +} diff --git a/flutter/lib/pages/debug/debug_persistence_failurelogs.dart b/flutter/lib/pages/debug/debug_persistence_failurelogs.dart new file mode 100644 index 0000000..9128eb8 --- /dev/null +++ b/flutter/lib/pages/debug/debug_persistence_failurelogs.dart @@ -0,0 +1,86 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:simplecloudnotifier/components/layout/scaffold.dart'; +import 'package:simplecloudnotifier/pages/debug/debug_persistence_failurelogfile.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/state/globals.dart'; +import 'package:simplecloudnotifier/utils/navi.dart'; +import 'package:path/path.dart' as path; +import 'package:simplecloudnotifier/utils/toaster.dart'; + +class DebugFailureLogsPage extends StatefulWidget { + final String dir; + + DebugFailureLogsPage({required this.dir}); + + @override + State createState() => _DebugFailureLogsPageState(); +} + +class _DebugFailureLogsPageState extends State { + List files = []; + + _DebugFailureLogsPageState() { + files = _listFilesInRawLogFolder(); + } + + @override + Widget build(BuildContext context) { + return SCNScaffold( + title: 'F-Logs', + showSearch: false, + child: ListView.separated( + itemCount: files.length, + itemBuilder: (context, listIndex) { + return GestureDetector( + onTap: () { + Navi.push(context, () => DebugFailureLogFilePage(path: files[listIndex])); + }, + child: Container( + padding: EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Row( + children: [ + Expanded(child: Text(path.basename(files[listIndex]), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), + IconButton( + icon: const Icon(FontAwesomeIcons.trash), + tooltip: 'Delete', + iconSize: 16, + color: Colors.red, + onPressed: () => _deleteFile(context, files[listIndex]), + ) + ], + ), + ), + ); + }, + separatorBuilder: (context, index) => Divider(), + ), + ); + } + + List _listFilesInRawLogFolder() { + final fse = Globals().rawFailureLogsDir.listSync(); + + ApplicationLog.debug("Found ${fse.length} files in raw log folder '${Globals().rawFailureLogsDir.path}'"); + + var paths = fse.where((element) => element is File).map((e) => e.path).toList(); + + paths.sort((a, b) => -1 * a.compareTo(b)); + + return paths; + } + + void _deleteFile(BuildContext context, String fil) { + final file = File(fil); + + file.deleteSync(); + + setState(() { + files = _listFilesInRawLogFolder(); + }); + + Toaster.info("Okay", "File deleted"); + } +} diff --git a/flutter/lib/state/application_log.dart b/flutter/lib/state/application_log.dart index f5ae651..0f21e33 100644 --- a/flutter/lib/state/application_log.dart +++ b/flutter/lib/state/application_log.dart @@ -1,6 +1,11 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:hive_flutter/hive_flutter.dart'; +import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/interfaces.dart'; import 'package:xid/xid.dart'; +import 'package:path/path.dart' as path; part 'application_log.g.dart'; @@ -76,6 +81,61 @@ class ApplicationLog { trace: trace?.toString() ?? '', )); } + + static void writeRawFailure(String message, Map extraData) async { + try { + await Globals().init(); + + final fn = path.join(Globals().rawFailureLogsDir.path, 'failure-${DateTime.now().toIso8601String()}.log'); + + var txt = "[TEXT]\n${message}\n\n"; + for (var k in extraData.keys) { + txt += "[${k}]\n${_debugToStr(extraData[k])}\n\n"; + } + + await File(fn).writeAsString(txt); + + ApplicationLog.debug("Wrote raw failure log to '${fn}' ('${message}')"); + } catch (e) { + print("Failed to : ${e}"); + } + } + + static String _debugToStr(dynamic v) { + if (v is String) { + return v; + } + if (v is StackTrace) { + return v.toString(); + } + + try { + final enc = new JsonEncoder.withIndent(" "); + return enc.convert(v); + } catch (e) { + // ignore + } + + try { + return jsonEncode(v); + } catch (e) { + // ignore + } + + try { + return v.toString(); + } catch (e) { + // ignore + } + + try { + return "${v}"; + } catch (e) { + // ignore + } + + return "<[!]FAILED_TO_PRINT_OBJECT>"; + } } @HiveType(typeId: 103) diff --git a/flutter/lib/state/globals.dart b/flutter/lib/state/globals.dart index 004fd2a..dfeea2d 100644 --- a/flutter/lib/state/globals.dart +++ b/flutter/lib/state/globals.dart @@ -2,7 +2,9 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:path/path.dart' as path; class Globals { static final Globals _singleton = Globals._internal(); @@ -26,6 +28,9 @@ class Globals { late SharedPreferences sharedPrefs; + late Directory appDocumentsDir; + late Directory rawFailureLogsDir; + bool get isInitialized => _initialized; Future init() async { @@ -61,6 +66,11 @@ class Globals { this.sharedPrefs = await SharedPreferences.getInstance(); + this.appDocumentsDir = await getApplicationDocumentsDirectory(); + + this.rawFailureLogsDir = Directory(path.join(Globals().appDocumentsDir.path, "rawlogs")); + await this.rawFailureLogsDir.create(recursive: true); + this._initialized = true; }