fixes multiple race condition issues

This commit is contained in:
otsmr 2026-04-24 23:14:48 +02:00
parent 919aec464e
commit e8d8e8b160
5 changed files with 128 additions and 90 deletions

View file

@ -15,6 +15,7 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
@ -158,7 +159,8 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
}
}
Mutex protectDownload = Mutex();
Mutex _protectDownload = Mutex();
Mutex _protectDecryption = Mutex();
Future<void> startDownloadMedia(MediaFile media, bool force) async {
final mediaService = MediaFileService(media);
@ -175,7 +177,7 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
return;
}
final isBlocked = await protectDownload.protect<bool>(() async {
final isBlocked = await _protectDownload.protect<bool>(() async {
final msg = await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaId);
if (msg == null || msg.downloadState != DownloadState.pending) {
@ -285,18 +287,27 @@ Future<void> requestMediaReupload(String mediaId) async {
}
Future<void> handleEncryptedFile(String mediaId) async {
await exclusiveAccess(
lockName: 'decryption-$mediaId',
mutex: _protectDecryption,
action: () async {
final mediaService = await MediaFileService.fromMediaId(mediaId);
if (mediaService == null) {
Log.error('Media file not found in database.');
return;
}
await twonlyDB.mediaFilesDao.updateMedia(
mediaId,
const MediaFilesCompanion(
downloadState: Value(DownloadState.downloaded),
),
if (mediaService.mediaFile.downloadState == DownloadState.ready) {
Log.info('Decryption of $mediaId already finished.');
return;
}
if (!mediaService.encryptedPath.existsSync()) {
Log.warn(
'Encrypted media file $mediaId does not exist anymore. Decryption probably already finished.',
);
return;
}
late Uint8List encryptedBytes;
try {
@ -307,9 +318,18 @@ Future<void> handleEncryptedFile(String mediaId) async {
return;
}
await twonlyDB.mediaFilesDao.updateMedia(
mediaId,
const MediaFilesCompanion(
downloadState: Value(DownloadState.downloaded),
),
);
try {
final chacha20 = FlutterChacha20.poly1305Aead();
final secretKeyData = SecretKeyData(mediaService.mediaFile.encryptionKey!);
final secretKeyData = SecretKeyData(
mediaService.mediaFile.encryptionKey!,
);
final secretBox = SecretBox(
encryptedBytes,
@ -345,6 +365,8 @@ Future<void> handleEncryptedFile(String mediaId) async {
mediaService.encryptedPath.deleteSync();
unawaited(apiService.downloadDone(mediaService.mediaFile.downloadToken!));
},
);
}
Future<void> makeMigrationToVersion91() async {

View file

@ -6,7 +6,7 @@ import 'package:twonly/locator.dart';
import 'package:twonly/main.dart';
import 'package:twonly/src/constants/keyvalue.keys.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/utils/exclusive_access.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:workmanager/workmanager.dart';

View file

@ -8,7 +8,7 @@ Future<T> exclusiveAccess<T>({
required Future<T> Function() action,
required Mutex mutex,
}) async {
final lockFile = File('${AppEnvironment.supportDir}/$lockName.lock');
final lockFile = File('${AppEnvironment.cacheDir}/$lockName.lock');
return mutex.protect(() async {
var lockAcquired = false;
@ -24,8 +24,8 @@ Future<T> exclusiveAccess<T>({
try {
final stat = lockFile.statSync();
if (stat.type != FileSystemEntityType.notFound) {
final age = DateTime.now().difference(stat.modified).inMilliseconds;
if (age > 1000) {
final age = DateTime.now().difference(stat.modified).inSeconds;
if (age > 10) {
lockFile.deleteSync();
continue;
}

View file

@ -1,14 +1,27 @@
import 'dart:convert';
import 'dart:io';
import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/log.dart';
class KeyValueStore {
static final Mutex _mutex = Mutex();
static Future<File> _getFilePath(String key) async {
return File('${AppEnvironment.supportDir}/keyvalue/$key.json');
}
static Future<void> delete(String key) async {
static Future<T> _exclusive<T>(String key, Future<T> Function() action) {
return exclusiveAccess(
lockName: 'keyvalue-$key',
mutex: _mutex,
action: action,
);
}
static Future<void> delete(String key) => _exclusive(key, () async {
try {
final file = await _getFilePath(key);
if (file.existsSync()) {
@ -17,11 +30,12 @@ class KeyValueStore {
} catch (e) {
Log.error('Error deleting file: $e');
}
}
});
static Future<Map<String, dynamic>?> get(String key) async {
try {
static Future<Map<String, dynamic>?> get(String key) =>
_exclusive(key, () async {
final file = await _getFilePath(key);
try {
if (file.existsSync()) {
final contents = await file.readAsString();
return jsonDecode(contents) as Map<String, dynamic>;
@ -29,12 +43,14 @@ class KeyValueStore {
return null;
}
} catch (e) {
Log.warn('Error reading file: $e');
Log.warn('Error reading file. Deleting it.: $e');
file.deleteSync();
return null;
}
}
});
static Future<void> put(String key, Map<String, dynamic> value) async {
static Future<void> put(String key, Map<String, dynamic> value) =>
_exclusive(key, () async {
try {
final file = await _getFilePath(key);
await file.parent.create(recursive: true);
@ -42,5 +58,5 @@ class KeyValueStore {
} catch (e) {
Log.error('Error writing file: $e');
}
}
});
}

View file

@ -6,7 +6,7 @@ import 'package:logging/logging.dart';
import 'package:mutex/mutex.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/exclusive_access.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart';
class Log {
static bool _isInitialized = false;