| 1 | import 'dart:convert'; |
| 2 | import 'dart:io'; |
| 3 | import 'dart:typed_data'; |
| 4 | |
| 5 | import 'package:cw_core/db/sqlite.dart'; |
| 6 | import 'package:cw_core/root_dir.dart'; |
| 7 | import 'package:flutter/foundation.dart'; |
| 8 | import 'package:flutter/material.dart'; |
| 9 | |
| 10 | Future<void> handleSqliteError(final dynamic error, final StackTrace stackTrace) async { |
| 11 | try { |
| 12 | final f = await sqliteDebugMarkerFile(); |
| 13 | f.createSync(); |
| 14 | } catch (_) {} |
| 15 | runApp( |
| 16 | SqliteErrorHandlerApp( |
| 17 | error: error, |
| 18 | stackTrace: stackTrace, |
| 19 | ), |
| 20 | ); |
| 21 | await Future.delayed(Duration(hours: 24)); |
| 22 | } |
| 23 | |
| 24 | class SqliteErrorHandlerApp extends StatelessWidget { |
| 25 | const SqliteErrorHandlerApp({ |
| 26 | super.key, |
| 27 | required this.error, |
| 28 | required this.stackTrace, |
| 29 | }); |
| 30 | |
| 31 | final dynamic error; |
| 32 | final StackTrace? stackTrace; |
| 33 | @override |
| 34 | Widget build(final BuildContext context) { |
| 35 | return MaterialApp( |
| 36 | debugShowCheckedModeBanner: false, |
| 37 | title: 'Cake Wallet Crash', |
| 38 | themeMode: ThemeMode.light, |
| 39 | home: SqliteErrorHandler( |
| 40 | error: error, |
| 41 | stackTrace: stackTrace, |
| 42 | ), |
| 43 | ); |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | class SqliteErrorHandler extends StatefulWidget { |
| 48 | SqliteErrorHandler({ |
| 49 | super.key, |
| 50 | required this.error, |
| 51 | required this.stackTrace, |
| 52 | }); |
| 53 | |
| 54 | final dynamic error; |
| 55 | final StackTrace? stackTrace; |
| 56 | |
| 57 | @override |
| 58 | State<SqliteErrorHandler> createState() => _SqliteErrorHandlerState(); |
| 59 | } |
| 60 | |
| 61 | class _SqliteErrorHandlerState extends State<SqliteErrorHandler> { |
| 62 | String log = ""; |
| 63 | |
| 64 | @override |
| 65 | Widget build(final BuildContext context) { |
| 66 | return Scaffold( |
| 67 | appBar: AppBar( |
| 68 | title: Text("Sqlite Recovery Screen"), |
| 69 | ), |
| 70 | body: SingleChildScrollView( |
| 71 | child: Padding( |
| 72 | padding: const EdgeInsets.all(8.0), |
| 73 | child: Column( |
| 74 | crossAxisAlignment: CrossAxisAlignment.start, |
| 75 | children: _buildNotice(), |
| 76 | ), |
| 77 | ), |
| 78 | ), |
| 79 | ); |
| 80 | } |
| 81 | |
| 82 | Widget _text(final dynamic element) { |
| 83 | return SelectableText( |
| 84 | element.toString(), |
| 85 | ); |
| 86 | } |
| 87 | |
| 88 | void _dbgRemoveDebugMarker() async { |
| 89 | final f = await sqliteDebugMarkerFile(); |
| 90 | f.delete(); |
| 91 | } |
| 92 | |
| 93 | void _corruptTheDb() async { |
| 94 | final dbFile = File("${(await getAppDir()).path}/cake.db"); |
| 95 | await writeBytesToFile(dbFile); |
| 96 | } |
| 97 | |
| 98 | void _dumpDb() async { |
| 99 | try { |
| 100 | final dbFile = File("${(await getAppDir()).path}/cake.db"); |
| 101 | final jsonText = JsonEncoder.withIndent(" ").convert(await dumpCustomDb(dbFile.path)); |
| 102 | setState(() { |
| 103 | log = jsonText; |
| 104 | }); |
| 105 | } catch (e, s) { |
| 106 | setState(() { |
| 107 | log = """ |
| 108 | e: $e |
| 109 | |
| 110 | s: $s |
| 111 | """; |
| 112 | }); |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | void _removeDb() async { |
| 117 | try { |
| 118 | File("${(await getAppDir()).path}/cake.db.old").deleteSync(); |
| 119 | } catch (_) {} |
| 120 | final dbFile = File("${(await getAppDir()).path}/cake.db"); |
| 121 | dbFile.renameSync("${(await getAppDir()).path}/cake.db.old"); |
| 122 | } |
| 123 | |
| 124 | List<Widget> _debugOptions() { |
| 125 | return [ |
| 126 | _debugButton("Remove debug marker", _dbgRemoveDebugMarker), |
| 127 | if (kDebugMode || kProfileMode) _debugButton("[DO NOT PRESS] Corrupt the DB", _corruptTheDb), |
| 128 | _debugButton("Remove DB", _removeDb), |
| 129 | _debugButton("Dump DB", _dumpDb), |
| 130 | ]; |
| 131 | } |
| 132 | |
| 133 | Widget _debugButton(String text, VoidCallback callback) { |
| 134 | return ElevatedButton( |
| 135 | onPressed: callback, |
| 136 | child: Text(text), |
| 137 | ); |
| 138 | } |
| 139 | |
| 140 | List<Widget> _buildNotice() { |
| 141 | return [ |
| 142 | _text("""Critical error occured in SQLite |
| 143 | This is very rare, usually caused by the device killing the app before it finishes writing file. |
| 144 | Here you can recover the database or recreate it to regain access to your wallets. |
| 145 | During this process some of the metadata may get lost. |
| 146 | Please contact support@cakewallet.com if you need any help."""), |
| 147 | Divider(), |
| 148 | ..._debugOptions(), |
| 149 | if (log.isNotEmpty) ...[ |
| 150 | Divider(), |
| 151 | SelectableText( |
| 152 | log, |
| 153 | style: TextStyle( |
| 154 | fontSize: 8, |
| 155 | ), |
| 156 | ), |
| 157 | ], |
| 158 | Divider(), |
| 159 | _text(widget.error), |
| 160 | Divider(), |
| 161 | _text(widget.stackTrace), |
| 162 | ]; |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | Future<void> writeBytesToFile(File file) async { |
| 167 | const int seek = 5; |
| 168 | const int count = 50000; |
| 169 | final raf = await file.open(mode: FileMode.writeOnlyAppend); |
| 170 | try { |
| 171 | await raf.setPosition(seek); |
| 172 | final zeros = Uint8List.fromList(List.generate(count, (_) => 0x42)); |
| 173 | await raf.writeFrom(zeros); |
| 174 | } finally { |
| 175 | await raf.close(); |
| 176 | } |
| 177 | } |