yarn 命令死循环问题分析解决

目录
  • 前言
  • 遇到什么问题呢
  • 分析原因
  • 分析源码
  • 解决方案
  • 结语

前言

最近有个想法,希望在一个 yarn workspace 项目中实现任意一个子包中安装依赖时,都执行一些类似于初始化、同步配置的动作。

然而在操作过程中遇到了一个关于 yarn --cwd 有趣的问题,特地记录下来,希望能对后来者有所帮助。

遇到什么问题呢

先交代一下我们项目的基本情况,它是一个通过 yarn workspace 管理的 monorepo 项目,使用的是 yarn v1.22.11 版本,目录结构大致如下:

monorepo
├── package.json
├── app-a
│   └── package.json
├── app-b
│   └── package.json
└── config
    └── package.json

其中 app-aapp-b 都使用了 config 这个共享包:

"dependencies": {
  "@monorepo/config": "../config",
}

我们需要在根目录的 package.json 中的 preinstall 钩子做一些初始化操作:

"scripts": {
  "preinstall": "./bin/init.sh",
}

此时我们在根目录执行 yarn 或者 yarn add <pkg-name>,都会触发 preinstall 这个钩子,但在 app-a 中执行 yarn是不会触发根目录的 preinstall 钩子的。

因此,我们需要分别在每个子包上都加上这行,也即在每个子包安装依赖时都执行一下根目录的 preinstall 命令:

"scripts": {
  "preinstall": "yarn --cwd ../ preinstall",
}

于是,奇怪的事情就发生了,当我在 app-a 中执行 yarn 的时候,它停留在安装 @monorepo/config 的阶段,同时我的电脑明显变得卡顿,于是打开 htop 一看,好家伙,满屏都是:

4ark   40987  26.3  0.5 409250368  78624   ??  R  8:36下午   0:00.09 /usr/local/bin/node /usr/local/bin/yarn --cwd ../ preinstall

CPU 占用率直接达到 100%,吓得我赶紧 kill 掉这些进程:

ps aux | grep preinstall | awk '{print $2}' | xargs kill -9

分析原因

惊吓过后,来分析一下原因,很显然这段命令陷入了死循环,导致越来越多进程,于是尝试在每个子包中都手动执行一遍 yarn --cwd ../ preinstall 后,发现一切正常,那问题出在哪呢?

于是我再执行了一遍 yarn,并且用以下命令将进程信息复制出来,以便分析:

ps -ef | pbcopy

随后验证我刚刚的猜测,的确是这个命令在不断触发自己,导致死循环:

UID   PID  PPID   C STIME   TTY     TIME CMD
501 50399 50379   0  8:50下午 ??   0:00.10 /usr/local/bin/node /usr/local/bin/yarn --cwd ../ preinstall
501 50400 50399   0  8:50下午 ??   0:00.11 /usr/local/bin/node /usr/local/bin/yarn --cwd ../ preinstall
501 50401 50400   0  8:50下午 ??   0:00.11 /usr/local/bin/node /usr/local/bin/yarn --cwd ../ preinstall
501 50402 50401   0  8:50下午 ??   0:00.12 /usr/local/bin/node /usr/local/bin/yarn --cwd ../ preinstall

由于三个分包执行的命令都一样,不清楚是不是由于某个分包引起,于是修改一下命令以便区分:

"scripts": {
  "preinstall": "echo app-a && yarn --cwd ../ preinstall",
}

随后发现问题是出现在 config 这个子包,于是我把这个子包的 preinstall 命令去掉,果然没有这个问题了,非常奇怪。

难道是 --cwd ../ 这个路径有问题?验证一下,把命令改成这样:

"scripts": {
  "preinstall": "pwd && yarn --cwd ../ preinstall",
}

发现 pwd 输出是这样子的:

/4ark/projects/monorepo/app-a/node_modules/@monorepo/config

从这里的输出我们发现了两个问题,第一个问题是:

  • yarn workspace 共享包的 preinstall 被执行的时候,其实已经被拷贝到 app-anode_modules 中,而不是在当前目录,因此 --cwd ../ 并不指向项目根目录。

这一点比较好理解,毕竟 config 作为一个依赖包,确实应该被拷贝到应用的 node_modules

而第二个问题就不太理解了,为什么明明设置了 --cwd ../,却依然在当前目录执行呢?按照预期 cwd 的指向应该是:

/4ark/projects/monorepo/app-a/node_modules/@monorepo

难道是我对 cwd 参数的理解有偏差?看一下 yarn 的文档中对 cwd 描述:

