@stacksjs/rpx 0.11.19 → 0.11.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acme-challenge.d.ts +13 -0
- package/dist/auth.d.ts +21 -0
- package/dist/bin/cli.js +252 -200
- package/dist/cert-inspect.d.ts +5 -3
- package/dist/{chunk-83dqq28c.js → chunk-1108y1dk.js} +1 -1
- package/dist/{chunk-0f32jmrb.js → chunk-c9brkawa.js} +1 -1
- package/dist/{chunk-w888yhnp.js → chunk-cqkz06bg.js} +1 -1
- package/dist/chunk-qs36t2rf.js +224 -0
- package/dist/daemon.d.ts +4 -1
- package/dist/https.d.ts +1 -0
- package/dist/index.d.ts +33 -1
- package/dist/index.js +7 -7
- package/dist/proxy-handler.d.ts +18 -1
- package/dist/proxy-pool.d.ts +5 -0
- package/dist/redirect.d.ts +40 -0
- package/dist/registry.d.ts +2 -1
- package/dist/site-resolver.d.ts +106 -0
- package/dist/site-splash.d.ts +19 -0
- package/dist/site-supervisor.d.ts +56 -0
- package/dist/start.d.ts +31 -8
- package/dist/types.d.ts +64 -0
- package/package.json +6 -7
- package/dist/chunk-rs8gqpax.js +0 -174
- package/src/cert-inspect.ts +0 -69
- package/src/colors.ts +0 -13
- package/src/config.ts +0 -45
- package/src/daemon-runner.ts +0 -180
- package/src/daemon.ts +0 -1209
- package/src/dns-state.ts +0 -116
- package/src/dns.ts +0 -568
- package/src/host-match.ts +0 -52
- package/src/host-routes.ts +0 -147
- package/src/hosts.ts +0 -283
- package/src/https.ts +0 -905
- package/src/index.ts +0 -161
- package/src/logger.ts +0 -19
- package/src/macos-trust.ts +0 -175
- package/src/on-demand.ts +0 -264
- package/src/origin-guard.ts +0 -127
- package/src/port-manager.ts +0 -183
- package/src/process-manager.ts +0 -164
- package/src/proxy-handler.ts +0 -387
- package/src/proxy-pool.ts +0 -1003
- package/src/registry.ts +0 -366
- package/src/sni.ts +0 -93
- package/src/start.ts +0 -1421
- package/src/static-files.ts +0 -201
- package/src/types.ts +0 -267
- package/src/utils.ts +0 -243
package/src/origin-guard.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Origin verification guard for "CDN in front of rpx" topologies.
|
|
3
|
-
*
|
|
4
|
-
* When rpx is the origin behind a CDN (e.g. CloudFront → rpx), the origin host
|
|
5
|
-
* is publicly resolvable, so a client could resolve it and hit rpx directly,
|
|
6
|
-
* bypassing the CDN's caching/WAF. The standard mitigation is a shared secret:
|
|
7
|
-
* the CDN injects a secret request header on the origin fetch, and the origin
|
|
8
|
-
* rejects any request to the protected hosts that lacks it.
|
|
9
|
-
*
|
|
10
|
-
* `createOriginGuard` returns a tiny pre-router gate you place in front of your
|
|
11
|
-
* fetch handler. It only guards the listed hosts (exact or `*.wildcard`) — every
|
|
12
|
-
* other host (e.g. apps served directly, not via the CDN) passes through
|
|
13
|
-
* untouched. ACME HTTP-01 challenge paths are exempt by default so cert renewal
|
|
14
|
-
* keeps working on the open `:80` listener.
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* const guard = createOriginGuard({
|
|
18
|
-
* header: 'x-origin-verify',
|
|
19
|
-
* value: process.env.ORIGIN_SECRET!,
|
|
20
|
-
* hosts: ['stacksjs.com', 'www.stacksjs.com', 'origin.stacksjs.com'],
|
|
21
|
-
* })
|
|
22
|
-
* Bun.serve({ fetch: req => guard(req) ?? handler(req, server) })
|
|
23
|
-
*/
|
|
24
|
-
import { timingSafeEqual } from 'node:crypto'
|
|
25
|
-
import { matchesWildcard } from './host-match'
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Compare a request-supplied secret against the expected value in constant time,
|
|
29
|
-
* so the shared secret can't be recovered byte-by-byte via response timing. The
|
|
30
|
-
* length check leaks only the secret's length (standard and acceptable); the
|
|
31
|
-
* byte comparison itself is timing-safe.
|
|
32
|
-
*/
|
|
33
|
-
function secretMatches(provided: string | null, expected: string): boolean {
|
|
34
|
-
if (provided == null)
|
|
35
|
-
return false
|
|
36
|
-
const a = Buffer.from(provided)
|
|
37
|
-
const b = Buffer.from(expected)
|
|
38
|
-
if (a.length !== b.length)
|
|
39
|
-
return false
|
|
40
|
-
return timingSafeEqual(a, b)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Lowercase and strip a single trailing dot so `Host: example.com.` ≡ `example.com`. */
|
|
44
|
-
function normalizeHost(host: string): string {
|
|
45
|
-
return host.toLowerCase().replace(/\.$/, '')
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface OriginGuardOptions {
|
|
49
|
-
/** Header the CDN injects on the origin hop (case-insensitive), e.g. `x-origin-verify`. */
|
|
50
|
-
header: string
|
|
51
|
-
/** Expected secret value. Requests to protected hosts must carry `header: value`. */
|
|
52
|
-
value: string
|
|
53
|
-
/** Hosts to protect — exact (`stacksjs.com`) or wildcard (`*.stacksjs.com`). Others pass through. */
|
|
54
|
-
hosts: string[]
|
|
55
|
-
/**
|
|
56
|
-
* Request paths exempt from the check (prefix match). Defaults to the ACME
|
|
57
|
-
* HTTP-01 challenge prefix so cert issuance/renewal is never blocked.
|
|
58
|
-
*/
|
|
59
|
-
exemptPaths?: string[]
|
|
60
|
-
/** Body returned on rejection. Defaults to a short plain-text message. */
|
|
61
|
-
forbiddenMessage?: string
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface OriginGuard {
|
|
65
|
-
/** Returns a 403 Response to short-circuit, or `undefined` to let the request proceed. */
|
|
66
|
-
(req: Request): Response | undefined
|
|
67
|
-
/** Whether a given hostname is in the protected set. */
|
|
68
|
-
protects: (hostname: string) => boolean
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const DEFAULT_EXEMPT = ['/.well-known/acme-challenge/']
|
|
72
|
-
|
|
73
|
-
function hostnameOf(req: Request): string {
|
|
74
|
-
const hostHeader = req.headers.get('host')
|
|
75
|
-
if (hostHeader)
|
|
76
|
-
return normalizeHost(hostHeader.split(':')[0])
|
|
77
|
-
try {
|
|
78
|
-
return normalizeHost(new URL(req.url).hostname)
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return ''
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function createOriginGuard(options: OriginGuardOptions): OriginGuard {
|
|
86
|
-
const header = options.header.toLowerCase()
|
|
87
|
-
const exact = new Set<string>()
|
|
88
|
-
const wildcards: string[] = []
|
|
89
|
-
for (const h of options.hosts) {
|
|
90
|
-
const host = normalizeHost(h)
|
|
91
|
-
if (host.startsWith('*.'))
|
|
92
|
-
wildcards.push(host)
|
|
93
|
-
else
|
|
94
|
-
exact.add(host)
|
|
95
|
-
}
|
|
96
|
-
const exemptPaths = options.exemptPaths ?? DEFAULT_EXEMPT
|
|
97
|
-
const forbidden = options.forbiddenMessage
|
|
98
|
-
?? 'Forbidden: direct origin access is not allowed; requests must arrive via the CDN.\n'
|
|
99
|
-
|
|
100
|
-
const protects = (hostname: string): boolean => {
|
|
101
|
-
const h = normalizeHost(hostname)
|
|
102
|
-
return exact.has(h) || wildcards.some(w => matchesWildcard(h, w))
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const guard: OriginGuard = ((req: Request): Response | undefined => {
|
|
106
|
-
const host = hostnameOf(req)
|
|
107
|
-
if (!protects(host))
|
|
108
|
-
return undefined
|
|
109
|
-
|
|
110
|
-
let pathname = '/'
|
|
111
|
-
try {
|
|
112
|
-
pathname = new URL(req.url).pathname
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
// fall through with '/'
|
|
116
|
-
}
|
|
117
|
-
if (exemptPaths.some(p => pathname.startsWith(p)))
|
|
118
|
-
return undefined
|
|
119
|
-
|
|
120
|
-
if (secretMatches(req.headers.get(header), options.value))
|
|
121
|
-
return undefined
|
|
122
|
-
|
|
123
|
-
return new Response(forbidden, { status: 403, headers: { 'content-type': 'text/plain' } })
|
|
124
|
-
}) as OriginGuard
|
|
125
|
-
guard.protects = protects
|
|
126
|
-
return guard
|
|
127
|
-
}
|
package/src/port-manager.ts
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import type { PortManager } from './types'
|
|
2
|
-
import * as net from 'node:net'
|
|
3
|
-
import { debugLog } from './utils'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Check if a port is in use
|
|
7
|
-
*/
|
|
8
|
-
export function isPortInUse(port: number, hostname: string, verbose?: boolean): Promise<boolean> {
|
|
9
|
-
debugLog('port', `Checking if port ${port} is in use on ${hostname}`, verbose)
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
const server = net.createServer()
|
|
12
|
-
|
|
13
|
-
// Add a timeout to ensure we don't hang indefinitely
|
|
14
|
-
const timeout = setTimeout(() => {
|
|
15
|
-
debugLog('port', `Checking port ${port} timed out, assuming it's in use`, verbose)
|
|
16
|
-
server.close()
|
|
17
|
-
resolve(true)
|
|
18
|
-
}, 3000) // 3 second timeout
|
|
19
|
-
|
|
20
|
-
server.once('error', (err: NodeJS.ErrnoException) => {
|
|
21
|
-
clearTimeout(timeout)
|
|
22
|
-
if (err.code === 'EADDRINUSE') {
|
|
23
|
-
debugLog('port', `Port ${port} is in use`, verbose)
|
|
24
|
-
resolve(true)
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
// Other errors should also be treated as port unavailable
|
|
28
|
-
debugLog('port', `Error checking port ${port}: ${err.message}`, verbose)
|
|
29
|
-
resolve(true)
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
server.once('listening', () => {
|
|
34
|
-
clearTimeout(timeout)
|
|
35
|
-
debugLog('port', `Port ${port} is available`, verbose)
|
|
36
|
-
server.close()
|
|
37
|
-
resolve(false)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
server.listen(port, hostname)
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
clearTimeout(timeout)
|
|
45
|
-
debugLog('port', `Exception checking port ${port}: ${err}`, verbose)
|
|
46
|
-
resolve(true)
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Find next available port
|
|
53
|
-
*/
|
|
54
|
-
export async function findAvailablePort(
|
|
55
|
-
startPort: number,
|
|
56
|
-
hostname: string,
|
|
57
|
-
verbose?: boolean,
|
|
58
|
-
maxAttempts = 50,
|
|
59
|
-
): Promise<number> {
|
|
60
|
-
debugLog('port', `Finding available port starting from ${startPort} (max attempts: ${maxAttempts})`, verbose)
|
|
61
|
-
let port = startPort
|
|
62
|
-
let attempts = 0
|
|
63
|
-
|
|
64
|
-
while (attempts < maxAttempts) {
|
|
65
|
-
attempts++
|
|
66
|
-
const isInUse = await isPortInUse(port, hostname, verbose)
|
|
67
|
-
|
|
68
|
-
if (!isInUse) {
|
|
69
|
-
debugLog('port', `Found available port: ${port} after ${attempts} attempts`, verbose)
|
|
70
|
-
return port
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
debugLog('port', `Port ${port} is in use, trying ${port + 1} (attempt ${attempts}/${maxAttempts})`, verbose)
|
|
74
|
-
port++
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
throw new Error(`Unable to find available port after ${maxAttempts} attempts starting from ${startPort}`)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Test if a port is actually connectable
|
|
82
|
-
*/
|
|
83
|
-
export function testPortConnectivity(
|
|
84
|
-
port: number,
|
|
85
|
-
hostname: string,
|
|
86
|
-
timeout = 5000,
|
|
87
|
-
verbose?: boolean,
|
|
88
|
-
): Promise<boolean> {
|
|
89
|
-
debugLog('port', `Testing connection to ${hostname}:${port}`, verbose)
|
|
90
|
-
return new Promise((resolve) => {
|
|
91
|
-
const socket = net.connect({
|
|
92
|
-
host: hostname,
|
|
93
|
-
port,
|
|
94
|
-
timeout,
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
socket.once('connect', () => {
|
|
98
|
-
debugLog('port', `Successfully connected to ${hostname}:${port}`, verbose)
|
|
99
|
-
socket.end()
|
|
100
|
-
resolve(true)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
socket.once('timeout', () => {
|
|
104
|
-
debugLog('port', `Connection to ${hostname}:${port} timed out`, verbose)
|
|
105
|
-
socket.destroy()
|
|
106
|
-
resolve(false)
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
socket.once('error', (err) => {
|
|
110
|
-
debugLog('port', `Failed to connect to ${hostname}:${port}: ${err.message}`, verbose)
|
|
111
|
-
socket.destroy()
|
|
112
|
-
resolve(false)
|
|
113
|
-
})
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export class DefaultPortManager implements PortManager {
|
|
118
|
-
usedPorts: Set<number> = new Set()
|
|
119
|
-
private hostname: string
|
|
120
|
-
private verbose?: boolean
|
|
121
|
-
private maxRetries: number
|
|
122
|
-
|
|
123
|
-
constructor(hostname: string = '0.0.0.0', verbose?: boolean, maxRetries = 50) {
|
|
124
|
-
this.hostname = hostname
|
|
125
|
-
this.verbose = verbose
|
|
126
|
-
this.maxRetries = maxRetries
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async getNextAvailablePort(startPort: number, testConnectivity = false): Promise<number> {
|
|
130
|
-
if (this.usedPorts.has(startPort)) {
|
|
131
|
-
// If we already have this port registered as used, find another one
|
|
132
|
-
return this.findNextAvailablePort(startPort + 1, testConnectivity)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const isInUse = await isPortInUse(startPort, this.hostname, this.verbose)
|
|
136
|
-
|
|
137
|
-
if (isInUse) {
|
|
138
|
-
return this.findNextAvailablePort(startPort + 1, testConnectivity)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// If requested, test that we can actually connect to this port
|
|
142
|
-
if (testConnectivity) {
|
|
143
|
-
const isConnectable = await testPortConnectivity(startPort, this.hostname, 3000, this.verbose)
|
|
144
|
-
if (!isConnectable) {
|
|
145
|
-
debugLog('port', `Port ${startPort} is available but not connectable, trying next port`, this.verbose)
|
|
146
|
-
return this.findNextAvailablePort(startPort + 1, testConnectivity)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Port is available, register it
|
|
151
|
-
this.usedPorts.add(startPort)
|
|
152
|
-
return startPort
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private async findNextAvailablePort(startPort: number, testConnectivity = false): Promise<number> {
|
|
156
|
-
const port = await findAvailablePort(startPort, this.hostname, this.verbose, this.maxRetries)
|
|
157
|
-
|
|
158
|
-
// If requested, test that we can actually connect to this port
|
|
159
|
-
if (testConnectivity) {
|
|
160
|
-
const isConnectable = await testPortConnectivity(port, this.hostname, 3000, this.verbose)
|
|
161
|
-
if (!isConnectable) {
|
|
162
|
-
// If the port isn't connectable, try the next one
|
|
163
|
-
if (port < startPort + this.maxRetries) {
|
|
164
|
-
return this.findNextAvailablePort(port + 1, testConnectivity)
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
throw new Error(`Unable to find a connectable port after ${this.maxRetries} attempts`)
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
this.usedPorts.add(port)
|
|
173
|
-
return port
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
releasePort(port: number): void {
|
|
177
|
-
debugLog('port', `Releasing port ${port}`, this.verbose)
|
|
178
|
-
this.usedPorts.delete(port)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Global port manager instance
|
|
183
|
-
export const portManager: DefaultPortManager = new DefaultPortManager()
|
package/src/process-manager.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import type { ChildProcess } from 'node:child_process'
|
|
2
|
-
import type { StartOptions } from './types'
|
|
3
|
-
import { spawn } from 'node:child_process'
|
|
4
|
-
import * as process from 'node:process'
|
|
5
|
-
import { log } from './logger'
|
|
6
|
-
import { debugLog, safeStringify } from './utils'
|
|
7
|
-
|
|
8
|
-
export interface ManagedProcess {
|
|
9
|
-
command: string
|
|
10
|
-
cwd: string
|
|
11
|
-
process: ChildProcess | null
|
|
12
|
-
env?: Record<string, string>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class ProcessManager {
|
|
16
|
-
private processes: Map<string, ManagedProcess> = new Map()
|
|
17
|
-
private isShuttingDown = false
|
|
18
|
-
|
|
19
|
-
async startProcess(id: string, options: StartOptions, verbose?: boolean): Promise<void> {
|
|
20
|
-
if (this.processes.has(id)) {
|
|
21
|
-
debugLog('start', `Process ${id} is already running`, verbose)
|
|
22
|
-
return
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const [cmd, ...args] = options.command.split(' ')
|
|
26
|
-
const cwd = options.cwd || process.cwd()
|
|
27
|
-
|
|
28
|
-
debugLog('start', `Starting process ${id}:`, verbose)
|
|
29
|
-
debugLog('start', ` Command: ${cmd} ${args.join(' ')}`, verbose)
|
|
30
|
-
debugLog('start', ` Working directory: ${cwd}`, verbose)
|
|
31
|
-
debugLog('start', ` Environment variables: ${safeStringify(options.env)}`, verbose)
|
|
32
|
-
|
|
33
|
-
const childProcess = spawn(cmd, args, {
|
|
34
|
-
cwd,
|
|
35
|
-
env: { ...process.env, ...options.env },
|
|
36
|
-
shell: true,
|
|
37
|
-
stdio: 'inherit',
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
this.processes.set(id, {
|
|
41
|
-
command: options.command,
|
|
42
|
-
cwd,
|
|
43
|
-
process: childProcess,
|
|
44
|
-
env: options.env,
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
return new Promise((resolve, reject) => {
|
|
48
|
-
childProcess.on('error', (err) => {
|
|
49
|
-
if (!this.isShuttingDown) {
|
|
50
|
-
debugLog('start', `Process ${id} failed to start: ${err}`, verbose)
|
|
51
|
-
this.processes.delete(id)
|
|
52
|
-
reject(err)
|
|
53
|
-
// Trigger cleanup on process error
|
|
54
|
-
process.emit('SIGINT')
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
childProcess.on('exit', (code) => {
|
|
59
|
-
if (!this.isShuttingDown && code !== null && code !== 0) {
|
|
60
|
-
debugLog('start', `Process ${id} exited with code ${code}`, verbose)
|
|
61
|
-
this.processes.delete(id)
|
|
62
|
-
reject(new Error(`Process ${id} exited with code ${code}`))
|
|
63
|
-
// Trigger cleanup on non-zero exit
|
|
64
|
-
process.emit('SIGINT')
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
if (verbose) {
|
|
69
|
-
childProcess.stdout?.on('data', (data) => {
|
|
70
|
-
debugLog('process', `[${id}] ${data.toString().trim()}`, true)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
childProcess.stderr?.on('data', (data) => {
|
|
74
|
-
debugLog('process', `[${id}] ERR: ${data.toString().trim()}`, true)
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Resolve after a delay to allow the process to start
|
|
79
|
-
setTimeout(() => {
|
|
80
|
-
if (!this.isShuttingDown && childProcess.killed) {
|
|
81
|
-
this.processes.delete(id)
|
|
82
|
-
reject(new Error(`Process ${id} was killed during startup`))
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
debugLog('start', `Process ${id} started successfully`, verbose)
|
|
86
|
-
resolve()
|
|
87
|
-
}
|
|
88
|
-
}, 1000)
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async stopProcess(id: string, verbose?: boolean): Promise<void> {
|
|
93
|
-
const managed = this.processes.get(id)
|
|
94
|
-
if (!managed?.process) {
|
|
95
|
-
debugLog('start', `No process found for ${id}`, verbose)
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
debugLog('start', `Stopping process ${id}`, verbose)
|
|
100
|
-
|
|
101
|
-
return new Promise((resolve) => {
|
|
102
|
-
if (!managed.process) {
|
|
103
|
-
resolve()
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
managed.process.once('exit', () => {
|
|
108
|
-
this.processes.delete(id)
|
|
109
|
-
debugLog('start', `Process ${id} stopped`, verbose)
|
|
110
|
-
resolve()
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
managed.process.kill('SIGTERM')
|
|
115
|
-
|
|
116
|
-
// Force kill after 3 seconds if process hasn't exited
|
|
117
|
-
setTimeout(() => {
|
|
118
|
-
if (managed.process) {
|
|
119
|
-
debugLog('start', `Force killing process ${id}`, verbose)
|
|
120
|
-
try {
|
|
121
|
-
managed.process.kill('SIGKILL')
|
|
122
|
-
}
|
|
123
|
-
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
124
|
-
catch (err) {
|
|
125
|
-
// Ignore errors during force kill
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}, 3000)
|
|
129
|
-
}
|
|
130
|
-
catch (err) {
|
|
131
|
-
debugLog('start', `Error stopping process ${id}: ${err}`, verbose)
|
|
132
|
-
this.processes.delete(id)
|
|
133
|
-
resolve()
|
|
134
|
-
}
|
|
135
|
-
})
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async stopAll(verbose?: boolean): Promise<void> {
|
|
139
|
-
if (this.isShuttingDown) {
|
|
140
|
-
debugLog('start', 'Already shutting down, skipping duplicate stopAll call', verbose)
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
this.isShuttingDown = true
|
|
145
|
-
debugLog('start', 'Stopping all processes', verbose)
|
|
146
|
-
|
|
147
|
-
const promises = Array.from(this.processes.keys()).map(id =>
|
|
148
|
-
this.stopProcess(id, verbose).catch((err) => {
|
|
149
|
-
log.error(`Failed to stop process ${id}:`, err)
|
|
150
|
-
}),
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
await Promise.allSettled(promises)
|
|
154
|
-
this.processes.clear()
|
|
155
|
-
this.isShuttingDown = false
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
isRunning(id: string): boolean {
|
|
159
|
-
const managed = this.processes.get(id)
|
|
160
|
-
return !!managed?.process && !managed.process.killed
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export const processManager: ProcessManager = new ProcessManager()
|