Skip to content

Commit 54b24b7

Browse files
committed
feat: enhance process management
1 parent b7e3861 commit 54b24b7

2 files changed

Lines changed: 135 additions & 19 deletions

File tree

packages/core/src/bridge/index.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ClientConfig, ServerConfig } from '@frp-bridge/types'
2-
import type { FrpProcessManagerOptions } from '../process'
2+
import type { FrpProcessManagerOptions, ProcessEvent } from '../process'
33
import type { CommandHandler, CommandHandlerContext, CommandResult, QueryHandler, QueryResult, RuntimeCommand, RuntimeContext, RuntimeEvent, RuntimeLogger, RuntimeMode, RuntimeQuery, RuntimeState, SnapshotStorage } from '../runtime'
44
import { homedir } from 'node:os'
55
import process from 'node:process'
@@ -14,11 +14,13 @@ import { DEFAULT_COMMAND_APPLY, DEFAULT_COMMAND_APPLY_RAW, DEFAULT_COMMAND_STOP,
1414
interface ConfigApplyPayload {
1515
config: Partial<ClientConfig | ServerConfig>
1616
restart?: boolean
17+
configPath?: string
1718
}
1819

1920
interface ConfigApplyRawPayload {
2021
content: string
2122
restart?: boolean
23+
configPath?: string
2224
}
2325

2426
interface FrpBridgeRuntimeOptions {
@@ -38,6 +40,7 @@ interface FrpBridgeProcessOptions extends Partial<Omit<FrpProcessManagerOptions,
3840
export interface FrpBridgeOptions {
3941
mode: 'client' | 'server'
4042
workDir?: string
43+
configPath?: string
4144
runtime?: FrpBridgeRuntimeOptions
4245
process?: FrpBridgeProcessOptions
4346
storage?: SnapshotStorage
@@ -67,6 +70,7 @@ export class FrpBridge {
6770
mode: options.process?.mode ?? options.mode,
6871
version: options.process?.version,
6972
workDir: processDir,
73+
configPath: options.configPath,
7074
logger: processLogger
7175
})
7276

@@ -97,6 +101,7 @@ export class FrpBridge {
97101
})
98102

99103
this.eventSink = options.eventSink
104+
this.setupProcessEventBridge()
100105
}
101106

102107
execute<TPayload, TResult = unknown>(command: RuntimeCommand<TPayload>): Promise<CommandResult<TResult>> {
@@ -171,7 +176,7 @@ export class FrpBridge {
171176
}
172177

173178
return this.runConfigMutation(async () => {
174-
this.process.writeConfigFile(content)
179+
this.process.updateConfigRaw(content)
175180
}, command.payload?.restart, ctx)
176181
}
177182

@@ -266,4 +271,26 @@ export class FrpBridge {
266271
events
267272
}
268273
}
274+
275+
private setupProcessEventBridge(): void {
276+
if (!this.eventSink) {
277+
return
278+
}
279+
280+
this.process.on('process:started', (event: ProcessEvent) => {
281+
this.eventSink?.(event as RuntimeEvent)
282+
})
283+
284+
this.process.on('process:stopped', (event: ProcessEvent) => {
285+
this.eventSink?.(event as RuntimeEvent)
286+
})
287+
288+
this.process.on('process:exited', (event: ProcessEvent) => {
289+
this.eventSink?.(event as RuntimeEvent)
290+
})
291+
292+
this.process.on('process:error', (event: ProcessEvent) => {
293+
this.eventSink?.(event as RuntimeEvent)
294+
})
295+
}
269296
}

packages/core/src/process/index.ts

