@@ -3,7 +3,7 @@ import { EventEmitter } from 'events'
3
3
import { Server } from 'http'
4
4
import express from 'express'
5
5
import { AddressInfo } from 'net'
6
- import { log , setupOAuthCallbackServerWithLongPoll } from './utils'
6
+ import { log , debugLog , DEBUG , setupOAuthCallbackServerWithLongPoll } from './utils'
7
7
8
8
export type AuthCoordinator = {
9
9
initializeAuth : ( ) => Promise < { server : Server ; waitForAuthCode : ( ) => Promise < string > ; skipBrowserAuth : boolean } >
@@ -17,8 +17,10 @@ export type AuthCoordinator = {
17
17
export async function isPidRunning ( pid : number ) : Promise < boolean > {
18
18
try {
19
19
process . kill ( pid , 0 ) // Doesn't kill the process, just checks if it exists
20
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Process ${ pid } is running` )
20
21
return true
21
- } catch {
22
+ } catch ( err ) {
23
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Process ${ pid } is not running` , err )
22
24
return false
23
25
}
24
26
}
@@ -29,21 +31,30 @@ export async function isPidRunning(pid: number): Promise<boolean> {
29
31
* @returns True if the lockfile is valid, false otherwise
30
32
*/
31
33
export async function isLockValid ( lockData : LockfileData ) : Promise < boolean > {
34
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , 'Checking if lockfile is valid' , lockData )
35
+
32
36
// Check if the lockfile is too old (over 30 minutes)
33
37
const MAX_LOCK_AGE = 30 * 60 * 1000 // 30 minutes
34
38
if ( Date . now ( ) - lockData . timestamp > MAX_LOCK_AGE ) {
35
39
log ( 'Lockfile is too old' )
40
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , 'Lockfile is too old' , {
41
+ age : Date . now ( ) - lockData . timestamp ,
42
+ maxAge : MAX_LOCK_AGE
43
+ } )
36
44
return false
37
45
}
38
46
39
47
// Check if the process is still running
40
48
if ( ! ( await isPidRunning ( lockData . pid ) ) ) {
41
49
log ( 'Process from lockfile is not running' )
50
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , 'Process from lockfile is not running' , { pid : lockData . pid } )
42
51
return false
43
52
}
44
53
45
54
// Check if the endpoint is accessible
46
55
try {
56
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , 'Checking if endpoint is accessible' , { port : lockData . port } )
57
+
47
58
const controller = new AbortController ( )
48
59
const timeout = setTimeout ( ( ) => controller . abort ( ) , 1000 )
49
60
@@ -52,9 +63,13 @@ export async function isLockValid(lockData: LockfileData): Promise<boolean> {
52
63
} )
53
64
54
65
clearTimeout ( timeout )
55
- return response . status === 200 || response . status === 202
66
+
67
+ const isValid = response . status === 200 || response . status === 202
68
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Endpoint check result: ${ isValid ? 'valid' : 'invalid' } ` , { status : response . status } )
69
+ return isValid
56
70
} catch ( error ) {
57
71
log ( `Error connecting to auth server: ${ ( error as Error ) . message } ` )
72
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , 'Error connecting to auth server' , error )
58
73
return false
59
74
}
60
75
}
@@ -66,28 +81,44 @@ export async function isLockValid(lockData: LockfileData): Promise<boolean> {
66
81
*/
67
82
export async function waitForAuthentication ( port : number ) : Promise < boolean > {
68
83
log ( `Waiting for authentication from the server on port ${ port } ...` )
84
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Waiting for authentication from server on port ${ port } ` )
69
85
70
86
try {
87
+ let attempts = 0 ;
71
88
while ( true ) {
89
+ attempts ++ ;
72
90
const url = `http://127.0.0.1:${ port } /wait-for-auth`
73
91
log ( `Querying: ${ url } ` )
74
- const response = await fetch ( url )
75
-
76
- if ( response . status === 200 ) {
77
- // Auth completed, but we don't return the code anymore
78
- log ( `Authentication completed by other instance` )
79
- return true
80
- } else if ( response . status === 202 ) {
81
- // Continue polling
82
- log ( `Authentication still in progress` )
83
- await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) )
84
- } else {
85
- log ( `Unexpected response status: ${ response . status } ` )
86
- return false
92
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Poll attempt ${ attempts } : ${ url } ` )
93
+
94
+ try {
95
+ const response = await fetch ( url )
96
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Poll response status: ${ response . status } ` )
97
+
98
+ if ( response . status === 200 ) {
99
+ // Auth completed, but we don't return the code anymore
100
+ log ( `Authentication completed by other instance` )
101
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Authentication completed by other instance` )
102
+ return true
103
+ } else if ( response . status === 202 ) {
104
+ // Continue polling
105
+ log ( `Authentication still in progress` )
106
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Authentication still in progress, will retry in 1s` )
107
+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) )
108
+ } else {
109
+ log ( `Unexpected response status: ${ response . status } ` )
110
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Unexpected response status` , { status : response . status } )
111
+ return false
112
+ }
113
+ } catch ( fetchError ) {
114
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Fetch error during poll` , fetchError )
115
+ // If we can't connect, we'll try again after a delay
116
+ await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) )
87
117
}
88
118
}
89
119
} catch ( error ) {
90
120
log ( `Error waiting for authentication: ${ ( error as Error ) . message } ` )
121
+ if ( DEBUG ) await debugLog ( global . currentServerUrlHash ! , `Error waiting for authentication` , error )
91
122
return false
92
123
}
93
124
}
@@ -110,13 +141,16 @@ export function createLazyAuthCoordinator(
110
141
initializeAuth : async ( ) => {
111
142
// If auth has already been initialized, return the existing state
112
143
if ( authState ) {
144
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Auth already initialized, reusing existing state' )
113
145
return authState
114
146
}
115
147
116
148
log ( 'Initializing auth coordination on-demand' )
149
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Initializing auth coordination on-demand' , { serverUrlHash, callbackPort } )
117
150
118
151
// Initialize auth using the existing coordinateAuth logic
119
152
authState = await coordinateAuth ( serverUrlHash , callbackPort , events )
153
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Auth coordination completed' , { skipBrowserAuth : authState . skipBrowserAuth } )
120
154
return authState
121
155
}
122
156
}
@@ -134,25 +168,42 @@ export async function coordinateAuth(
134
168
callbackPort : number ,
135
169
events : EventEmitter ,
136
170
) : Promise < { server : Server ; waitForAuthCode : ( ) => Promise < string > ; skipBrowserAuth : boolean } > {
171
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Coordinating authentication' , { serverUrlHash, callbackPort } )
172
+
137
173
// Check for a lockfile (disabled on Windows for the time being)
138
174
const lockData = process . platform === 'win32' ? null : await checkLockfile ( serverUrlHash )
175
+
176
+ if ( DEBUG ) {
177
+ if ( process . platform === 'win32' ) {
178
+ await debugLog ( serverUrlHash , 'Skipping lockfile check on Windows' )
179
+ } else {
180
+ await debugLog ( serverUrlHash , 'Lockfile check result' , { found : ! ! lockData , lockData } )
181
+ }
182
+ }
139
183
140
184
// If there's a valid lockfile, try to use the existing auth process
141
185
if ( lockData && ( await isLockValid ( lockData ) ) ) {
142
186
log ( `Another instance is handling authentication on port ${ lockData . port } ` )
187
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Another instance is handling authentication' , { port : lockData . port , pid : lockData . pid } )
143
188
144
189
try {
145
190
// Try to wait for the authentication to complete
191
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Waiting for authentication from other instance' )
146
192
const authCompleted = await waitForAuthentication ( lockData . port )
193
+
147
194
if ( authCompleted ) {
148
195
log ( 'Authentication completed by another instance' )
196
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Authentication completed by another instance, will use tokens from disk' )
149
197
150
198
// Setup a dummy server - the client will use tokens directly from disk
151
199
const dummyServer = express ( ) . listen ( 0 ) // Listen on any available port
200
+ const dummyPort = ( dummyServer . address ( ) as AddressInfo ) . port
201
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Started dummy server' , { port : dummyPort } )
152
202
153
203
// This shouldn't actually be called in normal operation, but provide it for API compatibility
154
204
const dummyWaitForAuthCode = ( ) => {
155
205
log ( 'WARNING: waitForAuthCode called in secondary instance - this is unexpected' )
206
+ if ( DEBUG ) debugLog ( serverUrlHash , 'WARNING: waitForAuthCode called in secondary instance - this is unexpected' ) . catch ( ( ) => { } )
156
207
// Return a promise that never resolves - the client should use the tokens from disk instead
157
208
return new Promise < string > ( ( ) => { } )
158
209
}
@@ -164,20 +215,25 @@ export async function coordinateAuth(
164
215
}
165
216
} else {
166
217
log ( 'Taking over authentication process...' )
218
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Taking over authentication process' )
167
219
}
168
220
} catch ( error ) {
169
221
log ( `Error waiting for authentication: ${ error } ` )
222
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Error waiting for authentication' , error )
170
223
}
171
224
172
225
// If we get here, the other process didn't complete auth successfully
226
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Other instance did not complete auth successfully, deleting lockfile' )
173
227
await deleteLockfile ( serverUrlHash )
174
228
} else if ( lockData ) {
175
- // Invalid lockfile, delete its
229
+ // Invalid lockfile, delete it
176
230
log ( 'Found invalid lockfile, deleting it' )
231
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Found invalid lockfile, deleting it' )
177
232
await deleteLockfile ( serverUrlHash )
178
233
}
179
234
180
235
// Create our own lockfile
236
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Setting up OAuth callback server' , { port : callbackPort } )
181
237
const { server, waitForAuthCode, authCompletedPromise } = setupOAuthCallbackServerWithLongPoll ( {
182
238
port : callbackPort ,
183
239
path : '/oauth/callback' ,
@@ -187,17 +243,21 @@ export async function coordinateAuth(
187
243
// Get the actual port the server is running on
188
244
const address = server . address ( ) as AddressInfo
189
245
const actualPort = address . port
246
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'OAuth callback server running' , { port : actualPort } )
190
247
191
248
log ( `Creating lockfile for server ${ serverUrlHash } with process ${ process . pid } on port ${ actualPort } ` )
249
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Creating lockfile' , { serverUrlHash, pid : process . pid , port : actualPort } )
192
250
await createLockfile ( serverUrlHash , process . pid , actualPort )
193
251
194
252
// Make sure lockfile is deleted on process exit
195
253
const cleanupHandler = async ( ) => {
196
254
try {
197
255
log ( `Cleaning up lockfile for server ${ serverUrlHash } ` )
256
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Cleaning up lockfile' )
198
257
await deleteLockfile ( serverUrlHash )
199
258
} catch ( error ) {
200
259
log ( `Error cleaning up lockfile: ${ error } ` )
260
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Error cleaning up lockfile' , error )
201
261
}
202
262
}
203
263
@@ -206,14 +266,19 @@ export async function coordinateAuth(
206
266
// Synchronous version for 'exit' event since we can't use async here
207
267
const configPath = getConfigFilePath ( serverUrlHash , 'lock.json' )
208
268
require ( 'fs' ) . unlinkSync ( configPath )
209
- } catch { }
269
+ if ( DEBUG ) console . error ( `[DEBUG] Removed lockfile on exit: ${ configPath } ` )
270
+ } catch ( error ) {
271
+ if ( DEBUG ) console . error ( `[DEBUG] Error removing lockfile on exit:` , error )
272
+ }
210
273
} )
211
274
212
275
// Also handle SIGINT separately
213
276
process . once ( 'SIGINT' , async ( ) => {
277
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Received SIGINT signal, cleaning up' )
214
278
await cleanupHandler ( )
215
279
} )
216
280
281
+ if ( DEBUG ) await debugLog ( serverUrlHash , 'Auth coordination complete, returning primary instance handlers' )
217
282
return {
218
283
server,
219
284
waitForAuthCode,
0 commit comments