2025-06-01 07:51:20 +00:00
|
|
|
import * as plugins from '../../plugins.js';
|
|
|
|
|
2025-06-01 12:27:15 +00:00
|
|
|
export interface CleanupOptions {
|
|
|
|
immediate?: boolean; // Force immediate destruction
|
|
|
|
allowDrain?: boolean; // Allow write buffer to drain
|
|
|
|
gracePeriod?: number; // Ms to wait before force close
|
|
|
|
}
|
|
|
|
|
2025-06-01 13:15:50 +00:00
|
|
|
export interface SafeSocketOptions {
|
|
|
|
port: number;
|
|
|
|
host: string;
|
|
|
|
onError?: (error: Error) => void;
|
|
|
|
onConnect?: () => void;
|
|
|
|
timeout?: number;
|
|
|
|
}
|
|
|
|
|
2025-06-01 07:51:20 +00:00
|
|
|
/**
|
|
|
|
* Safely cleanup a socket by removing all listeners and destroying it
|
|
|
|
* @param socket The socket to cleanup
|
|
|
|
* @param socketName Optional name for logging
|
2025-06-01 12:27:15 +00:00
|
|
|
* @param options Cleanup options
|
2025-06-01 07:51:20 +00:00
|
|
|
*/
|
2025-06-01 12:27:15 +00:00
|
|
|
export function cleanupSocket(
|
|
|
|
socket: plugins.net.Socket | plugins.tls.TLSSocket | null,
|
|
|
|
socketName?: string,
|
|
|
|
options: CleanupOptions = {}
|
|
|
|
): Promise<void> {
|
|
|
|
if (!socket || socket.destroyed) return Promise.resolve();
|
2025-06-01 07:51:20 +00:00
|
|
|
|
2025-06-01 12:27:15 +00:00
|
|
|
return new Promise<void>((resolve) => {
|
|
|
|
const cleanup = () => {
|
|
|
|
try {
|
|
|
|
// Remove all event listeners
|
|
|
|
socket.removeAllListeners();
|
|
|
|
|
|
|
|
// Destroy if not already destroyed
|
|
|
|
if (!socket.destroyed) {
|
|
|
|
socket.destroy();
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.error(`Error cleaning up socket${socketName ? ` (${socketName})` : ''}: ${err}`);
|
|
|
|
}
|
|
|
|
resolve();
|
|
|
|
};
|
2025-06-01 07:51:20 +00:00
|
|
|
|
2025-06-01 12:27:15 +00:00
|
|
|
if (options.immediate) {
|
|
|
|
// Immediate cleanup (old behavior)
|
|
|
|
socket.unpipe();
|
|
|
|
cleanup();
|
|
|
|
} else if (options.allowDrain && socket.writable) {
|
|
|
|
// Allow pending writes to complete
|
|
|
|
socket.end(() => cleanup());
|
|
|
|
|
|
|
|
// Force cleanup after grace period
|
|
|
|
if (options.gracePeriod) {
|
|
|
|
setTimeout(() => {
|
|
|
|
if (!socket.destroyed) {
|
|
|
|
cleanup();
|
|
|
|
}
|
|
|
|
}, options.gracePeriod);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Default: immediate cleanup
|
|
|
|
socket.unpipe();
|
|
|
|
cleanup();
|
2025-06-01 07:51:20 +00:00
|
|
|
}
|
2025-06-01 12:27:15 +00:00
|
|
|
});
|
2025-06-01 07:51:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a cleanup handler for paired sockets (client and server)
|
|
|
|
* @param clientSocket The client socket
|
|
|
|
* @param serverSocket The server socket (optional)
|
|
|
|
* @param onCleanup Optional callback when cleanup is done
|
|
|
|
* @returns A cleanup function that can be called multiple times safely
|
2025-06-01 12:27:15 +00:00
|
|
|
* @deprecated Use createIndependentSocketHandlers for better half-open support
|
2025-06-01 07:51:20 +00:00
|
|
|
*/
|
|
|
|
export function createSocketCleanupHandler(
|
|
|
|
clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
|
|
|
|
serverSocket?: plugins.net.Socket | plugins.tls.TLSSocket | null,
|
|
|
|
onCleanup?: (reason: string) => void
|
|
|
|
): (reason: string) => void {
|
|
|
|
let cleanedUp = false;
|
|
|
|
|
|
|
|
return (reason: string) => {
|
|
|
|
if (cleanedUp) return;
|
|
|
|
cleanedUp = true;
|
|
|
|
|
2025-06-01 12:27:15 +00:00
|
|
|
// Cleanup both sockets (old behavior - too aggressive)
|
|
|
|
cleanupSocket(clientSocket, 'client', { immediate: true });
|
2025-06-01 07:51:20 +00:00
|
|
|
if (serverSocket) {
|
2025-06-01 12:27:15 +00:00
|
|
|
cleanupSocket(serverSocket, 'server', { immediate: true });
|
2025-06-01 07:51:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Call cleanup callback if provided
|
|
|
|
if (onCleanup) {
|
|
|
|
onCleanup(reason);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2025-06-01 12:27:15 +00:00
|
|
|
/**
|
|
|
|
* Create independent cleanup handlers for paired sockets that support half-open connections
|
|
|
|
* @param clientSocket The client socket
|
|
|
|
* @param serverSocket The server socket
|
|
|
|
* @param onBothClosed Callback when both sockets are closed
|
|
|
|
* @returns Independent cleanup functions for each socket
|
|
|
|
*/
|
|
|
|
export function createIndependentSocketHandlers(
|
|
|
|
clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
|
|
|
|
serverSocket: plugins.net.Socket | plugins.tls.TLSSocket,
|
|
|
|
onBothClosed: (reason: string) => void
|
|
|
|
): { cleanupClient: (reason: string) => Promise<void>, cleanupServer: (reason: string) => Promise<void> } {
|
|
|
|
let clientClosed = false;
|
|
|
|
let serverClosed = false;
|
|
|
|
let clientReason = '';
|
|
|
|
let serverReason = '';
|
|
|
|
|
|
|
|
const checkBothClosed = () => {
|
|
|
|
if (clientClosed && serverClosed) {
|
|
|
|
onBothClosed(`client: ${clientReason}, server: ${serverReason}`);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const cleanupClient = async (reason: string) => {
|
|
|
|
if (clientClosed) return;
|
|
|
|
clientClosed = true;
|
|
|
|
clientReason = reason;
|
|
|
|
|
|
|
|
// Allow server to continue if still active
|
|
|
|
if (!serverClosed && serverSocket.writable) {
|
|
|
|
// Half-close: stop reading from client, let server finish
|
|
|
|
clientSocket.pause();
|
|
|
|
clientSocket.unpipe(serverSocket);
|
|
|
|
await cleanupSocket(clientSocket, 'client', { allowDrain: true, gracePeriod: 5000 });
|
|
|
|
} else {
|
|
|
|
await cleanupSocket(clientSocket, 'client', { immediate: true });
|
|
|
|
}
|
|
|
|
|
|
|
|
checkBothClosed();
|
|
|
|
};
|
|
|
|
|
|
|
|
const cleanupServer = async (reason: string) => {
|
|
|
|
if (serverClosed) return;
|
|
|
|
serverClosed = true;
|
|
|
|
serverReason = reason;
|
|
|
|
|
|
|
|
// Allow client to continue if still active
|
|
|
|
if (!clientClosed && clientSocket.writable) {
|
|
|
|
// Half-close: stop reading from server, let client finish
|
|
|
|
serverSocket.pause();
|
|
|
|
serverSocket.unpipe(clientSocket);
|
|
|
|
await cleanupSocket(serverSocket, 'server', { allowDrain: true, gracePeriod: 5000 });
|
|
|
|
} else {
|
|
|
|
await cleanupSocket(serverSocket, 'server', { immediate: true });
|
|
|
|
}
|
|
|
|
|
|
|
|
checkBothClosed();
|
|
|
|
};
|
|
|
|
|
|
|
|
return { cleanupClient, cleanupServer };
|
|
|
|
}
|
|
|
|
|
2025-06-01 07:51:20 +00:00
|
|
|
/**
|
|
|
|
* Setup socket error and close handlers with proper cleanup
|
|
|
|
* @param socket The socket to setup handlers for
|
|
|
|
* @param handleClose The cleanup function to call
|
2025-06-01 12:27:15 +00:00
|
|
|
* @param handleTimeout Optional custom timeout handler
|
2025-06-01 07:51:20 +00:00
|
|
|
* @param errorPrefix Optional prefix for error messages
|
|
|
|
*/
|
|
|
|
export function setupSocketHandlers(
|
|
|
|
socket: plugins.net.Socket | plugins.tls.TLSSocket,
|
|
|
|
handleClose: (reason: string) => void,
|
2025-06-01 12:27:15 +00:00
|
|
|
handleTimeout?: (socket: plugins.net.Socket | plugins.tls.TLSSocket) => void,
|
2025-06-01 07:51:20 +00:00
|
|
|
errorPrefix?: string
|
|
|
|
): void {
|
|
|
|
socket.on('error', (error) => {
|
|
|
|
const prefix = errorPrefix || 'Socket';
|
|
|
|
handleClose(`${prefix}_error: ${error.message}`);
|
|
|
|
});
|
|
|
|
|
|
|
|
socket.on('close', () => {
|
|
|
|
const prefix = errorPrefix || 'socket';
|
|
|
|
handleClose(`${prefix}_closed`);
|
|
|
|
});
|
|
|
|
|
|
|
|
socket.on('timeout', () => {
|
2025-06-01 12:27:15 +00:00
|
|
|
if (handleTimeout) {
|
|
|
|
handleTimeout(socket); // Custom timeout handling
|
|
|
|
} else {
|
|
|
|
// Default: just log, don't close
|
|
|
|
console.warn(`Socket timeout: ${errorPrefix || 'socket'}`);
|
|
|
|
}
|
2025-06-01 07:51:20 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pipe two sockets together with proper cleanup on either end
|
|
|
|
* @param socket1 First socket
|
|
|
|
* @param socket2 Second socket
|
|
|
|
*/
|
|
|
|
export function pipeSockets(
|
|
|
|
socket1: plugins.net.Socket | plugins.tls.TLSSocket,
|
|
|
|
socket2: plugins.net.Socket | plugins.tls.TLSSocket
|
|
|
|
): void {
|
|
|
|
socket1.pipe(socket2);
|
|
|
|
socket2.pipe(socket1);
|
2025-06-01 13:15:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a socket with immediate error handling to prevent crashes
|
|
|
|
* @param options Socket creation options
|
|
|
|
* @returns The created socket
|
|
|
|
*/
|
|
|
|
export function createSocketWithErrorHandler(options: SafeSocketOptions): plugins.net.Socket {
|
|
|
|
const { port, host, onError, onConnect, timeout } = options;
|
|
|
|
|
|
|
|
// Create socket with immediate error handler attachment
|
|
|
|
const socket = new plugins.net.Socket();
|
|
|
|
|
|
|
|
// Attach error handler BEFORE connecting to catch immediate errors
|
|
|
|
socket.on('error', (error) => {
|
|
|
|
console.error(`Socket connection error to ${host}:${port}: ${error.message}`);
|
|
|
|
if (onError) {
|
|
|
|
onError(error);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Attach connect handler if provided
|
|
|
|
if (onConnect) {
|
|
|
|
socket.on('connect', onConnect);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set timeout if provided
|
|
|
|
if (timeout) {
|
|
|
|
socket.setTimeout(timeout);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now attempt to connect - any immediate errors will be caught
|
|
|
|
socket.connect(port, host);
|
|
|
|
|
|
|
|
return socket;
|
2025-06-01 07:51:20 +00:00
|
|
|
}
|