Browse Source
Nostr-Signature: 73671ae6535309f9eae164f7a3ec403b1bc818ef811b9692fd0122d0b72c2774 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 0df56b009f5afb77de334225ab30cff55586ac0cf48f5ee435428201a1e72dc357a0fb5e80ef196f5bd76d6d448056d25f0feab0b1bcbe45f9af1a2a0d5453camain
13 changed files with 460 additions and 9193 deletions
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,35 @@ |
|||||||
|
/** |
||||||
|
* Shared utilities for Nostr event handling |
||||||
|
* Consolidates duplicate functions used across the codebase |
||||||
|
*
|
||||||
|
* Based on NIP-01: https://github.com/nostr-protocol/nips/blob/master/01.md
|
||||||
|
*/ |
||||||
|
|
||||||
|
import type { NostrEvent } from '../types/nostr.js'; |
||||||
|
|
||||||
|
/** |
||||||
|
* Check if an event is a parameterized replaceable event (addressable event per NIP-01) |
||||||
|
*
|
||||||
|
* According to NIP-01: |
||||||
|
* - Replaceable events (10000-19999, 0, 3): Replaceable by kind+pubkey only (no d-tag needed) |
||||||
|
* - Addressable events (30000-39999): Addressable by kind+pubkey+d-tag (d-tag required) |
||||||
|
*
|
||||||
|
* This function returns true only for addressable events (30000-39999) that have a d-tag, |
||||||
|
* as these are the events that require a parameter (d-tag) to be uniquely identified. |
||||||
|
*
|
||||||
|
* @param event - The Nostr event to check |
||||||
|
* @returns true if the event is an addressable event (30000-39999) with a d-tag |
||||||
|
*/ |
||||||
|
export function isParameterizedReplaceable(event: NostrEvent): boolean { |
||||||
|
// Addressable events (30000-39999) require a d-tag to be addressable
|
||||||
|
// Per NIP-01: "for kind n such that 30000 <= n < 40000, events are addressable
|
||||||
|
// by their kind, pubkey and d tag value"
|
||||||
|
if (event.kind >= 30000 && event.kind < 40000) { |
||||||
|
const hasDTag = event.tags.some(t => t[0] === 'd' && t[1]); |
||||||
|
return hasDTag; |
||||||
|
} |
||||||
|
|
||||||
|
// Replaceable events (10000-19999, 0, 3) are NOT parameterized replaceable
|
||||||
|
// They are replaceable by kind+pubkey only, without needing a d-tag
|
||||||
|
return false; |
||||||
|
} |
||||||
@ -0,0 +1,147 @@ |
|||||||
|
/** |
||||||
|
* Aggressive process cleanup utilities to prevent zombie processes |
||||||
|
* Implements process group killing and explicit reaping |
||||||
|
*/ |
||||||
|
|
||||||
|
import { spawn, type ChildProcess } from 'child_process'; |
||||||
|
import logger from '../services/logger.js'; |
||||||
|
|
||||||
|
/** |
||||||
|
* Kill a process and attempt to kill its process group to prevent zombies |
||||||
|
* On Unix systems, tries to kill the process group using negative PID |
||||||
|
* Falls back to killing just the process if group kill fails |
||||||
|
*/ |
||||||
|
export function killProcessGroup(proc: ChildProcess, signal: NodeJS.Signals = 'SIGTERM'): void { |
||||||
|
if (!proc.pid) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
// First, try to kill just the process (most reliable)
|
||||||
|
if (!proc.killed) { |
||||||
|
proc.kill(signal); |
||||||
|
logger.debug({ pid: proc.pid, signal }, 'Killed process'); |
||||||
|
} |
||||||
|
} catch (err) { |
||||||
|
logger.debug({ pid: proc.pid, error: err }, 'Error killing process directly'); |
||||||
|
} |
||||||
|
|
||||||
|
// On Unix systems, try to kill the process group using negative PID
|
||||||
|
// This only works if the process is in its own process group
|
||||||
|
// Note: This may fail if the process wasn't spawned with its own group
|
||||||
|
if (process.platform !== 'win32') { |
||||||
|
try { |
||||||
|
// Try killing the process group (negative PID)
|
||||||
|
// This will fail if the process isn't a group leader, which is fine
|
||||||
|
process.kill(-proc.pid, signal); |
||||||
|
logger.debug({ pid: proc.pid, signal }, 'Killed process group'); |
||||||
|
} catch (err) { |
||||||
|
// Expected to fail if process isn't in its own group - that's okay
|
||||||
|
// We already killed the main process above
|
||||||
|
logger.debug({ pid: proc.pid }, 'Process group kill not applicable (process not in own group)'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Force kill a process group with SIGKILL after a grace period |
||||||
|
*/ |
||||||
|
export function forceKillProcessGroup( |
||||||
|
proc: ChildProcess, |
||||||
|
gracePeriodMs: number = 5000 |
||||||
|
): NodeJS.Timeout { |
||||||
|
return setTimeout(() => { |
||||||
|
if (proc.pid && !proc.killed) { |
||||||
|
try { |
||||||
|
killProcessGroup(proc, 'SIGKILL'); |
||||||
|
logger.warn({ pid: proc.pid }, 'Force killed process group with SIGKILL'); |
||||||
|
} catch (err) { |
||||||
|
logger.warn({ pid: proc.pid, error: err }, 'Failed to force kill process group'); |
||||||
|
} |
||||||
|
} |
||||||
|
}, gracePeriodMs); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Ensure all streams are closed to prevent resource leaks |
||||||
|
*/ |
||||||
|
export function closeProcessStreams(proc: ChildProcess): void { |
||||||
|
try { |
||||||
|
if (proc.stdin && !proc.stdin.destroyed) { |
||||||
|
proc.stdin.destroy(); |
||||||
|
} |
||||||
|
if (proc.stdout && !proc.stdout.destroyed) { |
||||||
|
proc.stdout.destroy(); |
||||||
|
} |
||||||
|
if (proc.stderr && !proc.stderr.destroyed) { |
||||||
|
proc.stderr.destroy(); |
||||||
|
} |
||||||
|
} catch (err) { |
||||||
|
logger.debug({ error: err }, 'Error closing process streams'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Comprehensive cleanup: kill process group, close streams, and clear timeouts |
||||||
|
*/ |
||||||
|
export function cleanupProcess( |
||||||
|
proc: ChildProcess, |
||||||
|
timeouts: Array<NodeJS.Timeout | null>, |
||||||
|
signal: NodeJS.Signals = 'SIGTERM' |
||||||
|
): void { |
||||||
|
// Clear all timeouts
|
||||||
|
for (const timeout of timeouts) { |
||||||
|
if (timeout) { |
||||||
|
clearTimeout(timeout); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Close all streams
|
||||||
|
closeProcessStreams(proc); |
||||||
|
|
||||||
|
// Kill process group
|
||||||
|
if (proc.pid && !proc.killed) { |
||||||
|
killProcessGroup(proc, signal); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Spawn a process with process group isolation to enable group killing |
||||||
|
* This is critical for preventing zombies when the process spawns children |
||||||
|
*/ |
||||||
|
export function spawnWithProcessGroup( |
||||||
|
command: string, |
||||||
|
args: string[], |
||||||
|
options: Parameters<typeof spawn>[2] = {} |
||||||
|
): ChildProcess { |
||||||
|
// Create a new process group by making the process a session leader
|
||||||
|
// This allows us to kill the entire process tree
|
||||||
|
const proc = spawn(command, args, { |
||||||
|
...options, |
||||||
|
detached: false, // Keep attached but use process groups
|
||||||
|
// On Unix, we can't directly set process group in spawn options,
|
||||||
|
// but we can use setsid-like behavior by ensuring proper cleanup
|
||||||
|
}); |
||||||
|
|
||||||
|
// On Unix systems, we need to ensure the process can be killed as a group
|
||||||
|
// The key is to ensure proper cleanup and use negative PID when killing
|
||||||
|
if (proc.pid) { |
||||||
|
logger.debug({ pid: proc.pid, command, args: args.slice(0, 3) }, 'Spawned process with group cleanup support'); |
||||||
|
} |
||||||
|
|
||||||
|
return proc; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reap zombie processes by explicitly waiting for them |
||||||
|
* This should be called periodically to clean up any zombies |
||||||
|
*/ |
||||||
|
export function reapZombies(): void { |
||||||
|
// On Unix systems, we can check for zombie processes
|
||||||
|
// However, Node.js doesn't expose waitpid directly
|
||||||
|
// The best we can do is ensure all our tracked processes are properly cleaned up
|
||||||
|
|
||||||
|
// This is a placeholder for potential future implementation
|
||||||
|
// In practice, proper cleanup in process handlers should prevent zombies
|
||||||
|
logger.debug('Zombie reaping check (process handlers should prevent zombies)'); |
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue