@@ -18,6 +18,7 @@ import {
1818 isRealDevice ,
1919 getAdbReverseTunnels ,
2020 getCurrentWifiProxyConfig ,
21+ removeReverseTunnel ,
2122 ADBInstance ,
2223 UDID ,
2324} from './utils/adb' ;
@@ -89,6 +90,62 @@ export class AppiumInterceptorPlugin extends BasePlugin {
8990 super ( name , cliArgs ) ;
9091 log . debug ( `📱 Initializing plugin with CLI args: ${ JSON . stringify ( cliArgs ) } ` ) ;
9192 this . pluginArgs = Object . assign ( { } , DefaultPluginArgs , cliArgs as unknown as IPluginArgs ) ;
93+ this . registerProcessExitHandlers ( ) ;
94+ }
95+
96+ private registerProcessExitHandlers ( ) {
97+ let isCleaningUp = false ;
98+
99+ const cleanupAllProxies = async ( signal : string ) => {
100+ if ( isCleaningUp ) return ;
101+ isCleaningUp = true ;
102+
103+ const sessionIds = proxyCache . getAllSessionIds ( ) ;
104+ if ( sessionIds . length > 0 ) {
105+ log . info (
106+ `[Cleanup] Process received ${ signal } . Cleaning up ${ sessionIds . length } active proxy sessions...` ,
107+ ) ;
108+ for ( const sessionId of sessionIds ) {
109+ try {
110+ await this . clearProxy ( undefined , sessionId ) ;
111+ } catch ( err : any ) {
112+ log . error (
113+ `[Cleanup] Error during process exit cleanup for session ${ sessionId } : ${ err . message } ` ,
114+ ) ;
115+ }
116+ }
117+ }
118+
119+ if ( signal === 'SIGINT' || signal === 'SIGTERM' ) {
120+ // Send the signal to ourselves again so default or other handlers can run
121+ process . kill ( process . pid , signal ) ;
122+ }
123+ } ;
124+
125+ const cleanupWithTimeout = async ( signal : string , timeoutMs : number = 10000 ) => {
126+ return Promise . race ( [
127+ cleanupAllProxies ( signal ) ,
128+ new Promise ( ( _ , reject ) =>
129+ setTimeout ( ( ) => reject ( new Error ( 'Cleanup timeout' ) ) , timeoutMs ) ,
130+ ) ,
131+ ] ) ;
132+ } ;
133+
134+ process . once ( 'SIGINT' , async ( ) => {
135+ try {
136+ await cleanupWithTimeout ( 'SIGINT' ) ;
137+ } catch ( err : any ) {
138+ log . error ( `Cleanup failed or timed out: ${ err . message } ` ) ;
139+ }
140+ } ) ;
141+
142+ process . once ( 'SIGTERM' , async ( ) => {
143+ try {
144+ await cleanupWithTimeout ( 'SIGTERM' ) ;
145+ } catch ( err : any ) {
146+ log . error ( `Cleanup failed or timed out: ${ err . message } ` ) ;
147+ }
148+ } ) ;
92149 }
93150
94151 /**
@@ -163,6 +220,14 @@ export class AppiumInterceptorPlugin extends BasePlugin {
163220 const adb = driver . sessions [ sessionId ] ?. adb ;
164221 await this . clearProxy ( adb , sessionId ) ;
165222 }
223+
224+ const remainingSessions = proxyCache . getAllSessionIds ( ) ;
225+ for ( const sessionId of remainingSessions ) {
226+ log . warn (
227+ `[${ sessionId } ] Session still in proxyCache after unexpected shutdown. Forcing cleanup...` ,
228+ ) ;
229+ await this . clearProxy ( undefined , sessionId ) ;
230+ }
166231 }
167232
168233 async addMock ( _next : any , driver : any , config : MockConfig ) {
@@ -258,8 +323,15 @@ export class AppiumInterceptorPlugin extends BasePlugin {
258323 return proxy ;
259324 }
260325
261- private async setupProxy ( adb : ADBInstance , sessionId : string , deviceUDID : UDID , interceptionPort ?: number ) {
262- log . debug ( `setupProxy(sessionId=${ sessionId } , deviceUDID:${ deviceUDID } , interceptionPort:${ interceptionPort } )` ) ;
326+ private async setupProxy (
327+ adb : ADBInstance ,
328+ sessionId : string ,
329+ deviceUDID : UDID ,
330+ interceptionPort ?: number ,
331+ ) {
332+ log . debug (
333+ `setupProxy(sessionId=${ sessionId } , deviceUDID:${ deviceUDID } , interceptionPort:${ interceptionPort } )` ,
334+ ) ;
263335
264336 if ( proxyCache . get ( sessionId ) ) {
265337 log . warn ( `[${ sessionId } ] A proxy is already active for this session. Skipping setup.` ) ;
@@ -286,6 +358,7 @@ export class AppiumInterceptorPlugin extends BasePlugin {
286358 : parseJson ( this . pluginArgs . blacklisteddomains ) ,
287359 ) ;
288360 const proxy = await setupProxyServer (
361+ adb ,
289362 sessionId ,
290363 deviceUDID ,
291364 realDevice ,
@@ -307,18 +380,45 @@ export class AppiumInterceptorPlugin extends BasePlugin {
307380 }
308381 }
309382
310- private async clearProxy ( adb : ADBInstance , sessionId : string ) {
383+ private async clearProxy ( adb : ADBInstance | undefined , sessionId : string ) {
311384 const proxy = proxyCache . get ( sessionId ) ;
312385 if ( ! proxy ) {
313386 log . debug ( `[${ sessionId } ] No proxy registered for this session. Nothing to clear.` ) ;
314387 return ;
315388 }
316389
390+ const activeAdb = adb || proxy . options . adb ;
391+ if ( ! activeAdb ) {
392+ log . warn (
393+ `[${ sessionId } ] ADB instance is missing. Cannot revert proxy settings or remove reverse tunnels.` ,
394+ ) ;
395+ }
396+
317397 log . debug ( `[${ sessionId } ] Reverting device settings and cleaning up proxy resources...` ) ;
318398
319399 try {
320- // Revert WiFi settings to previous state or off
321- await configureWifiProxy ( adb , proxy . options . deviceUDID , false , proxy . previousGlobalProxy ) ;
400+ const isReal = proxy . options . isRealDevice ?? false ;
401+
402+ if ( activeAdb ) {
403+ // Revert WiFi settings to previous state or off
404+ await configureWifiProxy (
405+ activeAdb ,
406+ proxy . options . deviceUDID ,
407+ isReal ,
408+ proxy . previousGlobalProxy ,
409+ ) ;
410+
411+ // Explicitly remove the adb reverse tunnel if this is a real device
412+ if ( isReal ) {
413+ log . debug ( `[${ sessionId } ] Removing reverse tunnel for port ${ proxy . port } ...` ) ;
414+ try {
415+ await removeReverseTunnel ( activeAdb , proxy . options . deviceUDID , proxy . port ) ;
416+ } catch ( tunnelErr : any ) {
417+ log . warn ( `[${ sessionId } ] Failed to remove reverse tunnel: ${ tunnelErr . message } ` ) ;
418+ }
419+ }
420+ }
421+
322422 // Shutdown the local proxy server
323423 await cleanUpProxyServer ( proxy ) ;
324424 proxyCache . remove ( sessionId ) ;
0 commit comments