@@ -31,6 +31,22 @@ interface CliOptions {
3131 configPath ?: string ;
3232}
3333
34+ interface GatewayCliIo {
35+ stderr : {
36+ write ( chunk : string | Uint8Array ) : unknown ;
37+ } ;
38+ }
39+
40+ interface RunGatewayCliOptions {
41+ cwd ?: string ;
42+ loadConfig ?: typeof loadRayConfig ;
43+ startGateway ?: typeof startGateway ;
44+ stopGateway ?: typeof stopGateway ;
45+ onSignal ?: ( signal : NodeJS . Signals , listener : ( ) => void ) => unknown ;
46+ setExitCode ?: ( code : number ) => void ;
47+ now ?: ( ) => Date ;
48+ }
49+
3450const MAX_GATEWAY_CLI_ARGS = 16 ;
3551const MAX_GATEWAY_CLI_ARG_BYTES = 8_192 ;
3652const MAX_GATEWAY_CONFIG_PATH_CHARS = 4_096 ;
@@ -84,6 +100,15 @@ const startGatewayOptionKeys = new Set([
84100 "configPath" ,
85101 "warmupRetry" ,
86102] ) ;
103+ const runGatewayCliOptionKeys = new Set ( [
104+ "cwd" ,
105+ "loadConfig" ,
106+ "startGateway" ,
107+ "stopGateway" ,
108+ "onSignal" ,
109+ "setExitCode" ,
110+ "now" ,
111+ ] ) ;
87112const gatewayWarmupRetryOptionKeys = new Set ( [ "initialDelayMs" , "maxDelayMs" ] ) ;
88113const stopGatewayOptionKeys = new Set ( [ "signal" , "timeoutMs" ] ) ;
89114const acknowledgedExpectContinueRequests = new WeakSet < IncomingMessage > ( ) ;
@@ -206,6 +231,56 @@ function assertOptionalGatewayFunction(value: unknown, label: string): void {
206231 }
207232}
208233
234+ function assertGatewayCliIo ( io : unknown ) : asserts io is GatewayCliIo {
235+ if ( io === null || typeof io !== "object" || Array . isArray ( io ) ) {
236+ throw new Error ( "gateway cli io must be an object" ) ;
237+ }
238+
239+ const stderr = ( io as { stderr ?: unknown } ) . stderr ;
240+ if (
241+ stderr === null ||
242+ typeof stderr !== "object" ||
243+ Array . isArray ( stderr ) ||
244+ typeof ( stderr as { write ?: unknown } ) . write !== "function"
245+ ) {
246+ throw new Error ( "gateway cli io.stderr.write must be a function" ) ;
247+ }
248+ }
249+
250+ function assertGatewayCliPathValue ( value : unknown , label : string ) : asserts value is string {
251+ if ( typeof value !== "string" || value . length === 0 ) {
252+ throw new Error ( `${ label } must be a non-empty path` ) ;
253+ }
254+
255+ if ( / [ \0 \r \n ] / . test ( value ) ) {
256+ throw new Error ( `${ label } must not contain control characters` ) ;
257+ }
258+
259+ if ( value . trim ( ) !== value ) {
260+ throw new Error ( `${ label } must be a path without surrounding whitespace` ) ;
261+ }
262+
263+ if ( value . length > MAX_GATEWAY_CONFIG_PATH_CHARS ) {
264+ throw new Error ( `${ label } must be at most ${ MAX_GATEWAY_CONFIG_PATH_CHARS } characters` ) ;
265+ }
266+ }
267+
268+ function assertRunGatewayCliOptions ( value : unknown ) : asserts value is RunGatewayCliOptions {
269+ assertGatewayOptionsObject ( value , "gateway cli options" ) ;
270+ assertGatewayOptionKeys ( value , "gateway cli options" , runGatewayCliOptionKeys ) ;
271+
272+ if ( value . cwd !== undefined ) {
273+ assertGatewayCliPathValue ( value . cwd , "cwd" ) ;
274+ }
275+
276+ assertOptionalGatewayFunction ( value . loadConfig , "loadConfig" ) ;
277+ assertOptionalGatewayFunction ( value . startGateway , "startGateway" ) ;
278+ assertOptionalGatewayFunction ( value . stopGateway , "stopGateway" ) ;
279+ assertOptionalGatewayFunction ( value . onSignal , "onSignal" ) ;
280+ assertOptionalGatewayFunction ( value . setExitCode , "setExitCode" ) ;
281+ assertOptionalGatewayFunction ( value . now , "now" ) ;
282+ }
283+
209284function assertGatewayHandlerDependencies ( value : Record < string , unknown > ) : void {
210285 assertGatewayOptionsObject ( value . config , "config" ) ;
211286 assertOptionalGatewayObject ( value . runtime , "runtime" ) ;
@@ -765,7 +840,7 @@ function requireFlagValue(flag: string, value: string | undefined): string {
765840}
766841
767842function assertConfigPathFlagValue ( value : string , flag : string ) : void {
768- if ( / [ \r \n ] / . test ( value ) ) {
843+ if ( / [ \0 \ r\n ] / . test ( value ) ) {
769844 throw new Error ( `${ flag } must not contain control characters` ) ;
770845 }
771846
@@ -2368,49 +2443,75 @@ export async function stopGateway(
23682443 }
23692444}
23702445
2371- async function main ( ) : Promise < void > {
2372- const cli = parseCliArgs ( process . argv . slice ( 2 ) ) ;
2373- const { config, configPath } = await loadRayConfig ( {
2374- cwd : process . cwd ( ) ,
2375- ...( cli . configPath ? { configPath : cli . configPath } : { } ) ,
2376- } ) ;
2446+ export async function runGatewayCli (
2447+ argv : string [ ] = process . argv . slice ( 2 ) ,
2448+ io : GatewayCliIo = process ,
2449+ options : RunGatewayCliOptions = { } ,
2450+ ) : Promise < number > {
2451+ assertGatewayCliIo ( io ) ;
2452+ assertRunGatewayCliOptions ( options ) ;
23772453
2378- const gateway = await startGateway ( {
2379- config,
2380- ...( configPath ? { configPath } : { } ) ,
2381- } ) ;
2454+ const loadConfig = options . loadConfig ?? loadRayConfig ;
2455+ const start = options . startGateway ?? startGateway ;
2456+ const stop = options . stopGateway ?? stopGateway ;
2457+ const onSignal =
2458+ options . onSignal ??
2459+ ( ( signal : NodeJS . Signals , listener : ( ) => void ) => {
2460+ process . on ( signal , listener ) ;
2461+ } ) ;
2462+ const setExitCode =
2463+ options . setExitCode ??
2464+ ( ( code : number ) => {
2465+ process . exitCode = code ;
2466+ } ) ;
2467+ const now = options . now ?? ( ( ) => new Date ( ) ) ;
23822468
2383- const shutdown = async ( signal : NodeJS . Signals ) => {
2384- try {
2385- await stopGateway ( gateway , { signal } ) ;
2386- process . exit ( 0 ) ;
2387- } catch ( error ) {
2388- gateway . logger . error ( "gateway shutdown failed" , {
2389- signal,
2390- error : serializeError ( error ) ,
2391- } ) ;
2392- process . exit ( 1 ) ;
2393- }
2394- } ;
2469+ try {
2470+ const cli = parseCliArgs ( argv ) ;
2471+ const { config, configPath } = await loadConfig ( {
2472+ cwd : options . cwd ?? process . cwd ( ) ,
2473+ ...( cli . configPath ? { configPath : cli . configPath } : { } ) ,
2474+ } ) ;
23952475
2396- process . on ( "SIGINT" , ( ) => {
2397- void shutdown ( "SIGINT" ) ;
2398- } ) ;
2399- process . on ( "SIGTERM" , ( ) => {
2400- void shutdown ( "SIGTERM" ) ;
2401- } ) ;
2402- }
2476+ const gateway = await start ( {
2477+ config,
2478+ ...( configPath ? { configPath } : { } ) ,
2479+ } ) ;
24032480
2404- if ( process . argv [ 1 ] && import . meta. url === pathToFileURL ( process . argv [ 1 ] ) . href ) {
2405- void main ( ) . catch ( ( error ) => {
2406- console . error (
2407- JSON . stringify ( {
2408- ts : new Date ( ) . toISOString ( ) ,
2481+ const shutdown = async ( signal : NodeJS . Signals ) => {
2482+ try {
2483+ await stop ( gateway , { signal } ) ;
2484+ setExitCode ( 0 ) ;
2485+ } catch ( error ) {
2486+ gateway . logger . error ( "gateway shutdown failed" , {
2487+ signal,
2488+ error : serializeError ( error ) ,
2489+ } ) ;
2490+ setExitCode ( 1 ) ;
2491+ }
2492+ } ;
2493+
2494+ onSignal ( "SIGINT" , ( ) => {
2495+ void shutdown ( "SIGINT" ) ;
2496+ } ) ;
2497+ onSignal ( "SIGTERM" , ( ) => {
2498+ void shutdown ( "SIGTERM" ) ;
2499+ } ) ;
2500+
2501+ return 0 ;
2502+ } catch ( error ) {
2503+ io . stderr . write (
2504+ `${ JSON . stringify ( {
2505+ ts : now ( ) . toISOString ( ) ,
24092506 level : "error" ,
24102507 message : "gateway boot failed" ,
24112508 error : serializeError ( error ) ,
2412- } ) ,
2509+ } ) } \n` ,
24132510 ) ;
2414- process . exit ( 1 ) ;
2415- } ) ;
2511+ return 1 ;
2512+ }
2513+ }
2514+
2515+ if ( process . argv [ 1 ] && import . meta. url === pathToFileURL ( process . argv [ 1 ] ) . href ) {
2516+ process . exitCode = await runGatewayCli ( ) ;
24162517}
0 commit comments