共计 3943 个字符,预计需要花费 10 分钟才能阅读完成。
背景
近期在做一个 proto 文件解决的 CLI 工具,之前应用 proto 文件,个别分为两种形式:
- 间接援用 proto 文件,采纳运行时动静生成 JS 代码
- 通过 protoc 工具生成对应的 JS 文件,并在我的项目中援用
后者性能会更高一些,因为编译过程在程序运行之前,所以个别会采纳后者来应用。
问题景象
因为是一个通用的工具,所以 proto 文件也会是动静的,在本地环境简略的模仿了一下可能呈现的场景,而后终端执行 protoc 命令:
# grpc_tools_node_protoc 为 protoc Node.js 版本的封装
grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./src/main/proto --grpc_out=grpc_js:./src/main/proto ./protos/**/*.proto
发现所有运行失常,遂将对应代码写入脚本中,替换局部门路为变量,提交代码,发包,本地装置,验证。
后果就呈现了这样的问题:
Could not make proto path relative: ./protos/**/*.proto: No such file or directory
/usr/local/lib/node_modules/@infra-node/grpc-tools/bin/protoc.js:43
throw error;
^
Error: Command failed: /usr/local/lib/node_modules/@infra-node/grpc-tools/bin/protoc --plugin=protoc-gen-grpc=/usr/local/lib/node_modules/@infra-node/grpc-tools/bin/grpc_node_plugin --js_out=import_style=commonjs,binary:./src/main/proto --grpc_out=grpc_js:./src/main/proto ./protos/**/*.proto
Could not make proto path relative: ./protos/**/*.proto: No such file or directory
at ChildProcess.exithandler (child_process.js:303:12)
at ChildProcess.emit (events.js:315:20)
at maybeClose (internal/child_process.js:1021:16)
at Socket.<anonymous> (internal/child_process.js:443:11)
at Socket.emit (events.js:315:20)
at Pipe.<anonymous> (net.js:674:12) {
killed: false,
code: 1,
signal: null,
cmd: '/usr/local/lib/node_modules/@infra-node/grpc-tools/bin/protoc --plugin=protoc-gen-grpc=/usr/local/lib/node_modules/@infra-node/grpc-tools/bin/grpc_node_plugin --js_out=import_style=commonjs,binary:./src/main/proto --grpc_out=grpc_js:./src/main/proto ./protos/**/*.proto'
}
令人震惊,并且更令人匪夷所思的是,当我将 cmd
中的内容复制到终端中再次运行时,发现一切都是失常的。
震惊之余,还是从新查看本人的代码实现。
问题排查
首先是狐疑是不是执行命令所采纳的形式不对,以后所应用的是 exec
,因为 grpc_tools_node_protoc
也是一个封装的 Node.js 模块,所以顺带着看了它的源码,发现源码采纳的是 execFile
,而后去翻看 Node.js 的文档,查看两者是否会有区别,因为前边报错信息是 No such file or directory
,首先狐疑是不是因为 CLI 是全局装置而导致门路不对,所以针对性的看了一下两个 API 对于 current working directory 的定义,果不其然发现了一丢丢区别:
exec
的 cwd
参数形容为 Current working directory of the child process. Default: process.cwd().
,而 execFile
的 cwd
参数形容为 Current working directory of the child process.
。
看起来后者并没有默认值,那么是不是因为工作目录不对而导致的呢,所以咱们在代码中增加了 cwd
参数,从新进行验证流程。
后果,并没有什么区别,仍然是报错。
所以翻看了一下 Node.js 对于 exec
与 execFile
API 实现上的区别,来确认是否为 cwd
的起因,后果发现 exec
外部调用的就是 execFile
,那么根本能够确认两者在 cwd
参数的默认值解决上并不会有什么区别,同时在源码中增加了 DEBUG 信息输入查看 cwd
也的确是咱们预期的以后进行运行所在的目录。
既然问题不在这里,那么咱们就要从其余中央再进行剖析,因为对本人的代码比拟自信(也的确没有几行),所以又认真的看了一下 grpc-tools
的实现,发现代码是这样的:
var protoc = path.resolve(__dirname, 'protoc' + exe_ext);
var plugin = path.resolve(__dirname, 'grpc_node_plugin' + exe_ext);
var args = ['--plugin=protoc-gen-grpc=' + plugin].concat(process.argv.slice(2));
var child_process = execFile(protoc, args, function(error, stdout, stderr) {if (error) {throw error;}
});
其中上边程序报错所输入的 cmd
参数其实也就是这里的 args
参数的后果了。
出于好奇,咱们在源码处增加了一个 DEBUG 日志,后果发现了一个神奇的状况。
当咱们通过 Node.js exec
运行的时候,输入是这样的:
[
'/usr/local/bin/node',
'/usr/local/bin/grpc_tools_node_protoc',
'--js_out=import_style=commonjs,binary:./src/main/proto',
'--grpc_out=grpc_js:./src/main/proto',
'./protos/**/*.proto'
]
而咱们通过终端间接执行命令,输入后果是这样的:
[
'--plugin=protoc-gen-grpc=/usr/local/lib/node_modules/@infra-node/grpc-tools/bin/grpc_node_plugin',
'--js_out=import_style=commonjs,binary:./src/main/proto',
'--grpc_out=grpc_js:/./src/main/proto',
'./protos/examples/example-base-protos/kuaishou/base/base_message.proto'
]
两者的最初一个参数居然是不一样的。
所以尝试着将 proto 的具体文件门路放到命令中,再次通过 exec
的形式运行,发现果然一切正常,所以问题就出在了最初 proto
文件门路上,合着 protoc
并不反对 **
这种通配符的文件输出。
那么新的问题就来了,为什么两种不同的运行形式会导致传入的参数发生变化呢。
因为 Node.js 模块的可执行文件都是通过 package bin 来注册的,有理由狐疑是不是 NPM 做了一些小动作,所以写了一个 shell 文件,很简略的一句输入:
echo $* # 输入所有的参数
用反向排除法,如果咱们通过 sh test.sh **/*.json
可能失去 **/*.json
的输入,那么根本能够确定是 NPM 搞的鬼。
后果输入后果为:
package-lock.json package.json proto.json
通过终端来进行输入就曾经可能拿到一个残缺的文件门路了,阐明至多不是 NPM 的一些操作。
忽然间想到一种可能,键入 bash
而后再运行同样的命令 sh test.sh **/*.json
,果然咱们失去了 **/*.json
。
想到本人的终端应用的是 zsh
,所以翻看对应的文档,果然找到了对应的阐明:https://zsh.sourceforge.io/Do…,[自行翻到 14.8.6 Recursive Globbing]
当我刚意识到问题所在的时候,心田飘过一行 oh my f**king zsh
。
zsh
会将门路进行递归匹配,而后将其开展在执行参数中,所以最终起因也定位了,是因为 zsh
的一个便民性能导致我误以为是 protoc
的一个性能,最终在一个非 zsh
环境裸露问题。
总结
本次遇到的问题景象很诡异,然而起因却令人很无奈,好在排查的过程中还是比拟有播种的,被迫读了一些模块的源码,更深刻的理解了 proto 文件的整个编译过程。
在习惯了应用 zsh 之后,一些它所提供的能力让我会误以为是程序所提供的,整个问题排查过程中也没有往那方面去思考,也不知这样“好用”的工具会不会在其余场景再给我一些惊喜。