@@ -100,10 +100,13 @@ export class AndroidAdbInterceptor implements Interceptor {
100100 )
101101 ) ,
102102 port : proxyPort ,
103+ localTunnelPort : proxyPort ,
103104 certFingerprint : generateSPKIFingerprint ( this . config . https . certContent )
104105 } ;
105106 const intentData = urlSafeBase64 ( JSON . stringify ( setupParams ) ) ;
106107
108+ await this . adbClient . reverse ( options . deviceId , 'tcp:' + proxyPort , 'tcp:' + proxyPort ) . catch ( ( ) => { } ) ;
109+
107110 // Use ADB to launch the app with the proxy details
108111 await this . adbClient . startActivity ( options . deviceId , {
109112 wait : true ,
@@ -112,7 +115,40 @@ export class AndroidAdbInterceptor implements Interceptor {
112115 } ) ;
113116
114117 this . deviceProxyMapping [ proxyPort ] = this . deviceProxyMapping [ proxyPort ] || [ ] ;
115- this . deviceProxyMapping [ proxyPort ] . push ( options . deviceId ) ;
118+
119+ if ( ! this . deviceProxyMapping [ proxyPort ] . includes ( options . deviceId ) ) {
120+ this . deviceProxyMapping [ proxyPort ] . push ( options . deviceId ) ;
121+
122+ let tunnelConnectFailures = 0 ;
123+
124+ // The reverse tunnel can break when connecting/disconnecting from the VPN. This is a problem! It can
125+ // also break in other cases, e.g. when ADB is restarted for some reason. To handle this, we constantly
126+ // reinforce the tunnel while HTTP Toolkit is running & the device is connected.
127+ const tunnelCheckInterval = setInterval ( async ( ) => {
128+ if ( this . deviceProxyMapping [ proxyPort ] . includes ( options . deviceId ) ) {
129+ try {
130+ await this . adbClient . reverse ( options . deviceId , 'tcp:' + proxyPort , 'tcp:' + proxyPort )
131+ tunnelConnectFailures = 0 ;
132+ } catch ( e ) {
133+ tunnelConnectFailures += 1 ;
134+ console . log ( `${ options . deviceId } ADB tunnel failed` , e ) ;
135+
136+ if ( tunnelConnectFailures >= 5 ) {
137+ // After 10 seconds disconnected, give up
138+ console . log ( `${ options . deviceId } disconnected, dropping the ADB tunnel` ) ;
139+ this . deviceProxyMapping [ proxyPort ] = this . deviceProxyMapping [ proxyPort ]
140+ . filter ( id => id !== options . deviceId ) ;
141+ clearInterval ( tunnelCheckInterval ) ;
142+ }
143+ }
144+ } else {
145+ // Deactivation at shutdown will clear the proxy data, and so clear this interval
146+ // will automatically shut down.
147+ clearInterval ( tunnelCheckInterval ) ;
148+ }
149+ } , 2000 ) ;
150+ tunnelCheckInterval . unref ( ) ; // Don't let this block shutdown
151+ }
116152 }
117153
118154 async deactivate ( port : number | string ) : Promise < void | { } > {
0 commit comments