Electron 鸿蒙 PC 上 persist: session 存不住 cookie——排查到底发现路径被偷换成了 tmpfs
Electron 鸿蒙 PC 上 persist: session 存不住 cookie——排查到底发现路径被偷换成了 tmpfs
上周四晚上快十点的时候,用户群里炸了——七八个人同时说鸿蒙 PC 版每次打开都要重新登录。我看了一眼日志,登录接口返回 200,token 也拿到了,cookie 也写进去了,但重启 App 就是读不回来。
当时脑子里第一反应是 cookie 的过期时间设错了。我把 Max-Age 从默认的 session 级别改成 30 天,发了个热更新,心想稳了。结果用户反馈还是一样。
这就有点不对劲了。
我做的 App 叫雷达鸭,鸿蒙版在华为应用市场能搜到,桌面端我是用 Electron 套的。为了支持多账号切换,我用了session.fromPartition('persist:account_' + accountId)给每个账号建一个独立的 session,登录态互不干扰。这个方案在 Windows 上跑了三个月,一次问题没出过。
所以我一开始根本没往 session 本身的方向想。
第一轮:cookie 到底写进去了没?
我在主进程里加了一段日志,确认 cookie 在当前生命周期里是否正常工作:
// main.ts —— 记录 cookie 写入与回读状态const{session}=require('electron');constses=session.fromPartition('persist:account_1');ses.cookies.set({url:'https://api.example.com',name:'auth_token',value:'eyJhbGciOi...',expirationDate:Date.now()/1000+30*24*3600,}).then(()=>{console.log('[Cookie] set success');}).catch((err:Error)=>{console.error('[Cookie] set failed:',err.message);});// 立刻回读验证ses.cookies.get({url:'https://api.example.com'}).then((cookies)=>{console.log('[Cookie] get after set:',cookies.length,cookies.map((c:any)=>c.name));});日志输出很漂亮:[Cookie] set success,紧跟着[Cookie] get after set: 1 ['auth_token']。说明 cookie 在当前进程周期里写和读都没问题。
但用户退出重进,get就返回空数组了。cookie 没有落盘。
第二轮:怀疑 flush 时机
Electron 的 cookie 写入不是实时的,Chromium 内核有个异步 flush 机制。我想,会不会是鸿蒙 PC 上的 flush 间隔特别长,用户在 cookie 刷到磁盘之前就把 App 关掉了?
我在退出前手动调了 flush,甚至等了 2 秒再退出:
// main.ts —— 退出前强制刷盘const{app,session}=require('electron');app.on('before-quit',async()=>{constses=session.fromPartition('persist:account_1');try{awaitses.cookies.flushStore();console.log('[Cookie] flushStore completed');}catch(err:any){console.error('[Cookie] flushStore failed:',err.message);}});日志显示 flush 确实执行了。我还特意在 flush 后setTimeout等了 2 秒才允许进程退出。依然不行。
说真的,这时候我已经有点暴躁了。同一个代码,Windows 上好好的,鸿蒙上就是不行。我开始怀疑persist:这个前缀在鸿蒙上根本没生效——session 会不会被当成了内存级临时 session 处理?
第三轮:直接去磁盘上看
不猜了,直接扫文件系统。我把app.getPath('userData')下面的目录结构全打印出来:
// main.ts —— 扫描 session 存储目录const{app}=require('electron');constfs=require('fs');constpath=require('path');constuserData=app.getPath('userData');console.log('[Path] userData:',userData);constpartitionPath=path.join(userData,'Partitions','persist:account_1');console.log('[Path] expected partition:',partitionPath);if(fs.existsSync(partitionPath)){constfiles=fs.readdirSync(partitionPath);console.log('[Path] files in partition:',files);}else{console.log('[Path] partition directory DOES NOT EXIST');// 再往上看看 Partitions 目录本身constpartitionsDir=path.join(userData,'Partitions');if(fs.existsSync(partitionsDir)){console.log('[Path] Partitions dir contents:',fs.readdirSync(partitionsDir));}else{console.log('[Path] Partitions dir also missing');}}Windows 上这个路径下面能看到Cookies、Cookies-journal、Local Storage等一堆文件。但在鸿蒙 PC 上,Partitions/persist:account_1/目录根本就不存在。
更关键的是日志里的userData路径:
/run/user/1000/app_com.radarduck.hm/看到/run/user/这个前缀,我愣了好几秒。
如果你对 Linux 文件系统有点概念,应该知道/run/是一个 tmpfs——基于内存的临时文件系统,系统重启就清空。但就算知道这一点,当时的困惑也没减少:为什么 Electron 的userData会指向一个 tmpfs?这是谁决定的?
第四轮:路径重定向的真相
我在鸿蒙开发者文档和 electron-mus 的 GitHub issue 里翻了一圈,拼出了大致的原因。
HarmonyOS 为了实现应用沙箱隔离,对每个 HAP 应用的文件访问做了严格限制。Electron 在鸿蒙上运行时,底层用的是 electron-mus——一个把 Chromium 适配到鸿蒙 Mus 窗口系统的 Fork。这个 Fork 在初始化app.getPath('userData')时没有像标准 Electron 那样指向应用专属的持久化存储目录,而是直接用了 XDG 运行时目录,也就是/run/user/1000/。
而session.fromPartition('persist:xxx')的工作方式,是根据userData路径来拼接 partition 的存储位置——在userData/Partitions/persist:xxx/下面建目录。现在userData指向了 tmpfs,partition 自然也跟着进了 tmpfs。重启之后 tmpfs 清空,cookie 荡然无存。
但有个奇怪的地方:session.defaultSession的 cookie 为什么是好的?
我回头检查了一下,发现默认 session 的存储路径不走Partitions/子目录,而是直接在userData根下写Cookies文件。它确实也写到了 tmpfs 里——但 electron-mus 在 Chromium 初始化时用--user-data-dir参数,单独把默认 profile 的存储路径改写到了持久化目录(大概是/data/storage/el2/base/haps/entry/files/这一带)。
换句话说,只有自定义 partition 的路径没被改写。这在标准 Electron 里从来不是问题,因为userData本身就指向持久化目录。但在 electron-mus 上,defaultSession和自定义 session 走了两套路径解析逻辑,前者有特殊照顾,后者被晾在了 tmpfs 里。我在 electron-mus 的源码里确认了这一点——ChromiumSandbox::OverrideUserDataDir只处理了默认 profile 的路径,Partitions/子目录完全没管。
这设计,坦白讲,像是一个"先把默认的修了,自定义的回头再说"的半成品状态。倒不一定是开发者故意的,更像是在适配 HarmonyOS 沙箱时,默认 session 是 P0 优先级先修了,自定义 partition 的路径映射被排到了后面。
修复:绕过 persist 前缀
找到了根因,修复反而直接了。不依赖persist:的自动路径解析,自己拼绝对路径:
// main.ts —— 用绝对路径创建持久化 sessionconst{app,session}=require('electron');constfs=require('fs');constpath=require('path');/** * 获取鸿蒙应用的真实持久化存储根路径。 * 不能依赖 app.getPath('userData'),因为 electron-mus 可能指向 tmpfs。 */functiongetPersistentSessionRoot():string{// HarmonyOS 应用的持久化文件路径规范constdataDir='/data/storage/el2/base/haps/entry/files';if(fs.existsSync(dataDir)){returnpath.join(dataDir,'electron_sessions');}// 兜底:如果上面的路径也不存在(比如开发阶段用模拟器),// 退回到 userData,至少让问题在测试阶段暴露出来returnpath.join(app.getPath('userData'),'electron_sessions');}/** * 为指定账号创建独立的持久化 session。 * 用 fromPath 替代 fromPartition,绕过 persist: 前缀的路径解析。 */functioncreateAccountSession(accountId:string):Electron.Session{constroot=getPersistentSessionRoot();constsessionPath=path.join(root,`account_${accountId}`);if(!fs.existsSync(sessionPath)){fs.mkdirSync(sessionPath,{recursive:true});console.log('[Session] created dir:',sessionPath);}// fromPath 接受绝对路径,不会经过 persist: 前缀的路径映射逻辑constses=session.fromPath(sessionPath);console.log('[Session] created for account:',accountId,'at:',sessionPath);returnses;}// 用法示例constaliceSession=createAccountSession('user_alice');constbobSession=createAccountSession('user_bob');session.fromPath()接受一个绝对路径,完全绕过了persist:前缀的路径解析逻辑。你只要保证这个路径落在持久化存储上就行。
我一开始不太放心,专门在鸿蒙真机上反复开关 App 二十几次——每次退出前写 cookie,重启后读回来比对——确认完全没丢才合了代码。如果你也打算在鸿蒙上这么干,最好在你的测试机上也跑几轮重启验证,别像我一样等用户群炸了才知道有问题。
这个问题的本质是 electron-mus 对app.getPath('userData')的路径映射做得不够彻底——它只处理了默认 profile 的重定向,漏掉了自定义 partition。严格来说不算架构缺陷,就是个漏网的细节。等 electron-mus 后续版本应该会修。
但真正耗时的是排查过程:cookie 丢了,你第一反应肯定是检查 cookie 本身——过期时间、域名、SameSite 策略——而不是去查操作系统底层的文件路径映射。我绕了一大圈,中间甚至一度想把整个多账号方案推倒,改成手动把 token 存在 IndexedDB 里自己管理读写。说真的,IndexedDB 那条路我也不是完全没试——写了个 POC,在渲染进程里用electron-store存 token,主进程每次切账号手动清缓存。代码量多了一倍不提,token 自动刷新机制要跟 cookie 的自动携带逻辑完全重写,光是 edge case 就多出来七八个。在准备开搞重写之前,我决定再翻一遍 electron-mus 的 GitHub issues,结果就在第 3 页看到了那条关于路径映射的讨论——十几条评论,开发者也承认persist:session 的持久化还没适配好。行吧。
如果你也在做鸿蒙 PC 的 Electron 应用,用了session.fromPartition('persist:xxx'),建议上线前拿真机跑一遍重启后的 cookie 持久化。不然等你发现的时候,大概率跟我当时一样——大晚上对着日志和/run/user/1000/路径发呆。
老三,10+ 年软件开发经验,软件设计师 + 人工智能应用工程师。专注鸿蒙应用开发(ArkTS 北向)和 Web 前端,同时折腾 AI 自动化方向。不定期在 CSDN 分享鸿蒙和 AI 方面的技术文章。
本文遵循 MIT 协议,转载请注明出处。