mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-24 23:32:13 +00:00
fixing startup issues
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
7f7aba8e08
commit
6a611767fc
14 changed files with 166 additions and 48 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.2.3
|
## 0.2.8
|
||||||
|
|
||||||
- Fix: App did not launch sometimes on Android
|
- Fix: App did not launch sometimes on Android
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTask"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
|
|
||||||
14
lib/app.dart
14
lib/app.dart
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
|
@ -120,18 +119,21 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
bool _showOnboarding = true;
|
bool _showOnboarding = true;
|
||||||
bool _isLoaded = false;
|
bool _isLoaded = false;
|
||||||
bool _isTwonlyLocked = true;
|
bool _isTwonlyLocked = true;
|
||||||
|
bool _wasLogged = true;
|
||||||
|
|
||||||
(Future<int>?, bool) _proofOfWork = (null, false);
|
(Future<int>?, bool) _proofOfWork = (null, false);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
initAsync();
|
|
||||||
super.initState();
|
super.initState();
|
||||||
|
Log.info('AppWidgetState: initState started');
|
||||||
|
initAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
|
Log.info('AppWidgetState: initAsync started');
|
||||||
if (userService.isUserCreated) {
|
if (userService.isUserCreated) {
|
||||||
await FirebaseMessaging.instance.requestPermission();
|
unawaited(FirebaseMessaging.instance.requestPermission());
|
||||||
if (_isTwonlyLocked) {
|
if (_isTwonlyLocked) {
|
||||||
// do not change in case twonly was already unlocked at some point
|
// do not change in case twonly was already unlocked at some point
|
||||||
_isTwonlyLocked = userService.currentUser.screenLockEnabled;
|
_isTwonlyLocked = userService.currentUser.screenLockEnabled;
|
||||||
|
|
@ -158,6 +160,12 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (!_wasLogged) {
|
||||||
|
Log.info('AppWidgetState: build started (_isLoaded: $_isLoaded)');
|
||||||
|
if (_isLoaded) {
|
||||||
|
_wasLogged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!_isLoaded) {
|
if (!_isLoaded) {
|
||||||
return Center(child: Container());
|
return Center(child: Container());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
class AppEnvironment {
|
class AppEnvironment {
|
||||||
static late final String cacheDir;
|
static late final String cacheDir;
|
||||||
|
|
@ -14,6 +15,7 @@ class AppEnvironment {
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
cacheDir = (await getApplicationCacheDirectory()).path;
|
cacheDir = (await getApplicationCacheDirectory()).path;
|
||||||
supportDir = (await getApplicationSupportDirectory()).path;
|
supportDir = (await getApplicationSupportDirectory()).path;
|
||||||
|
Log.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void initTesting() {
|
static void initTesting() {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
import 'package:twonly/app.dart';
|
import 'package:twonly/app.dart';
|
||||||
|
|
@ -27,33 +28,50 @@ import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/services/user_discovery.service.dart';
|
import 'package:twonly/src/services/user_discovery.service.dart';
|
||||||
import 'package:twonly/src/utils/avatars.dart';
|
import 'package:twonly/src/utils/avatars.dart';
|
||||||
|
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/secure_storage.dart';
|
import 'package:twonly/src/utils/secure_storage.dart';
|
||||||
|
import 'package:twonly/src/utils/startup_guard.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||||
|
|
||||||
|
final _initMutex = Mutex();
|
||||||
|
|
||||||
/// This function is used to initialized the absolute minimum so it
|
/// This function is used to initialized the absolute minimum so it
|
||||||
/// can also be used by the backend without the UI was loaded.
|
/// can also be used by the backend without the UI was loaded.
|
||||||
Future<void> twonlyMinimumInitialization() async {
|
Future<void> twonlyMinimumInitialization() async {
|
||||||
SentryWidgetsFlutterBinding.ensureInitialized();
|
Log.info('twonlyMinimumInitialization: called');
|
||||||
|
await exclusiveAccess(
|
||||||
|
lockName: 'init',
|
||||||
|
mutex: _initMutex,
|
||||||
|
action: () async {
|
||||||
|
Log.info('twonlyMinimumInitialization: started');
|
||||||
|
setupLocator();
|
||||||
|
|
||||||
await AppEnvironment.init();
|
Log.info('twonlyMinimumInitialization: RustLib.init()');
|
||||||
Log.init();
|
await RustLib.init();
|
||||||
setupLocator();
|
|
||||||
|
|
||||||
await RustLib.init();
|
Log.info('twonlyMinimumInitialization: initFlutterCallbacksForRust()');
|
||||||
|
await initFlutterCallbacksForRust();
|
||||||
|
|
||||||
await initFlutterCallbacksForRust();
|
Log.info('twonlyMinimumInitialization: bridge.initializeTwonlyFlutter()');
|
||||||
|
await bridge.initializeTwonlyFlutter(
|
||||||
await bridge.initializeTwonlyFlutter(
|
config: bridge.TwonlyConfig(
|
||||||
config: bridge.TwonlyConfig(
|
databasePath: '${AppEnvironment.supportDir}/twonly.sqlite',
|
||||||
databasePath: '${AppEnvironment.supportDir}/twonly.sqlite',
|
dataDirectory: AppEnvironment.supportDir,
|
||||||
dataDirectory: AppEnvironment.supportDir,
|
),
|
||||||
),
|
);
|
||||||
|
Log.info('twonlyMinimumInitialization: finished');
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
final binding = SentryWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await AppEnvironment.init();
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
|
unawaited(StartupGuard.markAppStartup());
|
||||||
|
|
||||||
await twonlyMinimumInitialization();
|
await twonlyMinimumInitialization();
|
||||||
|
|
||||||
unawaited(initFCMService());
|
unawaited(initFCMService());
|
||||||
|
|
@ -77,6 +95,8 @@ void main() async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.info('User loaded.');
|
||||||
|
|
||||||
final settingsController = SettingsChangeProvider()..loadSettings();
|
final settingsController = SettingsChangeProvider()..loadSettings();
|
||||||
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||||
unawaited(initFileDownloader());
|
unawaited(initFileDownloader());
|
||||||
|
|
@ -94,15 +114,22 @@ void main() async {
|
||||||
}
|
}
|
||||||
|
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
unawaited(postStartupTasks());
|
// We wait for the first frame to be rendered before starting heavy tasks.
|
||||||
|
// This ensures the splash screen is dismissed on Android immediately.
|
||||||
|
binding.addPostFrameCallback((_) async {
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
unawaited(postStartupTasks());
|
||||||
|
unawaited(apiService.connect());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await apiService.listenToNetworkChanges();
|
await apiService.listenToNetworkChanges();
|
||||||
unawaited(apiService.connect());
|
|
||||||
|
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
|
|
||||||
Log.info('Initialization finished after ${stopwatch.elapsed}.');
|
Log.info(
|
||||||
|
'Initialization finished after ${stopwatch.elapsed}. Calling runApp...',
|
||||||
|
);
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
|
|
@ -154,6 +181,7 @@ Future<void> runMigrations() async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> postStartupTasks() async {
|
Future<void> postStartupTasks() async {
|
||||||
|
Log.info('Post startup started.');
|
||||||
// 1. Immediate background cleanup (Non-blocking for UI)
|
// 1. Immediate background cleanup (Non-blocking for UI)
|
||||||
await twonlyDB.messagesDao.purgeMessageTable();
|
await twonlyDB.messagesDao.purgeMessageTable();
|
||||||
unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts());
|
unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts());
|
||||||
|
|
@ -164,9 +192,11 @@ Future<void> postStartupTasks() async {
|
||||||
unawaited(setupPushNotification());
|
unawaited(setupPushNotification());
|
||||||
unawaited(finishStartedPreprocessing());
|
unawaited(finishStartedPreprocessing());
|
||||||
unawaited(createPushAvatars());
|
unawaited(createPushAvatars());
|
||||||
unawaited(initializeBackgroundTaskManager());
|
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(seconds: 10));
|
||||||
|
unawaited(initializeBackgroundTaskManager());
|
||||||
// 3. Delayed tasks (Wait for app to settle)
|
// 3. Delayed tasks (Wait for app to settle)
|
||||||
await Future.delayed(const Duration(minutes: 2));
|
await Future.delayed(const Duration(minutes: 2));
|
||||||
unawaited(performTwonlySafeBackup());
|
unawaited(performTwonlySafeBackup());
|
||||||
|
unawaited(cleanLogFile());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPurchases();
|
loadPurchases();
|
||||||
|
Log.info('PurchasesProvider: constructor finished');
|
||||||
}
|
}
|
||||||
|
|
||||||
SubscriptionPlan plan = SubscriptionPlan.Free;
|
SubscriptionPlan plan = SubscriptionPlan.Free;
|
||||||
|
|
@ -74,6 +75,7 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
|
|
||||||
Future<void> loadPurchases() async {
|
Future<void> loadPurchases() async {
|
||||||
final available = await iapConnection.isAvailable();
|
final available = await iapConnection.isAvailable();
|
||||||
|
Log.info('PurchasesProvider: IAP available: $available');
|
||||||
if (!available) {
|
if (!available) {
|
||||||
storeState = StoreState.notAvailable;
|
storeState = StoreState.notAvailable;
|
||||||
Log.warn('Store is not available');
|
Log.warn('Store is not available');
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/main.dart';
|
import 'package:twonly/main.dart';
|
||||||
|
|
@ -9,6 +10,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||||
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
||||||
import 'package:twonly/src/utils/keyvalue.dart';
|
import 'package:twonly/src/utils/keyvalue.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
import 'package:twonly/src/utils/startup_guard.dart';
|
||||||
import 'package:workmanager/workmanager.dart';
|
import 'package:workmanager/workmanager.dart';
|
||||||
|
|
||||||
// ignore: unreachable_from_main
|
// ignore: unreachable_from_main
|
||||||
|
|
@ -16,27 +18,27 @@ Future<void> initializeBackgroundTaskManager() async {
|
||||||
await Workmanager().initialize(callbackDispatcher);
|
await Workmanager().initialize(callbackDispatcher);
|
||||||
await Workmanager().cancelByUniqueName('fetch_data_from_server');
|
await Workmanager().cancelByUniqueName('fetch_data_from_server');
|
||||||
|
|
||||||
await Workmanager().registerPeriodicTask(
|
// await Workmanager().registerPeriodicTask(
|
||||||
'fetch_data_from_server',
|
// 'fetch_data_from_server',
|
||||||
'eu.twonly.periodic_task',
|
// 'eu.twonly.periodic_task',
|
||||||
frequency: const Duration(minutes: 20),
|
// frequency: const Duration(minutes: 20),
|
||||||
initialDelay: const Duration(minutes: 5),
|
// initialDelay: const Duration(minutes: 5),
|
||||||
existingWorkPolicy: ExistingPeriodicWorkPolicy.update,
|
// existingWorkPolicy: ExistingPeriodicWorkPolicy.update,
|
||||||
constraints: Constraints(
|
// constraints: Constraints(
|
||||||
networkType: NetworkType.connected,
|
// networkType: NetworkType.connected,
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void callbackDispatcher() {
|
void callbackDispatcher() {
|
||||||
Workmanager().executeTask((task, inputData) async {
|
Workmanager().executeTask((task, inputData) async {
|
||||||
AppState.isInBackgroundTask = true;
|
SentryWidgetsFlutterBinding.ensureInitialized();
|
||||||
switch (task) {
|
switch (task) {
|
||||||
case 'eu.twonly.periodic_task':
|
case 'eu.twonly.periodic_task':
|
||||||
if (await initBackgroundExecution()) {
|
// if (await initBackgroundExecution()) {
|
||||||
await handlePeriodicTask();
|
// await handlePeriodicTask();
|
||||||
}
|
// }
|
||||||
case 'eu.twonly.processing_task':
|
case 'eu.twonly.processing_task':
|
||||||
if (await initBackgroundExecution()) {
|
if (await initBackgroundExecution()) {
|
||||||
await handleProcessingTask();
|
await handleProcessingTask();
|
||||||
|
|
@ -51,6 +53,18 @@ void callbackDispatcher() {
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
|
||||||
Future<bool> initBackgroundExecution() async {
|
Future<bool> initBackgroundExecution() async {
|
||||||
|
// 1. Check startup guard IMMEDIATELY before doing ANYTHING else.
|
||||||
|
if (await StartupGuard.isAppStarting()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await AppEnvironment.init();
|
||||||
|
AppState.isInBackgroundTask = true;
|
||||||
|
|
||||||
|
if (await StartupGuard.isAppStarting()) {
|
||||||
|
Log.error('App is starting. Returning early.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (_isInitialized) {
|
if (_isInitialized) {
|
||||||
// Reload the users, as on Android the background isolate can
|
// Reload the users, as on Android the background isolate can
|
||||||
// stay alive for multiple hours between task executions
|
// stay alive for multiple hours between task executions
|
||||||
|
|
@ -64,6 +78,8 @@ Future<bool> initBackgroundExecution() async {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.info('Background task is initialized');
|
||||||
|
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:firebase_app_installations/firebase_app_installations.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||||
|
|
@ -117,6 +118,7 @@ Future<void> initFCMService() async {
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
|
SentryWidgetsFlutterBinding.ensureInitialized();
|
||||||
final isInitialized = await initBackgroundExecution();
|
final isInitialized = await initBackgroundExecution();
|
||||||
Log.info('Handling a background message: ${message.messageId}');
|
Log.info('Handling a background message: ${message.messageId}');
|
||||||
await handleRemoteMessage(message);
|
await handleRemoteMessage(message);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ class Log {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cleanLogFile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static String filterLogMessage(String msg) {
|
static String filterLogMessage(String msg) {
|
||||||
|
|
@ -106,7 +105,7 @@ Future<void> _writeLogToFile(LogRecord record) async {
|
||||||
final logFile = File('${AppEnvironment.supportDir}/app.log');
|
final logFile = File('${AppEnvironment.supportDir}/app.log');
|
||||||
|
|
||||||
final logMessage =
|
final logMessage =
|
||||||
'${clock.now().toString().split(".")[0]} ${record.level.name} [${AppState.isInBackgroundTask ? 'b' : 'f'}] [twonly] ${record.loggerName} > ${record.message}\n';
|
'${clock.now()} ${record.level.name} [${AppState.isInBackgroundTask ? 'b' : 'f'}] [twonly] ${record.loggerName} > ${record.message}\n';
|
||||||
|
|
||||||
return _protectFileAccess(() async {
|
return _protectFileAccess(() async {
|
||||||
if (!logFile.existsSync()) {
|
if (!logFile.existsSync()) {
|
||||||
|
|
|
||||||
43
lib/src/utils/startup_guard.dart
Normal file
43
lib/src/utils/startup_guard.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
|
class StartupGuard {
|
||||||
|
static Future<File> _getLockFile() async {
|
||||||
|
final path = (await getApplicationCacheDirectory()).path;
|
||||||
|
return File('$path/app_startup.lock');
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> markAppStartup() async {
|
||||||
|
try {
|
||||||
|
final file = await _getLockFile();
|
||||||
|
await file.writeAsString(
|
||||||
|
DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
);
|
||||||
|
Log.info('App is starting');
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Failed to mark app startup: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> isAppStarting() async {
|
||||||
|
try {
|
||||||
|
final file = await _getLockFile();
|
||||||
|
if (!file.existsSync()) return false;
|
||||||
|
|
||||||
|
final stat = file.statSync();
|
||||||
|
final diff = DateTime.now().difference(stat.modified);
|
||||||
|
|
||||||
|
final starting = diff.inSeconds < 30;
|
||||||
|
if (starting) {
|
||||||
|
Log.info(
|
||||||
|
'Startup guard: App is currently starting (${diff.inSeconds}s ago).',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return starting;
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Failed to check startup guard: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -176,7 +176,7 @@ class _LogViewerWidgetState extends State<LogViewerWidget> {
|
||||||
children: [
|
children: [
|
||||||
if (_showTimestamps && e.timestamp != null)
|
if (_showTimestamps && e.timestamp != null)
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '${e.timestamp.toString().split(' ')[1].split('.')[0]} ',
|
text: '${e.timestamp.toString().split(' ')[1]} ',
|
||||||
style: tsStyle,
|
style: tsStyle,
|
||||||
),
|
),
|
||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
|
|
@ -306,8 +306,9 @@ class _LogEntry {
|
||||||
String? level;
|
String? level;
|
||||||
var msg = trimmed;
|
var msg = trimmed;
|
||||||
|
|
||||||
// Try to parse leading timestamp (YYYY-MM-DD HH:MM:SS)
|
// Try to parse leading timestamp (YYYY-MM-DD HH:MM:SS.mmmmmm)
|
||||||
final tsRegex = RegExp(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(.*)$');
|
final tsRegex =
|
||||||
|
RegExp(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+(.*)$');
|
||||||
final mTs = tsRegex.firstMatch(trimmed);
|
final mTs = tsRegex.firstMatch(trimmed);
|
||||||
if (mTs != null) {
|
if (mTs != null) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 0.2.3+112
|
version: 0.2.8+117
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0
|
sdk: ^3.11.0
|
||||||
|
|
|
||||||
|
|
@ -55,19 +55,30 @@ pub(crate) async fn init_tracing(logs_dir: &std::path::Path, is_dart_available:
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_writers(logs_dir: &std::path::Path) -> (NonBlocking, NonBlocking) {
|
fn build_writers(logs_dir: &std::path::Path) -> (NonBlocking, NonBlocking) {
|
||||||
let file_appender = tracing_appender::rolling::RollingFileAppender::builder()
|
let file_appender_res = tracing_appender::rolling::RollingFileAppender::builder()
|
||||||
.rotation(tracing_appender::rolling::Rotation::DAILY)
|
.rotation(tracing_appender::rolling::Rotation::DAILY)
|
||||||
.filename_prefix("twonly")
|
.filename_prefix("twonly")
|
||||||
.filename_suffix("log")
|
.filename_suffix("log")
|
||||||
.build(logs_dir)
|
.build(logs_dir);
|
||||||
.expect("Failed to create file appender");
|
|
||||||
|
|
||||||
let (non_blocking_file, file_guard) = tracing_appender::non_blocking(file_appender);
|
let (non_blocking_file, file_guard) = match file_appender_res {
|
||||||
|
Ok(file_appender) => {
|
||||||
|
let (nb, guard) = tracing_appender::non_blocking(file_appender);
|
||||||
|
(nb, Some(guard))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create file appender: {}", e);
|
||||||
|
let (nb, guard) = tracing_appender::non_blocking(std::io::sink());
|
||||||
|
(nb, None)
|
||||||
|
}
|
||||||
|
};
|
||||||
let (non_blocking_stdout, stdout_guard) = tracing_appender::non_blocking(std::io::stdout());
|
let (non_blocking_stdout, stdout_guard) = tracing_appender::non_blocking(std::io::stdout());
|
||||||
|
|
||||||
TRACING_GUARDS
|
if let Some(fg) = file_guard {
|
||||||
.set(Mutex::new(Some((file_guard, stdout_guard))))
|
TRACING_GUARDS
|
||||||
.ok();
|
.set(Mutex::new(Some((fg, stdout_guard))))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
(non_blocking_stdout, non_blocking_file)
|
(non_blocking_stdout, non_blocking_file)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,10 @@ pub(super) fn get_twonly_flutter() -> Result<&'static TwonlyFlutter> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn initialize_twonly_flutter(config: TwonlyConfig) -> Result<()> {
|
pub async fn initialize_twonly_flutter(config: TwonlyConfig) -> Result<()> {
|
||||||
|
if GLOBAL_TWONLY.initialized() {
|
||||||
|
tracing::info!("twonly already initialized.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
let log_dir = PathBuf::from(&config.data_directory).join("log");
|
let log_dir = PathBuf::from(&config.data_directory).join("log");
|
||||||
init_tracing(&log_dir, true).await;
|
init_tracing(&log_dir, true).await;
|
||||||
tracing::info!("Initialized twonly workspace.");
|
tracing::info!("Initialized twonly workspace.");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue