Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vite can hang the node.js process if server is closed during file transform #18224

Open
7 tasks done
maxpatiiuk opened this issue Sep 28, 2024 · 2 comments
Open
7 tasks done
Labels
p2-edge-case Bug, but has workaround or limited in scope (priority)

Comments

@maxpatiiuk
Copy link

maxpatiiuk commented Sep 28, 2024

Describe the bug

If addWatchFile() is called by a Vite plugin after the server is closed, the watcher will be set, preventing Node.js process from exiting.

For example, if vite:css was in the middle of transforming a file when the server is closed, vite:css may call addWatchFile() , which will hang the process.

Reproduction

https://github.com/maxpatiiuk/vite-watcher-hangs

Steps to reproduce

  1. Clone this repository

    git clone https://github.com/maxpatiiuk/vite-watcher-hangs
  2. Install dependencies

    cd vite-watcher-hangs
    cd vite-app
    npm install
  3. Run the Vite server

    npx vite
  4. See that the Node.js process does not exit after the server is closed

    • Comment out the line 27 in vite.config.ts and see that the server exists correctly as this way the watcher is set before the server is closed.

System Info

System:
    OS: macOS 14.6.1
    CPU: (10) arm64 Apple M1 Pro
    Memory: 65.73 MB / 32.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.16.0 - ~/.nvm/versions/node/v20.16.0/bin/node
    Yarn: 1.22.19 - ~/.nvm/versions/node/v20.16.0/bin/yarn
    npm: 10.8.1 - ~/.nvm/versions/node/v20.16.0/bin/npm
  Browsers:
    Chrome: 129.0.6668.60
    Safari: 18.0
  npmPackages:
    vite: ^5.4.8 => 5.4.8

Used Package Manager

npm

Logs

Logs without `--debug`
> npx vite
Waiting 1s to ensure Vite server is fully started
Port 5173 is in use, trying another one...
Port 5174 is in use, trying another one...
Port 5175 is in use, trying another one...
Port 5176 is in use, trying another one...
Port 5177 is in use, trying another one...
Port 5178 is in use, trying another one...
Port 5179 is in use, trying another one...
Port 5180 is in use, trying another one...
Port 5181 is in use, trying another one...
Port 5182 is in use, trying another one...

  VITE v5.4.8  ready in 99 ms

  ➜  Local:   http://localhost:5183/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help
Starting index.html transform
Waiting 1s to ensure transform() is called before closing the server
Delaying CSS transform by 4s to ensure server is closed before transform completes
[ 'TTYWrap', 'TTYWrap', 'TTYWrap', 'TCPServerWrap', 'Timeout' ]
Close the server
Active resources before CSS transform:
[ 'TTYWrap', 'TTYWrap', 'TTYWrap' ]
Deprecation Warning: The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.

More info: https://sass-lang.com/d/legacy-js-api

