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 <omarh.ismail1@gmail.com>
This commit is contained in:
cyan
2026-06-15 01:21:14 +02:00
committed by GitHub
co-authored by Omar Hatem
parent 9face62eff
commit 241503774f
3 changed files with 209 additions and 0 deletions
+20
View File
@@ -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<void> _addColumnIfNotExists(
}
}
Future<File> sqliteDebugMarkerFile() async {
final appDir = await getAppDir();
final dbDebugMarker = p.join(appDir.path, ".sqlite_db_debug");
return File(dbDebugMarker);
}
Future<void> 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<void> _initDb({String? pathOverride}) async {
if (Platform.isLinux || Platform.isWindows) {
databaseFactory = databaseFactoryFfi;
}
+177
View File
@@ -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<void> 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<SqliteErrorHandler> createState() => _SqliteErrorHandlerState();
}
class _SqliteErrorHandlerState extends State<SqliteErrorHandler> {
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<Widget> _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<Widget> _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<void> 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();
}
}
@@ -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();
}
),
]
}),
);