Skip to content

fix(one): use a named pipe for the daemon IPC socket on Windows#725

Open
YevheniiKotyrlo wants to merge 1 commit into
onejs:mainfrom
YevheniiKotyrlo:fix/daemon-windows-named-pipe
Open

fix(one): use a named pipe for the daemon IPC socket on Windows#725
YevheniiKotyrlo wants to merge 1 commit into
onejs:mainfrom
YevheniiKotyrlo:fix/daemon-windows-named-pipe

Conversation

@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor

Summary

one daemon crashes immediately on Windows. The CLI↔daemon control channel binds a local socket with net.Server.listen(SOCKET_PATH), where SOCKET_PATH is ~/.one/daemon.sock. On Windows, Node implements local-domain sockets as named pipes, and the path must live under \\.\pipe\ — so a filesystem .sock path is rejected with EACCES. Because createIPCServer had no server.on('error') handler, that surfaced as an uncaught 'error' event and crashed the command on start:

Error: listen EACCES: permission denied C:\Users\…\.one\daemon.sock
    at createIPCServer (packages/one/src/daemon/ipc.ts)
    at startDaemon       (packages/one/src/daemon/server.ts)
    code: 'EACCES', syscall: 'listen'

Fix

Select each platform's native local-IPC primitive behind one helper, keeping the existing hand-rolled net server and the owner-only model:

function resolveSocketPath(): string {
  if (process.platform === 'win32') {
    const scope = createHash('sha256').update(SOCKET_DIR).digest('hex').slice(0, 8)
    return path.win32.join(String.raw`\\.\pipe`, `one-daemon-${scope}`)
  }
  return path.join(SOCKET_DIR, 'daemon.sock')
}
  • POSIX is unchanged — the AF_UNIX socket at ~/.one/daemon.sock, still chmod 0600 (now guarded, since chmod doesn't apply to a Windows pipe). The else branch is the original code path.
  • Windows uses a named pipe. The name is a deterministic per-user hash so the daemon and the independently-launched CLI derive the same pipe, and distinct users don't collide in the machine-global pipe namespace.
  • isDaemonRunning no longer pre-checks fs.existsSync(SOCKET_PATH) on Windows (a named pipe is not a filesystem entry, so the check always returned false and the daemon read as "not running"); it probes by connecting.
  • Added a server.on('error') handler so a bind failure reports an actionable message instead of crashing.

This is the standard approach for a fixed-name local daemon socket — keep the socket, pick the OS-native primitive. It matches Node's own net docs ("On Windows, the local domain is implemented using a named pipe … the path must refer to an entry in \\?\pipe\ or \\.\pipe\"), Nuxt (packages/vite/src/plugins/vite-node.ts), the OpenSSH agent ($SSH_AUTH_SOCK\\.\pipe\openssh-ssh-agent), VS Code (createStaticIPCHandle), Nx, and pm2.

Test plan

Validated on Windows 11 (Node 25):

  • Before: one daemon → uncaught EACCES on listen ~/.one/daemon.sock.
  • After: the named-pipe endpoint binds and round-trips (the daemon's ping/pong) where the .sock path failed.
  • oxlint and oxfmt --check clean on the changed file; tsc --noEmit reports no errors in it.
  • POSIX behaviour is unchanged by construction — its branch is the original path, and on Linux the resolved endpoint and permissions are identical to before this change.

Note on CI

The tests job exercises this change. The checks job's Security Audit step is currently red on a pre-existing, repo-wide advisory unrelated to this PR — hono <4.12.25 (GHSA-88fw-hqm2-52qc, hono is pinned to 4.12.5 in root resolutions), which also fails on main and is not in the workflow's --ignore list. It clears when hono is bumped; nothing in this PR touches it.

`one daemon` binds its CLI<->daemon control channel with
net.Server.listen(SOCKET_PATH). On Windows, Node implements local-domain
sockets as named pipes and the path must live under \.\pipe\, so a filesystem
~/.one/daemon.sock path is rejected with EACCES — which surfaced as an uncaught
server 'error' event and crashed the command on start.

Select each platform's native local-IPC primitive (the same approach VS Code,
Nx, pm2 and Nuxt's vite-node use): keep the AF_UNIX socket file on POSIX, use a
named pipe on Windows. The pipe name is a deterministic per-user hash so the
daemon and the CLI derive the same name independently and distinct users don't
collide in the machine-global pipe namespace.

Preserves the existing owner-only intent (chmod 0600 on the POSIX socket, now
guarded since it doesn't apply to a Windows pipe), drops the
fs.existsSync(SOCKET_PATH) precheck on Windows (a named pipe is not a filesystem
entry), and adds a server 'error' handler so a bind failure reports actionably
instead of crashing.

Validated on Windows 11: the named-pipe endpoint binds and round-trips
(ping/pong) where the .sock path returned EACCES.
@YevheniiKotyrlo YevheniiKotyrlo force-pushed the fix/daemon-windows-named-pipe branch from 40ca140 to 77ce259 Compare June 26, 2026 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant