青春时代是一个短暂的美梦,当你醒来时,它早已消失得无影无踪了。
 
夜月琉璃Lv46   
扒一扒迅雷的代码结构     
作者:jiawen
链接:https://juejin.cn/post/6890344584078721031
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

一.背景

    之前扒过飞书的源码,从代码设计架构层面里里外外学习一把,飞书还是挺大方的,源码在客户端和网页端都一览无余,不过好像新版本已经看不到了。相关的文章由于在内网技术论坛发过了不便于再发出来(泄露内部资料会被查水表的),因此这次周末抽时间换一个鸟窝来掏一掏。

    一不小心发现迅雷的客户端竟然也是基于 Electron 开发的,那代码就好扒拉了。(先吐槽一下这新版本的某lei为什么要抄钉钉的界面,这些年某lei都不知道自己要干什么了,每个版本都招人嫌)


Image


二.开撬

 2.1 一点背景知识说明

   基于前端技术栈 Electron 构建的桌面应用,本质上都是加载本地前端资源文件,而这些文件通常是用 asar 格式(类似 windows iso 镜像)的方式进行打包,然后运行时再通过挂在到内存实现前端资源文件 js/css/html/img 等文件的读取。

   这么说 asar 想办法挂载就可以随意阅读源码了吗?不是的。同时 asar 会提供一套通过加密方式防止任意解压,飞书就是这么做的,直接通过 asar extract 的方式无法解包出来。但是由于 node 端和 rust 构建的二进制文件如果打包到 asar 会导致无法链接到这些二进制文件,因此需要从 asar 中独立出来,因而导致有部分 js 文件仍然裸露在外面。不过即便没有任何 js 是暴露的仍然是有办法爆破的。

   啊,跑偏了,先不谈飞书,今天的主菜是迅雷。

   那迅雷的前端资源文件是怎么管理的呢?


Image


    是在下想多了,不好意思,迅雷梅川酷子,都摊着在那呢,根本没用 asar 打包/加密。


 2.2.开撬

    既然 js 都暴露了,也没什么好绕的,直接植入代码吧。我们都知道 Electron 是有 render 进程和 Node 进程的,接下来这一步需要猜猜看哪个文件是负责 render 主进程的?好吧不用猜,名字都非常人类可读,就 main-renderer(主窗口渲染进程)。打开找到 html 文件(js也可以)插入如下这串


Image


    双击启动,调试窗口出来了,可以大致看到整体页面结构了


Image

    然后看了一下,迅雷的悬浮小圆圈和主窗口,分别用一个 BrowserWindow 来实现。有趣的是那个小圆圈窗口其实并不小,鼠标悬停出来的那个浮窗也是它的一部分,为了让小圆圈在屏幕的任何位置都可以看到悬浮窗,所以整个小圆圈的 BrowserWindow 是大约 4 倍的悬浮窗口大小


Image

   

    独立窗口的检视界面 - 窗口实际是 4倍 浮窗大小,灰色部分全都是这个浮窗所使用的 BrowserWindow区域


Image


 2.3.一点防御措施

     从代码来看,nodejs 进程只有一个文件 main.js ,是 webpack 的构建产物,看源码这里的 BrowserWindow webPreference 参数是把 devTools 禁用掉的,导致直接在命令行里敲 openDevTools 是不能检视任意窗口的


Image


    当然了,这里即便是混淆过了也没关系,毕竟还是明文,把 1 改成 0,把它打开就好(双叹号/true/1啥都行,开心就好)。不过由于迅雷的窗口实在是太多了,下载弹窗是独立窗口,选择文件夹是独立窗口,各种广告窗口也是,需要改的配置点很多,这里就不列了,总共有 10 个窗口,这个配置点按需打开(批量替换也行,谨慎操作就行)。


