

如何优雅地 hack 用户的代码
source link: https://www.fly63.com/article/detial/11575
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

扫一扫分享
做基础技术的时候,会经常碰到一个问题就是如何让自己提供的代码对用户少侵入,无感。比如我提供了一个 SDK 收集 Node.js 进程的 HTTP 请求耗时,最简单的方式就是给用户提供一个 request 方法,然后让用户统一调用,这样我就可以在 request 里拿到这些数据。但是这种方式很多时候并不方便,这时候我们就需要去 hack Node.js 的 HTTP 模块或者给 Node.js 提 PR。在操作系统层面,有提供很多技术解决这种问题,比如 ebpf、uprobe、kprobe。但是应用层无法使用这种技术解决我们的问题,因为操作系统的这些技术针对的是底层的函数,比如我想知道一个 JS 函数的耗时,只能在 V8 层面或者 JS 层面去解决,V8 这方面似乎也没有提供很好能力,所以目前我们更多是考虑纯 JS 或者 Node.js 内核层面。本文介绍一些一种在 JS 层面 hack 用户代码的方式。
在 Node.js 中,统计 JS 函数的耗时通常的做法是 cpu profile,但是这种方式只能拿到一段时间的耗时,如果我想实时收集耗时数据,cpu profile 就有点难搞,最直接的就是定时收集 cpu profile 数据,然后我们手动去解析 profile 数据然后上报。除了这种方式外,本文介绍另外一种方式。就是通过 hack JS 代码的方式。假如有以下一个函数。
function compute() {
// do something
}
如果我们想统计这种函数的执行耗时,最自然的方式就是在函数的开始和结束的地方插入一些代码。但是我们不希望这种事情让用户手动去做,而是使用一种更优雅的方式。那就是通过分析源码,拿到 AST,然后重写 AST。我们看看怎么做。
const acorn = require('acorn');
const escodegen = require('escodegen');
const b = require('ast-types').builders;
const walk = require("acorn-walk");
const fs = require('fs');
// 分析源码,拿到 AST
const ast = acorn.parse(fs.readFileSync('./test.js', 'utf-8'), {
ecmaVersion: 'latest',
});
function inject(node) {
// 在函数前后插入代码
const entryNode = b.variableDeclaration('const', [b.variableDeclarator(b.identifier('start'), b.callExpression(
b.identifier('(() => { return Date.now(); })'), [],
))]);
const exitNode = b.returnStatement(b.callExpression(
b.identifier('((start) => {console.log(Date.now() - start);})'), [
b.identifier('start')
],
));
if (node.body.body) {
node.body.body.unshift(entryNode);
node.body.body.push(exitNode);
}
}
// 遍历 AST,修改 AST
walk.simple(ast, {
ArrowFunctionExpression: inject,
ArrowFunctionDeclaration: inject,
FunctionDeclaration: inject,
FunctionExpression: inject
});
// 根据修改的 AST 重新生成代码
const newCode = escodegen.generate(ast);
fs.writeFileSync('test.js', newCode)
执行上面的代码后拿到如下结果。
function compute() {
const start = (() => { return Date.now(); })();
return ((start) => {console.log(Date.now() - start);})(start);
}
这样我们就可以拿到每个函数的耗时数据了。但是这种方式是静态分析源码,落地起来需要用户主动操作,并不是那么友好。那么基于这个基础我们利用 V8 调试协议中的 Debugger domain 实现动态重写,这种方式还能重写 Node.js 内部的 JS 代码。 首先改一下测试代码。
function compute() {
// do something
}
setInterval(compute, 1000)
然后再看改写代码的逻辑。
const { Session } = require('inspector');
const acorn = require('acorn');
const escodegen = require('escodegen');
const b = require('ast-types').builders;
const walk = require("acorn-walk");
const session = new Session();
session.connect();
require('./test_ast');
// 监听 JS 代码解析事件,拿到所有的 JS
session.on('Debugger.scriptParsed', (message) => {
// 只处理这个文件
if (message.params.url.indexOf('test_ast') === -1) {
return;
}
// 拿到源码
session.post('Debugger.getScriptSource', {scriptId: message.params.scriptId}, (err, ret) => {
const ast = acorn.parse(ret.scriptSource, {
ecmaVersion: 'latest',
});
function inject(node) {
const entry = b.variableDeclaration('const', [b.variableDeclarator(b.identifier('start'), b.callExpression(
b.identifier('(() => { return Date.now(); })'), [],
))]);
const exit = b.returnStatement(b.callExpression(
b.identifier('((start) => {console.log(Date.now() - start);})'), [
b.identifier('start')
],
));
if (node.body.body) {
node.body.body.unshift(entry);
node.body.body.push(exit);
}
}
walk.simple(ast, {
ArrowFunctionExpression: inject,
ArrowFunctionDeclaration: inject,
FunctionDeclaration: inject,
FunctionExpression: inject
});
const newCode = escodegen.generate(ast);
// 分析完,重写 AST后生成新的代码,并重写
session.post('Debugger.setScriptSource', {
scriptId: message.params.scriptId,
scriptSource: newCode,
dryRun: false
});
})
});
session.post('Debugger.enable', () => {});
正常来说,setInterval 执行的函数没有东西输出,但是我们发现会不断输出 0,也就是耗时,因为这里使用毫秒级的统计,所以是 0,不过我们不需要关注这个。这样我们就完成了 hack 用户的代码,而对用户来说是无感的,唯一需要做的事情就是引入我们提供的一个 SDK。不过这种方式的难点在重写代码的逻辑,风险也比较大,但是如果我们解决了这个问题后,我们就可以随便 hack 用户的代码,做我们想做的事情,当然,是正事。
原文 https://zhuanlan.zhihu.com/p/518923387
Recommend
-
184
当时写这篇文章的时候才接触 mac 没多久,使用快两年之后,再次修订了本文。 0x00 前言谈及 macOS , 很多人喜欢和 Win 比个高下。在我看来, Win 虽在非编程类生态和易用性比 Mac 要好很多,可专业人士之所以专业是因为他能挑选适合的武器发挥
-
126
问与答 - @rootliang - 各位 V 友,是这样的,这段时间有家合伙伙伴型的物流公司的老总问我一个问题,如何监控包裹车上的包裹余量,包裹大小不定,想来想去貌似只有 NFC 标签能做到了,可是 NFC 一多会干扰信号,不知 V 友
-
107
版本树 / graph / network 干净简洁清晰 提交信息明确 易维护易读 举个反例:
-
123
2018-01-18 10:59 《歌手》和Jessie J如何优雅地商业互吹去年11月27日,我的手机突然收到永乐票务的一条短信:很遗憾地告诉你,我们刚刚接到主办方的通知,Jessie J和Flo Rida深圳演唱会...
-
110
-
89
-
77
被现代都市的光鲜亮丽所吸引,仿佛全北京都是三里屯
-
43
前段时间的美团和滴滴大战,又让人再次见识了疯狂补贴的力量。近些年的互联网产品,几乎宠坏了用户,烧钱是常态,红包、优惠券、免费送礼等补贴往往是营销人员最喜欢的方式,同时也是用户选择产品的标准之一。 我们发现:很多新推出的互联网产品基本都是不考虑盈利...
-
54
Linux - @invzhi - 以前是双系统,要用 MS Office 就换个系统写个报告什么的然而后来买了 SSD 直接装了 ArchLinux,这个时候问题来了,不得不用 office 的时候怎么办咧微软的 On
-
23
code小生 一个专注大前端领域的技术平台 公众号回复 Android 加入安卓技术群 编辑 | 起锋起笔公众号 来源丨...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK