三处改动(均来自 2026-10-16 真机日志实测):
1) 移除「同步记录」落盘
· LogUtil 顶部新增 ENABLE_FILE_LOG=false(发行版只输出 console),
真机排查时改 true 即可恢复落盘,所有调用点无需改动;
· init() 里顺手 unlink 历史版本遗留的 sync.log / mirror_diag.txt,
保证升级上来的设备沙箱也干净;
· SystemCalendarMirror 删掉 dumpDiagnosis 整个方法(不再导出 mirror_diag.txt),
连带去掉已无引用的 fs 依赖;
· SettingsPage「清理残留镜像账户」提示语由"见同步日志"改为"见下方"(清单本就在页面上)。
2) 镜像防护:系统日历清单为 0 时跳过创建
· 依据:2026-10-16 07:55:00 日志 `系统日历账户清单(共 0)` →
`镜像完成:本=0 … 失败=12`,12 个 bookId 连续 `201 CreateCalendar failed!`。
· 真因:设备重启后尚未解锁(direct boot),日历数据在加密存储里读不到,
getAllCalendars() 返回空列表 ⇒ createCalendar 必然失败,还可能留下半创建账户。
· 正常设备至少有一个本机日历,故"清单为 0"只可能是服务未就绪 ⇒ syncNow()
在 plan/syncBook 之前直接跳过本轮,等解锁后的下一次同步自然恢复。
3) 前台自动同步默认间隔 1 分钟 → 15 分钟
· 依据:2026-10-16 17:16–17:20 日志,5 分钟内连跑 5 轮
"12 本全量比对 + 出站镜像 + 提醒刷新",随后
`17:20:43 短时任务超时(配额用尽),后台同步将被挂起`
—— 前台把短时任务配额烧光,反而拖垮了真正的后台同步通道。
· 设备 preferences 里并无 sync_interval_minutes 键 ⇒ 走的就是这个默认值。
· AppSettings.getSyncIntervalMinutes 默认值 + Index.startAutoSync 的
setOrCreate 与 ?? 兜底 + SettingsPage 的 @State 初值,四处一并改为 15。
· 用户仍可在设置页选 1/5/15/30 分钟 或 1 小时。
验证:assembleHap 与 assembleApp 均 BUILD SUCCESSFUL。
135 lines
4.6 KiB
Plaintext
135 lines
4.6 KiB
Plaintext
// entry/src/main/ets/common/LogUtil.ets
|
||
// 日志工具:默认**只输出到 console(hilog)**,不落盘。
|
||
//
|
||
// ⭐ 0.0.8 起:发行版**不再写 sync.log**(原「同步记录」/诊断落盘功能已移除)。
|
||
// 需要真机排查同步/提醒问题时,把下面的 ENABLE_FILE_LOG 改成 true 重新编译即可恢复
|
||
// 落盘(含 512KB 自动截断保留后半),其余调用点一行都不用动。
|
||
// 关闭状态下会在 init 时顺手删掉历史版本遗留的 sync.log 与 mirror_diag.txt,
|
||
// 保证升级上来的设备沙箱也是干净的。
|
||
import { common } from '@kit.AbilityKit';
|
||
import { fileIo as fs } from '@kit.CoreFileKit';
|
||
import { util } from '@kit.ArkTS';
|
||
import { BusinessError } from '@kit.BasicServicesKit';
|
||
|
||
export class LogUtil {
|
||
/** ⭐ 落盘开关:发行版置 false(仅 console);真机排查时改 true 恢复文件日志 */
|
||
private static readonly ENABLE_FILE_LOG: boolean = false;
|
||
private static dir: string = '';
|
||
private static path: string = '';
|
||
private static readonly MAX_BYTES: number = 512 * 1024;
|
||
|
||
static init(context: common.Context): void {
|
||
try {
|
||
LogUtil.dir = context.filesDir;
|
||
LogUtil.path = `${LogUtil.dir}/sync.log`;
|
||
} catch (err) {
|
||
LogUtil.dir = '';
|
||
LogUtil.path = '';
|
||
}
|
||
if (!LogUtil.ENABLE_FILE_LOG && LogUtil.dir !== '') {
|
||
LogUtil.removeLegacyDebugFiles();
|
||
}
|
||
}
|
||
|
||
/** 删除历史版本遗留的调试落盘文件(sync.log / mirror_diag.txt) */
|
||
private static removeLegacyDebugFiles(): void {
|
||
for (const name of ['sync.log', 'mirror_diag.txt']) {
|
||
try {
|
||
fs.unlinkSync(`${LogUtil.dir}/${name}`);
|
||
} catch (err) {
|
||
// 文件不存在属正常,忽略
|
||
}
|
||
}
|
||
}
|
||
|
||
static logFilePath(): string {
|
||
return LogUtil.path;
|
||
}
|
||
|
||
/** 写一行日志:console + 文件(发行版不落盘;文件失败静默,不影响业务) */
|
||
static write(msg: string): void {
|
||
console.info(`SyncLog ${msg}`);
|
||
if (!LogUtil.ENABLE_FILE_LOG) {
|
||
return;
|
||
}
|
||
if (LogUtil.path === '') {
|
||
return;
|
||
}
|
||
try {
|
||
const d = new Date();
|
||
const p = (n: number): string => n < 10 ? '0' + n : String(n);
|
||
const ts: string = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}` +
|
||
` ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
||
// 超限截断:保留最后 256KB
|
||
try {
|
||
const st: fs.Stat = fs.statSync(LogUtil.path);
|
||
if (st.size > LogUtil.MAX_BYTES) {
|
||
LogUtil.truncate();
|
||
}
|
||
} catch (err) {
|
||
// 文件不存在属正常,直接创建写入
|
||
}
|
||
const file: fs.File = fs.openSync(LogUtil.path,
|
||
fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.APPEND);
|
||
fs.writeSync(file.fd, `[${ts}] ${msg}\n`);
|
||
fs.closeSync(file.fd);
|
||
} catch (err) {
|
||
// 写日志失败不影响业务
|
||
}
|
||
}
|
||
|
||
/** 截断:只保留文件末尾 256KB */
|
||
private static truncate(): void {
|
||
try {
|
||
const keep: number = 256 * 1024;
|
||
const st: fs.Stat = fs.statSync(LogUtil.path);
|
||
const size: number = Math.min(st.size, keep);
|
||
const file = fs.openSync(LogUtil.path, fs.OpenMode.READ_ONLY);
|
||
const buf: ArrayBuffer = new ArrayBuffer(size);
|
||
fs.readSync(file.fd, buf, { offset: st.size - size });
|
||
fs.closeSync(file.fd);
|
||
const out = fs.openSync(LogUtil.path,
|
||
fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC);
|
||
fs.writeSync(out.fd, `…(日志过长,已截断保留末尾)\n`);
|
||
fs.writeSync(out.fd, new Uint8Array(buf));
|
||
fs.closeSync(out.fd);
|
||
} catch (err) {
|
||
// 截断失败忽略,下次再试
|
||
}
|
||
}
|
||
|
||
static readAll(context: common.Context): string {
|
||
try {
|
||
if (LogUtil.path === '') {
|
||
LogUtil.init(context);
|
||
}
|
||
if (LogUtil.path === '') {
|
||
return '(无法确定日志路径)';
|
||
}
|
||
const st: fs.Stat = fs.statSync(LogUtil.path);
|
||
if (st.size === 0) {
|
||
return '(日志为空)';
|
||
}
|
||
const file = fs.openSync(LogUtil.path, fs.OpenMode.READ_ONLY);
|
||
const buf: ArrayBuffer = new ArrayBuffer(st.size);
|
||
fs.readSync(file.fd, buf);
|
||
fs.closeSync(file.fd);
|
||
const decoder: util.TextDecoder = util.TextDecoder.create('utf-8');
|
||
return decoder.decodeToString(new Uint8Array(buf));
|
||
} catch (err) {
|
||
return `(读取日志失败: ${(err as BusinessError).message})`;
|
||
}
|
||
}
|
||
|
||
static clear(context: common.Context): void {
|
||
try {
|
||
if (LogUtil.path === '') {
|
||
LogUtil.init(context);
|
||
}
|
||
fs.unlinkSync(LogUtil.path);
|
||
} catch (err) {
|
||
// 文件不存在等,忽略
|
||
}
|
||
}
|
||
}
|