99 writeFileSync ,
1010} from 'node:fs' ;
1111import { spawnSync , type SpawnSyncReturns } from 'node:child_process' ;
12+ import { randomUUID } from 'node:crypto' ;
1213import { homedir } from 'node:os' ;
1314import { dirname , join } from 'node:path' ;
1415import { localValidationError } from './errors.js' ;
@@ -69,6 +70,11 @@ interface CredentialsLockInfo {
6970 token ?: string ;
7071}
7172
73+ interface CredentialsLock {
74+ assertHeld : ( ) => void ;
75+ release : ( ) => void ;
76+ }
77+
7278interface RestrictiveModeOptions {
7379 platform ?: NodeJS . Platform ;
7480 env ?: NodeJS . ProcessEnv ;
@@ -261,29 +267,29 @@ function mutateCredentialsFile(
261267 mutate : ( file : CredentialsFile ) => CredentialsFile | undefined ,
262268) : void {
263269 mkdirSync ( dirname ( path ) , { recursive : true , mode : 0o700 } ) ;
264- const releaseLock = acquireCredentialsLock ( path ) ;
270+ const lock = acquireCredentialsLock ( path ) ;
265271 try {
266272 const file = readCredentialsFile ( { path } ) ;
267273 const nextFile = mutate ( file ) ;
268274 if ( nextFile === undefined ) return ;
275+ lock . assertHeld ( ) ;
269276 writeCredentialsAtomic ( path , nextFile ) ;
270277 } finally {
271- releaseLock ( ) ;
278+ lock . release ( ) ;
272279 }
273280}
274281
275282function writeCredentialsAtomic ( path : string , file : CredentialsFile ) : void {
276- mkdirSync ( dirname ( path ) , { recursive : true , mode : 0o700 } ) ;
277283 const tmp = `${ path } .tmp.${ process . pid } ` ;
278284 writeFileSync ( tmp , serializeCredentials ( file ) , { mode : 0o600 , encoding : 'utf8' } ) ;
279285 renameSync ( tmp , path ) ;
280286 ensureRestrictiveMode ( path ) ;
281287}
282288
283- function acquireCredentialsLock ( path : string ) : ( ) => void {
289+ function acquireCredentialsLock ( path : string ) : CredentialsLock {
284290 const lockPath = `${ path } .lock` ;
285291 const deadline = Date . now ( ) + CREDENTIALS_LOCK_WAIT_MS ;
286- const token = `${ process . pid } :${ Date . now ( ) } :${ Math . random ( ) . toString ( 16 ) . slice ( 2 ) } ` ;
292+ const token = `${ process . pid } :${ Date . now ( ) } :${ randomUUID ( ) } ` ;
287293 const lockInfo : Required < CredentialsLockInfo > = {
288294 pid : process . pid ,
289295 createdAt : Date . now ( ) ,
@@ -297,7 +303,10 @@ function acquireCredentialsLock(path: string): () => void {
297303 flag : 'wx' ,
298304 mode : 0o600 ,
299305 } ) ;
300- return ( ) => releaseCredentialsLock ( lockPath , token ) ;
306+ return {
307+ assertHeld : ( ) => assertCredentialsLockHeld ( lockPath , token ) ,
308+ release : ( ) => releaseCredentialsLock ( lockPath , token ) ,
309+ } ;
301310 } catch ( error ) {
302311 if ( ! isErrnoException ( error ) || error . code !== 'EEXIST' ) {
303312 throw error ;
@@ -341,14 +350,20 @@ function reclaimStaleCredentialsLock(lockPath: string): void {
341350 }
342351}
343352
353+ function assertCredentialsLockHeld ( lockPath : string , token : string ) : void {
354+ const lockInfo = readCredentialsLockInfo ( lockPath ) ;
355+ if ( lockInfo ?. token === token ) return ;
356+ throw localValidationError (
357+ 'credentialsLock' ,
358+ 'lost ownership of the credential update lock; retry the command' ,
359+ undefined ,
360+ 'field' ,
361+ ) ;
362+ }
363+
344364function releaseCredentialsLock ( lockPath : string , token : string ) : void {
345- let lockInfo : CredentialsLockInfo | undefined ;
346- try {
347- lockInfo = JSON . parse ( readFileSync ( lockPath , 'utf-8' ) ) as CredentialsLockInfo ;
348- } catch {
349- return ;
350- }
351- if ( lockInfo . token !== token ) return ;
365+ const lockInfo = readCredentialsLockInfo ( lockPath ) ;
366+ if ( lockInfo ?. token !== token ) return ;
352367 try {
353368 unlinkSync ( lockPath ) ;
354369 } catch ( error ) {
@@ -358,6 +373,14 @@ function releaseCredentialsLock(lockPath: string, token: string): void {
358373 }
359374}
360375
376+ function readCredentialsLockInfo ( lockPath : string ) : CredentialsLockInfo | undefined {
377+ try {
378+ return JSON . parse ( readFileSync ( lockPath , 'utf-8' ) ) as CredentialsLockInfo ;
379+ } catch {
380+ return undefined ;
381+ }
382+ }
383+
361384function isProcessAlive ( pid : number ) : boolean {
362385 if ( pid === process . pid ) return true ;
363386 try {
0 commit comments