Specifies a current working directory, instead of the default ./. Use this flag to perform an operation in a working directory that is not the current one.

This can make scripts nicer by avoiding the need to cd into a folder and then cd back out.

从文档的描述来看,cwd 的作用不就是代替 cd 吗,但现在的结果看来 yarn --cwd ../ preinstall 并不等价于 cd ../ && yarn preinstall

这就不得不让人疑惑 cwd 的定位方式了,在网上搜寻一番没找到相关的讨论,那只能自己动手丰衣足食,直接从 yarn 源码中寻找答案。

分析源码

前面我们说到,我们使用的是 yarn v1.22.11,在 yarn 的 GitHub 仓库中发现 v1 版本的最新版本停留在 v1.23.0-0,那我们就从这个版本的源码来进行分析,首先克隆代码到本地:

git clone --depth=1 https://github.com/yarnpkg/yarn

然后安装依赖并运行起来:

yarn && yarn watch

这时候它就会自动监听代码修改然后重新编译,我们查看 package.json 发现 yarn 的 bin 主要是调用 ./bin/yarn.js:

"bin": {
  "yarn": "./bin/yarn.js",
  "yarnpkg": "./bin/yarn.js"
},

也就是我们直接执行 bin/yarn.js 的效果就如同执行 yarn,试一下查看版本:

> /Users/4ark/projects/yarn/bin/yarn -v
1.23.0-0

PS:当然你也可以在项目目录下使用 npm link 把它挂载到本地中。

接下就是一番调试,终于定位到可以回答我们疑问的代码,在这里

function findProjectRoot(base: string): string {
  let prev = null;
  let dir = base;
  do {
    if (fs.existsSync(path.join(dir, constants.NODE_PACKAGE_JSON))) {
      return dir;
    }
    prev = dir;
    dir = path.dirname(dir);
  } while (dir !== prev);
  return base;
}
const cwd = command.shouldRunInCurrentCwd ? commander.cwd : findProjectRoot(commander.cwd);

可以看到 cwd 的定位方式是从当前目录寻找是否存在 package.json,若存在,则返回此目录,否则将目录经过 path.dirname 处理一遍,继续寻找,直到寻找到最外层。

那么这里最关键的是 path.dirname 的返回值,我们先看一下文档对于它的描述:

The path.dirname() method returns the directory name of a path, similar to the Unix dirname command. Trailing directory separators are ignored,

就是返回一个路径中的目录部分,作用与 unix 下的 dirname 命令一致,通常是这么使用的:

> dirname /4ark/app/index.js
/4ark/app
> dirname /4ark/app/packages/index.js
/4ark/app/packages

是不是会肤浅地认为它的作用就是返回一个路径的上一级目录?如果传入的是一个绝对路径,确实可以这么肤浅地认为,然而当传入的是一个相对路径时,情况就不一样了:

> dirname ../app/index.js
../app
> dirname ../../
../
> dirname ../

问: 会返回什么呢?

答案是:.,也就是当前目录。

那这里就能回答我们之前的问题,为什么在 node_module/@monorepo/config 中使用 yarn --cwd ../ preinstall 却在当前目录执行,因为它的上一级 node_modules/@monorepo 不存在 package.json,所以经过 dirname ../ 处理后 cwd 的指向就是当前目录。

如果对 node.js 中 path.dirname 的实现方式感兴趣,可以看这里 path.js#L538-L554

解决方案

摸清楚原因后,那解决这个问题也不是难事,只要我们把相对路径改成绝对路径,是不是就能解决这个问题了?

思考一下,其实 yarn --cwd ../ preinstall,把 ../ 改成绝对路径行不行呢?比如在本文的场景,../ 其实就是项目的根目录,那我们完全可以通过别的方式获取到项目的根目录,比如 在 git 中:

git rev-parse --show-toplevel

所以,我们把命令改成这样,问题就迎刃而解了:

- yarn --cwd ../ preinstall
+ yarn --cwd $(git rev-parse --show-toplevel) preinstall

那就不得不提一下,其实在 yarn v2 中新增了一个 --top-level 属性,它的作用刚好就是为了解决这个问题。

结语

其实我们再回过头来想,在本文的例子中,根本不需要在 config 目录中添加 preinstall 这个钩子,因为它作为共享包,每次修改都必然要在其它使用这个包的地方,重新安装一次,所以只要确保这些地方会执行 preinstall 就可以了,那也就意味着不会出现本文遇到的问题。