Active resources after CSS transform:
[ 'CloseReq', 'FSReqCallback', 'TTYWrap', 'TTYWrap', 'TTYWrap' ]
Server is closed BUT node.js process is still running because of the watcher set by vite:css plugin
[ 'CloseReq', 'FSReqCallback', 'TTYWrap', 'TTYWrap', 'TTYWrap' ]
Logs with `--debug`
> npx vite --debug
  vite:config bundled config file loaded in 41.34ms +0ms
  vite:config using resolved config: {
  vite:config   plugins: [
  vite:config     'vite:optimized-deps',
  vite:config     'vite:watch-package-data',
  vite:config     'vite:pre-alias',
  vite:config     'alias',
  vite:config     'vite:modulepreload-polyfill',
  vite:config     'vite:resolve',
  vite:config     'vite:html-inline-proxy',
  vite:config     'vite:css',
  vite:config     'vite:esbuild',
  vite:config     'vite:json',
  vite:config     'vite:wasm-helper',
  vite:config     'vite:worker',
  vite:config     'vite:asset',
  vite:config     'make-scss-take-longer',
  vite:config     'vite:wasm-fallback',
  vite:config     'vite:define',
  vite:config     'vite:css-post',
  vite:config     'vite:worker-import-meta-url',
  vite:config     'vite:asset-import-meta-url',
  vite:config     'vite:dynamic-import-vars',
  vite:config     'vite:import-glob',
  vite:config     'vite:client-inject',
  vite:config     'vite:css-analysis',
  vite:config     'vite:import-analysis'
  vite:config   ],
  vite:config   optimizeDeps: {
  vite:config     holdUntilCrawlEnd: true,
  vite:config     force: undefined,
  vite:config     esbuildOptions: { preserveSymlinks: false }
  vite:config   },
  vite:config   server: {
  vite:config     preTransformRequests: true,
  vite:config     host: undefined,
  vite:config     sourcemapIgnoreList: [Function: isInNodeModules$1],
  vite:config     middlewareMode: false,
  vite:config     fs: {
  vite:config       strict: true,
  vite:config       allow: [Array],
  vite:config       deny: [Array],
  vite:config       cachedChecks: undefined
  vite:config     }
  vite:config   },
  vite:config   configFile: '/Users/mak13180/site/esri/vite-watcher-hangs/vite-app/vite.config.ts',
  vite:config   configFileDependencies: [
  vite:config     '/Users/mak13180/site/esri/vite-watcher-hangs/vite-app/vite.config.ts'
  vite:config   ],
  vite:config   inlineConfig: {
  vite:config     root: undefined,
  vite:config     base: undefined,
  vite:config     mode: undefined,
  vite:config     configFile: undefined,
  vite:config     logLevel: undefined,
  vite:config     clearScreen: undefined,
  vite:config     optimizeDeps: { force: undefined },
  vite:config     server: { host: undefined }
  vite:config   },
  vite:config   root: '/Users/mak13180/site/esri/vite-watcher-hangs/vite-app',
  vite:config   base: '/',
  vite:config   decodedBase: '/',
  vite:config   rawBase: '/',
  vite:config   resolve: {
  vite:config     mainFields: [ 'browser', 'module', 'jsnext:main', 'jsnext' ],
  vite:config     conditions: [],
  vite:config     extensions: [
  vite:config       '.mjs',  '.js',
  vite:config       '.mts',  '.ts',
  vite:config       '.jsx',  '.tsx',
  vite:config       '.json'
  vite:config     ],
  vite:config     dedupe: [],
  vite:config     preserveSymlinks: false,
  vite:config     alias: [ [Object], [Object] ]
  vite:config   },
  vite:config   publicDir: '/Users/mak13180/site/esri/vite-watcher-hangs/vite-app/public',
  vite:config   cacheDir: '/Users/mak13180/site/esri/vite-watcher-hangs/vite-app/node_modules/.vite',
  vite:config   command: 'serve',
  vite:config   mode: 'development',
  vite:config   ssr: {
  vite:config     target: 'node',
  vite:config     optimizeDeps: { noDiscovery: true, esbuildOptions: [Object] }
  vite:config   },
  vite:config   isWorker: false,
  vite:config   mainConfig: null,
  vite:config   bundleChain: [],
  vite:config   isProduction: false,
  vite:config   css: { lightningcss: undefined },
  vite:config   esbuild: { jsxDev: true },
  vite:config   build: {
  vite:config     target: [ 'es2020', 'edge88', 'firefox78', 'chrome87', 'safari14' ],
  vite:config     cssTarget: [ 'es2020', 'edge88', 'firefox78', 'chrome87', 'safari14' ],
  vite:config     outDir: 'dist',
  vite:config     assetsDir: 'assets',
  vite:config     assetsInlineLimit: 4096,
  vite:config     cssCodeSplit: true,
  vite:config     sourcemap: false,
  vite:config     rollupOptions: {},
  vite:config     minify: 'esbuild',
  vite:config     terserOptions: {},
  vite:config     write: true,
  vite:config     emptyOutDir: null,
  vite:config     copyPublicDir: true,
  vite:config     manifest: false,
  vite:config     lib: false,
  vite:config     ssr: false,
  vite:config     ssrManifest: false,
  vite:config     ssrEmitAssets: false,
  vite:config     reportCompressedSize: true,
  vite:config     chunkSizeWarningLimit: 500,
  vite:config     watch: null,
  vite:config     commonjsOptions: { include: [Array], extensions: [Array] },
  vite:config     dynamicImportVarsOptions: { warnOnError: true, exclude: [Array] },
  vite:config     modulePreload: { polyfill: true },
  vite:config     cssMinify: true
  vite:config   },
  vite:config   preview: {
  vite:config     port: undefined,
  vite:config     strictPort: undefined,
  vite:config     host: undefined,
  vite:config     https: undefined,
  vite:config     open: undefined,
  vite:config     proxy: undefined,
  vite:config     cors: undefined,
  vite:config     headers: undefined
  vite:config   },
  vite:config   envDir: '/Users/mak13180/site/esri/vite-watcher-hangs/vite-app',
  vite:config   env: { BASE_URL: '/', MODE: 'development', DEV: true, PROD: false },
  vite:config   assetsInclude: [Function: assetsInclude],
  vite:config   logger: {
  vite:config     hasWarned: false,
  vite:config     info: [Function: info],
  vite:config     warn: [Function: warn],
  vite:config     warnOnce: [Function: warnOnce],
  vite:config     error: [Function: error],
  vite:config     clearScreen: [Function: clearScreen],
  vite:config     hasErrorLogged: [Function: hasErrorLogged]
  vite:config   },
  vite:config   packageCache: Map(1) {
  vite:config     'fnpd_/Users/mak13180/site/esri/vite-watcher-hangs/vite-app' => {
  vite:config       dir: '/Users/mak13180/site/esri/vite-watcher-hangs/vite-app',
  vite:config       data: [Object],
  vite:config       hasSideEffects: [Function: hasSideEffects],
  vite:config       webResolvedImports: {},
  vite:config       nodeResolvedImports: {},
  vite:config       setResolvedCache: [Function: setResolvedCache],
  vite:config       getResolvedCache: [Function: getResolvedCache]
  vite:config     },
  vite:config     set: [Function (anonymous)]
  vite:config   },
  vite:config   createResolver: [Function: createResolver],
  vite:config   worker: { format: 'iife', plugins: '() => plugins', rollupOptions: {} },
  vite:config   appType: 'spa',
  vite:config   experimental: { importGlobRestoreExtension: false, hmrPartialAccept: false },
  vite:config   getSortedPlugins: [Function: getSortedPlugins],
  vite:config   getSortedPluginHooks: [Function: getSortedPluginHooks]
  vite:config } +3ms
