@@ -339,26 +339,6 @@ if ($Sequential) {
339339 -- disable-interactivity
340340}
341341else {
342- # Prefer ThreadJob (lightweight). Fall back to Start-Job if unavailable.
343- # Suppress the noisy "module not found" error if ThreadJob isn't there --
344- # we handle it gracefully.
345- $useThreadJob = $false
346- if (Get-Command Start-ThreadJob - ErrorAction SilentlyContinue) {
347- $useThreadJob = $true
348- }
349- else {
350- $prevEAP = $ErrorActionPreference
351- $ErrorActionPreference = ' SilentlyContinue'
352- try {
353- Import-Module ThreadJob - ErrorAction SilentlyContinue 2> $null
354- if (Get-Command Start-ThreadJob - ErrorAction SilentlyContinue) {
355- $useThreadJob = $true
356- }
357- }
358- catch { $useThreadJob = $false }
359- finally { $ErrorActionPreference = $prevEAP }
360- }
361-
362342 $logDir = Join-Path $RunLogDir ' packages'
363343 New-Item - ItemType Directory - Path $logDir - Force | Out-Null
364344 Write-Host " Per-package logs: $logDir " - ForegroundColor DarkGray
@@ -409,94 +389,63 @@ else {
409389 Write-Host " "
410390 }
411391
412- Write-Host " Phase B: Installing remaining packages in parallel (throttle: $Throttle )..." - ForegroundColor Cyan
392+ Write-Host " Phase B: Installing remaining packages (throttle: $Throttle )..." - ForegroundColor Cyan
413393 Write-Host " "
414394
415- $scriptBlock = {
416- param ($pkg , $commonArgs , $logDir )
417- $log = Join-Path $logDir (" {0}.log" -f ($pkg -replace ' [^A-Za-z0-9._-]' , ' _' ))
418- $wgArgs = @ (' install' , ' --id' , $pkg , ' --exact' ) + $commonArgs
419- try {
420- $output = & winget @wgArgs 2>&1
421- $output | Out-File - FilePath $log - Encoding UTF8
422- [pscustomobject ]@ {
423- Package = $pkg
424- ExitCode = $LASTEXITCODE
425- Log = $log
426- }
427- }
428- catch {
429- " EXCEPTION: $_ " | Out-File - FilePath $log - Encoding UTF8
430- [pscustomobject ]@ {
431- Package = $pkg
432- ExitCode = -1
433- Log = $log
434- }
435- }
436- }
395+ # Per-package timeout (5 minutes). Any installer that takes longer is killed.
396+ $pkgTimeoutSec = 300
437397
438- $jobs = @ ()
439398 $completed = 0
440399 $total = $remainingPackages.Count
400+ $succeeded = 0
401+ $failed = [System.Collections.Generic.List [string ]]::new()
441402
442403 foreach ($pkg in $remainingPackages ) {
443- # Throttle: wait until a slot opens
444- while (@ ($jobs | Where-Object { $_.State -eq ' Running' }).Count -ge $Throttle ) {
445- $done = Wait-Job - Job $jobs - Any - Timeout 5
446- if ($done ) {
447- foreach ($j in @ ($done )) {
448- $r = Receive-Job - Job $j
449- $completed ++
450- $status = if ($r.ExitCode -eq 0 ) { ' OK ' } else { " FAIL($ ( $r.ExitCode ) )" }
451- $color = if ($r.ExitCode -eq 0 ) { ' Green' } else { ' Yellow' }
452- Write-Host (" [{0,3}/{1}] {2} {3}" -f $completed , $total , $status , $r.Package ) - ForegroundColor $color
453- Remove-Job - Job $j | Out-Null
454- }
455- # @(...) is critical: when the filter returns one job (or zero),
456- # the result collapses to a scalar and `$jobs += newJob` then calls
457- # op_Addition on PSRemotingJob -- which doesn't exist and throws.
458- $jobs = @ ($jobs | Where-Object { $_.State -eq ' Running' })
459- }
460- }
404+ $completed ++
405+ $log = Join-Path $logDir (" {0}.log" -f ($pkg -replace ' [^A-Za-z0-9._-]' , ' _' ))
406+ Write-Host (" [{0,3}/{1}] installing: {2}" -f $completed , $total , $pkg ) - ForegroundColor DarkGray - NoNewline
461407
462- Write-Host (" -> queued: $pkg " ) - ForegroundColor DarkGray
463- if ($useThreadJob ) {
464- $jobs = @ ($jobs ) + (Start-ThreadJob - ScriptBlock $scriptBlock - ArgumentList $pkg , $commonArgs , $logDir )
408+ $wgArgs = @ (' install' , ' --id' , $pkg , ' --exact' ) + $commonArgs
409+
410+ # Run winget as a process with a hard timeout so hung installers can't block forever.
411+ $psi = New-Object System.Diagnostics.ProcessStartInfo
412+ $psi.FileName = ' winget'
413+ $psi.Arguments = ($wgArgs -join ' ' )
414+ $psi.UseShellExecute = $false
415+ $psi.RedirectStandardOutput = $true
416+ $psi.RedirectStandardError = $true
417+ $psi.CreateNoWindow = $true
418+
419+ $proc = [System.Diagnostics.Process ]::Start($psi )
420+ $stdout = $proc.StandardOutput.ReadToEndAsync ()
421+ $stderr = $proc.StandardError.ReadToEndAsync ()
422+
423+ if ($proc.WaitForExit ($pkgTimeoutSec * 1000 )) {
424+ $code = $proc.ExitCode
425+ $output = $stdout.Result + " `n " + $stderr.Result
465426 }
466427 else {
467- $jobs = @ ($jobs ) + (Start-Job - ScriptBlock $scriptBlock - ArgumentList $pkg , $commonArgs , $logDir )
428+ # Timed out -- kill the process
429+ try { $proc.Kill () } catch { }
430+ $code = -1
431+ $output = " TIMEOUT after $pkgTimeoutSec seconds"
468432 }
469- }
433+ $proc .Dispose ()
470434
471- # Drain remaining (10 min timeout per batch to prevent infinite hangs)
472- $drainStart = Get-Date
473- while (@ ($jobs ).Count -gt 0 ) {
474- $done = Wait-Job - Job $jobs - Any - Timeout 10
475- if (-not $done ) {
476- # Check if we've been draining for too long (10 min total)
477- if (((Get-Date ) - $drainStart ).TotalMinutes -gt 10 ) {
478- Write-Warning " Some package installs appear stuck. Stopping remaining jobs..."
479- $jobs | Stop-Job - ErrorAction SilentlyContinue
480- $jobs | ForEach-Object {
481- $completed ++
482- Write-Host (" [{0,3}/{1}] TIMEOUT {2}" -f $completed , $total , $_.Name ) - ForegroundColor Red
483- Remove-Job - Job $_ - Force - ErrorAction SilentlyContinue
484- }
485- $jobs = @ ()
486- break
487- }
488- continue
435+ $output | Out-File - FilePath $log - Encoding UTF8
436+
437+ if ($code -eq 0 ) {
438+ Write-Host " OK" - ForegroundColor Green
439+ $succeeded ++
489440 }
490- foreach ($j in @ ($done )) {
491- $r = Receive-Job - Job $j
492- $completed ++
493- $status = if ($r.ExitCode -eq 0 ) { ' OK ' } else { " FAIL($ ( $r.ExitCode ) )" }
494- $color = if ($r.ExitCode -eq 0 ) { ' Green' } else { ' Yellow' }
495- Write-Host (" [{0,3}/{1}] {2} {3}" -f $completed , $total , $status , $r.Package ) - ForegroundColor $color
496- Remove-Job - Job $j | Out-Null
441+ else {
442+ Write-Host " FAIL($code )" - ForegroundColor Yellow
443+ $failed.Add ($pkg )
497444 }
498- $jobs = @ ($jobs | Where-Object { $_.State -eq ' Running' })
499445 }
446+
447+ Write-Host " "
448+ Write-Host (" Phase B complete: {0} succeeded, {1} failed out of {2}" -f $succeeded , $failed.Count , $total ) - ForegroundColor Cyan
500449}
501450
502451Write-Host " "
0 commit comments