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

Deno.Listener#close() doesn't release the address after accept() called #25480

Closed
kt3k opened this issue Sep 6, 2024 · 4 comments
Closed

Deno.Listener#close() doesn't release the address after accept() called #25480

kt3k opened this issue Sep 6, 2024 · 4 comments
Labels
bug Something isn't working correctly

Comments

@kt3k
Copy link
Member

kt3k commented Sep 6, 2024

Deno.Listener#close() can release the listening address synchronously if accept() is not called yet. The below example runs without errors:

let listener = Deno.listen({ port: 3000 });
listener.close();
listener = Deno.listen({ port: 3000 });
listener.close();

However if accept() is called on listener, it can't release the address synchronously at close() call. The below example throws AddrInUse error:

let listener = Deno.listen({ port: 3000 });
const a = listener.accept().then(() => console.log("accepted"), err => console.log(err));
listener.close();
listener = Deno.listen({ port: 3000 });
listener.close();

This behavior blocks port-scanning npm packages (e.g. npm:portfinder, npm:detect-port). ref #25175 #18301

@kt3k
Copy link
Member Author

kt3k commented Sep 6, 2024

I tried this patch to force close the tcp socket fd kt3k@31cdde6

But it ended up with the error fatal runtime error: IO Safety violation: owned file descriptor already closed with the 2nd snippet above. I'm not sure there's a way to avoid this error.

kt3k added a commit that referenced this issue Sep 8, 2024
A workaround for the issue #25480

`Deno.Listener` can't be closed synchronously after `accept()` is
called. This PR delays the `accept` call 2 ticks (The listener callback
is called 1 tick later. So the 1 tick delay is not enough), and makes
`net.Server` capable of being closed synchronously.

This unblocks `npm:detect-port` and `npm:portfinder`

closes #18301 
closes #25175
@littledivy
Copy link
Member

@kt3k Does it work if you move the net_cancel_listener_tcp op after core.close? Like this:

  close() {
    core.close(this.#rid);
    if (this.addr.transport === "tcp") {
      op_net_cancel_listener_tcp(this.#rid);
    }
  }

@littledivy
Copy link
Member

Here's an strace for Deno vs Node. https://gist.github.com/littledivy/21d35550829f7dae078c023075af3c70

close() on the first socket is not called by the time it is trying to bind it to a different port. By design, CancelHandle cancellation happens on the next tick. So:

const server = net.createServer(() => {}); // <-- accept().try_or_cancel(&cancel_handle)?.await

server.once("listening", () => {
  server.close(); // <-- cancel_handle.cancel()
  server.listen(8081, host2); // <-- err, cancellation is not complete

  setTimeout(() => server.listen(8081, host2))  // <-- works, cancellation complete on next tick
});
  1. For Deno's own TcpListener, I actually think we should preserve this behavior because it works well with async cancellation. Maybe we could update the signature of close to return a Promise?
close() {
  core.close(this.#rid)
  return new Promise(r => setTimout(r, 0));
}
  1. For Node.js net API: portfinder is known to be depending on synchronously closing the fd. It should be using the server.close(() => {}) callback. I think your workaround is good enough to keep it working.

@kt3k
Copy link
Member Author

kt3k commented Sep 10, 2024

@kt3k Does it work if you move the net_cancel_listener_tcp op after core.close? Like this:

  close() {
    core.close(this.#rid);
    if (this.addr.transport === "tcp") {
      op_net_cancel_listener_tcp(this.#rid);
    }
  }

This caused BadResource: Listener has been closed

Here's an strace for Deno vs Node. https://gist.github.com/littledivy/21d35550829f7dae078c023075af3c70

close() on the first socket is not called by the time it is trying to bind it to a different port. By design, CancelHandle cancellation happens on the next tick. So:

const server = net.createServer(() => {}); // <-- accept().try_or_cancel(&cancel_handle)?.await

server.once("listening", () => {
  server.close(); // <-- cancel_handle.cancel()
  server.listen(8081, host2); // <-- err, cancellation is not complete

  setTimeout(() => server.listen(8081, host2))  // <-- works, cancellation complete on next tick
});

Thanks for the analysis! This is good to know.

  1. For Deno's own TcpListener, I actually think we should preserve this behavior because it works well with async cancellation. Maybe we could update the signature of close to return a Promise?
close() {
  core.close(this.#rid)
  return new Promise(r => setTimout(r, 0));
}

This works for Deno.Listener#close, but doesn't for some npm packages as you mention..

  1. For Node.js net API: portfinder is known to be depending on synchronously closing the fd. It should be using the server.close(() => {}) callback. I think your workaround is good enough to keep it working.

Divy, thanks for the analysis! I'm closing this for now as there seems no better way than that workaround.

@kt3k kt3k closed this as completed Sep 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working correctly
Projects
None yet
Development

No branches or pull requests

3 participants