@@ -8,6 +8,13 @@ import { IDictionary, IErrors, INet } from "../../common/declarations";
88import { injector } from "../../common/yok" ;
99import { ITempService } from "../../definitions/temp-service" ;
1010
11+ const REPLACED_CLOSE_REASON = "Replaced by a newer debugger client" ;
12+
13+ interface IDebugProxySession {
14+ appDebugSocket : net . Socket ;
15+ webSocket ?: ws ;
16+ }
17+
1118export class AppDebugSocketProxyFactory
1219 extends EventEmitter
1320 implements IAppDebugSocketProxyFactory
@@ -18,7 +25,6 @@ export class AppDebugSocketProxyFactory
1825 constructor (
1926 private $logger : ILogger ,
2027 private $errors : IErrors ,
21- private $lockService : ILockService ,
2228 private $options : IOptions ,
2329 private $tempService : ITempService ,
2430 private $net : INet ,
@@ -132,7 +138,6 @@ export class AppDebugSocketProxyFactory
132138 projectName : string ,
133139 projectDir : string ,
134140 ) : Promise < ws . Server > {
135- let clientConnectionLockRelease : ( ) => void ;
136141 const cacheKey = `${ device . deviceInfo . identifier } -${ appId } ` ;
137142 const existingServer = this . deviceWebServers [ cacheKey ] ;
138143 if ( existingServer ) {
@@ -141,76 +146,83 @@ export class AppDebugSocketProxyFactory
141146 ) ;
142147 }
143148
144- // NOTE: We will try to provide command line options to select ports, at least on the localhost.
145- const localPort = await this . $net . getAvailablePortInRange ( 41000 ) ;
146-
147149 this . $logger . info (
148150 "\nSetting up debugger proxy...\nPress Ctrl + C to terminate, or disconnect.\n" ,
149151 ) ;
150152
151153 // NB: When the inspector frontend connects we might not have connected to the inspector backend yet.
152154 // That's why we use the verifyClient callback of the websocket server to stall the upgrade request until we connect.
153- // We store the socket that connects us to the device in the upgrade request object itself and later on retrieve it
154- // in the connection callback.
155-
156- let currentAppSocket : net . Socket = null ;
157- let currentWebSocket : ws = null ;
158- const server = new ws . Server ( < any > {
159- port : localPort ,
160- verifyClient : async (
155+ // We store the session in the upgrade request object itself and later on retrieve it in the connection callback.
156+
157+ // The runtime serves a single frontend, so the newest client takes over.
158+ // Handlers of a replaced session must leave the device socket alone, as
159+ // by then it belongs to the session that replaced it.
160+ let currentSession : IDebugProxySession = null ;
161+ let pendingConnect : Promise < void > = Promise . resolve ( ) ;
162+
163+ const connectToApp = async ( ) : Promise < IDebugProxySession > => {
164+ const previousSession = currentSession ;
165+ currentSession = null ;
166+ if ( previousSession ) {
167+ previousSession . webSocket ?. close ( 1000 , REPLACED_CLOSE_REASON ) ;
168+ await device . destroyDebugSocket ( appId ) ;
169+ }
170+
171+ const appDebugSocket = await device . getDebugSocket (
172+ appId ,
173+ projectName ,
174+ projectDir ,
175+ ) ;
176+ appDebugSocket . on ( "error" , ( err : Error ) => {
177+ this . $logger . trace ( "Error on debugger deviceSocket" , err ) ;
178+ } ) ;
179+ this . $logger . info ( "Backend socket created." ) ;
180+
181+ currentSession = { appDebugSocket } ;
182+ return currentSession ;
183+ } ;
184+
185+ const server = await this . listenOnAvailablePort ( 41000 , {
186+ verifyClient : (
161187 info : any ,
162188 callback : ( res : boolean , code ?: number , message ?: string ) => void ,
163189 ) => {
164- let acceptHandshake = true ;
165- clientConnectionLockRelease = null ;
166-
167- try {
168- clientConnectionLockRelease = await this . $lockService . lock (
169- `debug-connection-${ device . deviceInfo . identifier } -${ appId } .lock` ,
170- ) ;
171-
172- this . $logger . info ( "Frontend client connected." ) ;
173- let appDebugSocket ;
174- if ( currentAppSocket ) {
175- currentAppSocket . removeAllListeners ( ) ;
176- currentAppSocket = null ;
177- if ( currentWebSocket ) {
178- currentWebSocket . removeAllListeners ( ) ;
179- currentWebSocket . close ( ) ;
180- currentWebSocket = null ;
181- }
182- await device . destroyDebugSocket ( appId ) ;
190+ this . $logger . info ( "Frontend client connected." ) ;
191+
192+ // Not released on "connection": that never fires for a client that
193+ // drops during the handshake.
194+ pendingConnect = pendingConnect . then ( async ( ) => {
195+ try {
196+ info . req [ "__session" ] = await connectToApp ( ) ;
197+ callback ( true ) ;
198+ } catch ( err ) {
199+ err . deviceIdentifier = device . deviceInfo . identifier ;
200+ this . $logger . trace ( err ) ;
201+ this . emit ( CONNECTION_ERROR_EVENT_NAME , err ) ;
202+ this . $logger . warn (
203+ `Cannot connect to device socket. The error message is '${ err . message } '.` ,
204+ ) ;
205+ callback ( false ) ;
183206 }
184- appDebugSocket = await device . getDebugSocket (
185- appId ,
186- projectName ,
187- projectDir ,
188- ) ;
189- currentAppSocket = appDebugSocket ;
190- this . $logger . info ( "Backend socket created." ) ;
191- info . req [ "__deviceSocket" ] = appDebugSocket ;
192- } catch ( err ) {
193- if ( clientConnectionLockRelease ) {
194- clientConnectionLockRelease ( ) ;
195- }
196- err . deviceIdentifier = device . deviceInfo . identifier ;
197- this . $logger . trace ( err ) ;
198- this . emit ( CONNECTION_ERROR_EVENT_NAME , err ) ;
199- acceptHandshake = false ;
200- this . $logger . warn (
201- `Cannot connect to device socket. The error message is '${ err . message } '.` ,
202- ) ;
203- }
204-
205- callback ( acceptHandshake ) ;
207+ } ) ;
206208 } ,
207209 } ) ;
208210 this . deviceWebServers [ cacheKey ] = server ;
209211 server . on ( "connection" , ( webSocket , req ) => {
210- currentWebSocket = webSocket ;
211212 const encoding = "utf16le" ;
212213
213- const appDebugSocket : net . Socket = ( < any > req ) [ "__deviceSocket" ] ;
214+ webSocket . on ( "error" , ( err ) => {
215+ this . $logger . trace ( "Error on debugger websocket" , err ) ;
216+ } ) ;
217+
218+ const session : IDebugProxySession = ( < any > req ) [ "__session" ] ;
219+ if ( session !== currentSession || session . appDebugSocket . destroyed ) {
220+ webSocket . close ( 1000 , REPLACED_CLOSE_REASON ) ;
221+ return ;
222+ }
223+
224+ session . webSocket = webSocket ;
225+ const appDebugSocket = session . appDebugSocket ;
214226 const packets = new MessageUnpackStream ( ) ;
215227 appDebugSocket . pipe ( packets ) ;
216228
@@ -230,14 +242,6 @@ export class AppDebugSocketProxyFactory
230242 }
231243 } ) ;
232244
233- webSocket . on ( "error" , ( err ) => {
234- this . $logger . trace ( "Error on debugger websocket" , err ) ;
235- } ) ;
236-
237- appDebugSocket . on ( "error" , ( err ) => {
238- this . $logger . trace ( "Error on debugger deviceSocket" , err ) ;
239- } ) ;
240-
241245 webSocket . on ( "message" , ( message ) => {
242246 const msg = message . toString ( ) ;
243247 if ( process . env . DEBUG_DEVTOOLS_SOCKETS ) {
@@ -253,28 +257,65 @@ export class AppDebugSocketProxyFactory
253257 } ) ;
254258
255259 appDebugSocket . on ( "close" , ( ) => {
256- currentAppSocket = null ;
257260 this . $logger . trace ( "Backend socket closed!" ) ;
261+ if ( currentSession === session ) {
262+ currentSession = null ;
263+ }
258264 webSocket . close ( ) ;
259265 } ) ;
260266
261267 webSocket . on ( "close" , async ( ) => {
262- currentWebSocket = null ;
263268 this . $logger . trace ( "Frontend socket closed!" ) ;
264269 appDebugSocket . unpipe ( packets ) ;
265270 packets . destroy ( ) ;
271+ if ( currentSession !== session ) {
272+ return ;
273+ }
274+
275+ currentSession = null ;
266276 await device . destroyDebugSocket ( appId ) ;
267277 if ( ! this . $options . watch ) {
268278 process . exit ( 0 ) ;
269279 }
270280 } ) ;
271-
272- clientConnectionLockRelease ( ) ;
273281 } ) ;
274282
275283 return server ;
276284 }
277285
286+ private async listenOnAvailablePort (
287+ startPort : number ,
288+ options : ws . ServerOptions ,
289+ ) : Promise < ws . Server > {
290+ // The port can still get taken after the check, by another CLI for example.
291+ let port = await this . $net . getAvailablePortInRange ( startPort ) ;
292+ while ( true ) {
293+ const result = await new Promise < ws . Server | NodeJS . ErrnoException > (
294+ ( resolve ) => {
295+ const server = new ws . Server ( { ...options , port } ) ;
296+ server . once ( "listening" , ( ) => {
297+ server . removeAllListeners ( "error" ) ;
298+ server . on ( "error" , ( err : Error ) => {
299+ this . $logger . trace ( "Error on debugger web socket server" , err ) ;
300+ } ) ;
301+ resolve ( server ) ;
302+ } ) ;
303+ server . once ( "error" , resolve ) ;
304+ } ,
305+ ) ;
306+
307+ if ( result instanceof ws . Server ) {
308+ return result ;
309+ }
310+
311+ if ( result . code !== "EADDRINUSE" ) {
312+ throw result ;
313+ }
314+
315+ port = await this . $net . getAvailablePortInRange ( port + 1 ) ;
316+ }
317+ }
318+
278319 public removeAllProxies ( ) {
279320 let deviceId ;
280321 for ( deviceId in this . deviceWebServers ) {
0 commit comments