From c5bcd2448b0846c0f8e10e2e8f48ac16340659d1 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 16 Sep 2026 16:49:26 +0000 Subject: [PATCH] =?UTF-8?q?Ignore=20sync=20api.close()=E2=80=99s=20SIGTERM?= =?UTF-8?q?=20for=20purposes=20of=20error=20printing=20and=20exit=20status?= =?UTF-8?q?=20(#64276)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 002153795466afc9cc4c9258b5aabc479b4f4351) --- .../typescript/test/sync/shutdown.test.ts | 36 ++++++++++++++++++ tsc/internal/api/server.go | 9 ++++- tsc/internal/api/server_test.go | 38 +++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 packages/typescript/test/sync/shutdown.test.ts create mode 100644 tsc/internal/api/server_test.go diff --git a/packages/typescript/test/sync/shutdown.test.ts b/packages/typescript/test/sync/shutdown.test.ts new file mode 100644 index 0000000000000..030e959af88fa --- /dev/null +++ b/packages/typescript/test/sync/shutdown.test.ts @@ -0,0 +1,36 @@ +import assert from "node:assert"; +import { spawn } from "node:child_process"; +import { test } from "node:test"; + +test("close shuts down the API process cleanly", async () => { + const script = ` + import { API } from "@typescript/typescript/unstable/sync"; + const api = new API({ cwd: process.cwd() }); + api.updateSnapshot().dispose(); + api.close(); + `; + + const results = await Promise.all(Array.from({ length: 20 }, () => runNode(script))); + for (const result of results) { + assert.equal(result.status, 0); + assert.equal(result.stderr, ""); + } +}); + +function runNode(script: string): Promise<{ status: number | null; stderr: string; }> { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, [ + "--conditions", + "@typescript/source", + "--experimental-strip-types", + "--input-type=module", + "--eval", + script, + ]); + let stderr = ""; + child.stderr.setEncoding("utf8"); + child.stderr.on("data", chunk => stderr += chunk); + child.on("error", reject); + child.on("close", status => resolve({ status, stderr })); + }); +} diff --git a/tsc/internal/api/server.go b/tsc/internal/api/server.go index 817ddebffa2e7..cb3c4a0672f34 100644 --- a/tsc/internal/api/server.go +++ b/tsc/internal/api/server.go @@ -120,5 +120,12 @@ func (s *StdioServer) Run(ctx context.Context) error { callbackFS.SetConnection(ctx, conn) } - return conn.Run(ctx) + return serverRunError(ctx, conn.Run(ctx)) +} + +func serverRunError(ctx context.Context, err error) error { + if ctx.Err() != nil { + return nil + } + return err } diff --git a/tsc/internal/api/server_test.go b/tsc/internal/api/server_test.go new file mode 100644 index 0000000000000..00607cdde5c90 --- /dev/null +++ b/tsc/internal/api/server_test.go @@ -0,0 +1,38 @@ +package api + +import ( + "context" + "errors" + "testing" + + "gotest.tools/v3/assert" +) + +func TestServerRunError(t *testing.T) { + t.Parallel() + + t.Run("EOF", func(t *testing.T) { + t.Parallel() + assert.NilError(t, serverRunError(context.Background(), nil)) + }) + + t.Run("context cancellation", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + cancel() + assert.NilError(t, serverRunError(ctx, context.Canceled)) + }) + + t.Run("unrelated cancellation", func(t *testing.T) { + t.Parallel() + err := context.Canceled + assert.ErrorIs(t, serverRunError(context.Background(), err), err) + }) + + t.Run("context cancellation supersedes server error", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + cancel() + assert.NilError(t, serverRunError(ctx, errors.New("server failed"))) + }) +}