@@ -6,6 +6,7 @@ import type { ClientConfig, ProxyConfig, ServerConfig } from '@frp-bridge/types'
66import type { ChildProcess } from 'node:child_process'
77import type { RuntimeLogger } from '../runtime'
88import { spawn } from 'node:child_process'
9+ import { EventEmitter } from 'node:events'
910import { chmodSync , existsSync , readFileSync , unlinkSync , writeFileSync } from 'node:fs'
1011import { homedir } from 'node:os'
1112import { consola } from 'consola'
@@ -14,9 +15,23 @@ import { BINARY_NAMES } from '../constants'
1415import { ErrorCode , FrpBridgeError } from '../errors'
1516import { 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+
1730export 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