review: M2 成果审查后的质量加固
三路+对照审查 M2 全部成果。桥核心(M1,本期未改)与契约骨架零问题;发现集中在新的资源/启动/容器代码,已修: 高危 - ResourceManager.updateGame 原子化:下载→解压暂存→校验 <gamestart>/index.html→才删旧并 rename 换入。 修复"先删旧 gamehall 再解压,失败即把可用资源砖掉"且 catch『keep local』失效的数据丢失风险 - BridgeGameContainer.setupBridge:先建桥并赋值 this.bridge,能力注册/配置加载整体兜底 try/catch。 修复"config/registration 抛错→bridge 永远 undefined→H5 所有 callHandler 静默"违反 H5 零改动铁律 中危 - VersionResolver:空 gameid 守卫(大厅不参与 game 树匹配,避免误命中 gamelist 空 id 节点); 两树 version 平手时取"有下载地址"的一棵(避免选中只有版本号无下载地址的层)。新增 2 单测,覆盖率 83.7% - Downloader:dataReceive 写盘失败标记并于请求结束后抛出(避免磁盘满致截断文件被当作下载成功) - FileSystem:rmrf 的 statSync 纳入 try(兑现"不存在/竞态则忽略"承诺);copyRawFileTo 按 byteOffset/byteLength 切片写入(避免 Uint8Array 为大池子视图时写入多余字节致 zip 损坏);新增 rename - ConfigManager.fetchSecondary:二级节点本层版本字段也并入(此前只换 channellist/agentlist 会丢节点自身版本/下载地址) 清理/抗回归 - 跨域白名单路径改用 ResourceManager.resourceRoot()(单一来源,避免与领域层路径定义漂移致 V2 回归) - 前后台订阅移到 setupBridge(桥就位后),避免冷启动 FOREGROUND 早于桥到达被丢 - prepareBuiltin 单次读取内置包(去掉"先整包读探测、再整包读拷贝"双读) - LocalUploadServer 删死码 isRunning/running - 文档化延期项:weburl 分支不注入 app_data(同 Android,远程大厅自带)、app_gamename 待 M3 扩展 VersionXml、 子游戏切换 firstProgressDone 复位 TODO(M3 T-M3-06) devecocli build 通过;单测全通过;模拟器复测 app_data/V2 fetch/桥双向/特殊字符往返均无回归。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,8 +34,15 @@ const KEY_URLPATH: string = 'urlpath';
|
||||
const KEY_UPURLPATH: string = 'upurlpath';
|
||||
/** 内置预置包 rawfile 名(可选;缺失则依赖远程下载)。 */
|
||||
const BUILTIN_ZIP_RAWFILE: string = 'gamehall_builtin.zip';
|
||||
/** 资源根目录名(沙箱 filesDir 下)。容器的 file:// 跨域白名单也用它,单一来源避免漂移。 */
|
||||
export const RESOURCE_ROOT_DIRNAME: string = 'tsgames';
|
||||
|
||||
export class ResourceManager {
|
||||
/** 资源根绝对路径(= <filesDir>/tsgames)。供容器 setPathAllowingUniversalAccess 复用,杜绝路径重复定义。 */
|
||||
static resourceRoot(context: common.Context): string {
|
||||
return `${context.filesDir}/${RESOURCE_ROOT_DIRNAME}`;
|
||||
}
|
||||
|
||||
private static readonly log: Logger = Logger.tag('ResourceManager');
|
||||
private readonly context: common.Context;
|
||||
private readonly config: AppConfig;
|
||||
@@ -50,7 +57,7 @@ export class ResourceManager {
|
||||
}
|
||||
|
||||
private computePaths(): ResourcePaths {
|
||||
const upurlpath: string = `${this.context.filesDir}/tsgames`;
|
||||
const upurlpath: string = ResourceManager.resourceRoot(this.context);
|
||||
const urlpath: string = `${upurlpath}/${this.config.gamedir}`;
|
||||
const gameDir: string = `${urlpath}/${this.config.gamestart}`;
|
||||
const indexPath: string = `${gameDir}/index.html`;
|
||||
@@ -92,18 +99,14 @@ export class ResourceManager {
|
||||
* 内置包顶层即含 gamehall/...,解压到 urlpath 即可。无内置包则跳过(依赖远程下载)。
|
||||
*/
|
||||
async prepareBuiltin(): Promise<void> {
|
||||
let bytesAvailable: boolean = true;
|
||||
// 单次读取:直接尝试拷贝,rawfile 缺失即抛错 → 跳过(避免"先整包读探测、再整包读拷贝"两次入内存)
|
||||
const tmpZip: string = `${this.cachedPaths.upurlpath}/builtin.zip`;
|
||||
try {
|
||||
this.context.resourceManager.getRawFileContentSync(BUILTIN_ZIP_RAWFILE);
|
||||
FileSystem.copyRawFileTo(this.context.resourceManager, BUILTIN_ZIP_RAWFILE, tmpZip);
|
||||
} catch (e) {
|
||||
bytesAvailable = false;
|
||||
}
|
||||
if (!bytesAvailable) {
|
||||
ResourceManager.log.w(`no builtin package (${BUILTIN_ZIP_RAWFILE}); rely on remote download`);
|
||||
return;
|
||||
}
|
||||
const tmpZip: string = `${this.cachedPaths.upurlpath}/builtin.zip`;
|
||||
FileSystem.copyRawFileTo(this.context.resourceManager, BUILTIN_ZIP_RAWFILE, tmpZip);
|
||||
FileSystem.ensureDir(this.cachedPaths.urlpath);
|
||||
await Unzipper.unzip(tmpZip, this.cachedPaths.urlpath);
|
||||
FileSystem.rmrf(tmpZip);
|
||||
@@ -111,17 +114,32 @@ export class ResourceManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从远程下载 zip 并更新游戏资源(契约 §5.4):
|
||||
* 下载 game_download(?a=ts) → 删旧 gamehall → 解压到 urlpath → 删 zip。
|
||||
* 从远程下载 zip 并更新游戏资源(契约 §5.4)。**原子化**:
|
||||
* 下载 → 解压到暂存目录 → 校验 staging/<gamestart>/index.html → 校验通过才删旧 gamehall 并换入。
|
||||
* 任一步失败(下载/解压/坏包/顶层缺 gamehall)旧资源保持不动,避免"删旧后解压失败把可用资源砖掉"。
|
||||
*/
|
||||
async updateGame(downloadUrl: string, onProgress?: ProgressCallback): Promise<void> {
|
||||
const url: string = downloadUrl.includes('?') ? downloadUrl : `${downloadUrl}?a=${Date.now()}`;
|
||||
const zipPath: string = `${this.cachedPaths.upurlpath}/dowlod_zip/${Date.now()}Projects.zip`;
|
||||
await Downloader.download(url, zipPath, onProgress);
|
||||
FileSystem.rmrf(this.cachedPaths.gameDir);
|
||||
FileSystem.ensureDir(this.cachedPaths.urlpath);
|
||||
await Unzipper.unzip(zipPath, this.cachedPaths.urlpath);
|
||||
FileSystem.rmrf(zipPath);
|
||||
ResourceManager.log.i(`game updated from ${url}`);
|
||||
const staging: string = `${this.cachedPaths.urlpath}/.staging_${Date.now()}`;
|
||||
try {
|
||||
await Downloader.download(url, zipPath, onProgress);
|
||||
FileSystem.rmrf(staging);
|
||||
FileSystem.ensureDir(staging);
|
||||
await Unzipper.unzip(zipPath, staging);
|
||||
// 校验新包顶层结构(远程 zip 顶层应含 <gamestart>/index.html)
|
||||
const stagedGame: string = `${staging}/${this.config.gamestart}`;
|
||||
if (!FileSystem.exists(`${stagedGame}/index.html`)) {
|
||||
throw new Error('updated package missing <gamestart>/index.html');
|
||||
}
|
||||
// 原子换入:旧资源直到此刻仍可用
|
||||
FileSystem.rmrf(this.cachedPaths.gameDir);
|
||||
FileSystem.ensureDir(this.cachedPaths.urlpath);
|
||||
FileSystem.rename(stagedGame, this.cachedPaths.gameDir);
|
||||
ResourceManager.log.i(`game updated from ${url}`);
|
||||
} finally {
|
||||
FileSystem.rmrf(zipPath);
|
||||
FileSystem.rmrf(staging);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user