Waiting 1s to ensure Vite server is fully started
  vite:deps Hash is consistent. Skipping. Use --force to override. +0ms
Port 5173 is in use, trying another one...
Port 5174 is in use, trying another one...
Port 5175 is in use, trying another one...
Port 5176 is in use, trying another one...
Port 5177 is in use, trying another one...
Port 5178 is in use, trying another one...
Port 5179 is in use, trying another one...
Port 5180 is in use, trying another one...
Port 5181 is in use, trying another one...
Port 5182 is in use, trying another one...

  VITE v5.4.8  ready in 126 ms

  ➜  Local:   http://localhost:5183/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help
Starting index.html transform
Waiting 1s to ensure transform() is called before closing the server
  vite:resolve 1.12ms /src/main.ts -> /Users/mak13180/site/esri/vite-watcher-hangs/vite-app/src/main.ts +0ms
  vite:load 1.05ms [fs] /src/main.ts +0ms
  vite:resolve 0.41ms ./style.scss -> /Users/mak13180/site/esri/vite-watcher-hangs/vite-app/src/style.scss +8ms
  vite:import-analysis 1.87ms [1 imports rewritten] src/main.ts +0ms
  vite:transform 6.65ms /src/main.ts +0ms
  vite:load 1.18ms [fs] /src/style.scss +8ms
Delaying CSS transform by 4s to ensure server is closed before transform completes
[ 'TTYWrap', 'TTYWrap', 'TTYWrap', 'TCPServerWrap', 'Timeout' ]
Close the server
Active resources before CSS transform:
[ 'TTYWrap', 'TTYWrap', 'TTYWrap' ]
Deprecation Warning: The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.

More info: https://sass-lang.com/d/legacy-js-api

Active resources after CSS transform:
[ 'CloseReq', 'FSReqCallback', 'TTYWrap', 'TTYWrap', 'TTYWrap' ]
Server is closed BUT node.js process is still running because of the watcher set by vite:css plugin
[ 'CloseReq', 'FSReqCallback', 'TTYWrap', 'TTYWrap', 'TTYWrap' ]

Explanation of the issue

Multiple built-in Vite plugins are setting watchers for files. For example, vite:css plugin:

this.addWatchFile(file)

These watchers are set even if server.watcher.close() was called.

These watchers in turn prevent the Node.js process from exiting, hanging it.

Solutions:

  • In server.close(), if there are pending requests, after the requests are processed, close all watchers again if any were open.
  • OR Make PluginContext.addWatchFile() a noop after the watcher is closed
  • OR Add a check for watcher being closed before calling addWatchFile (error prone)

Real-world example

I am starting Vite dev server inside Vitest global setup file. (Vite dev server is used for Puppeteer)

The Vite dev server is closed in the global teardown file.

If some test fails, the teardown is called early, while Vite dev server might still be in the process of transforming a CSS file.

The vite:css plugin may set a file watcher, prevent Vitest from exiting.

Vitest prints this message:

close timed out after 10000ms
Tests closed successfully but something prevents Vite server from exiting
You can try to identify the cause by enabling "hanging-process" reporter. See https://vitest.dev/config/#reporters

The hanging-process reporter points to file system watchers set by vite:css plugin.

Validations

@maxpatiiuk
Copy link
Author

Until the bug is fixed, my workaround is to add this to my vitest global teardown script:

    /**
     * Workaround for https://github.com/vitejs/vite/issues/18224
     */
    const intervalTime = 100;
    const interval = setInterval(() => {
      void devServer.watcher.close();
    }, intervalTime);
    // Prevent interval from keeping the process open
    interval.unref();

@maxpatiiuk maxpatiiuk changed the title Vite can hang the node.js process if server is exited during file transform Vite can hang the node.js process if server is closed during file transform Sep 29, 2024
@sapphi-red sapphi-red added the p2-edge-case Bug, but has workaround or limited in scope (priority) label Sep 30, 2024
@20manas
Copy link

20manas commented Oct 3, 2024

Encountered the same issue. Last version which seems to be working is 5.3.5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
p2-edge-case Bug, but has workaround or limited in scope (priority)
Projects
None yet
Development

No branches or pull requests

3 participants