From d4f7fbc718a86d0f76abba09228a383ac73e639a Mon Sep 17 00:00:00 2001 From: mikewong23571 Date: Fri, 13 Mar 2026 03:15:35 +0800 Subject: [PATCH] fix: align native daemon port hash with client on Windows (#734) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The client (connection.rs) and native daemon (native/daemon.rs) used different get_port_for_session() implementations on Windows: - Client: i32, .chars(), djb2 — (hash << 5) - hash + c - Daemon: i64, .bytes(), Java hashCode — hash * 31 + b For session name "default", client computes port 50838 while the daemon binds on 51174, causing a 5-second timeout and startup failure. Fix: align native/daemon.rs to use the identical djb2 algorithm from connection.rs (i32, chars, djb2), so both sides agree on the port. Unix is unaffected (uses Unix domain sockets, no port hashing). Tests: add port hash regression tests to all three implementations (native/daemon.rs, connection.rs, daemon.ts) to prevent future drift. Fixes #705 --- cli/src/connection.rs | 9 +++++++++ cli/src/native/daemon.rs | 25 +++++++++++++++++++++---- src/daemon.test.ts | 25 ++++++++++++++++++++++++- src/daemon.ts | 2 +- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/cli/src/connection.rs b/cli/src/connection.rs index dfd064a..f6ce676 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -792,4 +792,13 @@ mod tests { assert!(!is_transient_error("Permission denied")); assert!(!is_transient_error("Daemon not found")); } + + #[test] + #[cfg(windows)] + fn test_get_port_for_session() { + assert_eq!(get_port_for_session("default"), 50838); + assert_eq!(get_port_for_session("my-session"), 63105); + assert_eq!(get_port_for_session("work"), 51184); + assert_eq!(get_port_for_session(""), 49152); + } } diff --git a/cli/src/native/daemon.rs b/cli/src/native/daemon.rs index 464be34..3d2bfc3 100644 --- a/cli/src/native/daemon.rs +++ b/cli/src/native/daemon.rs @@ -258,9 +258,26 @@ fn get_daemon_socket_dir() -> PathBuf { #[cfg(windows)] fn get_port_for_session(session: &str) -> u16 { - let mut hash: i64 = 0; - for b in session.bytes() { - hash = hash.wrapping_mul(31).wrapping_add(b as i64); + let mut hash: i32 = 0; + for c in session.chars() { + hash = ((hash << 5).wrapping_sub(hash)).wrapping_add(c as i32); + } + 49152 + ((hash.unsigned_abs() as u32 % 16383) as u16) +} + +#[cfg(test)] +#[cfg(windows)] +mod tests { + use super::*; + + #[test] + fn test_port_matches_client_algorithm() { + // These values are computed by the identical djb2 implementation in + // connection.rs. Both sides must agree on the port for the daemon to + // start successfully. + assert_eq!(get_port_for_session("default"), 50838); + assert_eq!(get_port_for_session("my-session"), 63105); + assert_eq!(get_port_for_session("work"), 51184); + assert_eq!(get_port_for_session(""), 49152); } - 49152 + (hash.unsigned_abs() % 16383) as u16 } diff --git a/src/daemon.test.ts b/src/daemon.test.ts index bdafc0b..7ffcd1d 100644 --- a/src/daemon.test.ts +++ b/src/daemon.test.ts @@ -3,7 +3,7 @@ import * as os from 'os'; import * as path from 'path'; import * as net from 'net'; import { EventEmitter } from 'events'; -import { getSocketDir, safeWrite } from './daemon.js'; +import { getSocketDir, safeWrite, getPortForSession } from './daemon.js'; /** * HTTP request detection pattern used in daemon.ts to prevent cross-origin attacks. @@ -159,3 +159,26 @@ describe('safeWrite', () => { expect(socket.listenerCount('close')).toBe(0); }); }); + +describe('getPortForSession', () => { + it('returns consistent port for "default"', () => { + expect(getPortForSession('default')).toBe(50838); + }); + + it('returns consistent port for named sessions', () => { + expect(getPortForSession('my-session')).toBe(63105); + expect(getPortForSession('work')).toBe(51184); + }); + + it('returns base port for empty session', () => { + expect(getPortForSession('')).toBe(49152); + }); + + it('returns port within dynamic range (49152-65535)', () => { + for (const name of ['default', 'my-session', 'work', 'test', 'a']) { + const port = getPortForSession(name); + expect(port).toBeGreaterThanOrEqual(49152); + expect(port).toBeLessThanOrEqual(65535); + } + }); +}); diff --git a/src/daemon.ts b/src/daemon.ts index 220b2e9..cfa6cc1 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -185,7 +185,7 @@ export function getSession(): string { * Get port number for TCP mode (Windows) * Uses a hash of the session name to get a consistent port */ -function getPortForSession(session: string): number { +export function getPortForSession(session: string): number { let hash = 0; for (let i = 0; i < session.length; i++) { hash = (hash << 5) - hash + session.charCodeAt(i);