图片来源:https://unsplash.com/photos/KXtMGheovdw
故事来源于我在研究 request 源码过程中,跑某个测试用例时使用 node 方式启动 karma 文件所遇到的问题,其中包含以下问题:
1、child_process模块是什么
2、spawn方法执行原理
3、karma是什么
4、windows下的环境变量原理
5、nvm-windows的使用
记录如下:
1、child_process模块是什么
当我在试图使用 request 测试用例 browser(一个测试用例)调试 request 时,发生了运行报错的问题,环境是在 windows 下。 所以这里顺便讲一下我遇到的问题,即 node.js 中 require('child_process').spawn 的用法,当调用 require('child_process').spawn() 方法时如下:
这里就要提一下 node.js 的 child_process 模块的作用。见名知意,它是处理子进程相关的模块,其中的 spawn 方法即是生成子进程的方法。spawn 方法会接收三个主要参数,分别是要执行的文件名、执行参数和一个选项配置。
2、spawn方法执行原理
spawn 方法内部会调用 ChildProcess 构造函数实例化一个子进程 child,然后用这个实例 child 去调用 spawn 方法,而 spawn 是 ChildProcess.prototype 上的一个方法,如下:
通过这两行的设置后,ChildProcess.prototype 的隐式原型(__proto__)指向了EventEmitter.prototype,ChildProcess 的隐式原型(__proto__)指向了 EventEmitter,形成如下关系:
所以 child.spawn() 调用时,会访问 child 的隐式原型,即 ChildProcess 上是否有 spawn 方法,发现有,即执行。而 ChildProcess 上的 spawn 方法内部是在用调用 spawn 的实例(this)上的 _handle 所持有的 spawn 方法。
但我们注意, this._handle 是一个 Process 的实例,而 Process 不是 node 内部的对象(是系统内部绑定),如下:
所以 this.handle 本质上是一个进程,当我们试图用单步调试进入 this._handle.spawn() 的执行内部时,发现只会跳转到如下的地方:
然后就直接得到了 err 结果,如下:
并且识别到了结果是一个 ENOENT 类型的错误,然后在抛出错误的地方将其抛出,如下:
最终,我们得到整个程序的运行结果,即抛出异常在控制台内,程序退出,如下:
经过这次分析,我们应该得知平时经常会遇到的 ENOENT 错误是怎么得来的,并且能够读懂异常返回的错误信息,这是通知我们在使用 spawn 方法启动 karma 进程时遇到了 ENOENT 错误。
那么接下来我们需要弄清 ENOENT 究竟表示什么错误。首先,这其实是 node.js 内部关于各种错误枚举定义的一种,表示“所要执行的文件不存在”,具体为什么叫 ENOENT,其实是缩写自“Error NO ENTry”,参考自 Why does ENOENT mean “No such file or directory”?
我们命中的错误名是 -4058,即对应 ENOENT
完整的 err_map 如下:
UV_E2BIG:-4093
UV_EACCES:-4092
UV_EADDRINUSE:-4091
UV_EADDRNOTAVAIL:-4090
UV_EAFNOSUPPORT:-4089
UV_EAGAIN:-4088
UV_EAI_ADDRFAMILY:-3000
UV_EAI_AGAIN:-3001
UV_EAI_BADFLAGS:-3002
UV_EAI_BADHINTS:-3013
UV_EAI_CANCELED:-3003
UV_EAI_FAIL:-3004
UV_EAI_FAMILY:-3005
UV_EAI_MEMORY:-3006
UV_EAI_NODATA:-3007
UV_EAI_NONAME:-3008
UV_EAI_OVERFLOW:-3009
UV_EAI_PROTOCOL:-3014
UV_EAI_SERVICE:-3010
UV_EAI_SOCKTYPE:-3011
UV_EALREADY:-4084
UV_EBADF:-4083
UV_EBUSY:-4082
UV_ECANCELED:-4081
UV_ECHARSET:-4080
UV_ECONNABORTED:-4079
UV_ECONNREFUSED:-4078
UV_ECONNRESET:-4077
UV_EDESTADDRREQ:-4076
UV_EEXIST:-4075
UV_EFAULT:-4074
UV_EFBIG:-4036
UV_EFTYPE:-4028
UV_EHOSTDOWN:-4031
UV_EHOSTUNREACH:-4073
UV_EILSEQ:-4027
UV_EINTR:-4072
UV_EINVAL:-4071
UV_EIO:-4070
UV_EISCONN:-4069
UV_EISDIR:-4068
UV_ELOOP:-4067
UV_EMFILE:-4066
UV_EMLINK:-4032
UV_EMSGSIZE:-4065
UV_ENAMETOOLONG:-4064
UV_ENETDOWN:-4063
UV_ENETUNREACH:-4062
UV_ENFILE:-4061
UV_ENOBUFS:-4060
UV_ENODEV:-4059UV_ENOENT:-4058
UV_ENOMEM:-4057
UV_ENONET:-4056
UV_ENOPROTOOPT:-4035
UV_ENOSPC:-4055
UV_ENOSYS:-4054
UV_ENOTCONN:-4053
UV_ENOTDIR:-4052
UV_ENOTEMPTY:-4051
UV_ENOTSOCK:-4050
UV_ENOTSUP:-4049
UV_ENOTTY:-4029
UV_ENXIO:-4033
UV_EOF:-4095
UV_EPERM:-4048
UV_EPIPE:-4047
UV_EPROTO:-4046
UV_EPROTONOSUPPORT:-4045
UV_EPROTOTYPE:-4044
UV_ERANGE:-4034
UV_EREMOTEIO:-4030
UV_EROFS:-4043
UV_ESHUTDOWN:-4042
UV_ESPIPE:-4041
UV_ESRCH:-4040
UV_ETIMEDOUT:-4039
UV_ETXTBSY:-4038
UV_EXDEV:-4037
UV_UNKNOWN:-4094
怎么样,是不是豁然开朗,既然知道了错误的含义,我们就不用对着控制台的错误满脸迷茫 ,这不就是所运行的文件不存在嘛。
那么下一步就是解决文件不存在的问题了,其实解决到这里我已经到了该睡觉的时间(0:30),然后我一边想着明天要怎么解决这个问题,一边睡觉去了。
到了第二天,我自然想到尝试两种方案解决这个问题,不就是要让 spawn 执行的文件存在嘛
1、将执行的 karma 改为引入项目依赖中已经安装的 karma
2、全局安装 karma
这里也就解释清了为什么一开始跑 browser 测试用例时会报这个错了,就是因为程序中所要执行的文件没有指明来源,所以会默认找当前环境下(也就是从node全局包中)寻找该文件。但我没有全局安装过 karma,所以会报 ENOENT 错误。这样一看,合情合理
猜测是没问题的,但是执行起来会发现又有其他问题,首先我们尝试第一种方案:改用项目中已经安装的依赖 karma
结果不可行的,错误信息很清楚地告诉我们,spawn方法关于可执行文件的参数必须传递字符串,也就是只能从全局中寻找 karma ,而不能直接把 karma 可执行文件引入到这里传给 spawn。
既然第一种方案 pass 掉了,那么我们来尝试第二种方案:全局安装
3、karma是什么
这里我们需要先了解以下 karma 是什么,以便能够更好地解决遇到的问题。karma 其实是 AngularJS 那个时代诞生的测试工具(test runner)。安装方式提供作为项目依赖包安装或是作为 cli 安装在 node 全局下的,其基于一个配置文件运行所配置的测试用例,可用于测试 js 单元模块。
我们接着上面,在全局安装 karma 后依然报同样的错如下,也就是全局下 karma 可执行文件依然没有找到
这是为什么呢?首先,编译器告诉我们没找到,我们需要去核实一下是不是的确是我们的问题,首先查看当前所使用的 node 在哪里
4、windows下的环境变量原理
这里所执行的 node 是从环境变量的 path 下所寻找的,windows下的环境变量分为“用户变量”和“系统变量”。用户变量是当前windows登录用户下的环境变量,而系统变量是系统全局共用的环境变量(不过大多数windows个人用户都只在单用户下进行使用)
所以我们需要看一下这里执行的 node 程序是放在哪里的,如下:
这里有 %NVMHONE% 和 %NVM_SYMLINK% 两个引用变量,我们看一下它们的值
它们分别对应的是“C:Program Filesnvm”和“C:Program Filesnodejs”。
写到这里,其实我不太想继续过多的分析细节了,因为我大概已经知道关键原因了。其实这一切可以归结为一个乌龙问题,就是我在windows下同时安装了 node.js 和 nvm,而 nvm 的作用是接管关于 node 不同版本的使用。但由于 windows 环境变量机制,导致npm、node和nvm这三者同时存在于环境变量中,但它们的关系是相冲突的,这在nvm官网有详细解释:如果你决定使用 nvm 管理node使用,那就不可以自行安装node和npm及其环境变量,否则会发生不可预料的关联错误。如下:
细心的你可能会发现,我在上面全局安装karma的时候,为什么全局安装后仍然找不到karma,是由于npm的环境变量设置,导致它被安装到了“C:UsersAdministratorAppDataRoamingnpmnode_moduleskarmabinkarma”下,如图:
5、nvm-windows的使用
所以我决定卸载之前自行安装的 node.js 和 npm 并移除响应的环境变量,操作如下:
1、手动清掉环境变量中关于node和npm的配置
2、手动删除电脑里安装的node.js和npm(node进程可能由于被占用而无法删除,需要关掉项目进程、vscode等)
3、卸载nvm并重新安装
但在windows上做开发,事情往往没有那么顺利。我再次遇到了别的坑,首先我在重新安装nvm时,选择安装在惯常的“C:Program Files”下,这也满足nvm的安装要求如下:
安装nvm倒是正常,安装后它会自动向用户变量和系统变量中均添加所需的环境变量,以使得你可以在任意地方直接使用nvm。既然nvm正常安装了,我们用它的目的当然时管理node.js,那么我们任意安装一个版本的node.js,如下:
OK,也没问题,那我们在当前shell启用新下载的node.js,如下
Ooops,我隐约感到不对劲(凭经验),于是我开始试着从 github issues 中查找类似的情况,查到了如下的说法:
果然,nvm安装的目录不能存在空格或特殊字符,这是 windows 环境下开发常遇到的一个大坑。知道为什么不建议使用 windows 做node相关的开发了吧,如果不是对windows特别有把握,或者曾经有过折腾java相关的经验,实在不建议用windows做开发。否则可能会在各种环境问题上浪费很多精力,而这些东西其实对于开发本身并没什么帮助,最多是对不同操作系统的工作原理会有很多的认识而已。
既然定位到了问题,那么便有的解决办法,如下:
1、卸载nvm
2、重新安装nvm,并指定安装在不包含空格的目录
3、使用nvm安装所需版本的node.js
这样一顿操作后,我终于看到了 nvm 顺利安装在我的电脑上的景象,如下:
这里我的配置我的 vscode 默认 shell 为 git 下的 bash,所以可以以更友好的界面使用 shell,具体配置方式如下:
先找到 vscode 设置中关于 shell 的配置,然后点击“在setting.json中编辑”
然后在图中标红的地方选择你安装的git的bin目录下的bash.exe,保存即可。
呼~
折腾这么半天其实才把卡住我们的环境问题搞定,那么赶紧全局安装 karma 试试吧。稍等别急,这里其实还有坑,就是 npm 源的地址如果用默认的国际源,在下载包时经常会导致失败,因为国际源上某些包的存放地址可能需要代理才能访问下载,所以如果遇到这种情况,请记得将 npm 源切为国内的源,比如 淘宝注册源
好了,看看 karma 下载后的效果:
Boom!裂开了吗?
稍安勿躁,冷静一下。我们可以求助于万能的 stack overflow (你现在正在踩的坑,可能几年前就有人踩过了)
通过 stack overflow 的回答,我们得到以下灵感:
1、尝试打印 process.env.PATH 查看 spawn 所能执行的文件有哪些
2、使用 cross-spawn 替换 spawn,解决 windows下PATH的问题
3、使用 exec 替换 spawn 来执行文件
4、手动在环境变量中添加要执行的文件
到这里我们应该更进一步了解 spawn 这个东西了,毕竟我们整个排除过程都是围绕因它执行失败而发生的,spawn 这个东西的本质是启用进程来执行一个可执行文件,这个可执行文件自然是从当前进程的环境变量中所查找的,即 process.env.PATH。那么我们是不是可以利用第1点启发,观察PATH下有没有karma这个东西:
虽然我们能在 vscode 的控制台下直接运行 karma,如下图。
那是因为我们在当前 node 环境下全局安装了 karma,但这不意味着在我们整个操作系统的环境变量中也存在 karma 可执行文件的地址。不信你试试打开window默认的shell:
第2点再次提醒了我们可能是 windows 开发环境的问题,如下:
在尝试使用 cross-spawn 替换掉默认的 spawn 后如下:
Nice!突破性的进展
至于测试用例运行出错,那是测试用例本身的问题了。但我们解决了无法通过 spawn 运行 karma 的问题,这不由地吸引着我去拜读 cross-spawn 的源码究竟是如何解决了我们的问题。不过在花时间看 cross-spawn 具体实现之前,我先看了它的“诞生原因”源自 node 中的一个 issue:
由此可见,我花了这么多时间纠结在 windows 如何让 spawn 执行 karma,其实本来就是 node 的 spawn 模块所不支持的一种操作。由此可见我在 node.js 方面的经验还很缺乏,不过这次之后,起码让我对 spawn 有了更多的了解(踩过的坑都将成为宝贵的经验)。
第3、4点其实不太好,第3点属于曲线救国,用 exec 替代 spawn,但毕竟是两个模块,有着各自的作用,可能只在某些情况下适用,第4点就更傻了,难道我们以后要每次把需要 spawn 执行的文件添加进 PATH ?
不过这里有必要了解以下 exec 模块的使用:其功能其实就相当于把你手动在控制台执行的命令作为一个字符串传给 exec 执行,然后通过 exec 的执行回调得到执行的结果和异常。
OK,以一个成功的测试用例运行截图收场(前面的使用 cross_spawn 运行测试用例报错是因为我之前修改过的测试用例代码中所创建的服务器地址协议。我之前以为是因为所启动服务器协议为https而失败,所以我改成了http。其实不是这个问题,所以当我将其改回为https,测试用例成功运行如下):
测试用例完整代码如下:
'use strict'
按照惯例,善始善终。经过这一番折腾,我们应该总结一下收获,如下:
1、遇到程序报错不要慌,冷静分析,巧用调试技巧探寻程序内部运行原理
2、追本溯源,去看程序中所用到的开源项目源地址关于此类问题大家的讨论(github issues、stack overflow)
3、利用既有经验,开阔思路,不要被问题的表面所迷惑,要大胆猜测问题背后的原因
4、不要回避问题,网上关于问题的很多解决方法属于”曲线救国“。虽然能够快速解决当下的问题,但问题只是被绕过去了,而不是真正的解决了,可能日后你还会碰到类似但用同样方式绕不过去的。
5、关于 node 相关的开发,尽量不要使用 Windows