关于shell:记一个生产工具过于智能导致的坑

120次阅读

共计 3943 个字符,预计需要花费 10 分钟才能阅读完成。

背景

近期在做一个 proto 文件解决的 CLI 工具,之前应用 proto 文件,个别分为两种形式:

  1. 间接援用 proto 文件,采纳运行时动静生成 JS 代码
  2. 通过 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 的定义,果不其然发现了一丢丢区别:

execcwd 参数形容为 Current working directory of the child process. Default: process.cwd().,而 execFilecwd 参数形容为 Current working directory of the child process.

看起来后者并没有默认值,那么是不是因为工作目录不对而导致的呢,所以咱们在代码中增加了 cwd 参数,从新进行验证流程。

后果,并没有什么区别,仍然是报错。

所以翻看了一下 Node.js 对于 execexecFile 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 之后,一些它所提供的能力让我会误以为是程序所提供的,整个问题排查过程中也没有往那方面去思考,也不知这样“好用”的工具会不会在其余场景再给我一些惊喜。

正文完
 0