不过,多踩坑也不是坏事,只要搞清楚背后的原因,问题也就不是问题。

以上就是yarn 命令死循环问题分析解决的详细内容,更多关于yarn 命令死循环的资料请关注我们其它相关文章!

(0)

相关推荐

  • Yarn与Lerna管理monorepo使用详解

    目录 什么是 Yarn workspace 如何使用 Yarn workspace Lerna 安装依赖的方式 Yarn workspace 与 Lerna 结合 结合的方式 角色的分配 好处 使用 什么是 Yarn workspace Yarn workspace 是 Yarn 提供的 monorepo 下,管理依赖的机制.对代码仓库下,多个 package 的依赖,进行管理:将共同的依赖,做 hosting(提升).这样,可以防止 package 中的包重复安装. workspace 机制,

  • Yarn安装项目依赖报error An unexpected error occurred: “XXXXX:ESOCKETTIMEOUT”问题解决

    目录 引言 重新设置 引言 刚来到公司,拿到react的项目,这不是就要安装依赖了嘛,下面就是我遇到的问题及解决问题的过程,希望可以帮到遇到同样问题的Web友人. 首先小眼一喵,看到了yarn.lock文件,这时候心里暗自窃喜,这不很简单嘛,使用yarn安装,一顿操作猛如虎后,过了XXX时间,出现了下面的错误. 此时耳边仿佛听到了“凉凉夜色 为你思念成河”,收,回归正题. 大致错误的意思是请求这个资源的时候出现了超时 不过问题不大,我可是见过大风大浪的男人,冲冲冲 这时候就想起了设置淘宝镜像,于

  • pnpm对npm及yarn降维打击详解

    目录 正文 npm2 yarn pnpm 总结 正文 大家最近是不是经常听到 pnpm,我也一样.今天研究了一下它的机制,确实厉害,对 yarn 和 npm 可以说是降维打击. 那具体好在哪里呢? 我们一起来看一下. 我们按照包管理工具的发展历史,从 npm2 开始讲起: npm2 用 node 版本管理工具把 node 版本降到 4,那 npm 版本就是 2.x 了. 然后找个目录,执行下 npm init -y,快速创建个 package.json. 然后执行 npm install exp

  • 利用yarn代替npm管理前端项目模块依赖的方法详解

    本文主要给大家介绍了关于yarn代替npm管理前端项目模块依赖的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 什么是 yarn? 简单来说,yarn 是一个与 npm 功能相同的工具,用于前端项目的依赖管理.在使用 npm 的项目中,使用 npm 命令的地方都可以使用 yran 来代替. 为什么要使用 yarn 替代 npm 呢?yarn 相对 npm 来说,主要的特点有: 离线.并行安装:依赖并行安装,缓存已下载过的依赖并优先使用,各种优化使得安装依赖速度显著提升

  • 创建项目及包管理yarn create vite源码学习

    目录 1.引言 2.走进“yarn create vite”的源码 2.1 Vite 创建项目的方式: 2.1.1 终端交互方式创建项目: 2.1.2 终端指定模版创建项目: 2.2 源码分析: 2.2.1 终端参数解析: 2.2.2 交互收集数据: 2.2.3 目录初始化: 2.2.4 拷贝模板文件夹: 2.2.5 重写 gitignore 名称: 2.2.6 重写 package 字段: 2.2.7 后续操作提示: 3. 总结 1.引言 我们在编程学习的过程中也会写一些项目的模板,这样的模板

  • yarn 命令死循环问题分析解决

    目录 前言 遇到什么问题呢 分析原因 分析源码 解决方案 结语 前言 最近有个想法,希望在一个 yarn workspace 项目中实现任意一个子包中安装依赖时,都执行一些类似于初始化.同步配置的动作. 然而在操作过程中遇到了一个关于 yarn --cwd 有趣的问题,特地记录下来,希望能对后来者有所帮助. 遇到什么问题呢 先交代一下我们项目的基本情况,它是一个通过 yarn workspace 管理的 monorepo 项目,使用的是 yarn v1.22.11 版本,目录结构大致如下: mo

  • Jenkins安装的时区问题分析解决

    目录 一.首先根据官方的方式去修改启动参数 二.用另外一种办法,更改系统时区 正常情况下,jenkins是Java执行在Java容器,比如tomcat容器之下,只要改了tomcat的时区就行. 我这里是为了方便后续的代码可用性测试,用的是Ubuntu中apt在线安装,也只是安装了jdk然后让他自己运行.所以符合官网在Jenkins的启动参数方面考虑. 一.首先根据官方的方式去修改启动参数 参考:https://www.jenkins.io/doc/book/using/change-time-z

  • 通过jstack分析解决进程死锁问题实例代码

    刚才用jstack解决了一个进程死锁的问题--其实早就解决了,也知道原因,只是一直没找到死锁的位置,不太甘心而已. 流程大致如下: (0)环境要求,JDK1.6及以上 (1)先找到进程的PID,Windows下,打开进程管理器,按照名字排序,可以找到叫做javaw.exe的进程(java虚拟机进程一律叫做javaw.exe),要找出哪个是你的进程,记住当前进程列表,然后重启你的进程,PID刷新过的那个即是你的进程. (2)在CMD下运行:jstack pid,jstack会在console上打出

  • 在Linux中使用tcpdump命令捕获与分析数据包详解

    前言 tcpdump 是一个有名的命令行数据包分析工具.我们可以使用 tcpdump 命令捕获实时 TCP/IP 数据包,这些数据包也可以保存到文件中.之后这些捕获的数据包可以通过 tcpdump 命令进行分析.tcpdump 命令在网络层面进行故障排除时变得非常方便. tcpdump 在大多数 Linux 发行版中都能用,对于基于 Debian 的Linux,可以使用 apt 命令安装它. # apt install tcpdump -y 在基于 RPM 的 Linux 操作系统上,可以使用下

  • python 采用paramiko 远程执行命令及报错解决

    这篇文章主要介绍了python 采用paramiko 远程执行命令及报错解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 import sys import paramiko import config_reader from check_utils import standout_print, parse_remainsize_response_lines, error_out_print from time import time cla

  • 分析解决Python中sqlalchemy数据库连接池QueuePool异常

    目录 数据库相关错误的解决办法 错误一:数据库连接池超过限制 错误二:数据库事务未回滚 数据库相关错误的解决办法 错误一:数据库连接池超过限制 SqlAlchemy QueuePool limit overflow 造成连接数超过数据库连接池的限制,有两方面的原因,第一个是由于数据库连接池数比较小,因此当连接数稍微增加的时候就会超过限制,另一个原因就是在使用完数据库连接后未能即使释放,最后造成数据连接数持续增加从而超出数据库连接池的限制,所以我们也可以从这两个方面来解决这个问题,但是根本上还是得

  • node版本升级npm命令警告原因及解决

    目录 引言 一.报错原因 二.解决办法 引言 使用 nvm 升级 node 版本,从 v12.5.0 升级到 v16.15.1,升级完成后,使用 npm 命令时总是出现警告: npm WARN config global '--global', '--local' are deprecated. Use '--location=global' instead . 一.报错原因 升级 node 版本后,npm 没有同步升级到对应版本,所以出现 WARN . 二.解决办法 将 npm 升级到最新版本

  • SpringCloud Config统一配置中心问题分析解决与客户端动态刷新实现

    目录 一.问题分析及解决方案 1.问题分析 2.解决方案 二.手动刷新 1.添加服务监控 2.暴露服务端点 3.刷新业务类controller 4.手动刷新 三.自动刷新 什么是总线 基本原理 一.问题分析及解决方案 1.问题分析 上一章我们讲过远程仓储统一管理配置信息,客户端可以通过统一配置服务中心 config server 服务端获取配置信息.现在我们来做一个改变,并进行分析. 首先启动注册中心.统一配置中心configserver服务端.订单服务.浏览器访问地址:http://local

  • 滥用@PathVariable导致bug原因分析解决

    目录 前言 复现 3个匹配步骤 1,根据Path精准匹配 2,如果精准匹配没有成功,就开始模糊匹配 3,如果模糊匹配还匹配不上,就返回null 最后 前言 最近测试同学反馈,上周上线的一个功能会偶然性的报404,按理说这个功能在测试环境已经测试通过,也在线上运行了好几天,怎么会突然报错呢. 一开始以为是前端同学请求的接口有误,但是测试又说只是偶然性的404,几率也不高,于是打开日志找到对应的接口,一眼看到了接口上定义的@PathVariable,再一看参数,基本就确定是开发同学为了偷懒又误用@P

  • Java thread.isInterrupted() 返回值不确定结果分析解决

    目录 一.代码 二.分析结果 三.解决方案 一.代码 先上代码(以下这段代码会有多种执行结果) @Test public void test_interrupted_thread() throws Exception { InterruptThread interruptThread = new InterruptThread(); interruptThread.start(); interruptThread.interrupt(); System.out.println("interrup

随机推荐