From 82d22414a069ba3df2659e8257031e037ffa3791 Mon Sep 17 00:00:00 2001 From: bin <17426470+boyan01@users.noreply.github.com> Date: Mon, 20 Nov 2023 17:42:24 +0800 Subject: [PATCH] improve error handle when open database failed --- lib/app.dart | 63 ++++++++++++--- lib/main.dart | 14 ++-- lib/ui/landing/landing_failed.dart | 105 +++++++++++++++++++------ lib/ui/provider/database_provider.dart | 11 ++- lib/utils/db/db_key_value.dart | 2 +- 5 files changed, 148 insertions(+), 47 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index ccab187add..8ec6f08876 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -8,10 +8,12 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart' hide Consumer, FutureProvider, Provider; +import 'package:path/path.dart' as p; import 'package:provider/provider.dart'; import 'account/notification_service.dart'; import 'constants/brightness_theme_data.dart'; +import 'constants/constants.dart'; import 'constants/resources.dart'; import 'generated/l10n.dart'; import 'ui/home/bloc/conversation_list_bloc.dart'; @@ -28,6 +30,7 @@ import 'ui/provider/mention_cache_provider.dart'; import 'ui/provider/setting_provider.dart'; import 'ui/provider/slide_category_provider.dart'; import 'utils/extension/extension.dart'; +import 'utils/file.dart'; import 'utils/hook.dart'; import 'utils/logger.dart'; import 'utils/platform.dart'; @@ -53,6 +56,38 @@ class App extends HookConsumerWidget { precacheImage( const AssetImage(Resources.assetsImagesChatBackgroundPng), context); + final appDatabaseInitError = ref.watch(appDatabaseInitErrorProvider); + if (appDatabaseInitError != null) { + var error = appDatabaseInitError; + if (error is DriftRemoteException) { + error = error.remoteCause; + } + if (error is SqliteException) { + return _App( + home: DatabaseOpenFailedPage( + error: error, + closeDatabaseCallback: () => ref.read(appDatabaseProvider).close(), + deleteDatabaseCallback: () => dropDatabaseFile( + mixinDocumentsDirectory.path, + kDbFileName, + ), + openDatabaseCallback: () async { + final db = ref.refresh(appDatabaseProvider); + try { + await db.settingKeyValue.initialize; + ref.read(appDatabaseInitErrorProvider.notifier).state = null; + } catch (e) { + w('reOpenDatabaseCallback error: $e'); + ref.read(appDatabaseInitErrorProvider.notifier).state = e; + } + }, + ), + ); + } else { + return _App(home: OpenAppFailedPage(error: error)); + } + } + final initialized = useMemoizedFuture( () => ref.read(multiAuthStateNotifierProvider.notifier).initialized, null, @@ -93,20 +128,24 @@ class _LoginApp extends HookConsumerWidget { } if (error is SqliteException) { return _App( - home: DatabaseOpenFailedPage(error: error), + home: DatabaseOpenFailedPage( + error: error, + openDatabaseCallback: () => + ref.read(databaseProvider.notifier).open(), + closeDatabaseCallback: () => + ref.read(databaseProvider.notifier).close(), + deleteDatabaseCallback: () async { + final identityNumber = context.account?.identityNumber; + if (identityNumber == null) return; + await dropDatabaseFile( + p.join(mixinDocumentsDirectory.path, identityNumber), + kDbFileName, + ); + }, + ), ); } else { - return _App( - home: LandingFailedPage( - title: context.l10n.unknowError, - message: error.toString(), - actions: [ - ElevatedButton( - onPressed: () {}, - child: Text(context.l10n.exit), - ) - ]), - ); + return _App(home: OpenAppFailedPage(error: error)); } } diff --git a/lib/main.dart b/lib/main.dart index 3c5aec4048..afc7113148 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,7 +21,6 @@ import 'package:window_size/window_size.dart'; import 'app.dart'; import 'bloc/custom_bloc_observer.dart'; -import 'db/app/app_database.dart'; import 'ui/home/home.dart'; import 'ui/provider/database_provider.dart'; import 'utils/app_lifecycle.dart'; @@ -104,12 +103,15 @@ Future main(List args) async { Bloc.observer = CustomBlocObserver(); } - final appDatabase = AppDatabase.connect(fromMainIsolate: true); - await appDatabase.settingKeyValue.initialize; + final container = ProviderContainer(); + try { + await container.read(appDatabaseProvider).settingKeyValue.initialize; + } catch (error, stacktrace) { + e('failed to initialize setting key value: $error\n$stacktrace'); + container.read(appDatabaseInitErrorProvider.notifier).state = error; + } runApp(ProviderScope( - overrides: [ - appDatabaseProvider.overrideWithValue(appDatabase), - ], + parent: container, child: const OverlaySupport.global(child: App()), )); diff --git a/lib/ui/landing/landing_failed.dart b/lib/ui/landing/landing_failed.dart index 035e6870e0..ccf60859c8 100644 --- a/lib/ui/landing/landing_failed.dart +++ b/lib/ui/landing/landing_failed.dart @@ -5,25 +5,34 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:path/path.dart' as p; -import '../../constants/constants.dart'; import '../../utils/extension/extension.dart'; import '../../utils/file.dart'; import '../../widgets/dialog.dart'; -import '../provider/database_provider.dart'; import 'landing.dart'; // https://sqlite.org/rescode.html const _kSqliteCorrupt = 11; const _kSqliteLocked = 6; const _kSqliteNotADb = 26; +const _kSqliteBusy = 5; + +typedef DeleteDatabaseCallback = Future Function(); +typedef OpenDatabaseCallback = Future Function(); +typedef CloseDatabaseCallback = Future Function(); class DatabaseOpenFailedPage extends StatelessWidget { const DatabaseOpenFailedPage({ required this.error, + required this.openDatabaseCallback, + required this.deleteDatabaseCallback, + required this.closeDatabaseCallback, super.key, }); final SqliteException error; + final OpenDatabaseCallback openDatabaseCallback; + final DeleteDatabaseCallback deleteDatabaseCallback; + final CloseDatabaseCallback closeDatabaseCallback; @override Widget build(BuildContext context) { @@ -41,14 +50,31 @@ class DatabaseOpenFailedPage extends StatelessWidget { final canDeleteDatabase = const {_kSqliteCorrupt, _kSqliteNotADb}.contains(error.resultCode); + final canRetry = error.resultCode == _kSqliteBusy; + return LandingFailedPage( title: context.l10n.failedToOpenDatabase, message: message, actions: [ if (canDeleteDatabase) - const Padding( - padding: EdgeInsets.only(bottom: 16), - child: _RecreateDatabaseButton(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: _RecreateDatabaseButton( + openDatabaseCallback: openDatabaseCallback, + deleteDatabaseCallback: deleteDatabaseCallback, + closeDatabaseCallback: closeDatabaseCallback, + ), + ), + if (canRetry) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: _Button( + onTap: () async { + await closeDatabaseCallback(); + await openDatabaseCallback(); + }, + text: context.l10n.retry, + ), ), _Button( onTap: () { @@ -62,14 +88,19 @@ class DatabaseOpenFailedPage extends StatelessWidget { } class _RecreateDatabaseButton extends HookConsumerWidget { - const _RecreateDatabaseButton(); + const _RecreateDatabaseButton({ + required this.openDatabaseCallback, + required this.deleteDatabaseCallback, + required this.closeDatabaseCallback, + }); + + final OpenDatabaseCallback openDatabaseCallback; + final DeleteDatabaseCallback deleteDatabaseCallback; + final CloseDatabaseCallback closeDatabaseCallback; @override Widget build(BuildContext context, WidgetRef ref) => TextButton( onPressed: () async { - final identityNumber = context.account?.identityNumber; - if (identityNumber == null) return; - final result = await showConfirmMixinDialog( context, context.l10n.databaseRecreateTips, @@ -78,23 +109,9 @@ class _RecreateDatabaseButton extends HookConsumerWidget { if (result != DialogEvent.positive) { return; } - await ref.read(databaseProvider.notifier).close(); - // Rename the old database file to a new name with timestamp. - final now = DateTime.now(); - renameFileWithTime( - p.join(mixinDocumentsDirectory.path, identityNumber, - '$kDbFileName.db'), - now); - await Future.forEach( - [ - File(p.join(mixinDocumentsDirectory.path, identityNumber, - '$kDbFileName.db-shm')), - File(p.join(mixinDocumentsDirectory.path, identityNumber, - '$kDbFileName.db-wal')) - ].where((e) => e.existsSync()), - (element) => element.delete(), - ); - await ref.read(databaseProvider.notifier).open(); + await closeDatabaseCallback(); + await deleteDatabaseCallback(); + await openDatabaseCallback(); }, child: Text( context.l10n.continueText, @@ -105,6 +122,19 @@ class _RecreateDatabaseButton extends HookConsumerWidget { ); } +Future dropDatabaseFile(String dbDir, String dbName) async { + // Rename the old database file to a new name with timestamp. + final now = DateTime.now(); + renameFileWithTime(p.join(dbDir, '$dbName.db'), now); + await Future.forEach( + [ + File(p.join(dbDir, '$dbName.db-shm')), + File(p.join(dbDir, '$dbName.db-wal')) + ].where((e) => e.existsSync()), + (element) => renameFileWithTime(element.path, now), + ); +} + class _Button extends StatelessWidget { const _Button({required this.text, required this.onTap}); @@ -169,3 +199,26 @@ class LandingFailedPage extends StatelessWidget { ), ); } + +/// Failed to open app with an unknown error. +class OpenAppFailedPage extends StatelessWidget { + const OpenAppFailedPage({ + required this.error, + super.key, + }); + + final dynamic error; + + @override + Widget build(BuildContext context) => LandingFailedPage( + title: context.l10n.unknowError, + message: error.toString(), + actions: [ + ElevatedButton( + onPressed: () { + exit(1); + }, + child: Text(context.l10n.exit), + ) + ]); +} diff --git a/lib/ui/provider/database_provider.dart b/lib/ui/provider/database_provider.dart index 6431c08467..753f0a6688 100644 --- a/lib/ui/provider/database_provider.dart +++ b/lib/ui/provider/database_provider.dart @@ -12,8 +12,15 @@ import 'account/multi_auth_provider.dart'; import 'hive_key_value_provider.dart'; import 'slide_category_provider.dart'; -final appDatabaseProvider = - Provider((ref) => throw UnimplementedError()); +final appDatabaseProvider = StateProvider( + (ref) { + final db = AppDatabase.connect(fromMainIsolate: true); + ref.onDispose(db.close); + return db; + }, +); + +final appDatabaseInitErrorProvider = StateProvider((ref) => null); final databaseProvider = StateNotifierProvider.autoDispose>( diff --git a/lib/utils/db/db_key_value.dart b/lib/utils/db/db_key_value.dart index 098f7fe350..a5c581007a 100644 --- a/lib/utils/db/db_key_value.dart +++ b/lib/utils/db/db_key_value.dart @@ -56,7 +56,7 @@ typedef AppKeyValue = _BaseDbKeyValue; class _BaseDbKeyValue extends ChangeNotifier { _BaseDbKeyValue({required this.group, required KeyValueDao dao}) : _dao = dao { - _loadProperties().whenComplete(_initCompleter.complete); + _initCompleter.complete(_loadProperties()); _subscription = dao.watchTableHasChanged(group).listen((event) { _loadProperties(); });