fix: align native daemon port hash with client on Windows (#734)
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
This commit is contained in:
@@ -792,4 +792,13 @@ mod tests {
|
|||||||
assert!(!is_transient_error("Permission denied"));
|
assert!(!is_transient_error("Permission denied"));
|
||||||
assert!(!is_transient_error("Daemon not found"));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,9 +258,26 @@ fn get_daemon_socket_dir() -> PathBuf {
|
|||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn get_port_for_session(session: &str) -> u16 {
|
fn get_port_for_session(session: &str) -> u16 {
|
||||||
let mut hash: i64 = 0;
|
let mut hash: i32 = 0;
|
||||||
for b in session.bytes() {
|
for c in session.chars() {
|
||||||
hash = hash.wrapping_mul(31).wrapping_add(b as i64);
|
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-1
@@ -3,7 +3,7 @@ import * as os from 'os';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as net from 'net';
|
import * as net from 'net';
|
||||||
import { EventEmitter } from 'events';
|
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.
|
* 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);
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+1
-1
@@ -185,7 +185,7 @@ export function getSession(): string {
|
|||||||
* Get port number for TCP mode (Windows)
|
* Get port number for TCP mode (Windows)
|
||||||
* Uses a hash of the session name to get a consistent port
|
* 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;
|
let hash = 0;
|
||||||
for (let i = 0; i < session.length; i++) {
|
for (let i = 0; i < session.length; i++) {
|
||||||
hash = (hash << 5) - hash + session.charCodeAt(i);
|
hash = (hash << 5) - hash + session.charCodeAt(i);
|
||||||
|
|||||||
Reference in New Issue
Block a user