Lines changed: 106 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { ClientConfig, ProxyConfig, ServerConfig } from '@frp-bridge/types'
66
import type { ChildProcess } from 'node:child_process'
77
import type { RuntimeLogger } from '../runtime'
88
import { spawn } from 'node:child_process'
9+
import { EventEmitter } from 'node:events'
910
import { chmodSync, existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
1011
import { homedir } from 'node:os'
1112
import { consola } from 'consola'
@@ -14,9 +15,23 @@ import { BINARY_NAMES } from '../constants'
1415
import { ErrorCode, FrpBridgeError } from '../errors'
1516
import { commandExists, downloadFile, ensureDir, executeCommand, getDownloadUrl, getLatestVersion, getPlatform, parseToml, toToml } from '../utils'
1617

18+
export interface ProcessEvent {
19+
type: 'process:started' | 'process:stopped' | 'process:exited' | 'process:error'
20+
timestamp: number
21+
payload?: {
22+
code?: number
23+
signal?: string
24+
error?: string
25+
pid?: number
26+
uptime?: number
27+
}
28+
}
29+
1730
export interface FrpProcessManagerOptions {
1831
/** Working directory for FRP files */
1932
workDir?: string
33+
/** Path to config file (overrides default) */
34+
configPath?: string
2035
/** FRP version (defaults to latest) */
2136
version?: string
2237
/** Mode: client or server */
@@ -43,7 +58,7 @@ export interface NodeInfo {
4358
/**
4459
* Manages FRP client/server lifecycle, config, and tunnels
4560
*/
46-
export class FrpProcessManager {
61+
export class FrpProcessManager extends EventEmitter {
4762
private readonly workDir: string
4863
private version: string | null = null
4964
private readonly mode: 'client' | 'server'
@@ -52,16 +67,18 @@ export class FrpProcessManager {
5267
private process: ChildProcess | null = null
5368
private configPath: string
5469
private binaryPath: string
70+
private uptime: number | null = null
71+
private isManualStop = false
5572

5673
constructor(options: FrpProcessManagerOptions) {
74+
super()
5775
this.mode = options.mode
5876
this.specifiedVersion = options.version
5977
this.workDir = options.workDir || join(homedir(), '.frp-bridge')
78+
this.configPath = options.configPath || join(this.workDir, `frp${this.mode === 'client' ? 'c' : 's'}.toml`)
6079
this.logger = options.logger ?? consola.withTag('FrpProcessManager')
6180

6281
ensureDir(this.workDir)
63-
64-
this.configPath = join(this.workDir, `frp${this.mode === 'client' ? 'c' : 's'}.toml`)
6582
// Binary path will be set after version is determined
6683
this.binaryPath = ''
6784
}
@@ -177,7 +194,7 @@ export class FrpProcessManager {
177194

178195
/** Update configuration */
179196
updateConfig(config: Partial<ClientConfig | ServerConfig>): void {
180-
const current = this.getConfig() || {}
197+
const current = this.getConfig()
181198
const merged = { ...current, ...config }
182199
const content = toToml(merged)
183200

@@ -205,16 +222,19 @@ export class FrpProcessManager {
205222
}
206223

207224
/** Read raw config file contents */
208-
readConfigFile(): string | null {
225+
getConfigRaw(): string | null {
209226
if (!existsSync(this.configPath)) {
210227
return null
211228
}
212229
return readFileSync(this.configPath, 'utf-8')
213230
}
214231

215232
/** Overwrite config file with provided content */
216-
writeConfigFile(content: string): void {
217-
ensureDir(this.workDir)
233+
updateConfigRaw(content: string): void {
234+
const targetDir = this.configPath.includes('/') || this.configPath.includes('\\')
235+
? this.configPath.substring(0, Math.max(this.configPath.lastIndexOf('/'), this.configPath.lastIndexOf('\\')))
236+
: this.workDir
237+
ensureDir(targetDir)
218238
writeFileSync(this.configPath, content, 'utf-8')
219239
}
220240

@@ -238,17 +258,18 @@ export class FrpProcessManager {
238258
stdio: 'inherit'
239259
})
240260

241-
this.process.on('error', (err) => {
242-
this.logger.error('FRP process error', { error: err })
243-
this.process = null
244-
})
261+
this.uptime = Date.now()
262+
this.isManualStop = false
263+
this.setupProcessListeners()
245264

246-
this.process.on('exit', (code) => {
247-
if (code !== 0) {
248-
this.logger.error('FRP process exited with non-zero code', { code })
265+
this.emit('process:started', {
266+
type: 'process:started',
267+
timestamp: Date.now(),
268+
payload: {
269+
pid: this.process?.pid,
270+
uptime: 0
249271
}
250-
this.process = null
251-
})
272+
} satisfies ProcessEvent)
252273
}
253274

254275
/** Stop FRP process */
@@ -257,9 +278,20 @@ export class FrpProcessManager {
257278
return
258279
}
259280

281+
this.isManualStop = true
282+
260283
return new Promise((resolve) => {
261284
this.process!.on('exit', () => {
285+
const uptime = this.uptime ? Date.now() - this.uptime : undefined
286+
287+
this.emit('process:stopped', {
288+
type: 'process:stopped',
289+
timestamp: Date.now(),
290+
payload: { uptime }
291+
} satisfies ProcessEvent)
292+
262293
this.process = null
294+
this.uptime = null
263295
resolve()
264296
})
265297

@@ -276,7 +308,14 @@ export class FrpProcessManager {
276308

277309
/** Check if process is running */
278310
isRunning(): boolean {
279-
return this.process !== null && !this.process.killed
311+
if (!this.process) {
312+
return false
313+
}
314+
315+
// 检查进程是否存在且未被杀死
316+
// 注意:this.process.exitCode 为 null 表示进程仍在运行
317+
// this.process.signalCode 为 null 也表示没有收到终止信号
318+
return this.process.exitCode === null && this.process.signalCode === null
280319
}
281320

282321
/** Add node (for client mode) */
@@ -453,4 +492,54 @@ export class FrpProcessManager {
453492

454493
return tunnels
455494
}
495+
496+
/**
497+
* Query current process status
498+
*/
499+
queryProcess() {
500+
const uptime = this.uptime ? Date.now() - this.uptime : 0
501+
502+
return {
503+
pid: this.process?.pid,
504+
uptime
505+
}
506+
}
507+
508+
private setupProcessListeners(): void {
509+
if (!this.process) {
510+
return
511+
}
512+
513+
this.process.on('exit', (code, signal) => {
514+
const uptime = this.uptime ? Date.now() - this.uptime : undefined
515+
516+
if (!this.isManualStop) {
517+
this.emit('process:exited', {
518+
type: 'process:exited',
519+
timestamp: Date.now(),
520+
payload: {
521+
code: code ?? undefined,
522+
signal: signal ?? undefined,
523+
uptime
524+
}
525+
} satisfies ProcessEvent)
526+
}
527+
528+
this.process = null
529+
this.uptime = null
530+
})
531+
532+
this.process.on('error', (error) => {
533+
this.emit('process:error', {
534+
type: 'process:error',
535+
timestamp: Date.now(),
536+
payload: {
537+
error: error.message,
538+
pid: this.process?.pid
539+
}
540+
} satisfies ProcessEvent)
541+
542+
this.logger.error('FRP process error', { error })
543+
})
544+
}
456545
}

0 commit comments

Comments
 (0)