三.进程结构

   呃……然后要干啥……好像也没什么好看的了,代码是混淆过的,也没有 map 文件。而且前端部分的代码也没什么技术含量可以说的,哪个 web 页面都那样。那看看进程分工吧。


 3.1.进程树

   在进程树里可以看出来,几乎全部的进程都是 Thunder.exe,可见 Thunder.exe 作为进程派发入口(类似 server 的网关,而并不直接是业务本身),用户启动的时候传参是 --StartType:DesktopIcon,随后它唤起了两组进程,一组是 Electron main 进程,main 进程唤起相关的 renderer;然后是下载的 SDK 服务 DownlaodSDKServer

   那么迅雷的进程关系差不多是清楚了:多个 Electron 窗口,对应一个 DownloadSDK


Image


 3.2.通信方式

    那么 Electron 的进程(甭管 main-process 还是 renderer-process,统称 electron进程) 和 DownloadSDK 是如何通信的呢?

    进程间通信一般都是依靠 ipc 管道的形式来实现。不过迅雷似乎没按套路来,它的 DownloadSDK 是控制台程序,意味着很有可能是通过 stdio 的方式来进行交互的(后续证明不是)。

通过观察进程打开的句柄,看到很诡异的一个现象:DownloadSDK 并没有打开任何 ipc 管道,反倒是前端进程打开了一个


Image


  3.2.1.前端的 ipc

    而 Electron 打开的这个 handler 进程名称,查了一下,竟然全是 Electron 进程使用的,而且是所有进程。


Image


    那么不妨做出一个大胆的推测:前端多窗口之间是靠自建的 ipc 通道实现的,而 ipc 1 server N client 的方式,那么 server 很有可能就是在主窗口上的,也就是前文看到那个及其明显的 main-renderer 进程,通过控制台查看,确实如此,nodejs net 方式创建了一个 server,并且将一个叫做 __xdasIPCServerInstance 的对象暴露在全局环境供前端 js 调用,也即 jsapi


Image


    而小窗口并不存在上述 server 实例,而相对应的有一个 client 实例


Image


  3.2.2.和 DownloadSDK 的通讯方式

    这样看起来就很奇葩了,前端进程之间是通过自建的 ipc 管道通信的,但是并没有跟 DownloadSDK 有任何通信管道,难道它俩是心有灵犀无言自通?啊这……程序员是唯物主义的!

    那怎么查它到底是怎么跟前端进程交互的呢?既然前端暴露了 server sdk instance,那意味着 DownLoadSDK 肯定是以一种 proxy 的方式暴露在这上面作为 jsapi 的。可以拿【创建一个下载任务api】来顺藤摸瓜。看了主窗口的 server instance 一下果然有这个方法:createTask ,应该就是前端用于创建下载任务用的 api


Image


   chrome 浏览器里查代码不方便,转战 vscode 看源码,搜索 createTask 这个函数的声明位置,看到这一段(篇幅控制,此处删减了部分代码)

