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

Pressing Ctrl+C when --inspector-brk is used but debugger is not connected hangs Vitest #5511

Closed
6 tasks done
sheremet-va opened this issue Apr 8, 2024 · 7 comments
Closed
6 tasks done
Labels
p3-minor-bug An edge case that only affects very specific usage (priority)

Comments

@sheremet-va
Copy link
Member

Describe the bug

When trying to debug Vitest with a break, Vitest hangs indefinitely if no connection is ever established.

Reproduction

vitest --inspect-brk --no-file-parallelism
Ctrl+C

https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/basic?initialPath=__vitest__/

System Info

Any

Used Package Manager

npm

Validations

@sheremet-va sheremet-va added pending triage p3-minor-bug An edge case that only affects very specific usage (priority) and removed pending triage labels Apr 8, 2024
@AriPerkkio
Copy link
Member

AriPerkkio commented Apr 8, 2024

This is what's happening when CTRL+c is pressed:

import { isMainThread, Worker } from "node:worker_threads";
import { fileURLToPath } from "node:url";
import inspector from "node:inspector";

const filename = fileURLToPath(import.meta.url);

if (isMainThread) {
  const worker = new Worker(filename);
  await new Promise((r) => setTimeout(r, 1_000));

  console.log("Terminating...");
  const timeout = setTimeout(() => console.log("Stuck here"), 2_000);
  await worker.terminate();

  clearTimeout(timeout);
  console.log("Done");
} else {
  inspector.open();
  inspector.waitForDebugger();
}
$ node worker.mjs 
Debugger listening on ws://127.0.0.1:9229/65cec5ee-87d3-4a0c-b481-ac9d257a96f5
For help, see: https://nodejs.org/en/docs/inspector
Terminating...
Stuck here

The ctx.close() is stuck here:

close: () => pool.destroy(),

@sheremet-va
Copy link
Member Author

Maybe we can send the event Runtime.runIfWaitingForDebugger somehow before terminating 🤔

@AriPerkkio
Copy link
Member

For that I think we would need to connect to the Worker's inspector session from main thread with web socket. There's also session.connectToMainThread() but I've never got it working and there are no examples for it anywhere.

For the inspector session's web socket connection we have similar setups here:

await vitest.waitForStderr('Debugger listening on ')
const url = vitest.stderr.split('\n')[0].replace('Debugger listening on ', '')
const { receive, send } = await createChannel(url)

Worker not terminating when it's paused in waitForDebugger() sounds like a Node bug to me. Though it's still somehow able to terminate when default CTRL+c happens and process.stdin raw mode is not overwritten. 🤔

@sheremet-va
Copy link
Member Author

sheremet-va commented Apr 9, 2024

For that I think we would need to connect to the Worker's inspector session from main thread with web socket.

Can't we send postMessage from the main thread to the worker where the inspector is already initiated? If setTimeout is not blocked, I assume it also will not be blocked

@AriPerkkio
Copy link
Member

AriPerkkio commented Apr 9, 2024

Inside the Worker setTimeout should be blocked when waitForDebugger() is active. I don't think Worker receives anything from parentPort.postMessage either.

But connecting to the inspector session and sending Runtime.runIfWaitingForDebugger like done here should work:

const paused = receive('Debugger.paused')
send({ method: 'Runtime.runIfWaitingForDebugger' })
const { params } = await paused

@AriPerkkio
Copy link
Member

This would work:

import { isMainThread, parentPort, Worker } from "node:worker_threads";
import { fileURLToPath } from "node:url";
import inspector from "node:inspector";
import WebSocket from "ws";

const filename = fileURLToPath(import.meta.url);

if (isMainThread) {
  const worker = new Worker(filename);
  const url = await new Promise((resolve) => worker.on("message", resolve));

  const timeout = setTimeout(async () => {
    console.log("Timeout");

    const ws = new WebSocket(url);
    await new Promise((resolve) => ws.on("open", resolve));

    ws.send(JSON.stringify({ method: "Runtime.runIfWaitingForDebugger", id: 2 }));
    ws.close();
  }, 2_000).unref();

  await worker.terminate();
  clearTimeout(timeout);
  console.log("Done");
} else {
  inspector.open();
  parentPort.postMessage(inspector.url());
  inspector.waitForDebugger();

  console.log("Running"); // Never logged
}

@AriPerkkio
Copy link
Member

AriPerkkio commented May 17, 2024

This should be fixed in Node v22.2.0: nodejs/node#52971

$ node -v
v22.2.0

$ node worker-repro.mjs 
Debugger listening on ws://127.0.0.1:9229/2b239484-37a6-4a4d-ba9d-099db758906d
For help, see: https://nodejs.org/en/docs/inspector
Terminating...
Done 

@github-actions github-actions bot locked and limited conversation to collaborators Jun 1, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
p3-minor-bug An edge case that only affects very specific usage (priority)
Projects
None yet
Development

No branches or pull requests

2 participants