denoland/deno #7824 調査メモ
Windowsだけで発生するAddrInUse
を直すために頑張ってみる
import { serve } from "https://deno.land/std/http/server.ts";
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of s) {
req.respond({ body: "Hello world\n" });
}
このコードを deno run --watch --unstable --allow-net net.ts
で起動して、その後ファイルを編集してwatcherによる再起動をやると AddrInUse
が出るとのこと。
手元の環境(Linux)では再現しなかったのと、issueではWindowsを使っている方からの報告が相次いでいることから、Windows固有のエラーと思われる。
serve({ port: 8000 })
したときに呼び出される op は
function listen({ hostname, ...options }) {
const res = opListen({
transport: "tcp",
hostname: typeof hostname === "undefined" ? "0.0.0.0" : hostname,
...options,
});
return new Listener(res.rid, res.localAddr);
}
より op_listen
だと分かる。
op_listen
は以下のように定義されている
fn op_listen(
state: &mut OpState,
args: Value,
_zero_copy: &mut [ZeroCopyBuf],
) -> Result<Value, AnyError> {
let permissions = state.borrow::<Permissions>();
match serde_json::from_value(args)? {
ListenArgs {
transport,
transport_args: ArgsEnum::Ip(args),
} => {
{
if transport == "udp" {
super::check_unstable(state, "Deno.listenDatagram");
}
permissions.check_net(&args.hostname, args.port)?;
}
let addr = resolve_addr(&args.hostname, args.port)?;
let (rid, local_addr) = if transport == "tcp" {
listen_tcp(state, addr)?
} else {
listen_udp(state, addr)?
};
debug!(
"New listener {} {}:{}",
rid,
local_addr.ip().to_string(),
local_addr.port()
);
Ok(json!({
"rid": rid,
"localAddr": {
"hostname": local_addr.ip().to_string(),
"port": local_addr.port(),
"transport": transport,
},
}))
}
// 以下略
listen_tcp(state, addr)?
が怪しそう
listen_tcp
の先頭の行に let std_listener = std::net::TcpListener::bind(&addr)?;
がある
std::net
の中身を追っていってみる。
ここで知りたいのは、LinuxだとOKでWindowsだとだめなのはなぜなのか、ということ。watcherで再起動する際、特別な処理はしていない(押さえたポートを解放するとか)が、Linuxではこれでも問題なく動いている。bindしたポートの解放処理はどこで行われるのか?(予想は何かがDropされたとき、だが、どうだろうか)
Rust標準ライブラリの TcpListener::bind
が以下の通り
pub fn bind(addr: io::Result<&SocketAddr>) -> io::Result<TcpListener> {
let addr = addr?;
init();
let sock = Socket::new(addr, c::SOCK_STREAM)?;
// On platforms with Berkeley-derived sockets, this allows to quickly
// rebind a socket, without needing to wait for the OS to clean up the
// previous one.
//
// On Windows, this allows rebinding sockets which are actively in use,
// which allows “socket hijacking”, so we explicitly don't set it here.
// https://docs.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse
#[cfg(not(windows))]
setsockopt(&sock, c::SOL_SOCKET, c::SO_REUSEADDR, 1 as c_int)?;
// Bind our new socket
let (addrp, len) = addr.into_inner();
cvt(unsafe { c::bind(*sock.as_inner(), addrp, len as _) })?;
// Start listening
cvt(unsafe { c::listen(*sock.as_inner(), 128) })?;
Ok(TcpListener { inner: sock })
}
何やら興味深いコメントがある 👀
On Windows, this allows rebinding sockets which are actively in use,
which allows “socket hijacking”, so we explicitly don't set it here.
https://docs.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse
ポートをあとから奪えるモードにしていると、"socket hijacking" が起きてセキュリティ的に危ないよ、みたいなことが書いてあった(多分)
なので windows ではこのポート奪えるモードにはしていない、ということっぽい。
一方、Windows以外ではポートあとから奪えるモードを設定しているため、以前使っていたポートが使用中であっても問題なく使える、ということが今回の問題の原因と考えられる。
ちなみに、windowsの Socket
の Drop
を探しに行ったら、
// https://github.com/rust-lang/rust/blob/1.48.0/library/std/src/sys/windows/net.rs#L414-L418
impl Drop for Socket {
fn drop(&mut self) {
let _ = unsafe { c::closesocket(self.0) };
}
}
が見つかった。 c::closesocket
はWin32 API の
だと思われる。
結局、watcherがファイル変更を検知して再起動をしようとするときに、今起動している分の後始末をきちんとしてから再起動をしないといけない、という問題に帰着
select! {
_ = debounce.next() => {
is_file_changed = true;
info!(
"{} File change detected! Restarting!",
colors::intense_blue("Watcher"),
);
},
_ = func => {},
};
具体的には↑で、ファイル変更検知が先に起こったら (=debounce.next()
が先に Ready
になったら)、func
のほうをキレイにしないといけない。
tokio::select!
のドキュメント を見ると、一番最初に完了したブランチ以外は "cancel" される、と書いてあるが、この "cancel" が具体的にどのような処理を表しているのかはよくわからない。
Refs:
debounce.next()
が先に終わったことによって select!
が完了したら、その後に長い sleep を挟んで、select!
後にバインドしたポートが解放されているか を確認してみる。つまり、func
によって押さえられたリソースの解放は select!
の "canceling" によって行われるのか、を確認する。
Linux上で上記の検証を行ったところ、長い sleep のときにはポートは解放されていた。つまり、リソース解放は問題なく行われているということ。
いよいよWindowsで検証しないとよくわからない状況になってきたので、なんとかする
Denoのコードリーディングをしているときに気になった doc commentの修正漏れ
Returns a future.
とあるけど実際はただの Result
を返しているというもの
/// Resolve network address. Returns a future.
pub fn resolve_addr(hostname: &str, port: u16) -> Result<SocketAddr, AnyError> {
// Default to localhost if given just the port. Example: ":80"
// 以下略
(https://github.com/denoland/deno/blob/v1.6.0/cli/resolve_addr.rs#L7 より)
これを直そうと思っていたら、to_socket_addrs()
が同期APIで、tokioにそれの非同期版APIとして lookup_host
という関数 が用意されていることを知った。
そのあたりをまとめて、テストも追加して、PRを立てた。