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:
@@ -30,19 +30,29 @@ export class FileSystem {
|
||||
fs.mkdirSync(path, true);
|
||||
}
|
||||
|
||||
/** 递归删除文件或目录(不存在则忽略)。 */
|
||||
/** 递归删除文件或目录(不存在/竞态则忽略,绝不抛错——对齐注释承诺)。 */
|
||||
static rmrf(path: string): void {
|
||||
if (!FileSystem.exists(path)) {
|
||||
return;
|
||||
}
|
||||
const stat: fs.Stat = fs.statSync(path);
|
||||
if (stat.isDirectory()) {
|
||||
fs.rmdirSync(path);
|
||||
} else {
|
||||
fs.unlinkSync(path);
|
||||
try {
|
||||
const stat: fs.Stat = fs.statSync(path);
|
||||
if (stat.isDirectory()) {
|
||||
fs.rmdirSync(path);
|
||||
} else {
|
||||
fs.unlinkSync(path);
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
FileSystem.log.w(`rmrf(${path}) ignored: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** 重命名/移动(同文件系统)。用于资源原子换入。 */
|
||||
static rename(oldPath: string, newPath: string): void {
|
||||
fs.renameSync(oldPath, newPath);
|
||||
}
|
||||
|
||||
/** 写文本(覆盖;自动建父目录)。 */
|
||||
static writeText(path: string, content: string): void {
|
||||
FileSystem.ensureDir(FileSystem.dirOf(path));
|
||||
@@ -76,8 +86,9 @@ export class FileSystem {
|
||||
FileSystem.ensureDir(FileSystem.dirOf(destPath));
|
||||
const file: fs.File = fs.openSync(destPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC);
|
||||
try {
|
||||
// ArkTS:Uint8Array.buffer 是底层 ArrayBuffer,writeSync 接受 ArrayBuffer
|
||||
fs.writeSync(file.fd, bytes.buffer as ArrayBuffer);
|
||||
// 按 Uint8Array 的实际视图(byteOffset/byteLength)写入,避免底层 buffer 为大池子视图时写入多余字节致 zip 损坏
|
||||
const view: ArrayBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
|
||||
fs.writeSync(file.fd, view);
|
||||
} finally {
|
||||
fs.closeSync(file);
|
||||
}
|
||||
|
||||
@@ -22,11 +22,20 @@ export class Downloader {
|
||||
FileSystem.rmrf(savePath);
|
||||
const file: fs.File = fs.openSync(savePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC);
|
||||
let offset: number = 0;
|
||||
let writeError: string = '';
|
||||
const req: http.HttpRequest = http.createHttp();
|
||||
|
||||
req.on('dataReceive', (chunk: ArrayBuffer) => {
|
||||
fs.writeSync(file.fd, chunk, { offset });
|
||||
offset += chunk.byteLength;
|
||||
// 写盘失败(磁盘满等)发生在事件回调内,不会拒绝 requestInStream;记录后于结束时抛出,避免截断文件被当作成功
|
||||
if (writeError !== '') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
fs.writeSync(file.fd, chunk, { offset });
|
||||
offset += chunk.byteLength;
|
||||
} catch (e) {
|
||||
writeError = (e as Error).message;
|
||||
}
|
||||
});
|
||||
if (onProgress !== undefined) {
|
||||
req.on('dataReceiveProgress', (info: http.DataReceiveProgressInfo) => {
|
||||
@@ -46,6 +55,9 @@ export class Downloader {
|
||||
if (code < 200 || code >= 300) {
|
||||
throw new Error(`download http ${code}`);
|
||||
}
|
||||
if (writeError !== '') {
|
||||
throw new Error(`download write failed: ${writeError}`);
|
||||
}
|
||||
Downloader.log.i(`download ${url} -> ${savePath} (${offset} bytes)`);
|
||||
} finally {
|
||||
req.off('dataReceive');
|
||||
|
||||
@@ -12,21 +12,16 @@ export class LocalUploadServer {
|
||||
private static readonly log: Logger = Logger.tag('LocalUploadServer');
|
||||
/** 占位端口,M3 实现真实服务时改为动态绑定后的实际端口。 */
|
||||
private port: number = 8099;
|
||||
private running: boolean = false;
|
||||
|
||||
/** 启动本机服务(M2 占位:仅置位,不绑定端口)。 */
|
||||
/** 启动本机服务(M2 占位:不绑定端口)。 */
|
||||
async start(): Promise<void> {
|
||||
// TODO(M3): @ohos.net.socket 绑定 127.0.0.1:<port>,接收截图上传
|
||||
this.running = true;
|
||||
LocalUploadServer.log.i(`start (stub) baseUrl=${this.baseUrl()}`);
|
||||
}
|
||||
|
||||
/** 停止本机服务(M3 实现真实服务后关闭 socket)。 */
|
||||
async stop(): Promise<void> {
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this.running;
|
||||
// TODO(M3): 关闭 socket
|
||||
}
|
||||
|
||||
/** 推给 H5 的上传地址(对齐 Android `http://<本机IP>:<端口>/testurl`)。 */
|
||||
|
||||
Reference in New Issue
Block a user