@@ -85,21 +85,29 @@ public sealed class CliInstallationTests
8585 fs.writeFileSync(shimPath, shim);
8686 if (process.platform !== 'win32') fs.chmodSync(shimPath, 0o755);
8787 if (fs.existsSync(path.join(__dirname, 'pipe-holder'))) {
88- const { spawnSync } = require('node:child_process');
89- const shellCommand = `setsid /bin/sh -c 'echo $$ > ${shellQuote(path.join(__dirname, 'pipe-holder.pid'))}; exec /bin/sleep 60' holder &`;
90- spawnSync('/bin/sh', ['-c', shellCommand], { stdio: 'inherit' });
88+ const { spawn } = require('node:child_process');
89+ const childScript = 'printf "%s" "$$" > "$1.tmp" && mv "$1.tmp" "$1" && exec /bin/sleep 60';
90+ const holder = spawn('setsid', ['/bin/sh', '-c', childScript, 'holder',
91+ path.join(__dirname, 'pipe-holder.pid')], { detached: true, stdio: 'inherit' });
92+ holder.unref();
9193 }
9294 if (fs.existsSync(path.join(__dirname, 'hang'))) {
93- fs.writeFileSync (path.join(__dirname, 'child.pid'), String(process.pid ));
95+ writeProcessId (path.join(__dirname, 'child.pid'));
9496 setInterval(() => {}, 1000);
9597 }
9698 if (fs.existsSync(path.join(__dirname, 'overflow'))) {
9799 process.stdout.write('x'.repeat(8192));
98100 setInterval(() => {}, 1000);
99101 }
100- fs.writeFileSync (path.join(__dirname, 'child.pid'), String(process.pid ));
102+ writeProcessId (path.join(__dirname, 'child.pid'));
101103 console.log('installation output');
102104 console.error('installation diagnostics');
105+
106+ function writeProcessId(filePath) {
107+ const temporaryPath = filePath + '.tmp';
108+ fs.writeFileSync(temporaryPath, String(process.pid));
109+ fs.renameSync(temporaryPath, filePath);
110+ }
103111 """ ;
104112
105113 [ Test ]
@@ -185,9 +193,7 @@ public async Task InstallOrUpdate_ReportsFinalOutputWhenConsumerResumesAfterProc
185193 await Assert . That ( updates . Current . Stage ) . IsEqualTo ( CliInstallationStage . PackageManagerStarted ) ;
186194 var childPidPath = Path . Combine ( fixture . NpmRoot , BinDirectoryName , ChildPidFileName ) ;
187195 using var markerTimeout = new CancellationTokenSource ( TimeSpan . FromSeconds ( 3 ) ) ;
188- await WaitForFileAsync ( childPidPath , markerTimeout . Token ) ;
189- var childPid = int . Parse ( await File . ReadAllTextAsync ( childPidPath ) ,
190- System . Globalization . CultureInfo . InvariantCulture ) ;
196+ var childPid = await WaitForProcessIdAsync ( childPidPath , markerTimeout . Token ) ;
191197 await WaitForProcessExitAsync ( childPid , markerTimeout . Token ) ;
192198
193199 var sawFinalOutput = false ;
@@ -228,9 +234,7 @@ public async Task InstallOrUpdate_EnforcesTimeoutWhileConsumerIsPausedAfterStart
228234 await Assert . That ( updates . Current . Stage ) . IsEqualTo ( CliInstallationStage . PackageManagerStarted ) ;
229235
230236 using var deadline = new CancellationTokenSource ( TimeSpan . FromSeconds ( 5 ) ) ;
231- await WaitForFileAsync ( childPidPath , deadline . Token ) ;
232- var childPid = int . Parse ( await File . ReadAllTextAsync ( childPidPath ) ,
233- System . Globalization . CultureInfo . InvariantCulture ) ;
237+ var childPid = await WaitForProcessIdAsync ( childPidPath , deadline . Token ) ;
234238 await WaitForProcessExitAsync ( childPid , deadline . Token ) ;
235239 await Assert . That ( IsProcessRunning ( childPid ) ) . IsFalse ( ) ;
236240
@@ -275,12 +279,8 @@ await File.WriteAllTextAsync(Path.Combine(fixture.NpmRoot, BinDirectoryName, Pip
275279 fixture . LocalApplicationDataRoot , CancellationToken . None ) . GetAsyncEnumerator ( ) ;
276280 await Assert . That ( await updates . MoveNextAsync ( ) ) . IsTrue ( ) ;
277281 using var markerTimeout = new CancellationTokenSource ( TimeSpan . FromSeconds ( 3 ) ) ;
278- await WaitForFileAsync ( rootPidFile , markerTimeout . Token ) ;
279- await WaitForFileAsync ( holderPidFile , markerTimeout . Token ) ;
280- var rootPid = int . Parse ( await File . ReadAllTextAsync ( rootPidFile ) ,
281- System . Globalization . CultureInfo . InvariantCulture ) ;
282- holderPid = int . Parse ( await File . ReadAllTextAsync ( holderPidFile ) ,
283- System . Globalization . CultureInfo . InvariantCulture ) ;
282+ var rootPid = await WaitForProcessIdAsync ( rootPidFile , markerTimeout . Token ) ;
283+ holderPid = await WaitForProcessIdAsync ( holderPidFile , markerTimeout . Token ) ;
284284 await WaitForProcessExitAsync ( rootPid , markerTimeout . Token ) ;
285285 await Assert . That ( IsProcessRunning ( rootPid ) ) . IsFalse ( ) ;
286286 await Assert . That ( IsProcessRunning ( holderPid ) ) . IsTrue ( ) ;
@@ -354,12 +354,7 @@ public async Task InstallOrUpdate_CancellationStopsAndJoinsPackageManager()
354354 await Assert . That ( updates . Current . Stage ) . IsEqualTo ( CliInstallationStage . PackageManagerStarted ) ;
355355 var childPidPath = Path . Combine ( fixture . NpmRoot , BinDirectoryName , ChildPidFileName ) ;
356356 using var markerTimeout = new CancellationTokenSource ( TimeSpan . FromSeconds ( 3 ) ) ;
357- while ( ! File . Exists ( childPidPath ) )
358- {
359- await Task . Delay ( TimeSpan . FromMilliseconds ( 10 ) , markerTimeout . Token ) ;
360- }
361-
362- var childPid = int . Parse ( await File . ReadAllTextAsync ( childPidPath ) , System . Globalization . CultureInfo . InvariantCulture ) ;
357+ var childPid = await WaitForProcessIdAsync ( childPidPath , markerTimeout . Token ) ;
363358 var competingOptions = fixture . Options with { InstallationLockTimeout = TimeSpan . FromMilliseconds ( 100 ) } ;
364359 var lockException = await Assert . That ( async ( ) =>
365360 await RunInstallationAsync ( fixture , competingOptions ) ) . ThrowsException ( ) ;
@@ -413,12 +408,8 @@ public async Task CancellationWithDetachedPipeHolderSurfacesUnconfirmedCleanup()
413408 fixture . LocalApplicationDataRoot , cancellation . Token ) . GetAsyncEnumerator ( ) ;
414409 await Assert . That ( await updates . MoveNextAsync ( ) ) . IsTrue ( ) ;
415410 using var markerTimeout = new CancellationTokenSource ( TimeSpan . FromSeconds ( 3 ) ) ;
416- await WaitForFileAsync ( packageManagerPidFile , markerTimeout . Token ) ;
417- await WaitForFileAsync ( holderPidFile , markerTimeout . Token ) ;
418- var packageManagerPid = int . Parse ( await File . ReadAllTextAsync ( packageManagerPidFile ) ,
419- System . Globalization . CultureInfo . InvariantCulture ) ;
420- holderProcessId = int . Parse ( await File . ReadAllTextAsync ( holderPidFile ) ,
421- System . Globalization . CultureInfo . InvariantCulture ) ;
411+ var packageManagerPid = await WaitForProcessIdAsync ( packageManagerPidFile , markerTimeout . Token ) ;
412+ holderProcessId = await WaitForProcessIdAsync ( holderPidFile , markerTimeout . Token ) ;
422413 await Assert . That ( IsProcessRunning ( packageManagerPid ) ) . IsTrue ( ) ;
423414 await Assert . That ( IsProcessRunning ( holderProcessId ) ) . IsTrue ( ) ;
424415
@@ -519,10 +510,20 @@ private static string FindExecutablePath(string executableName)
519510 return Path . GetFullPath ( executable ?? throw new InvalidOperationException ( SetsidRequiredMessage ) ) ;
520511 }
521512
522- private static async Task WaitForFileAsync ( string path , CancellationToken cancellationToken )
513+ private static async Task < int > WaitForProcessIdAsync ( string path , CancellationToken cancellationToken )
523514 {
524- while ( ! File . Exists ( path ) )
515+ while ( true )
525516 {
517+ if ( File . Exists ( path ) )
518+ {
519+ var contents = await File . ReadAllTextAsync ( path , cancellationToken ) ;
520+ if ( int . TryParse ( contents , System . Globalization . CultureInfo . InvariantCulture , out var processId ) &&
521+ processId > 0 )
522+ {
523+ return processId ;
524+ }
525+ }
526+
526527 await Task . Delay ( TimeSpan . FromMilliseconds ( 10 ) , cancellationToken ) ;
527528 }
528529 }
0 commit comments