createTask(e, t) {
        return n(this, void 0, void 0, function* () {
          .....
          }
          switch (e) {
            case h.DownloadKernel.TaskType.P2sp:
              ...
            case h.DownloadKernel.TaskType.Bt:
             ...
            case h.DownloadKernel.TaskType.Emule:
             ...
            case h.DownloadKernel.TaskType.Group:
              ...
            case h.DownloadKernel.TaskType.Magnet:
             ...
            default:
              i = !1;
          }
          return (
            ...
            _.fireTaskEvent(h.DownloadKernel.TaskEventType.TaskCreated, [
          );
        });
      }

    没跑了,证实了我前面的猜想,这个 __xdasIPCServerInstance 就是 download sdk 封装到前端的 proxy

    继续查,这个 fireTaskEvent 是怎么处理的,阅读代码过程繁琐按下不提,就看这两段代码(有删减整理)

// 片段一
(e.getDownloadSdkVersion = function () {
  let e = a.join(__rootDir, "../bin/SDK/DownloadSDKServer.exe");
  return v.getFileVersion(e);
}),

// 片段二
y = l.default(o.join(__rootDir, "../bin/ThunderHelper.node"));
let F = "/ssdkver " + u.DownloadKernelManager.getDownloadSdkVersion();
B.push(F)
y.shellExecute(0, "open", o, B, H, "SW_SHOW");

    很显然,DownloadSDK 是通过一个 ThunderHelper.node nodejs addon 模块来启动、通信的。

    我们知道, nodejs 可以通过 ffi 等方式实现内存共享,以达到两个进程不需要通过 pipe/sock 等管道就达到通信的目的。而通过工具观察 Thunder.exe 的唤起关系、句柄关系,两者的关系就更加一目了然了:ELectron 前端进程加载 DownloadSDK 进程,并且通过 \Sessions\5\BaseNamedObjects\xx@22123720|SendShareMemory 这种内存通道来实现的通信,句柄一一对应上了。


Image


四.总结

    扒拉了半天,扒完了有点空虚是怎么回事

  •     迅雷的代码架构关系是轻 node 而重前端,把所有的 node 加载、进程管理、多窗口通信都放在前端进程的主窗口进程里。关于这个做法,我尊重而不认同。前端进程不应该做太重的底层交互,尤其是 js 这种单线程语言,天然的就运行效率低,而且主窗口使用这么频繁就不怕卡住吗
  •     Electron 天然就有 ipc 通信能力,完全可以在 node 端做一个消息网关,达成每个窗口通信的能力,完全不需要自建一个 ipc server-client 体系。可能这也是一开始就把大量工作放在前端(主窗口)了导致后期的程序设计受限。说不定是个历史包袱
  •     用一个 node addon 的方式来跟 DownloadSDK 来通信,这点是可以点个赞的,虽然是业界标准(飞书是通过rust,基本原理类似),但是我目前所负责的业务并没有做到这样,所以在惭愧的同时也给它点个赞
  •     迅雷使用的 Electron 版本是 9.2.1vscode 也是这个版本,好神奇!非常好奇为何业界都用这个版本,事实上 electron 9.x 最新版本已经更新到 9.3.3 了(20201028日)这个 9.2.1 有什么魔力让业界都用它吗
  •     这里说明一下,Electron 6.0 开始就不支持 windows7(sp1) 及以下的版本了。
  •     我在 win7 系统上用迅雷安装器安装迅雷最新版本,发现 electron 用的是 1.8.6 版本
  •     Electron 的主入口是处理过了的,通过 Thunder.exe 程序干了很多除了启动前端以外的事情,这个定制还是挺棒的,因为这样就可以把各种进程模块管理起来,不会出现多个独立进程。就我所看到的不少 Electron 应用其实都没有定制过。

  •    以上是纯粹技术挖掘,没有破坏到迅雷的核心机密,仅做学习交流使用哈


Image


五.关于我们

    我们部门有若干桌面端应用,用户基数大,挑战大,招聘有 electron 经验或者前端技术专家。经常会做一些好玩的桌面端实践,也会深入挖掘和学习一些 app 的程序设计。如果有兴趣的话欢迎联系我,简历请狠砸我!私信我获取联系方式哦!都可以聊一下,说不定就成为同事了,没有你想象的那么难!

    还记得开头说的扒了飞书的源码的事情吗?想看吗?想就投简历吧哈哈哈哈



 5  已被阅读了4543次  楼主 2021-01-18 22:01:36
回复列表

回复:扒一扒迅雷的代码结构

桂公网安备 45010302000666号 桂ICP备14001770-3号
感谢景安网络提供数据空间
本站CDN由七牛云提供支持
网站已接入ipv6
免责声明: 本网不承担任何由内容提供商提供的信息所引起的争议和法律责任。
如果某些内容侵犯了您的权益,请通过右侧按钮与我们联系
Your IP: 3.148.106.49 , 2024-11-22 04:42:02 , Processed in 0.8948 second(s).
Powered by HadSky 8.4.9
知道创宇云安全