V1.4.5: 修正下载失败的错误归因 + 清理残留临时文件(为定位"空间不足"误报)

现象: 极米 H1S 上报告「曲库分区空间不足(当前可用 6.04 GB)」——数字自相矛盾。

核实(本地实测 Muse 协议):
- 曲库压缩包 430,575,974 字节;从 gzip 尾部 ISIZE 读出解压后为 965MB,
  峰值占用约 1.4GB,6.04GB 可用空间根本不构成空间不足。
- 结论: 真正抛出的异常另有原因(消息里含 no space left),
  被 isNoSpaceError() 只按消息文本匹配而误判成了"空间不足"。

修复:
- readableError 不再只看异常消息: 报告完整异常链 + 可用空间,
  并用真实写入探针(AppPaths.probeWrite,写 8MB 再删)区分
  "真的写不进去" 与 "其实能写"(inode 耗尽/配额/ROM 限制同样报 ENOSPC,
  而 usableSpace 仍显示数 GB)。
- 失败时上报单行完整诊断到远程日志:
  DB-SYNC download failed | <异常链> | root/free/total/envState/probe/proc-mounts。
- 界面失败文案直接显示原始异常,不再用可能误导的"空间不足"结论。
- AppPaths 新增 probeWrite() 与 diagnose()(含 /proc/mounts 相关挂载点)。
- MuseDbSync 失败时同时清理 muse.db.gz.download 与 muse.db.download:
  原先失败只删前者,残留的 965MB 分片在反复重试时会白占空间。

版本 1.4.4(24) 到 1.4.5(25);签名证书不变。
This commit is contained in:
zijunchiang
2026-09-26 16:42:59 +08:00
parent a467719675
commit 1ceba8ef7f
5 changed files with 69 additions and 16 deletions
+1 -1
View File
@@ -160,7 +160,7 @@ $BT/apksigner verify 目标名.apk
### 版本号
- `app/build.gradle`:`versionName "1.4.4"` / `versionCode 24`(18~21 被本机临时诊断包占用过)
- `app/build.gradle`:`versionName "1.4.5"` / `versionCode 25`(18~21 被本机临时诊断包占用过)
- App 内显示统一用纯版本号(不加 `(versionCode)` 括号)
- ⚠️ **发布构建已开启 NewApi 闸门**(`lint { checkReleaseBuilds true; fatal += ['NewApi'] }`):
任何「调用了高于 `minSdk 21` 且未做版本守卫的 API」都会让 `assembleRelease` 直接失败。
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId "com.local.ktv"
minSdk 21
targetSdk 25
versionCode 24
versionName "1.4.4"
versionCode 25
versionName "1.4.5"
ndk {
// 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版
// 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则
@@ -4,6 +4,7 @@ import android.content.Context
import android.os.Environment
import android.util.Log
import java.io.File
import java.io.FileOutputStream
/**
* 麦动 KTV 的存储命名空间。
@@ -121,4 +122,48 @@ object AppPaths {
?: "/storage/emulated/0"
runCatching { File("$env/muse").deleteRecursively() }
}
private fun size(text: Long): String =
String.format(java.util.Locale.ROOT, "%.2fGB", text / 1024.0 / 1024.0 / 1024.0)
/**
* 真实写入探针:写 [sizeMB] 兆再删掉,验证「**现在真的能写**」。
*
* `usableSpace` 只反映**块空间**;inode 耗尽、配额、FUSE/ROM 限制都会返回 ENOSPC
* 但 still 报着几 GB 可用 —— 所以判断"是不是空间问题"必须靠真实写入,而不是看数字。
*/
fun probeWrite(dir: File, sizeMB: Int = 8): String = runCatching {
val probe = File(dir, ".write_probe.tmp")
try {
val buffer = ByteArray(1024 * 1024)
FileOutputStream(probe).use { out ->
repeat(sizeMB) { out.write(buffer) }
out.flush()
out.fd.sync()
}
"ok(${sizeMB}MB)"
} finally {
runCatching { probe.delete() }
}
}.getOrElse { "${it.javaClass.simpleName}: ${it.message}" }
private fun mountLines(): String = runCatching {
File("/proc/mounts").readLines()
.filter { line ->
line.contains("sdcard") || line.contains("/data ") ||
line.contains("/storage") || line.contains("/mnt")
}
.take(12)
.joinToString(" | ") { it.substringBefore(" ").trim() }
}.getOrNull() ?: "(unreadable)"
/** 一次性诊断串:空间、真实可写性、挂载点。失败时上报远程日志并显示在界面上。 */
fun diagnose(): String {
val dir = databaseDir
val free = runCatching { dir.usableSpace }.getOrDefault(0L)
val total = runCatching { dir.totalSpace }.getOrDefault(0L)
val envState = runCatching { Environment.getExternalStorageState() }.getOrNull()
return "root=${root.absolutePath} free=${size(free)} total=${size(total)} " +
"envState=$envState probe=${probeWrite(dir)} mounts=[${mountLines()}]"
}
}
@@ -88,22 +88,26 @@ object DatabaseBootstrapper {
.any { "enospc" in it || "no space left" in it }
private fun readableError(error: Throwable): Throwable {
val target = MuseDatabase.defaultDbFile()
val free = target.parentFile?.usableSpace ?: 0L
// 完整异常链:老设备上「表面原因」常常误导(例如 ENOSPC 其实不是块空间不足)。
val chain = generateSequence(error) { it.cause }
.take(5)
.joinToString(" <- ") { "${it.javaClass.simpleName}: ${it.message}" }
val diagnose = runCatching { AppPaths.diagnose() }.getOrDefault("(diagnose failed)")
MiliLog.warn("DB-SYNC download failed | $chain | $diagnose")
Log.w(TAG, "Database download failed: $chain | $diagnose")
if (isNoSpaceError(error)) {
val target = MuseDatabase.defaultDbFile()
val free = target.parentFile?.usableSpace ?: 0L
MiliLog.warn(
"DB-SYNC 空间不足: free=${formatSize(free)} target=${target.absolutePath} " +
"err=${error.message}",
)
// ⚠️ 不能只看异常消息就断言"空间不足":inode 耗尽 / 配额 / ROM 限制同样报 ENOSPC,
// 而 usableSpace 可能还显示好几 GB(本机实测:报空间不足时可用 6.04GB)。
// 用真实写入探针区分"真的写不进去" vs "其实能写"。
val probe = runCatching { AppPaths.probeWrite(target.parentFile!!) }.getOrDefault("?")
return IllegalStateException(
"曲库分区空间不足(当前可用 ${formatSize(free)})," +
"已清理下载临时文件,请释放空间后重试",
"曲库写入被系统拒绝(探针 $probe,可用 ${formatSize(free)})\n原始错误:$chain",
error,
)
}
val message = error.message?.takeIf { it.isNotBlank() } ?: "曲库下载失败"
Log.w(TAG, "Database download failed: $message")
MiliLog.warn("DB-SYNC download failed: ${error.javaClass.simpleName}: $message")
return IllegalStateException(message, error)
return IllegalStateException("曲库下载失败:$chain(可用 ${formatSize(free)})", error)
}
}
@@ -101,7 +101,11 @@ object MuseDbSync {
onProgress(100, "曲库就绪")
return target
} finally {
gzFile.delete()
// 失败时两个临时文件都要清掉:残留的 .gz.download(430MB) + .download(965MB)
// 白占约 1.4GB,反复重试时正是"空间不足"的常见来源。
// 成功路径上 tmpDb 已 rename 成正式库,这里的 delete 是空操作。
runCatching { gzFile.delete() }
runCatching { tmpDb.delete() }
}
}