From cb93f66ccde5e24e46c356cc1ef3a2af0f4f393d Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 21 Jul 2026 18:11:18 -0400 Subject: [PATCH] Fix typespec-java emitter build hanging parallel pnpm build (SIGTTIN/SIGTTOU) The Java emitter build intermittently hung a full `pnpm build`, stopping right after "using current checkout" and never printing "Copy generator sources from core". Root cause: turbo runs each task in its own background process group under the build's controlling terminal. When a PowerShell cmdlet renders Write-Progress (notably `Copy-Item -Recurse` in Copy-Sources.ps1), pwsh touches that terminal from a background process group and the OS stops the process with SIGTTIN/SIGTTOU -- it never resumes, so the build hangs forever. It only affects typespec-java (the sole package that shells out to pwsh) and is intermittent (progress only renders when the copy is slow enough under load), which is why it survived earlier fix attempts and is invisible when output is piped. Fix: set `$ProgressPreference = 'SilentlyContinue'` in the .ps1 build scripts so no cmdlet touches the terminal. Also harden the pinned-core read: only `git fetch` when the commit is missing locally, and make git/pwsh non-interactive (GIT_TERMINAL_PROMPT=0, `pwsh -NonInteractive`) so a prompt can never block the headless build. Extract archives with ZipFile.ExtractToDirectory instead of the slow Expand-Archive. --- ...build-progress-hang-2026-07-21-17-30-00.md | 7 ++++ packages/typespec-java/Build-Generator.ps1 | 5 +++ packages/typespec-java/Copy-Sources.ps1 | 10 +++++ packages/typespec-java/CoreCommit.ps1 | 41 ++++++++++++++++--- packages/typespec-java/package.json | 4 +- 5 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 .chronus/changes/fix-java-emitter-build-progress-hang-2026-07-21-17-30-00.md diff --git a/.chronus/changes/fix-java-emitter-build-progress-hang-2026-07-21-17-30-00.md b/.chronus/changes/fix-java-emitter-build-progress-hang-2026-07-21-17-30-00.md new file mode 100644 index 0000000000..01b5d90b28 --- /dev/null +++ b/.chronus/changes/fix-java-emitter-build-progress-hang-2026-07-21-17-30-00.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@azure-tools/typespec-java" +--- + +Fix the Java emitter build intermittently hanging a full parallel `pnpm build`. The build scripts run under turbo, which puts each task in a background process group under the build's controlling terminal; when a PowerShell cmdlet (notably `Copy-Item -Recurse`) rendered `Write-Progress`, pwsh touched that terminal and the OS stopped it with SIGTTIN/SIGTTOU, hanging forever. The `.ps1` build scripts now set `$ProgressPreference = 'SilentlyContinue'`. Also make the pinned-`core/` fetch conditional (only when the commit is missing) and non-interactive (`GIT_TERMINAL_PROMPT=0`, `pwsh -NonInteractive`) to remove secondary hang/network vectors. diff --git a/packages/typespec-java/Build-Generator.ps1 b/packages/typespec-java/Build-Generator.ps1 index 437b78acf5..2e75a5b461 100644 --- a/packages/typespec-java/Build-Generator.ps1 +++ b/packages/typespec-java/Build-Generator.ps1 @@ -10,6 +10,11 @@ $ErrorActionPreference = "Stop" +# See Copy-Sources.ps1: silence progress so no cmdlet touches the controlling +# terminal from turbo's background process group (which would SIGTTIN/SIGTTOU-stop +# this process and hang the parallel build). +$ProgressPreference = "SilentlyContinue" + $packageRoot = $PSScriptRoot $generatorRoot = Join-Path $packageRoot "generator" $generatorPom = Join-Path $generatorRoot "pom.xml" diff --git a/packages/typespec-java/Copy-Sources.ps1 b/packages/typespec-java/Copy-Sources.ps1 index 607b8db41a..45c8e6f8e1 100644 --- a/packages/typespec-java/Copy-Sources.ps1 +++ b/packages/typespec-java/Copy-Sources.ps1 @@ -13,6 +13,16 @@ $ErrorActionPreference = "Stop" +# Disable progress rendering. Under a full parallel `pnpm build`, turbo runs each +# task's process in its own background process group under the build's controlling +# terminal. When a cmdlet here (notably `Copy-Item -Recurse`, which emits a +# Write-Progress) renders progress, PowerShell touches that terminal from a +# background process group and the OS stops the process with SIGTTIN/SIGTTOU -- it +# never resumes, so the emitter build appears to hang forever (intermittently, and +# only for this package since it is the only one that shells out to pwsh). Silencing +# progress removes that terminal interaction entirely. +$ProgressPreference = "SilentlyContinue" + $packageRoot = $PSScriptRoot $repoRoot = Resolve-Path (Join-Path $packageRoot ".." "..") $coreRoot = Join-Path $repoRoot "core" diff --git a/packages/typespec-java/CoreCommit.ps1 b/packages/typespec-java/CoreCommit.ps1 index 3cbc8299fb..815dcbe29e 100644 --- a/packages/typespec-java/CoreCommit.ps1 +++ b/packages/typespec-java/CoreCommit.ps1 @@ -41,6 +41,19 @@ function Get-CoreSourceRoot { [string] $PackageRoot ) + # Never let any git invocation below block on an interactive credential or + # host-key prompt. During a parallel `pnpm build` the emitter build runs + # headless (turbo captures its stdio / attaches a pseudo-terminal), so a git + # prompt has nowhere to read from and would hang the whole build forever -- + # observed as an intermittent, Java-only build hang. Make git fail fast instead. + $env:GIT_TERMINAL_PROMPT = "0" + + # Silence progress: a cmdlet rendering Write-Progress (e.g. the archive + # extraction below) touches the controlling terminal, and under turbo's + # background process group that stops this process with SIGTTIN/SIGTTOU and + # hangs the build. Scoped to this function so callers are unaffected. + $ProgressPreference = "SilentlyContinue" + $originSha = (git -C $CoreRoot rev-parse HEAD).Trim() if ($LASTEXITCODE -ne 0) { Write-Error "Failed to read the current SHA of the 'core' submodule at: $CoreRoot`nMake sure it is checked out (git submodule update --init --recursive)." @@ -65,9 +78,17 @@ function Get-CoreSourceRoot { return @{ Root = $liveRoot; TempDir = $null } } - # Make sure the target commit is available locally (it may be newer than what - # has been fetched); ignore failure if it is already present. - git -C $CoreRoot fetch --quiet origin $targetSha 2>$null + # Make sure the target commit is available locally (the pin may be newer than + # what has been fetched). Only hit the network when the commit is genuinely + # missing: this code runs on EVERY emitter build and doc regen, and a `git + # fetch` on core/ is the only per-build network operation in the whole repo + # build, so doing it unconditionally turns any transient network/credential + # stall into a Java-only build hang. `git cat-file -e ^{commit}` is a + # cheap, offline existence check. + git -C $CoreRoot cat-file -e "${targetSha}^{commit}" 2>$null + if ($LASTEXITCODE -ne 0) { + git -C $CoreRoot fetch --quiet origin $targetSha 2>$null + } # Only move forward: use the target when it is a descendant (newer) of the # current checkout. `merge-base --is-ancestor A B` exits 0 when A is an ancestor @@ -85,23 +106,31 @@ function Get-CoreSourceRoot { $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("typespec-java-core-" + [System.Guid]::NewGuid().ToString("N")) New-Item -ItemType Directory -Path $tempDir | Out-Null $zipPath = Join-Path $tempDir "core.zip" + $extractDir = Join-Path $tempDir "src" try { - # Produce a zip (not tar) and expand it with the built-in Expand-Archive: + # Produce a zip (not tar) and expand it with .NET's ZipFile.ExtractToDirectory: # this keeps the whole flow inside PowerShell/.NET and avoids depending on an # external `tar`, whose behavior varies by platform (notably Git for Windows' # GNU tar treats a drive-letter path like C:\...\core.tar as a remote host). + # + # ExtractToDirectory is used instead of Expand-Archive on purpose: the archived + # subtree holds several thousand files, and Expand-Archive emits a per-entry + # Write-Progress that makes it pathologically slow on that many files (worse when + # its output is captured by a parallel `pnpm build`), to the point the emitter + # build looks hung. ZipFile.ExtractToDirectory has no such overhead. git -C $CoreRoot archive --format=zip -o $zipPath "${targetSha}:$script:CoreJavaSubtree" if ($LASTEXITCODE -ne 0) { Write-Error "Failed to archive core commit ${targetSha}:$script:CoreJavaSubtree." } - Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $extractDir) } catch { Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue throw } Remove-Item $zipPath -Force - return @{ Root = $tempDir; TempDir = $tempDir } + return @{ Root = $extractDir; TempDir = $tempDir } } # Clean up the temporary extraction created by Get-CoreSourceRoot (no-op when the diff --git a/packages/typespec-java/package.json b/packages/typespec-java/package.json index b8aa438970..43e339844e 100644 --- a/packages/typespec-java/package.json +++ b/packages/typespec-java/package.json @@ -30,8 +30,8 @@ "scripts": { "clean": "rimraf ./dist ./temp ./generator", "build": "pnpm build:emitter && pnpm build:generator", - "build:emitter": "pwsh -NoProfile -File Copy-Sources.ps1 && tsc -p .", - "build:generator": "pwsh -NoProfile -File Build-Generator.ps1", + "build:emitter": "pwsh -NoProfile -NonInteractive -File Copy-Sources.ps1 && tsc -p .", + "build:generator": "pwsh -NoProfile -NonInteractive -File Build-Generator.ps1", "watch": "tsc -p . --watch", "test": "vitest run", "regen-docs": "pnpm run build:emitter && tspd doc . --enable-experimental --output-dir ../../website/src/content/docs/docs/emitters/clients/typespec-java/reference --skip-js"