Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion lib/src/platform/io/model_download_manager_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,10 @@ class DefaultModelDownloadManager implements ModelDownloadManager {
}
}

// A path whose resolution is denied (errno 5) and whose type also reads as
// `notFound` (for example a parent ACL denial) is walked past like a missing
// one. The operation then fails later, as an access denial on create or
// write, or a cache miss for cacheOnly; it never reports success.
String _cacheLockKey(Directory cacheDirectory) {
var ancestor = path.normalize(path.absolute(cacheDirectory.path));
final missing = <String>[];
Expand Down Expand Up @@ -1861,7 +1865,8 @@ enum _Removal { removed, inUse, skipped }
/// with `ERROR_ACCESS_DENIED`, and so does the attribute query behind
/// [FileSystemEntity.type], which reports that failure as
/// [FileSystemEntityType.notFound]; callers tell it from a real denial by
/// that type.
/// that type. A different ACL denial can read as `notFound` too; see
/// `_cacheLockKey` for what happens then.
bool _mayBeWindowsDeletePending(FileSystemException error) =>
Platform.isWindows && error.osError?.errorCode == 5;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ void main() {
});

setUp(() async {
root = await Directory.systemTemp.createTemp('llamadart_cache_lock_');
final created = await Directory.systemTemp.createTemp(
'llamadart_cache_lock_',
);
root = Directory(await created.resolveSymbolicLinks());
addTearDown(() => root.delete(recursive: true));
server = await _HoldingServer.start();
addTearDown(server.close);
Expand Down
48 changes: 41 additions & 7 deletions test/unit/platform/io/model_download_manager_io_removal_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,15 @@ void main() {
return finishDelete;
}

Future<void> clear() =>
IOOverrides.runWithIOOverrides(manager.clear, overrides);
Future<T> withOverrides<T>(Future<T> Function() body) =>
IOOverrides.runWithIOOverrides(body, overrides);

/// Starts [clear] and holds it at the removal lock, after it has checked
/// the entry, until the returned function runs.
Future<void> clear() => withOverrides(manager.clear);

/// Starts [operation] and holds it at the removal lock, after it has
/// checked the entry, until the returned function runs.
Future<({Future<void> clearing, void Function() proceed})>
clearHeldAtRemovalLock() async {
heldAtRemovalLock(Future<void> Function() operation) async {
final waiting = Completer<void>();
final proceed = Completer<void>();
modelCacheEntryLocker = (lockPath, {required throwIfCancelled}) async {
Expand All @@ -163,15 +165,24 @@ void main() {
}
}

final clearing = clear();
final clearing = withOverrides(operation);
addTearDown(() async {
release();
await clearing.then<void>((_) {}, onError: (Object _) {});
});
await waiting.future;
await waiting.future.timeout(
const Duration(seconds: 10),
onTimeout: () => throw StateError(
'The operation never reached the .removal.lock: it no longer takes '
'the lock this test holds it at.',
),
);
return (clearing: clearing, proceed: release);
}

Future<({Future<void> clearing, void Function() proceed})>
clearHeldAtRemovalLock() => heldAtRemovalLock(manager.clear);

void expectOnlyLocksLeftOnceDeleted(void Function() finishDelete) {
finishDelete();
expect(
Expand Down Expand Up @@ -212,6 +223,29 @@ void main() {
expectOnlyLocksLeftOnceDeleted(finishDelete);
});

test('is skipped by remove when the deletion happens while it waits for '
'the removal lock', () async {
final cacheKey = (await manager.list()).single.cacheKey;
final held = await heldAtRemovalLock(() => manager.remove(cacheKey));
final finishDelete = markPendingDelete();
held.proceed();

await held.clearing;
expectOnlyLocksLeftOnceDeleted(finishDelete);
});

test('is skipped by prune when the deletion happens while it waits for '
'the removal lock', () async {
final held = await heldAtRemovalLock(
() => manager.prune(maxAge: Duration.zero),
);
final finishDelete = markPendingDelete();
held.proceed();

await held.clearing;
expectOnlyLocksLeftOnceDeleted(finishDelete);
});

test('is not skipped when the entry still reads as a directory: clear '
'reports the access denial', () async {
markPendingDelete();
Expand Down
Loading