V1.5.3: 远程日志不再静默失效(+本地日志文件);补 403 受限识别
现场两个问题分别定位: 一、"歌曲源全部失效" —— 已确认是**源站对该出口 IP 的授权/风控**,非客户端可修 - 实测(本机与设备同一出口 188.253.112.231): · 主源 http://w.w345.my/i.php → **HTTP 403 Forbidden**(nginx 拦截页); · 镜像源(desktok.top / mm.kk456.top / w.w345.my:66 / z.w345.my / b.sktv.top / w345.my) 返回 200,但对**每一首歌(含不存在的编号 1111111/9999999)都返回同一个广告/测试资源** https://wb66.cn/f/d/7b7c9/10188555.ts; · 换全新随机设备身份(mac/sn)结果完全相同 → 限制在**出口 IP** 层面,与设备无关; · 7 host × ls=1~3 × (720/1080 × h265 0/1) 全组合探测:0 个真源; · 旧的试用领取接口 api.php 已 **404**,App 无法自行领取窗口。 - 结论:源站侧无有效授权(i.php 的 ordertype=0、mwsLogin 的 license_box_count=0 / license_duration=0 均指向此)。**需更换公网 IP 或与歌曲源服务方确认续订**。 我们拒绝这些地址是正确行为(否则就是 1.4.10 修过的"广告被当成歌曲播放")。 二、"手动更新数据库后远程日志失效" —— 数据库没问题,日志客户端有隐患,两者都已处理 - 实测新曲库 day=261003(md5 300AFD957BF332F02C17ED94A9F4E4CF,431,530,367 字节, 解压 1.01GB、69.5 万首): 用 App 的**真实 SQL**(SONG_SELECT/推荐/中文搜索/拼音搜索/ 歌手检索/歌单/语种/歌手列表)逐条跑通 → **结构完全兼容,更新曲库不会破坏 App**。 - 日志侧修复(原来确实可能"静默失效"): · MiliLog.init 改为**可重复调用并自动重启发送线程**(旧实现 `if (started.getAndSet(true)) return`, 线程一旦退出就永久不再发送且无任何迹象); · sendLoop 整体加 catch(Throwable) 兜底,任何异常都不允许发送线程退出; · 新增**本地日志文件** <存储根>/logs/ktv.log(>1MB 滚动为 .prev), 远程日志不可用时现场可直接取文件;启动打 APP-INIT 行给出路径; · MiliLog.init 提前到 KtvApplication.onCreate,早于 MainActivity,早期崩溃也有日志。 - 补 403 识别: KtvJsBridge 的 403 判定从仅 /music/do.php 扩展到 /i.php 也认 (主源 i.php 就是 403);SongUrlResolver 记录本轮是否见过 403,全部失败且见过 403 时 同样走 Eac19WindowGuard 提示换 IP。 AdVideoFilter: 仅保留高区分度标记 wb66,去掉过于宽松的 demo(避免误伤真源 URL 里的随机 token)。 版本 1.5.2(34) → 1.5.3(35);签名证书不变。测试 9 例全通过。
This commit is contained in:
@@ -55,12 +55,20 @@ object MiliLog {
|
||||
private const val RETRY_SLEEP_MS = 1500L
|
||||
private const val FAILURE_BACKOFF_MS = 10_000L
|
||||
|
||||
/** 本地日志文件:远程日志一旦不可用(网络/服务端问题),现场仍能取证。 */
|
||||
private const val LOCAL_LOG_DIR = "logs"
|
||||
private const val LOCAL_LOG_NAME = "ktv.log"
|
||||
private const val LOCAL_LOG_MAX_BYTES = 1024L * 1024L
|
||||
|
||||
private val started = AtomicBoolean(false)
|
||||
private val running = AtomicBoolean(false)
|
||||
private val publicIpFetching = AtomicBoolean(false)
|
||||
private val queue = LinkedBlockingQueue<String>(MAX_QUEUE)
|
||||
private var senderThread: Thread? = null
|
||||
|
||||
/** 本地日志文件(后台线程写入,绝不占用主线程 I/O)。 */
|
||||
@Volatile private var localLogFile: java.io.File? = null
|
||||
|
||||
@Volatile private var serverName: String = "android"
|
||||
|
||||
/** 主机 IP:公网出口 IP([PUBLIC_IP_URL] 查询,5 分钟缓存)。 */
|
||||
@@ -74,13 +82,50 @@ object MiliLog {
|
||||
|
||||
/** 用 app context 初始化并启动发送线程。可重复调用。 */
|
||||
fun init(context: Context) {
|
||||
if (started.getAndSet(true)) return
|
||||
serverName = sanitize(Build.MODEL).ifEmpty { "android" }
|
||||
// 首次初始化时准备本地日志文件。
|
||||
if (localLogFile == null) {
|
||||
runCatching {
|
||||
val dir = java.io.File(AppPaths.root, LOCAL_LOG_DIR)
|
||||
if (dir.exists() || dir.mkdirs()) localLogFile = java.io.File(dir, LOCAL_LOG_NAME)
|
||||
}
|
||||
}
|
||||
if (!started.getAndSet(true)) {
|
||||
serverName = sanitize(Build.MODEL).ifEmpty { "android" }
|
||||
}
|
||||
// ⚠️ 只要**没有存活的发送线程**就(重新)启动它。
|
||||
// 旧实现是 `if (started.getAndSet(true)) return`:一旦发送线程因任何原因退出
|
||||
// (例如有人调用过 [shutdown],或线程意外终止),后续 init 全是空操作,
|
||||
// 日志只入队、永远不再发送 —— 现场表现就是「远程日志失效」,而且无从察觉。
|
||||
val alive = senderThread?.isAlive == true
|
||||
if (running.get() && alive) {
|
||||
ensureServerIp()
|
||||
return
|
||||
}
|
||||
running.set(true)
|
||||
senderThread = Thread({ sendLoop() }, "mili-glog").also { it.isDaemon = true; it.start() }
|
||||
Log.i(TAG, "glog sender (re)started")
|
||||
ensureServerIp()
|
||||
}
|
||||
|
||||
/** 本地日志文件(供现场取证);未初始化时返回 null。 */
|
||||
fun localLogPath(): String? = localLogFile?.absolutePath
|
||||
|
||||
/**
|
||||
* 追加一行到本地日志(**由发送线程调用**,不在调用方线程做磁盘 I/O)。
|
||||
* 超过 [LOCAL_LOG_MAX_BYTES] 就滚动一次,保证文件不会无限增长。
|
||||
*/
|
||||
private fun appendLocal(line: String) {
|
||||
val file = localLogFile ?: return
|
||||
runCatching {
|
||||
if (file.length() > LOCAL_LOG_MAX_BYTES) {
|
||||
val prev = java.io.File(file.parentFile, "$LOCAL_LOG_NAME.prev")
|
||||
prev.delete()
|
||||
file.renameTo(prev)
|
||||
}
|
||||
file.appendText(line + "\n", Charsets.UTF_8)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新上报用的客户端网络/身份字段。
|
||||
*
|
||||
@@ -232,31 +277,40 @@ object MiliLog {
|
||||
private fun sendLoop() {
|
||||
var consecutiveFailures = 0
|
||||
while (running.get()) {
|
||||
val body: String = try {
|
||||
queue.take()
|
||||
} catch (e: InterruptedException) {
|
||||
break
|
||||
}
|
||||
var sent = post(body)
|
||||
if (!sent) {
|
||||
sleepQuietly(RETRY_SLEEP_MS)
|
||||
sent = post(body)
|
||||
}
|
||||
if (sent) {
|
||||
consecutiveFailures = 0
|
||||
// 顺手把已积压的日志发完。
|
||||
var next: String? = queue.poll()
|
||||
while (next != null) {
|
||||
post(next)
|
||||
next = queue.poll()
|
||||
try {
|
||||
val body: String = try {
|
||||
queue.take()
|
||||
} catch (e: InterruptedException) {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
consecutiveFailures++
|
||||
Log.w(TAG, "glog send dropped (failures=$consecutiveFailures): ${body.take(120)}")
|
||||
if (consecutiveFailures >= 3) {
|
||||
// 本地留一份(远程日志不可用时,现场仍可取证)。
|
||||
appendLocal(body)
|
||||
var sent = post(body)
|
||||
if (!sent) {
|
||||
sleepQuietly(RETRY_SLEEP_MS)
|
||||
sent = post(body)
|
||||
}
|
||||
if (sent) {
|
||||
consecutiveFailures = 0
|
||||
sleepQuietly(FAILURE_BACKOFF_MS)
|
||||
// 顺手把已积压的日志发完。
|
||||
var next: String? = queue.poll()
|
||||
while (next != null) {
|
||||
appendLocal(next)
|
||||
post(next)
|
||||
next = queue.poll()
|
||||
}
|
||||
} else {
|
||||
consecutiveFailures++
|
||||
Log.w(TAG, "glog send dropped (failures=$consecutiveFailures): ${body.take(120)}")
|
||||
if (consecutiveFailures >= 3) {
|
||||
consecutiveFailures = 0
|
||||
sleepQuietly(FAILURE_BACKOFF_MS)
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
// 兜底:发送线程**绝不能因为任何异常退出**,否则远程日志会静默失效。
|
||||
Log.w(TAG, "glog loop error: ${t.javaClass.simpleName}: ${t.message}")
|
||||
sleepQuietly(1000L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user