Repository: denoland/deno
Pull request: fix(ext/net): remove socket file when dropping unix listener

This PR wraps `UnixListener`, adds socket path info to it, and removes the socket file at drop time. By this change, removing unix listener automatically cleans up the socket file, which enables, for example, running unix domain socket server script with `--watch` option.

closes #31929
(probably also) closes #16699 (see https://github.com/denoland/deno/issues/16699#issuecomment-2406243583 )

Changed files:
filename: ext/net/ops_unix.rs
patch: @@ -47,6 +47,34 @@ impl Resource for UnixDatagramResource {
   }
 }
 
+/// A wrapper around `UnixListener` that holds the socket path and removes it on drop.
+pub struct UnixListenerWithPath {
+  listener: UnixListener,
+  path: PathBuf,
+}
+
+impl UnixListenerWithPath {
+  pub fn new(listener: UnixListener, path: PathBuf) -> Self {
+    Self { listener, path }
+  }
+
+  pub async fn accept(
+    &self,
+  ) -> std::io::Result<(UnixStream, tokio::net::unix::SocketAddr)> {
+    self.listener.accept().await
+  }
+
+  pub fn local_addr(&self) -> std::io::Result<tokio::net::unix::SocketAddr> {
+    self.listener.local_addr()
+  }
+}
+
+impl Drop for UnixListenerWithPath {
+  fn drop(&mut self) {
+    let _ = std::fs::remove_file(&self.path);
+  }
+}
+
 #[derive(Serialize)]
 pub struct UnixAddr {
   pub path: Option<String>,
@@ -66,7 +94,7 @@ pub async fn op_net_accept_unix(
   let resource = state
     .borrow()
     .resource_table
-    .get::<NetworkListenerResource<UnixListener>>(rid)
+    .get::<NetworkListenerResource<UnixListenerWithPath>>(rid)
     .map_err(|_| NetError::ListenerClosed)?;
   let listener = RcRef::map(&resource, |r| &r.listener)
     .try_borrow_mut()
@@ -188,10 +216,12 @@ pub fn op_net_listen_unix(
       Some(&api_call_expr),
     )
     .map_err(NetError::Permission)?;
-  let listener = UnixListener::bind(address_path)?;
+  let listener = UnixListener::bind(&address_path)?;
   let local_addr = listener.local_addr()?;
   let pathname = local_addr.as_pathname().map(pathstring).transpose()?;
-  let listener_resource = NetworkListenerResource::new(listener);
+  let listener_with_path =
+    UnixListenerWithPath::new(listener, address_path.to_path_buf());
+  let listener_resource = NetworkListenerResource::new(listener_with_path);
   let rid = state.resource_table.add(listener_resource);
   Ok((rid, pathname))
 }

filename: ext/net/raw.rs
patch: @@ -442,7 +442,7 @@ network_stream!(
     Unix,
     unix,
     tokio::net::UnixStream,
-    tokio::net::UnixListener,
+    crate::ops_unix::UnixListenerWithPath,
     tokio::net::unix::SocketAddr,
     crate::io::UnixStreamResource,
     tokio::net::unix::OwnedReadHalf,

filename: tests/integration/watcher_tests.rs
patch: @@ -1535,6 +1535,49 @@ async fn test_watch_serve() {
   check_alive_then_kill(child);
 }
 
+/// Ensures that Deno.serve with unix socket operates properly after a watch restart.
+#[cfg(unix)]
+#[test(flaky)]
+async fn test_watch_serve_unix_socket() {
+  let t = TempDir::new();
+  let socket_path = t.path().join("test.sock");
+  let file_to_watch = t.path().join("file_to_watch.js");
+  let file_content = format!(
+    r#"
+      console.error("serving");
+      await Deno.serve({{path: "{}"}}, () => new Response("hello"));
+    "#,
+    socket_path.to_string().replace('\\', "\\\\")
+  );
+  file_to_watch.write(&file_content);
+
+  let mut child = util::deno_cmd()
+    .current_dir(t.path())
+    .arg("run")
+    .arg("--watch")
+    .arg("--allow-read")
+    .arg("--allow-write")
+    .arg("-L")
+    .arg("debug")
+    .arg(&file_to_watch)
+    .env("NO_COLOR", "1")
+    .piped_output()
+    .spawn()
+    .unwrap();
+  let (mut _stdout_lines, mut stderr_lines) = child_lines(&mut child);
+
+  wait_contains("Listening on", &mut stderr_lines).await;
+  // Note that we start serving very quickly, so we specifically want to wait for this message
+  wait_contains(r#"Watching paths: [""#, &mut stderr_lines).await;
+
+  file_to_watch.write(&file_content);
+
+  wait_contains("serving", &mut stderr_lines).await;
+  wait_contains("Listening on", &mut stderr_lines).await;
+
+  check_alive_then_kill(child);
+}
+
 #[test(flaky)]
 async fn run_watch_dynamic_imports() {
   let t = TempDir::new();

filename: tests/unit/remove_test.ts
patch: @@ -247,11 +247,11 @@ Deno.test(
       // MAKE TEMPORARY UNIX SOCKET
       const path = Deno.makeTempDirSync() + "/test.sock";
       const listener = Deno.listen({ transport: "unix", path });
-      listener.close();
       Deno.statSync(path); // check if unix socket exists
 
       await Deno[method](path);
       assertThrows(() => Deno.statSync(path), Deno.errors.NotFound);
+      listener.close();
     }
   },
 );