nvm icon indicating copy to clipboard operation
nvm copied to clipboard

`nvm uninstall` slowed down by (sometimes infinite) recursion into symlinked directories

Open ericyhwang opened this issue 1 year ago • 2 comments

The issue

nvm uninstall, specifically the nvm_check_file_permissions helper, is unnecessarily slowed down by recursing into symlinked directories inside the global node_modules. In rare cases, it can get into infinite recursion.

Context

npm link ../related-module can be used to test changes in another module, without needing to repeatedly publish or pack on each change. For some reason, instead of a direct symlink, NPM creates 2 layers of symlinks - from the original repo's node_modules/related-module to the global node_modules, then from the global node_modules to the destination directory.

nvm_check_file_permissions follows the symlink from the global node_modules directory and recurses into the destination tree, which can take a long time if there are a lot of symlinks or large symlinked directories.

Also, if the symlink destination happens to itself have a circular symlink somewhere, nvm_check_file_permissions goes into infinite recursion, which is how I discovered this issue. 😓

Proposal

I believe nvm_check_file_permissions shouldn't need to follow symlinks, as all that matters is whether the current user has permission remove the symlinks in the global node_modules directory.

Changing the directory check to skip symlinks with && [ ! -L "$FILE" ] worked for me to resolve the infinite recursion and let the uninstall finish. With the change, a symlink will go to the else case, which checks that the current user can delete it.

Anything I'm missing?

I could create a PR, but it'd likely just be the source code change for now, since I'm not sure I'll have time to look at writing a good test for it this week or next.

Minimal repro

Pick an unused Node version for the fresh install. Here, I picked a non-LTS non-latest version, as it's less likely to be in use.

Setup

nvm install v19.8.1
cd $(npm root -g)
ln -s . im-a-circular-symlink

Run ls -al to see the symlink in the global node_modules.

To attempt the deletion:

nvm use default
NVM_DEBUG=1 nvm uninstall v19.8.1

Watch the infinite recursion for a while, Ctrl+C to stop.

More practical repro

This demonstrates it's possible to get into the situation under a more normal scenario.

First, cd to a directory you clone Git repos to.

nvm install v19.8.1
git clone https://github.com/share/sharedb.git
cd sharedb/examples/rich-text
npm link ../..
nvm use default
NVM_DEBUG=1 nvm uninstall v19.8.1

Someone would run npm link ../.. to try the example with the local code, instead of with the package installed off NPM.

Run ls -al $(npm root -g) to see the symlink in global node_modules that was created by npm link.

Issue template

Operating system and version:

MacOS 13.3.1 (22E261)

nvm debug output:

nvm --version: v0.39.3
$TERM_PROGRAM: Apple_Terminal
$SHELL: /bin/bash
$SHLVL: 1
whoami: 'ehwang'
${HOME}: /Users/ehwang
${NVM_DIR}: '${HOME}/.nvm'
${PATH}: ${NVM_DIR}/versions/node/v18.16.0/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:${HOME}/.npm/bin:${HOME}/go/bin:${HOME}/Library/Python/3.10/bin
$PREFIX: ''
${NPM_CONFIG_PREFIX}: ''
$NVM_NODEJS_ORG_MIRROR: ''
$NVM_IOJS_ORG_MIRROR: ''
shell version: 'GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin22)'
uname -a: 'Darwin 22.4.0 Darwin Kernel Version 22.4.0: Mon Mar 6 21:00:17 PST 2023; root:xnu-8796.101.5~3/RELEASE_X86_64 x86_64'
checksum binary: 'shasum'
OS version: macOS 13.3.1 22E261
awk: /usr/bin/awk, awk version 20200816
curl: /usr/bin/curl, curl 7.87.0 (x86_64-apple-darwin22.0) libcurl/7.87.0 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.11 nghttp2/1.51.0
wget: not found
sed: /usr/bin/sed
cut: /usr/bin/cut
basename: /usr/bin/basename
rm: /bin/rm
mkdir: /bin/mkdir
xargs: /usr/bin/xargs
git: /usr/bin/git, git version 2.39.2 (Apple Git-143)
grep: /usr/bin/grep, grep (BSD grep, GNU compatible) 2.6.0-FreeBSD
nvm current: v18.16.0
which node: ${NVM_DIR}/versions/node/v18.16.0/bin/node
which iojs: 
which npm: ${NVM_DIR}/versions/node/v18.16.0/bin/npm
npm config get prefix: ${NVM_DIR}/versions/node/v18.16.0
npm root -g: ${NVM_DIR}/versions/node/v18.16.0/lib/node_modules

nvm ls output:

       v14.19.3
       v16.20.0
       v18.14.0
->     v18.16.0
default -> v18 (-> v18.16.0)
iojs -> N/A (default)
unstable -> N/A (default)
node -> stable (-> v18.16.0) (default)
stable -> 18.16 (-> v18.16.0) (default)
lts/* -> lts/hydrogen (-> v18.16.0)
lts/argon -> v4.9.1 (-> N/A)
lts/boron -> v6.17.1 (-> N/A)
lts/carbon -> v8.17.0 (-> N/A)
lts/dubnium -> v10.24.1 (-> N/A)
lts/erbium -> v12.22.12 (-> N/A)
lts/fermium -> v14.21.3 (-> N/A)
lts/gallium -> v16.20.0
lts/hydrogen -> v18.16.0

How did you install nvm?

Install script in README

What steps did you perform?

Ran nvm uninstall v14.19.3

What happened?

Command hangs for over an hour before I stopped it to investigate

What did you expect to happen?

The Node version to get uninstalled in a reasonable amount of time

Is there anything in any of your profile files that modifies the PATH?

Yes. Just one for Python and Go binaries, irrelevant to this.

ericyhwang avatar Apr 25 '23 05:04 ericyhwang

That sounds great, and makes a lot of sense - I see the slowdown myself but I also have a lot of linked global modules.

Any chance you'd be up for a PR that includes a test case with such a symlink cycle?

ljharb avatar Apr 29 '23 03:04 ljharb

For anyone reaching this via Google wondering how to uninstall when it's hanging indefinitely: a workaround for now is to wipe the node_modules and then run the nvm uninstall command.

AndrewSouthpaw avatar May 08 '23 17:05 AndrewSouthpaw