From 241503774f64e345ff5ec372775d0a09acf55e1a Mon Sep 17 00:00:00 2001 From: cyan Date: Mon, 15 Jun 2026 01:21:14 +0200 Subject: [PATCH] dev: add recovery screen when sqlite db doesn't open (#3179) * dev: add recovery screen when sqlite db doesn't open * Update cw_core/lib/db/sqlite_debug.dart [skip ci] --------- Co-authored-by: Omar Hatem --- cw_core/lib/db/sqlite.dart | 20 ++ cw_core/lib/db/sqlite_debug.dart | 177 ++++++++++++++++++ .../screens/settings/other_settings_page.dart | 12 ++ 3 files changed, 209 insertions(+) create mode 100644 cw_core/lib/db/sqlite_debug.dart diff --git a/cw_core/lib/db/sqlite.dart b/cw_core/lib/db/sqlite.dart index bcd1a275d..bbcbf1941 100644 --- a/cw_core/lib/db/sqlite.dart +++ b/cw_core/lib/db/sqlite.dart @@ -1,8 +1,10 @@ import 'dart:io'; +import 'package:cw_core/db/sqlite_debug.dart'; import 'package:cw_core/root_dir.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:path/path.dart' as p; Database? db; @@ -22,7 +24,25 @@ Future _addColumnIfNotExists( } } +Future sqliteDebugMarkerFile() async { + final appDir = await getAppDir(); + final dbDebugMarker = p.join(appDir.path, ".sqlite_db_debug"); + return File(dbDebugMarker); +} + Future initDb({String? pathOverride}) async { + final dbDebugMarker = await sqliteDebugMarkerFile(); + try { + if (dbDebugMarker.existsSync()) { + throw Exception("Debug marker is present"); + } + await _initDb(pathOverride: pathOverride); + } catch (e, s) { + await handleSqliteError(e, s); + } +} + +Future _initDb({String? pathOverride}) async { if (Platform.isLinux || Platform.isWindows) { databaseFactory = databaseFactoryFfi; } diff --git a/cw_core/lib/db/sqlite_debug.dart b/cw_core/lib/db/sqlite_debug.dart new file mode 100644 index 000000000..8e4a3538e --- /dev/null +++ b/cw_core/lib/db/sqlite_debug.dart @@ -0,0 +1,177 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:cw_core/db/sqlite.dart'; +import 'package:cw_core/root_dir.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +Future handleSqliteError(final dynamic error, final StackTrace stackTrace) async { + try { + final f = await sqliteDebugMarkerFile(); + f.createSync(); + } catch (_) {} + runApp( + SqliteErrorHandlerApp( + error: error, + stackTrace: stackTrace, + ), + ); + await Future.delayed(Duration(hours: 24)); +} + +class SqliteErrorHandlerApp extends StatelessWidget { + const SqliteErrorHandlerApp({ + super.key, + required this.error, + required this.stackTrace, + }); + + final dynamic error; + final StackTrace? stackTrace; + @override + Widget build(final BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Cake Wallet Crash', + themeMode: ThemeMode.light, + home: SqliteErrorHandler( + error: error, + stackTrace: stackTrace, + ), + ); + } +} + +class SqliteErrorHandler extends StatefulWidget { + SqliteErrorHandler({ + super.key, + required this.error, + required this.stackTrace, + }); + + final dynamic error; + final StackTrace? stackTrace; + + @override + State createState() => _SqliteErrorHandlerState(); +} + +class _SqliteErrorHandlerState extends State { + String log = ""; + + @override + Widget build(final BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Sqlite Recovery Screen"), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildNotice(), + ), + ), + ), + ); + } + + Widget _text(final dynamic element) { + return SelectableText( + element.toString(), + ); + } + + void _dbgRemoveDebugMarker() async { + final f = await sqliteDebugMarkerFile(); + f.delete(); + } + + void _corruptTheDb() async { + final dbFile = File("${(await getAppDir()).path}/cake.db"); + await writeBytesToFile(dbFile); + } + + void _dumpDb() async { + try { + final dbFile = File("${(await getAppDir()).path}/cake.db"); + final jsonText = JsonEncoder.withIndent(" ").convert(await dumpCustomDb(dbFile.path)); + setState(() { + log = jsonText; + }); + } catch (e, s) { + setState(() { + log = """ +e: $e + +s: $s +"""; + }); + } + } + + void _removeDb() async { + try { + File("${(await getAppDir()).path}/cake.db.old").deleteSync(); + } catch (_) {} + final dbFile = File("${(await getAppDir()).path}/cake.db"); + dbFile.renameSync("${(await getAppDir()).path}/cake.db.old"); + } + + List _debugOptions() { + return [ + _debugButton("Remove debug marker", _dbgRemoveDebugMarker), + if (kDebugMode || kProfileMode) _debugButton("[DO NOT PRESS] Corrupt the DB", _corruptTheDb), + _debugButton("Remove DB", _removeDb), + _debugButton("Dump DB", _dumpDb), + ]; + } + + Widget _debugButton(String text, VoidCallback callback) { + return ElevatedButton( + onPressed: callback, + child: Text(text), + ); + } + + List _buildNotice() { + return [ + _text("""Critical error occured in SQLite +This is very rare, usually caused by the device killing the app before it finishes writing file. +Here you can recover the database or recreate it to regain access to your wallets. +During this process some of the metadata may get lost. +Please contact support@cakewallet.com if you need any help."""), + Divider(), + ..._debugOptions(), + if (log.isNotEmpty) ...[ + Divider(), + SelectableText( + log, + style: TextStyle( + fontSize: 8, + ), + ), + ], + Divider(), + _text(widget.error), + Divider(), + _text(widget.stackTrace), + ]; + } +} + +Future writeBytesToFile(File file) async { + const int seek = 5; + const int count = 50000; + final raf = await file.open(mode: FileMode.writeOnlyAppend); + try { + await raf.setPosition(seek); + final zeros = Uint8List.fromList(List.generate(count, (_) => 0x42)); + await raf.writeFrom(zeros); + } finally { + await raf.close(); + } +} diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 1670d6631..9593f8b6b 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_selector.dart'; @@ -14,11 +16,13 @@ import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/utils/share_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; +import 'package:cw_core/root_dir.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/db/sqlite.dart'; import 'package:flutter/material.dart'; +import 'package:path/path.dart' as p; class OtherSettingsPage extends BasePage { OtherSettingsPage(this._otherSettingsViewModel) { @@ -211,6 +215,14 @@ class OtherSettingsPage extends BasePage { ), ); }), + ListItemRegularRow( + keyValue: '[dev] fake corrupt sqlite db', + label: '[dev] fake corrupt sqlite db', + onTap: () async { + final dbDebugMarker = await sqliteDebugMarkerFile(); + dbDebugMarker.create(); + } + ), ] }), );