Retry file resource cleanup when the core stops

This commit is contained in:
evgeny committed 2026-10-28 11:36:13 +01:00
1 parent a8c9bc6049
commit 5db51e64de
4 files changed
+24 -4

No files matched your search

@@ -40,7 +40,7 @@ class FileStatusException(val status: Int) : IOException("member_files status=$s
* URI копируется на Dispatchers.IO в стабильный upload-source. При отмене transfer ждёт terminal
* native-событие (закрытый handle) до удаления своего source/destination; остановка core завершает ожидания.
* Работа удерживает wake lock и CoreActivity.transfer, ожидание решения о замене снимает оба удержания.
* Ошибки cleanup логируются независимо; неосвобождённый wake lock сохраняется для следующей попытки.
* Ошибки cleanup логируются независимо; неосвобождённый wake lock сохраняется до следующей операции/остановки core.
* Скачанная копия остаётся в cacheDir/member_files для внешнего приложения/повторного SAVE.
*/
class MemberFilesRepository(context: Context) {
@@ -86,7 +86,7 @@ class MemberFilesRepository(context: Context) {
private fun protect(active: Boolean) {
if (active) {
val lock = wakeLock ?: (app.getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "utun:member_files").also { wakeLock = it }
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "utun:member_files").also { wakeLock = it; it.setReferenceCounted(false) }
if (!lock.isHeld) {
lock.acquire()
log("DEBUG", "wake lock acquired token=${_operation.value?.token}")
@@ -337,6 +337,10 @@ class MemberFilesRepository(context: Context) {
val error = CancellationException("core stopped")
pending.values.toList().forEach { it.result.completeExceptionally(error) }
transfers.values.toList().forEach { it.result.completeExceptionally(error) }
if (task?.isCompleted != false && wakeLock != null) {
protect(false)
log("DEBUG", "core stop cleanup wakePending=${wakeLock != null}")
}
CoreActivity.reset()
}
fun dismiss() { if (_operation.value?.active == false) _operation.value = null; HeadlessService.refreshFiles() }
@@ -92,7 +92,8 @@ class MemberFilesViewModel(private val saved: SavedStateHandle) : ViewModel() {
membersRepository.refresh(group)
}
/** Открыть явный адрес, например из уведомления передачи, и обновить участников; server самовольно не заменяется. */
/** Открыть явный адрес, например из уведомления передачи, и обновить участников.
* Ненулевой server сохраняется; при server=0 выбирается первый известный хранитель. */
fun openLocation(location: FileLocation) {
navigate(location)
val group = location.group.toULong().toString()
@@ -207,7 +208,7 @@ class MemberFilesViewModel(private val saved: SavedStateHandle) : ViewModel() {
}
/** MKDIR использует name, RENAME — name и entry, REMOVE — entry; работает только в собственном дереве вне avatars.
* Конкурирующая мутация/активная передача отклоняет запуск. При успехе перечитывает текущий каталог;
* Конкурирующая мутация/активная операция репозитория отклоняет запуск. При успехе перечитывает текущий каталог;
* ошибка задаёт status и логируется, CancellationException передаётся дальше. Отмена не откатывает серверный commit. */
fun mutate(command: Int, name: String = "", entry: MemberFilesProtocol.Entry? = null) {
val location = _state.value.location ?: return
@@ -13,10 +13,13 @@ class PowerManager {
return WakeLock(flags, tag, failures).also { locks.add(it) }
}
class WakeLock(val flags: Int, val tag: String, var failures: Set<String>) {
var counted = true
private set
var timeout = 0L
var releaseCalls = 0
var held = false
val isHeld: Boolean get() { check("query" !in failures) { "wake lock query unavailable" }; return held }
fun setReferenceCounted(value: Boolean) { counted = value }
fun acquire() = acquire(0)
fun acquire(timeout: Long) {
this.timeout = timeout
@@ -147,6 +147,7 @@ class MemberFilesFailureTest {
repository.save(export(), uri)
pumpUntil(::finished)
val lock = context.powerManager!!.locks.single()
assertFalse("one repository owner must allow repeated release without reference-count underflow", lock.counted)
assertTrue(lock.held)
assertEquals(listOf(true, false), NativeLib.activityRequests)
diagnostic("wake lock release failed")
@@ -157,6 +158,17 @@ class MemberFilesFailureTest {
assertFalse(lock.held)
}
@Test fun coreStopRetriesWakeReleaseForAnAlreadyFinishedOperation() = runTest(dispatcher) {
context.powerManager!!.failures = setOf("release")
repository.save(export(), uri)
pumpUntil(::finished)
val lock = context.powerManager!!.locks.single()
assertTrue(lock.held)
lock.failures = emptySet()
repository.coreStopped()
assertFalse("core stop left a completed operation's lock held", lock.held)
}
@Test fun standbyFailureDuringCleanupIsLoggedWithoutLosingCompletedExport() = runTest(dispatcher) {
context.contentResolver.output = { NativeLib.failActivityRequest = true; java.io.ByteArrayOutputStream() }
repository.save(export(), uri)