From f4f4c1ca8e993eaecb9a06f8788d5278374581cb Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 31 Mar 2026 19:05:32 -0400 Subject: [PATCH 001/103] Upload playground standalone emitters --- .../templates/stages/emitter-stages.yml | 22 +++++++ .../scripts/upload-bundled-emitter.js | 15 +++++ packages/bundle-uploader/src/index.ts | 58 ++++++++++++++++++- .../src/upload-browser-package.ts | 23 ++++++++ .../eng/pipeline/publish.yml | 1 + .../eng/pipeline/publish.yml | 1 + .../playground-component/import-map.ts | 41 +++++++++++-- .../src/components/react-pages/playground.tsx | 10 +++- 8 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 eng/emitters/scripts/upload-bundled-emitter.js diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 9fc53174f91..3c79111ae57 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -82,6 +82,11 @@ parameters: type: string default: "3.12" + # Whether to bundle and upload the emitter package to the playground package storage. + - name: UploadPlaygroundBundle + type: boolean + default: false + stages: # Build stage # Responsible for building the autorest generator and typespec emitter packages @@ -333,6 +338,23 @@ stages: ArtifactPath: $(buildArtifactsPath) LanguageShortName: ${{ parameters.LanguageShortName }} + - ${{ if parameters.UploadPlaygroundBundle }}: + - script: npm install -g pnpm + displayName: Install pnpm for playground bundle upload + - script: pnpm install --filter "@typespec/bundle-uploader..." + displayName: Install bundle-uploader dependencies + workingDirectory: $(Build.SourcesDirectory) + - script: pnpm build --filter "@typespec/bundle-uploader..." + displayName: Build bundle-uploader + workingDirectory: $(Build.SourcesDirectory) + - task: AzureCLI@1 + displayName: Upload playground bundle + inputs: + azureSubscription: "Azure SDK Engineering System" + scriptLocation: inlineScript + inlineScript: node ./eng/emitters/scripts/upload-bundled-emitter.js ${{ parameters.PackagePath }} + workingDirectory: $(Build.SourcesDirectory) + templateContext: outputs: - output: pipelineArtifact diff --git a/eng/emitters/scripts/upload-bundled-emitter.js b/eng/emitters/scripts/upload-bundled-emitter.js new file mode 100644 index 00000000000..d22febef09f --- /dev/null +++ b/eng/emitters/scripts/upload-bundled-emitter.js @@ -0,0 +1,15 @@ +// @ts-check +import { resolve } from "path"; +import { bundleAndUploadStandalonePackage } from "../../../packages/bundle-uploader/dist/src/index.js"; +import { repoRoot } from "../../common/scripts/helpers.js"; + +const packageRelativePath = process.argv[2]; +if (!packageRelativePath) { + console.error("Usage: node upload-bundled-emitter.js "); + console.error(" e.g. node upload-bundled-emitter.js packages/http-client-csharp"); + process.exit(1); +} + +const packagePath = resolve(repoRoot, packageRelativePath); + +await bundleAndUploadStandalonePackage({ packagePath }); diff --git a/packages/bundle-uploader/src/index.ts b/packages/bundle-uploader/src/index.ts index 757403df988..03c51df8186 100644 --- a/packages/bundle-uploader/src/index.ts +++ b/packages/bundle-uploader/src/index.ts @@ -16,6 +16,57 @@ function logSuccess(message: string) { logInfo(pc.green(`✔ ${message}`)); } +export interface BundleAndUploadStandalonePackageOptions { + /** + * Absolute path to the package directory. + */ + packagePath: string; + + /** + * Name for the index (e.g. "@typespec/http-client-csharp"). + * Defaults to the package name from package.json. + */ + indexName?: string; +} + +/** + * Bundle and upload a standalone package that is not part of the pnpm workspace. + * Creates both a versioned index and a latest.json index. + */ +export async function bundleAndUploadStandalonePackage({ + packagePath, + indexName: indexNameOverride, +}: BundleAndUploadStandalonePackageOptions) { + const bundle = await createTypeSpecBundle(packagePath); + const manifest = bundle.manifest; + const indexName = indexNameOverride ?? manifest.name; + logInfo(`Bundling standalone package: ${manifest.name}@${manifest.version}`); + + const uploader = new TypeSpecBundledPackageUploader(new AzureCliCredential()); + await uploader.createIfNotExists(); + + const result = await uploader.upload(bundle); + if (result.status === "uploaded") { + logSuccess(`Bundle for package ${manifest.name}@${manifest.version} uploaded.`); + } else { + logInfo(`Bundle for package ${manifest.name} already exists for version ${manifest.version}.`); + } + + const importMap: Record = {}; + for (const [key, value] of Object.entries(result.imports)) { + importMap[joinUnix(manifest.name, key)] = value; + } + + const index = { + version: manifest.version, + imports: importMap, + }; + await uploader.updateIndex(indexName, index); + logSuccess(`Updated index for ${indexName}@${manifest.version}.`); + await uploader.updateLatestIndex(indexName, index); + logSuccess(`Updated latest index for ${indexName}.`); +} + export interface BundleAndUploadPackagesOptions { repoRoot: string; /** @@ -85,9 +136,12 @@ export async function bundleAndUploadPackages({ } } logInfo(`Import map for ${indexVersion}:`, importMap); - await uploader.updateIndex(indexName, { + const index = { version: indexVersion, imports: importMap, - }); + }; + await uploader.updateIndex(indexName, index); logSuccess(`Updated index for version ${indexVersion}.`); + await uploader.updateLatestIndex(indexName, index); + logSuccess(`Updated latest index for ${indexName}.`); } diff --git a/packages/bundle-uploader/src/upload-browser-package.ts b/packages/bundle-uploader/src/upload-browser-package.ts index eec752029d0..c432d308fa4 100644 --- a/packages/bundle-uploader/src/upload-browser-package.ts +++ b/packages/bundle-uploader/src/upload-browser-package.ts @@ -75,6 +75,29 @@ export class TypeSpecBundledPackageUploader { }); } + async getLatestIndex(name: string): Promise { + const blob = this.#container.getBlockBlobClient(`indexes/${name}/latest.json`); + if (await blob.exists()) { + const response = await blob.download(); + const body = await response.blobBody; + const existingContent = await body?.text(); + if (existingContent) { + return JSON.parse(existingContent); + } + } + return undefined; + } + + async updateLatestIndex(name: string, index: PackageIndex) { + const blob = this.#container.getBlockBlobClient(`indexes/${name}/latest.json`); + const content = JSON.stringify(index); + await blob.upload(content, content.length, { + blobHTTPHeaders: { + blobContentType: "application/json; charset=utf-8", + }, + }); + } + async #uploadManifest(manifest: BundleManifest) { try { const blob = this.#container.getBlockBlobClient( diff --git a/packages/http-client-csharp/eng/pipeline/publish.yml b/packages/http-client-csharp/eng/pipeline/publish.yml index e369e98bebd..2154939eeb9 100644 --- a/packages/http-client-csharp/eng/pipeline/publish.yml +++ b/packages/http-client-csharp/eng/pipeline/publish.yml @@ -64,6 +64,7 @@ extends: LanguageShortName: "csharp" HasNugetPackages: true CadlRanchName: "@typespec/http-client-csharp" + UploadPlaygroundBundle: true AdditionalInitializeSteps: - task: UseDotNet@2 inputs: diff --git a/packages/http-client-python/eng/pipeline/publish.yml b/packages/http-client-python/eng/pipeline/publish.yml index 69fe74aa10e..d134468d578 100644 --- a/packages/http-client-python/eng/pipeline/publish.yml +++ b/packages/http-client-python/eng/pipeline/publish.yml @@ -31,3 +31,4 @@ extends: CadlRanchName: "@typespec/http-client-python" EnableCadlRanchReport: false PythonVersion: "3.11" + UploadPlaygroundBundle: true diff --git a/website/src/components/playground-component/import-map.ts b/website/src/components/playground-component/import-map.ts index 88c30465fba..d0a985bad60 100644 --- a/website/src/components/playground-component/import-map.ts +++ b/website/src/components/playground-component/import-map.ts @@ -9,10 +9,40 @@ export interface ImportMap { imports: Record; } +const pkgsBaseUrl = "https://typespec.blob.core.windows.net/pkgs"; + +async function fetchAdditionalPackageImports( + packageNames: readonly string[], +): Promise> { + const imports: Record = {}; + + const results = await Promise.allSettled( + packageNames.map(async (name) => { + const url = `${pkgsBaseUrl}/indexes/${name}/latest.json`; + const response = await fetch(url); + if (!response.ok) { + console.warn(`Failed to load latest index for ${name}: ${response.status}`); + return undefined; + } + return JSON.parse(await response.text()) as ImportMap; + }), + ); + + for (const result of results) { + if (result.status === "fulfilled" && result.value) { + Object.assign(imports, result.value.imports); + } + } + + return imports; +} + export async function loadImportMap({ latestVersion, + additionalPackages = [], }: { latestVersion: string; + additionalPackages?: readonly string[]; }): Promise { const optionsScript = document.querySelector("script[type=playground-options]"); if (optionsScript === undefined) { @@ -24,14 +54,17 @@ export async function loadImportMap({ const parsed = new URLSearchParams(window.location.search); const requestedVersion = parsed.get("version"); - const importMapUrl = `https://typespec.blob.core.windows.net/pkgs/indexes/typespec/${ + const importMapUrl = `${pkgsBaseUrl}/indexes/typespec/${ requestedVersion ?? latestVersion }.json`; - const response = await fetch(importMapUrl); - const content = await response.text(); + const [mainResponse, additionalImports] = await Promise.all([ + fetch(importMapUrl), + fetchAdditionalPackageImports(additionalPackages), + ]); - const importMap = JSON.parse(content); + const importMap: ImportMap = JSON.parse(await mainResponse.text()); + Object.assign(importMap.imports, additionalImports); (window as any).importShim.addImportMap(importMap); diff --git a/website/src/components/react-pages/playground.tsx b/website/src/components/react-pages/playground.tsx index 10e9d9bed06..ba25bd85490 100644 --- a/website/src/components/react-pages/playground.tsx +++ b/website/src/components/react-pages/playground.tsx @@ -3,6 +3,11 @@ import { useEffect, useState, type ReactNode } from "react"; import { FluentLayout } from "../fluent/fluent-layout"; import { loadImportMap, type VersionData } from "../playground-component/import-map"; +const additionalPlaygroundPackages = [ + "@typespec/http-client-csharp", + "@typespec/http-client-python", +] as const; + export const AsyncPlayground = ({ latestVersion, fallback, @@ -15,7 +20,10 @@ export const AsyncPlayground = ({ WebsitePlayground: typeof import("../playground-component/playground").WebsitePlayground; }>(undefined as any); useEffect(() => { - Promise.all([loadImportMap({ latestVersion }), import("../playground-component/playground")]) + Promise.all([ + loadImportMap({ latestVersion, additionalPackages: additionalPlaygroundPackages }), + import("../playground-component/playground"), + ]) .then((x) => setMod({ versionData: x[0] as any, WebsitePlayground: x[1].WebsitePlayground })) .catch((e) => { throw e; From bddc4f5a994b287c352f51b577d856953d67c5f9 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 31 Mar 2026 19:12:06 -0400 Subject: [PATCH 002/103] use latest instead --- packages/bundle-uploader/src/index.ts | 20 ++++--------- .../src/upload-browser-package.ts | 29 +++++++++++++++++++ .../playground-component/import-map.ts | 2 +- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/bundle-uploader/src/index.ts b/packages/bundle-uploader/src/index.ts index 03c51df8186..f71aa97e4a7 100644 --- a/packages/bundle-uploader/src/index.ts +++ b/packages/bundle-uploader/src/index.ts @@ -21,25 +21,18 @@ export interface BundleAndUploadStandalonePackageOptions { * Absolute path to the package directory. */ packagePath: string; - - /** - * Name for the index (e.g. "@typespec/http-client-csharp"). - * Defaults to the package name from package.json. - */ - indexName?: string; } /** * Bundle and upload a standalone package that is not part of the pnpm workspace. - * Creates both a versioned index and a latest.json index. + * Uploads the bundle files and writes a `latest.json` under the package's blob directory + * (e.g. `@typespec/http-client-csharp/latest.json`). */ export async function bundleAndUploadStandalonePackage({ packagePath, - indexName: indexNameOverride, }: BundleAndUploadStandalonePackageOptions) { const bundle = await createTypeSpecBundle(packagePath); const manifest = bundle.manifest; - const indexName = indexNameOverride ?? manifest.name; logInfo(`Bundling standalone package: ${manifest.name}@${manifest.version}`); const uploader = new TypeSpecBundledPackageUploader(new AzureCliCredential()); @@ -57,14 +50,11 @@ export async function bundleAndUploadStandalonePackage({ importMap[joinUnix(manifest.name, key)] = value; } - const index = { + await uploader.updatePackageLatest(manifest.name, { version: manifest.version, imports: importMap, - }; - await uploader.updateIndex(indexName, index); - logSuccess(`Updated index for ${indexName}@${manifest.version}.`); - await uploader.updateLatestIndex(indexName, index); - logSuccess(`Updated latest index for ${indexName}.`); + }); + logSuccess(`Updated ${manifest.name}/latest.json for version ${manifest.version}.`); } export interface BundleAndUploadPackagesOptions { diff --git a/packages/bundle-uploader/src/upload-browser-package.ts b/packages/bundle-uploader/src/upload-browser-package.ts index c432d308fa4..ac43a600501 100644 --- a/packages/bundle-uploader/src/upload-browser-package.ts +++ b/packages/bundle-uploader/src/upload-browser-package.ts @@ -98,6 +98,35 @@ export class TypeSpecBundledPackageUploader { }); } + /** Read the latest.json for a package from `{pkgName}/latest.json`. */ + async getPackageLatest(pkgName: string): Promise { + const blob = this.#container.getBlockBlobClient( + normalizePath(join(pkgName, "latest.json")), + ); + if (await blob.exists()) { + const response = await blob.download(); + const body = await response.blobBody; + const existingContent = await body?.text(); + if (existingContent) { + return JSON.parse(existingContent); + } + } + return undefined; + } + + /** Write the latest.json for a package at `{pkgName}/latest.json`. */ + async updatePackageLatest(pkgName: string, index: PackageIndex) { + const blob = this.#container.getBlockBlobClient( + normalizePath(join(pkgName, "latest.json")), + ); + const content = JSON.stringify(index); + await blob.upload(content, content.length, { + blobHTTPHeaders: { + blobContentType: "application/json; charset=utf-8", + }, + }); + } + async #uploadManifest(manifest: BundleManifest) { try { const blob = this.#container.getBlockBlobClient( diff --git a/website/src/components/playground-component/import-map.ts b/website/src/components/playground-component/import-map.ts index d0a985bad60..a013ca28873 100644 --- a/website/src/components/playground-component/import-map.ts +++ b/website/src/components/playground-component/import-map.ts @@ -18,7 +18,7 @@ async function fetchAdditionalPackageImports( const results = await Promise.allSettled( packageNames.map(async (name) => { - const url = `${pkgsBaseUrl}/indexes/${name}/latest.json`; + const url = `${pkgsBaseUrl}/${name}/latest.json`; const response = await fetch(url); if (!response.ok) { console.warn(`Failed to load latest index for ${name}: ${response.status}`); From bb15e692330d08acf9269c55da393f521817a54c Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 1 Apr 2026 11:21:07 -0700 Subject: [PATCH 003/103] Apply suggestion from @timotheeguerin --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 3c79111ae57..5b12ae69aa2 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -344,7 +344,7 @@ stages: - script: pnpm install --filter "@typespec/bundle-uploader..." displayName: Install bundle-uploader dependencies workingDirectory: $(Build.SourcesDirectory) - - script: pnpm build --filter "@typespec/bundle-uploader..." + - script: pnpm --filter "@typespec/bundle-uploader..." build displayName: Build bundle-uploader workingDirectory: $(Build.SourcesDirectory) - task: AzureCLI@1 From 5883d12fdda87c6e940e775eddc4e0c744b12a90 Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 16:40:13 -0700 Subject: [PATCH 004/103] fix: strip leading slash from PackagePath in upload-bundled-emitter Pipeline PackagePath values start with '/' (e.g. '/packages/http-client-csharp') which causes path.resolve() to treat it as an absolute path, ignoring repoRoot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/scripts/upload-bundled-emitter.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eng/emitters/scripts/upload-bundled-emitter.js b/eng/emitters/scripts/upload-bundled-emitter.js index d22febef09f..449d37984e4 100644 --- a/eng/emitters/scripts/upload-bundled-emitter.js +++ b/eng/emitters/scripts/upload-bundled-emitter.js @@ -10,6 +10,8 @@ if (!packageRelativePath) { process.exit(1); } -const packagePath = resolve(repoRoot, packageRelativePath); +// Strip leading "/" so resolve() doesn't treat it as an absolute path. +// Pipeline PackagePath values start with "/" (e.g. "/packages/http-client-csharp"). +const packagePath = resolve(repoRoot, packageRelativePath.replace(/^\//, "")); await bundleAndUploadStandalonePackage({ packagePath }); From 2814ed0fc7ced527cc47532081f4190ab2bd8daf Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 2 Apr 2026 13:07:20 -0700 Subject: [PATCH 005/103] fix: build emitter before playground bundle upload The emitter packages (http-client-csharp, http-client-python) are outside the pnpm workspace, so pnpm build doesn't cover them. Add npm ci + npm run build:emitter steps before bundling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 5b12ae69aa2..3038c4a00db 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -341,6 +341,12 @@ stages: - ${{ if parameters.UploadPlaygroundBundle }}: - script: npm install -g pnpm displayName: Install pnpm for playground bundle upload + - script: npm ci + displayName: Install emitter dependencies for playground bundle + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} + - script: npm run build:emitter + displayName: Build emitter for playground bundle + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} - script: pnpm install --filter "@typespec/bundle-uploader..." displayName: Install bundle-uploader dependencies workingDirectory: $(Build.SourcesDirectory) From 0bbb3c7ac92873345b13992e2da726f144744b0d Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 2 Apr 2026 14:20:39 -0700 Subject: [PATCH 006/103] fix: use npm run build instead of build:emitter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 3038c4a00db..8f080b2a3f1 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -344,7 +344,7 @@ stages: - script: npm ci displayName: Install emitter dependencies for playground bundle workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} - - script: npm run build:emitter + - script: npm run build displayName: Build emitter for playground bundle workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} - script: pnpm install --filter "@typespec/bundle-uploader..." From 32bb4e72415762e39e40ef0bc5c68f6a1e7de450 Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 2 Apr 2026 14:25:18 -0700 Subject: [PATCH 007/103] feat(http-client-csharp): add playground support Browser-compatible emitter (dynamic imports), playground-server-url option, .NET playground server, and serialization optimization. From PR #10189. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../emitter/src/code-model-writer.ts | 12 +- .../http-client-csharp/emitter/src/emitter.ts | 212 +++++++++++++----- .../emitter/src/lib/utils.ts | 4 +- .../http-client-csharp/emitter/src/options.ts | 7 + packages/http-client-csharp/package.json | 1 + .../playground-server/.gitignore | 2 + .../playground-server/Dockerfile | 33 +++ .../playground-server/Program.cs | 162 +++++++++++++ .../playground-server.csproj | 10 + 9 files changed, 380 insertions(+), 63 deletions(-) create mode 100644 packages/http-client-csharp/playground-server/.gitignore create mode 100644 packages/http-client-csharp/playground-server/Dockerfile create mode 100644 packages/http-client-csharp/playground-server/Program.cs create mode 100644 packages/http-client-csharp/playground-server/playground-server.csproj diff --git a/packages/http-client-csharp/emitter/src/code-model-writer.ts b/packages/http-client-csharp/emitter/src/code-model-writer.ts index 994d0bad2e7..e60360c32b5 100644 --- a/packages/http-client-csharp/emitter/src/code-model-writer.ts +++ b/packages/http-client-csharp/emitter/src/code-model-writer.ts @@ -8,6 +8,16 @@ import { CSharpEmitterContext } from "./sdk-context.js"; import { CodeModel } from "./type/code-model.js"; import { Configuration } from "./type/configuration.js"; +/** + * Serializes the code model to a JSON string with reference tracking. + * @param context - The CSharp emitter context + * @param codeModel - The code model to serialize + * @beta + */ +export function serializeCodeModel(context: CSharpEmitterContext, codeModel: CodeModel): string { + return prettierOutput(JSON.stringify(buildJson(context, codeModel), transformJSONProperties, 2)); +} + /** * Writes the code model to the output folder. Should only be used by autorest.csharp. * @param context - The CSharp emitter context @@ -22,7 +32,7 @@ export async function writeCodeModel( ) { await context.program.host.writeFile( resolvePath(outputFolder, tspOutputFileName), - prettierOutput(JSON.stringify(buildJson(context, codeModel), transformJSONProperties, 2)), + serializeCodeModel(context, codeModel), ); } diff --git a/packages/http-client-csharp/emitter/src/emitter.ts b/packages/http-client-csharp/emitter/src/emitter.ts index 936f79dd191..f372b430f73 100644 --- a/packages/http-client-csharp/emitter/src/emitter.ts +++ b/packages/http-client-csharp/emitter/src/emitter.ts @@ -12,10 +12,7 @@ import { Program, resolvePath, } from "@typespec/compiler"; -import fs, { statSync } from "fs"; -import { dirname } from "path"; -import { fileURLToPath } from "url"; -import { writeCodeModel, writeConfiguration } from "./code-model-writer.js"; +import { writeCodeModel, writeConfiguration, serializeCodeModel } from "./code-model-writer.js"; import { _minSupportedDotNetSdkVersion, configurationFileName, @@ -34,14 +31,21 @@ import { Configuration } from "./type/configuration.js"; /** * Look for the project root by looking up until a `package.json` is found. * @param path Path to start looking + * @param statSyncFn The statSync function (injected to avoid top-level fs import) */ -function findProjectRoot(path: string): string | undefined { +function findProjectRoot( + path: string, + statSyncFn: (p: string) => { isFile(): boolean }, +): string | undefined { let current = path; while (true) { const pkgPath = joinPaths(current, "package.json"); - const stats = checkFile(pkgPath); - if (stats?.isFile()) { - return current; + try { + if (statSyncFn(pkgPath)?.isFile()) { + return current; + } + } catch { + // file doesn't exist } const parent = getDirectoryPath(current); if (parent === current) { @@ -107,64 +111,46 @@ export async function emitCodeModel( // Apply optional code model update callback const updatedRoot = updateCodeModel ? updateCodeModel(root, sdkContext) : root; - const generatedFolder = resolvePath(outputFolder, "src", "Generated"); - - if (!fs.existsSync(generatedFolder)) { - fs.mkdirSync(generatedFolder, { recursive: true }); - } - - // emit tspCodeModel.json - await writeCodeModel(sdkContext, updatedRoot, outputFolder); - const namespace = updatedRoot.name; const configurations: Configuration = createConfiguration(options, namespace, sdkContext); - //emit configuration.json - await writeConfiguration(sdkContext, configurations, outputFolder); + const playgroundServerUrl = + options["playground-server-url"] || + (typeof globalThis.process === "undefined" + ? ((globalThis as any).__TYPESPEC_PLAYGROUND_SERVER_URL__ ?? "http://localhost:5174") + : undefined); - const csProjFile = resolvePath( - outputFolder, - "src", - `${configurations["package-name"]}.csproj`, - ); - logger.info(`Checking if ${csProjFile} exists`); - - const emitterPath = options["emitter-extension-path"] ?? import.meta.url; - const projectRoot = findProjectRoot(dirname(fileURLToPath(emitterPath))); - const generatorPath = resolvePath( - projectRoot + "/dist/generator/Microsoft.TypeSpec.Generator.dll", - ); + if (playgroundServerUrl) { + // Playground mode: serialize and send directly to server without writing to virtual FS + const codeModelJson = serializeCodeModel(sdkContext, updatedRoot); + const configJson = JSON.stringify(configurations, null, 2) + "\n"; + await generateViaPlaygroundServer( + playgroundServerUrl, + sdkContext, + outputFolder, + codeModelJson, + configJson, + options["generator-name"], + ); + } else { + // Local mode: write files and run .NET generator + await writeCodeModel(sdkContext, updatedRoot, outputFolder); + await writeConfiguration(sdkContext, configurations, outputFolder); - try { - const result = await execCSharpGenerator(sdkContext, { - generatorPath: generatorPath, - outputFolder: outputFolder, + await runLocalGenerator(sdkContext, diagnostics, { + outputFolder, + packageName: configurations["package-name"] ?? "", generatorName: options["generator-name"], - newProject: options["new-project"] || !checkFile(csProjFile), + newProject: options["new-project"], debug: options.debug ?? false, + emitterExtensionPath: options["emitter-extension-path"], + logger, }); - if (result.exitCode !== 0) { - const isValid = diagnostics.pipe( - await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), - ); - // if the dotnet sdk is valid, the error is not dependency issue, log it as normal - if (isValid) { - throw new Error( - `Failed to generate the library. Exit code: ${result.exitCode}.\nStackTrace: \n${result.stderr}`, - ); - } + + if (!options["save-inputs"]) { + context.program.host.rm(resolvePath(outputFolder, tspOutputFileName)); + context.program.host.rm(resolvePath(outputFolder, configurationFileName)); } - } catch (error: any) { - const isValid = diagnostics.pipe( - await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), - ); - // if the dotnet sdk is valid, the error is not dependency issue, log it as normal - if (isValid) throw new Error(error, { cause: error }); - } - if (!options["save-inputs"]) { - // delete - context.program.host.rm(resolvePath(outputFolder, tspOutputFileName)); - context.program.host.rm(resolvePath(outputFolder, configurationFileName)); } } } @@ -199,6 +185,7 @@ export function createConfiguration( "generate-protocol-methods", "generate-convenience-methods", "emitter-extension-path", + "playground-server-url", ]; const derivedOptions = Object.fromEntries( Object.entries(options).filter(([key]) => !skipKeys.includes(key)), @@ -291,10 +278,113 @@ function validateDotNetSdkVersionCore( } } -function checkFile(pkgPath: string) { +/** + * Sends the code model and configuration to a playground server for C# generation. + * Used when the emitter runs in a browser environment. + */ +async function generateViaPlaygroundServer( + serverUrl: string, + sdkContext: CSharpEmitterContext, + outputFolder: string, + codeModelJson: string, + configJson: string, + generatorName: string, +): Promise { + const response = await fetch(`${serverUrl}/generate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + codeModel: codeModelJson, + configuration: configJson, + generatorName, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Playground server error (${response.status}): ${errorText}`); + } + + const result: { files: Array<{ path: string; content: string }> } = await response.json(); + + for (const file of result.files) { + await sdkContext.program.host.writeFile(resolvePath(outputFolder, file.path), file.content); + } +} + +/** + * Runs the .NET generator locally via subprocess. + * Uses dynamic imports for Node.js modules (fs, path, url) to keep the + * emitter module loadable in browser environments. + */ +async function runLocalGenerator( + sdkContext: CSharpEmitterContext, + diagnostics: ReturnType, + options: { + outputFolder: string; + packageName: string; + generatorName: string; + newProject: boolean; + debug: boolean; + emitterExtensionPath?: string; + logger: Logger; + }, +): Promise { + const fs = await import("fs"); + const { dirname } = await import("path"); + const { fileURLToPath } = await import("url"); + + const generatedFolder = resolvePath(options.outputFolder, "src", "Generated"); + + if (!fs.existsSync(generatedFolder)) { + fs.mkdirSync(generatedFolder, { recursive: true }); + } + + const csProjFile = resolvePath( + options.outputFolder, + "src", + `${options.packageName}.csproj`, + ); + options.logger.info(`Checking if ${csProjFile} exists`); + + const emitterPath = options.emitterExtensionPath ?? import.meta.url; + const projectRoot = findProjectRoot(dirname(fileURLToPath(emitterPath)), fs.statSync); + const generatorPath = resolvePath( + projectRoot + "/dist/generator/Microsoft.TypeSpec.Generator.dll", + ); + + const checkFile = (path: string) => { + try { + return fs.statSync(path); + } catch { + return undefined; + } + }; + try { - return statSync(pkgPath); - } catch (error) { - return undefined; + const result = await execCSharpGenerator(sdkContext, { + generatorPath: generatorPath, + outputFolder: options.outputFolder, + generatorName: options.generatorName, + newProject: options.newProject || !checkFile(csProjFile), + debug: options.debug, + }); + if (result.exitCode !== 0) { + const isValid = diagnostics.pipe( + await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), + ); + // if the dotnet sdk is valid, the error is not dependency issue, log it as normal + if (isValid) { + throw new Error( + `Failed to generate the library. Exit code: ${result.exitCode}.\nStackTrace: \n${result.stderr}`, + ); + } + } + } catch (error: any) { + const isValid = diagnostics.pipe( + await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), + ); + // if the dotnet sdk is valid, the error is not dependency issue, log it as normal + if (isValid) throw new Error(error, { cause: error }); } } diff --git a/packages/http-client-csharp/emitter/src/lib/utils.ts b/packages/http-client-csharp/emitter/src/lib/utils.ts index aa8af4d3889..8275ef27bf7 100644 --- a/packages/http-client-csharp/emitter/src/lib/utils.ts +++ b/packages/http-client-csharp/emitter/src/lib/utils.ts @@ -9,7 +9,7 @@ import { } from "@azure-tools/typespec-client-generator-core"; import { getNamespaceFullName, Namespace, NoTarget, Type } from "@typespec/compiler"; import { Visibility } from "@typespec/http"; -import { spawn, SpawnOptions } from "child_process"; +import type { SpawnOptions } from "child_process"; import { CSharpEmitterContext } from "../sdk-context.js"; export async function execCSharpGenerator( @@ -22,6 +22,7 @@ export async function execCSharpGenerator( debug: boolean; }, ): Promise<{ exitCode: number; stderr: string; proc: any }> { + const { spawn } = await import("child_process"); const command = "dotnet"; const args = [ "--roll-forward", @@ -110,6 +111,7 @@ export async function execAsync( args: string[] = [], options: SpawnOptions = {}, ): Promise<{ exitCode: number; stdio: string; stdout: string; stderr: string; proc: any }> { + const { spawn } = await import("child_process"); const child = spawn(command, args, options); return new Promise((resolve, reject) => { diff --git a/packages/http-client-csharp/emitter/src/options.ts b/packages/http-client-csharp/emitter/src/options.ts index 9ae08884a9e..dfa82a09fb3 100644 --- a/packages/http-client-csharp/emitter/src/options.ts +++ b/packages/http-client-csharp/emitter/src/options.ts @@ -22,6 +22,7 @@ export interface CSharpEmitterOptions { "generate-protocol-methods"?: boolean; "generate-convenience-methods"?: boolean; "package-name"?: string; + "playground-server-url"?: string; license?: { name: string; company?: string; @@ -133,6 +134,12 @@ export const CSharpEmitterOptionsSchema: JSONSchemaType = description: "The SDK context options that implement the `CreateSdkContextOptions` interface from the [`@azure-tools/typespec-client-generator-core`](https://www.npmjs.com/package/@azure-tools/typespec-client-generator-core) package to be used by the CSharp emitter.", }, + "playground-server-url": { + type: "string", + nullable: true, + description: + "URL of a playground server that runs the .NET generator. When set, the emitter sends the code model to this server instead of spawning a local dotnet process. Used for browser-based playground environments.", + }, }, required: [], }; diff --git a/packages/http-client-csharp/package.json b/packages/http-client-csharp/package.json index e0bbfda7540..da59c45fd8a 100644 --- a/packages/http-client-csharp/package.json +++ b/packages/http-client-csharp/package.json @@ -30,6 +30,7 @@ "build:emitter": "tsc -p ./emitter/tsconfig.build.json", "build:generator": "dotnet build ./generator", "build": "npm run build:emitter && npm run build:generator && npm run extract-api", + "dev:playground": "npx concurrently -k -n playground,server -c cyan,green \"npx vite --config ../../packages/playground-website/vite.config.ts ../../packages/playground-website\" \"dotnet run --project ./playground-server\"", "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", "watch": "tsc -p ./emitter/tsconfig.build.json --watch", "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", diff --git a/packages/http-client-csharp/playground-server/.gitignore b/packages/http-client-csharp/playground-server/.gitignore new file mode 100644 index 00000000000..cd42ee34e87 --- /dev/null +++ b/packages/http-client-csharp/playground-server/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile new file mode 100644 index 00000000000..c6894a3ab17 --- /dev/null +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -0,0 +1,33 @@ +# Build from the http-client-csharp package root: +# docker build -f playground-server/Dockerfile -t csharp-playground-server . +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +# Build the generator (populates dist/generator/) +COPY generator/ generator/ +COPY global.json . +RUN dotnet build generator -c Release + +# Build the server +COPY playground-server/playground-server.csproj playground-server/ +RUN dotnet restore playground-server/playground-server.csproj +COPY playground-server/ playground-server/ +RUN dotnet publish playground-server -c Release -o /app + +# Copy generator output +RUN cp -r dist/generator /app/generator + +# Need full SDK (not just aspnet) because the server spawns `dotnet` to run the generator DLL +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime +WORKDIR /app +COPY --from=build /app . + +RUN groupadd -r playground && useradd -r -g playground playground +USER playground + +ENV ASPNETCORE_URLS=http://+:5174 +ENV DOTNET_ENVIRONMENT=Production +ENV GENERATOR_PATH=/app/generator/Microsoft.TypeSpec.Generator.dll + +EXPOSE 5174 +ENTRYPOINT ["dotnet", "playground-server.dll"] diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs new file mode 100644 index 00000000000..85bd18aff1f --- /dev/null +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.RateLimiting; +using Microsoft.AspNetCore.RateLimiting; + +var builder = WebApplication.CreateBuilder(args); + +var allowedOrigins = new HashSet(StringComparer.OrdinalIgnoreCase) +{ + "http://localhost:5173", // vite dev + "http://localhost:4173", // vite preview + "http://localhost:3000", +}; +var playgroundUrl = Environment.GetEnvironmentVariable("PLAYGROUND_URL"); +if (!string.IsNullOrEmpty(playgroundUrl) && Uri.TryCreate(playgroundUrl, UriKind.Absolute, out var uri)) +{ + allowedOrigins.Add(uri.GetLeftPart(UriPartial.Authority)); +} + +builder.Services.AddCors(); +builder.Services.AddRateLimiter(options => +{ + options.RejectionStatusCode = 429; + options.AddFixedWindowLimiter("generate", limiter => + { + limiter.PermitLimit = 10; + limiter.Window = TimeSpan.FromMinutes(1); + limiter.QueueLimit = 2; + limiter.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + }); +}); + +var app = builder.Build(); + +app.UseCors(policy => policy + .WithOrigins([.. allowedOrigins]) + .AllowAnyMethod() + .AllowAnyHeader()); + +app.UseRateLimiter(); + +// Resolve the generator DLL path. Default: dist/generator in the http-client-csharp package. +var generatorPath = Environment.GetEnvironmentVariable("GENERATOR_PATH") + ?? Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "dist", "generator", "Microsoft.TypeSpec.Generator.dll")); + +if (!File.Exists(generatorPath)) +{ + Console.Error.WriteLine($"WARNING: Generator DLL not found at {generatorPath}"); + Console.Error.WriteLine("Set GENERATOR_PATH environment variable to the correct path."); +} +else +{ + Console.WriteLine($"Generator DLL: {generatorPath}"); +} + +app.MapGet("/health", () => Results.Ok(new +{ + status = "ok", + generatorFound = File.Exists(generatorPath), + generatorPath +})); + +app.MapPost("/generate", async (HttpRequest request) => +{ + var body = await JsonSerializer.DeserializeAsync( + request.Body, GenerateJsonContext.Default.GenerateRequest); + + if (body?.CodeModel is null || body?.Configuration is null) + { + return Results.BadRequest(new { error = "Missing 'codeModel' or 'configuration' fields" }); + } + + if (!File.Exists(generatorPath)) + { + return Results.StatusCode(503); + } + + // Create a temporary working directory + var tempDir = Path.Combine(Path.GetTempPath(), "tsp-playground", Guid.NewGuid().ToString("N")); + var generatedDir = Path.Combine(tempDir, "src", "Generated"); + Directory.CreateDirectory(generatedDir); + + try + { + // Write the input files the generator expects + await File.WriteAllTextAsync(Path.Combine(tempDir, "tspCodeModel.json"), body.CodeModel); + await File.WriteAllTextAsync(Path.Combine(tempDir, "Configuration.json"), body.Configuration); + + var generatorName = body.GeneratorName ?? "ScmCodeModelGenerator"; + + // Run the .NET generator as a subprocess (same approach as the TypeSpec emitter) + var psi = new ProcessStartInfo + { + FileName = "dotnet", + ArgumentList = { "--roll-forward", "Major", generatorPath, tempDir, "-g", generatorName, "--new-project" }, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using var process = Process.Start(psi)!; + var stdout = await process.StandardOutput.ReadToEndAsync(); + var stderr = await process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + return Results.Json( + new GenerateErrorResponse($"Generator failed with exit code {process.ExitCode}", stderr), + GenerateJsonContext.Default.GenerateErrorResponse, + statusCode: 500); + } + + // Collect all generated files + var files = new List(); + if (Directory.Exists(tempDir)) + { + foreach (var filePath in Directory.EnumerateFiles(tempDir, "*", SearchOption.AllDirectories)) + { + // Skip the input files + var fileName = Path.GetFileName(filePath); + if (fileName is "tspCodeModel.json" or "Configuration.json") + continue; + + var relativePath = Path.GetRelativePath(tempDir, filePath).Replace('\\', '/'); + var content = await File.ReadAllTextAsync(filePath); + files.Add(new GeneratedFile(relativePath, content)); + } + } + + return Results.Json( + new GenerateResponse(files), + GenerateJsonContext.Default.GenerateResponse); + } + finally + { + // Clean up temp directory + try { Directory.Delete(tempDir, recursive: true); } catch { } + } +}).RequireRateLimiting("generate"); + +var url = Environment.GetEnvironmentVariable("ASPNETCORE_URLS") ?? "http://localhost:5174"; +Console.WriteLine($"C# playground server listening on {url}"); +app.Run(url); + +// --- Request/Response types --- + +record GenerateRequest(string? CodeModel, string? Configuration, string? GeneratorName); +record GeneratedFile(string Path, string Content); +record GenerateResponse(List Files); +record GenerateErrorResponse(string Error, string? Details); + +[JsonSerializable(typeof(GenerateRequest))] +[JsonSerializable(typeof(GenerateResponse))] +[JsonSerializable(typeof(GenerateErrorResponse))] +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +partial class GenerateJsonContext : JsonSerializerContext { } diff --git a/packages/http-client-csharp/playground-server/playground-server.csproj b/packages/http-client-csharp/playground-server/playground-server.csproj new file mode 100644 index 00000000000..8c5ce456c81 --- /dev/null +++ b/packages/http-client-csharp/playground-server/playground-server.csproj @@ -0,0 +1,10 @@ + + + + net10.0 + enable + enable + PlaygroundServer + + + From 6d29a07addbd27a99a2bb20d99bf4a314b8970d4 Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 2 Apr 2026 16:11:43 -0700 Subject: [PATCH 008/103] fix: use build:emitter instead of build (no .NET SDK needed) The publish stage doesn't have the .NET SDK installed. Only the TypeScript emitter build is needed for the playground bundle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 8f080b2a3f1..3038c4a00db 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -344,7 +344,7 @@ stages: - script: npm ci displayName: Install emitter dependencies for playground bundle workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} - - script: npm run build + - script: npm run build:emitter displayName: Build emitter for playground bundle workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} - script: pnpm install --filter "@typespec/bundle-uploader..." From fb1e8acc7b1b8e4ace42481f805c7cc67c25849f Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 2 Apr 2026 16:14:02 -0700 Subject: [PATCH 009/103] fix: parameterize build script for playground bundle Python uses 'build' (default), C# uses 'build:emitter' to skip the .NET generator build which requires the .NET SDK. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../pipelines/templates/stages/emitter-stages.yml | 8 +++++++- packages/http-client-csharp/eng/pipeline/publish.yml | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 3038c4a00db..ae7e24dab43 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -87,6 +87,12 @@ parameters: type: boolean default: false + # The npm script to run to build the emitter for playground bundling. + # Default is "build". C# uses "build:emitter" to skip the .NET generator build. + - name: PlaygroundBundleBuildScript + type: string + default: "build" + stages: # Build stage # Responsible for building the autorest generator and typespec emitter packages @@ -344,7 +350,7 @@ stages: - script: npm ci displayName: Install emitter dependencies for playground bundle workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} - - script: npm run build:emitter + - script: npm run ${{ parameters.PlaygroundBundleBuildScript }} displayName: Build emitter for playground bundle workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} - script: pnpm install --filter "@typespec/bundle-uploader..." diff --git a/packages/http-client-csharp/eng/pipeline/publish.yml b/packages/http-client-csharp/eng/pipeline/publish.yml index 2154939eeb9..07fedb5cb09 100644 --- a/packages/http-client-csharp/eng/pipeline/publish.yml +++ b/packages/http-client-csharp/eng/pipeline/publish.yml @@ -65,6 +65,7 @@ extends: HasNugetPackages: true CadlRanchName: "@typespec/http-client-csharp" UploadPlaygroundBundle: true + PlaygroundBundleBuildScript: "build:emitter" AdditionalInitializeSteps: - task: UseDotNet@2 inputs: From 1936c278d5186d07cbcaf3fc0968f1bcafcf3202 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 09:54:34 -0700 Subject: [PATCH 010/103] fix(bundler): use empty shims for Node.js modules that break in browser The url module polyfill references Deno which crashes in browser. Set fs, url, and child_process to empty shims since emitter code that uses them is behind runtime guards and never executes in the browser. Keep path polyfilled since it's commonly used in browser-safe code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/bundler/src/bundler.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/bundler/src/bundler.ts b/packages/bundler/src/bundler.ts index 0fc8f5dfc31..37f2681b6da 100644 --- a/packages/bundler/src/bundler.ts +++ b/packages/bundler/src/bundler.ts @@ -239,7 +239,22 @@ async function createEsBuildContext( format: "esm", target: "es2024", minify, - plugins: [virtualPlugin, nodeModulesPolyfillPlugin({}), ...plugins], + plugins: [ + virtualPlugin, + nodeModulesPolyfillPlugin({ + modules: { + fs: "empty", + path: true, + url: "empty", + child_process: "empty", + "node:fs": "empty", + "node:path": true, + "node:url": "empty", + "node:child_process": "empty", + }, + }), + ...plugins, + ], }); } From 3d6888226aaa06804bccc37ff087813e98a47d78 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 09:57:18 -0700 Subject: [PATCH 011/103] Revert "fix(bundler): use empty shims for Node.js modules that break in browser" This reverts commit 1936c278d5186d07cbcaf3fc0968f1bcafcf3202. --- packages/bundler/src/bundler.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/bundler/src/bundler.ts b/packages/bundler/src/bundler.ts index 37f2681b6da..0fc8f5dfc31 100644 --- a/packages/bundler/src/bundler.ts +++ b/packages/bundler/src/bundler.ts @@ -239,22 +239,7 @@ async function createEsBuildContext( format: "esm", target: "es2024", minify, - plugins: [ - virtualPlugin, - nodeModulesPolyfillPlugin({ - modules: { - fs: "empty", - path: true, - url: "empty", - child_process: "empty", - "node:fs": "empty", - "node:path": true, - "node:url": "empty", - "node:child_process": "empty", - }, - }), - ...plugins, - ], + plugins: [virtualPlugin, nodeModulesPolyfillPlugin({}), ...plugins], }); } From c39bd690d3f9371fa89f6025d5fa29019d94844d Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 10:04:42 -0700 Subject: [PATCH 012/103] fix(http-client-csharp): use browser override for playground Use the package.json browser field to swap the Node.js emit-generate module with a browser stub that calls the playground server via fetch. This follows the same pattern as alloy-framework/alloy#376. - emit-generate.ts: Node.js implementation (uses fs, child_process, url) - emit-generate.browser.ts: browser stub (uses fetch to call server) - emitter.ts: clean of all Node.js imports, delegates to generate() - package.json: browser field maps the module swap The bundler respects the browser field and swaps the import at build time, so no Node.js polyfills are needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../emitter/src/code-model-writer.ts | 12 +- .../emitter/src/emit-generate.browser.ts | 42 +++ .../emitter/src/emit-generate.ts | 199 ++++++++++++ .../http-client-csharp/emitter/src/emitter.ts | 283 ++---------------- .../emitter/src/lib/utils.ts | 4 +- .../http-client-csharp/emitter/src/options.ts | 7 - packages/http-client-csharp/package.json | 5 +- 7 files changed, 264 insertions(+), 288 deletions(-) create mode 100644 packages/http-client-csharp/emitter/src/emit-generate.browser.ts create mode 100644 packages/http-client-csharp/emitter/src/emit-generate.ts diff --git a/packages/http-client-csharp/emitter/src/code-model-writer.ts b/packages/http-client-csharp/emitter/src/code-model-writer.ts index e60360c32b5..994d0bad2e7 100644 --- a/packages/http-client-csharp/emitter/src/code-model-writer.ts +++ b/packages/http-client-csharp/emitter/src/code-model-writer.ts @@ -8,16 +8,6 @@ import { CSharpEmitterContext } from "./sdk-context.js"; import { CodeModel } from "./type/code-model.js"; import { Configuration } from "./type/configuration.js"; -/** - * Serializes the code model to a JSON string with reference tracking. - * @param context - The CSharp emitter context - * @param codeModel - The code model to serialize - * @beta - */ -export function serializeCodeModel(context: CSharpEmitterContext, codeModel: CodeModel): string { - return prettierOutput(JSON.stringify(buildJson(context, codeModel), transformJSONProperties, 2)); -} - /** * Writes the code model to the output folder. Should only be used by autorest.csharp. * @param context - The CSharp emitter context @@ -32,7 +22,7 @@ export async function writeCodeModel( ) { await context.program.host.writeFile( resolvePath(outputFolder, tspOutputFileName), - serializeCodeModel(context, codeModel), + prettierOutput(JSON.stringify(buildJson(context, codeModel), transformJSONProperties, 2)), ); } diff --git a/packages/http-client-csharp/emitter/src/emit-generate.browser.ts b/packages/http-client-csharp/emitter/src/emit-generate.browser.ts new file mode 100644 index 00000000000..7ea955cc207 --- /dev/null +++ b/packages/http-client-csharp/emitter/src/emit-generate.browser.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +// Browser implementation: sends code model to a playground server via fetch. + +import { resolvePath } from "@typespec/compiler"; +import { CSharpEmitterContext } from "./sdk-context.js"; +import type { GenerateOptions } from "./emit-generate.js"; + +export async function generate( + sdkContext: CSharpEmitterContext, + codeModelJson: string, + configJson: string, + options: GenerateOptions, +): Promise { + const serverUrl = + (globalThis as any).__TYPESPEC_PLAYGROUND_SERVER_URL__ ?? "http://localhost:5174"; + + const response = await fetch(`${serverUrl}/generate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + codeModel: codeModelJson, + configuration: configJson, + generatorName: options.generatorName, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Playground server error (${response.status}): ${errorText}`); + } + + const result: { files: Array<{ path: string; content: string }> } = await response.json(); + + for (const file of result.files) { + await sdkContext.program.host.writeFile( + resolvePath(options.outputFolder, file.path), + file.content, + ); + } +} diff --git a/packages/http-client-csharp/emitter/src/emit-generate.ts b/packages/http-client-csharp/emitter/src/emit-generate.ts new file mode 100644 index 00000000000..7e88c2d6e3e --- /dev/null +++ b/packages/http-client-csharp/emitter/src/emit-generate.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +// Node.js implementation: runs the .NET generator locally via subprocess. + +import { + createDiagnosticCollector, + Diagnostic, + getDirectoryPath, + joinPaths, + NoTarget, + resolvePath, +} from "@typespec/compiler"; +import fs from "fs"; +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { + _minSupportedDotNetSdkVersion, + configurationFileName, + tspOutputFileName, +} from "./constants.js"; +import { createDiagnostic } from "./lib/lib.js"; +import { execAsync, execCSharpGenerator } from "./lib/utils.js"; +import { CSharpEmitterContext } from "./sdk-context.js"; + +export interface GenerateOptions { + outputFolder: string; + packageName: string; + generatorName: string; + newProject: boolean; + debug: boolean; + saveInputs: boolean; + emitterExtensionPath?: string; +} + +function findProjectRoot(path: string): string | undefined { + let current = path; + while (true) { + const pkgPath = joinPaths(current, "package.json"); + try { + if (fs.statSync(pkgPath)?.isFile()) { + return current; + } + } catch { + // file doesn't exist + } + const parent = getDirectoryPath(current); + if (parent === current) { + return undefined; + } + current = parent; + } +} + +function checkFile(pkgPath: string) { + try { + return fs.statSync(pkgPath); + } catch { + return undefined; + } +} + +export async function generate( + sdkContext: CSharpEmitterContext, + codeModelJson: string, + configJson: string, + options: GenerateOptions, +): Promise { + const diagnostics = createDiagnosticCollector(); + + const generatedFolder = resolvePath(options.outputFolder, "src", "Generated"); + if (!fs.existsSync(generatedFolder)) { + fs.mkdirSync(generatedFolder, { recursive: true }); + } + + // Write code model and configuration to disk for the generator + await sdkContext.program.host.writeFile( + resolvePath(options.outputFolder, tspOutputFileName), + codeModelJson, + ); + await sdkContext.program.host.writeFile( + resolvePath(options.outputFolder, configurationFileName), + configJson, + ); + + const csProjFile = resolvePath( + options.outputFolder, + "src", + `${options.packageName}.csproj`, + ); + + const emitterPath = options.emitterExtensionPath ?? import.meta.url; + const projectRoot = findProjectRoot(dirname(fileURLToPath(emitterPath))); + const generatorPath = resolvePath( + projectRoot + "/dist/generator/Microsoft.TypeSpec.Generator.dll", + ); + + try { + const result = await execCSharpGenerator(sdkContext, { + generatorPath: generatorPath, + outputFolder: options.outputFolder, + generatorName: options.generatorName, + newProject: options.newProject || !checkFile(csProjFile), + debug: options.debug, + }); + if (result.exitCode !== 0) { + const isValid = diagnostics.pipe( + await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), + ); + if (isValid) { + throw new Error( + `Failed to generate the library. Exit code: ${result.exitCode}.\nStackTrace: \n${result.stderr}`, + ); + } + } + } catch (error: any) { + const isValid = diagnostics.pipe( + await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), + ); + if (isValid) throw new Error(error, { cause: error }); + } + + if (!options.saveInputs) { + sdkContext.program.host.rm(resolvePath(options.outputFolder, tspOutputFileName)); + sdkContext.program.host.rm(resolvePath(options.outputFolder, configurationFileName)); + } + + sdkContext.program.reportDiagnostics(diagnostics.diagnostics); +} + +/** @internal */ +export async function _validateDotNetSdk( + sdkContext: CSharpEmitterContext, + minMajorVersion: number, +): Promise<[boolean, readonly Diagnostic[]]> { + const diagnostics = createDiagnosticCollector(); + try { + const result = await execAsync("dotnet", ["--version"], { stdio: "pipe" }); + return diagnostics.wrap( + diagnostics.pipe(validateDotNetSdkVersionCore(result.stdout, minMajorVersion)), + ); + } catch (error: any) { + if (error && "code" in error && error["code"] === "ENOENT") { + diagnostics.add( + createDiagnostic({ + code: "invalid-dotnet-sdk-dependency", + messageId: "missing", + format: { + dotnetMajorVersion: `${minMajorVersion}`, + downloadUrl: "https://dotnet.microsoft.com/", + }, + target: NoTarget, + }), + ); + } + return diagnostics.wrap(false); + } +} + +function validateDotNetSdkVersionCore( + version: string, + minMajorVersion: number, +): [boolean, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + if (version) { + const dotIndex = version.indexOf("."); + const firstPart = dotIndex === -1 ? version : version.substring(0, dotIndex); + const major = Number(firstPart); + + if (isNaN(major)) { + return diagnostics.wrap(false); + } + if (major < minMajorVersion) { + diagnostics.add( + createDiagnostic({ + code: "invalid-dotnet-sdk-dependency", + messageId: "invalidVersion", + format: { + installedVersion: version, + dotnetMajorVersion: `${minMajorVersion}`, + downloadUrl: "https://dotnet.microsoft.com/", + }, + target: NoTarget, + }), + ); + return diagnostics.wrap(false); + } + return diagnostics.wrap(true); + } else { + diagnostics.add( + createDiagnostic({ + code: "general-error", + format: { message: "Cannot get the installed .NET SDK version." }, + target: NoTarget, + }), + ); + return diagnostics.wrap(false); + } +} diff --git a/packages/http-client-csharp/emitter/src/emitter.ts b/packages/http-client-csharp/emitter/src/emitter.ts index f372b430f73..fba12988b45 100644 --- a/packages/http-client-csharp/emitter/src/emitter.ts +++ b/packages/http-client-csharp/emitter/src/emitter.ts @@ -6,55 +6,17 @@ import { createDiagnosticCollector, Diagnostic, EmitContext, - getDirectoryPath, - joinPaths, - NoTarget, Program, - resolvePath, } from "@typespec/compiler"; -import { writeCodeModel, writeConfiguration, serializeCodeModel } from "./code-model-writer.js"; -import { - _minSupportedDotNetSdkVersion, - configurationFileName, - tspOutputFileName, -} from "./constants.js"; +import { generate } from "./emit-generate.js"; import { createModel } from "./lib/client-model-builder.js"; -import { createDiagnostic } from "./lib/lib.js"; import { LoggerLevel } from "./lib/logger-level.js"; import { Logger } from "./lib/logger.js"; -import { execAsync, execCSharpGenerator } from "./lib/utils.js"; import { CSharpEmitterOptions, resolveOptions } from "./options.js"; import { createCSharpEmitterContext, CSharpEmitterContext } from "./sdk-context.js"; import { CodeModel } from "./type/code-model.js"; import { Configuration } from "./type/configuration.js"; -/** - * Look for the project root by looking up until a `package.json` is found. - * @param path Path to start looking - * @param statSyncFn The statSync function (injected to avoid top-level fs import) - */ -function findProjectRoot( - path: string, - statSyncFn: (p: string) => { isFile(): boolean }, -): string | undefined { - let current = path; - while (true) { - const pkgPath = joinPaths(current, "package.json"); - try { - if (statSyncFn(pkgPath)?.isFile()) { - return current; - } - } catch { - // file doesn't exist - } - const parent = getDirectoryPath(current); - if (parent === current) { - return undefined; - } - current = parent; - } -} - /** * Creates a code model by executing the full emission logic. * This function can be called by downstream emitters to generate a code model and collect diagnostics. @@ -114,44 +76,22 @@ export async function emitCodeModel( const namespace = updatedRoot.name; const configurations: Configuration = createConfiguration(options, namespace, sdkContext); - const playgroundServerUrl = - options["playground-server-url"] || - (typeof globalThis.process === "undefined" - ? ((globalThis as any).__TYPESPEC_PLAYGROUND_SERVER_URL__ ?? "http://localhost:5174") - : undefined); - - if (playgroundServerUrl) { - // Playground mode: serialize and send directly to server without writing to virtual FS - const codeModelJson = serializeCodeModel(sdkContext, updatedRoot); - const configJson = JSON.stringify(configurations, null, 2) + "\n"; - await generateViaPlaygroundServer( - playgroundServerUrl, - sdkContext, - outputFolder, - codeModelJson, - configJson, - options["generator-name"], - ); - } else { - // Local mode: write files and run .NET generator - await writeCodeModel(sdkContext, updatedRoot, outputFolder); - await writeConfiguration(sdkContext, configurations, outputFolder); - - await runLocalGenerator(sdkContext, diagnostics, { - outputFolder, - packageName: configurations["package-name"] ?? "", - generatorName: options["generator-name"], - newProject: options["new-project"], - debug: options.debug ?? false, - emitterExtensionPath: options["emitter-extension-path"], - logger, - }); - - if (!options["save-inputs"]) { - context.program.host.rm(resolvePath(outputFolder, tspOutputFileName)); - context.program.host.rm(resolvePath(outputFolder, configurationFileName)); - } - } + // Serialize code model and configuration + const codeModelJson = JSON.stringify(updatedRoot, null, 2); + const configJson = JSON.stringify(configurations, null, 2) + "\n"; + + // Generate C# code via platform-specific implementation. + // In Node.js this runs the .NET generator locally. + // In the browser this sends the code model to a playground server. + await generate(sdkContext, codeModelJson, configJson, { + outputFolder, + packageName: configurations["package-name"] ?? "", + generatorName: options["generator-name"], + newProject: options["new-project"], + debug: options.debug ?? false, + saveInputs: options["save-inputs"] ?? false, + emitterExtensionPath: options["emitter-extension-path"], + }); } } @@ -185,7 +125,6 @@ export function createConfiguration( "generate-protocol-methods", "generate-convenience-methods", "emitter-extension-path", - "playground-server-url", ]; const derivedOptions = Object.fromEntries( Object.entries(options).filter(([key]) => !skipKeys.includes(key)), @@ -200,191 +139,3 @@ export function createConfiguration( license: sdkContext.sdkPackage.licenseInfo, }; } - -/** check the dotnet sdk installation. - * Report diagnostic if dotnet sdk is not installed or its version does not meet prerequisite - * @param sdkContext - The SDK context - * @param minVersionRequisite - The minimum required major version - * @returns A tuple containing whether the SDK is valid and any diagnostics - * @internal - */ -export async function _validateDotNetSdk( - sdkContext: CSharpEmitterContext, - minMajorVersion: number, -): Promise<[boolean, readonly Diagnostic[]]> { - const diagnostics = createDiagnosticCollector(); - try { - const result = await execAsync("dotnet", ["--version"], { stdio: "pipe" }); - return diagnostics.wrap( - diagnostics.pipe(validateDotNetSdkVersionCore(sdkContext, result.stdout, minMajorVersion)), - ); - } catch (error: any) { - if (error && "code" in error && error["code"] === "ENOENT") { - diagnostics.add( - createDiagnostic({ - code: "invalid-dotnet-sdk-dependency", - messageId: "missing", - format: { - dotnetMajorVersion: `${minMajorVersion}`, - downloadUrl: "https://dotnet.microsoft.com/", - }, - target: NoTarget, - }), - ); - } - return diagnostics.wrap(false); - } -} - -function validateDotNetSdkVersionCore( - sdkContext: CSharpEmitterContext, - version: string, - minMajorVersion: number, -): [boolean, readonly Diagnostic[]] { - const diagnostics = createDiagnosticCollector(); - if (version) { - const dotIndex = version.indexOf("."); - const firstPart = dotIndex === -1 ? version : version.substring(0, dotIndex); - const major = Number(firstPart); - - if (isNaN(major)) { - return diagnostics.wrap(false); - } - if (major < minMajorVersion) { - diagnostics.add( - createDiagnostic({ - code: "invalid-dotnet-sdk-dependency", - messageId: "invalidVersion", - format: { - installedVersion: version, - dotnetMajorVersion: `${minMajorVersion}`, - downloadUrl: "https://dotnet.microsoft.com/", - }, - target: NoTarget, - }), - ); - return diagnostics.wrap(false); - } - return diagnostics.wrap(true); - } else { - diagnostics.add( - createDiagnostic({ - code: "general-error", - format: { message: "Cannot get the installed .NET SDK version." }, - target: NoTarget, - }), - ); - return diagnostics.wrap(false); - } -} - -/** - * Sends the code model and configuration to a playground server for C# generation. - * Used when the emitter runs in a browser environment. - */ -async function generateViaPlaygroundServer( - serverUrl: string, - sdkContext: CSharpEmitterContext, - outputFolder: string, - codeModelJson: string, - configJson: string, - generatorName: string, -): Promise { - const response = await fetch(`${serverUrl}/generate`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - codeModel: codeModelJson, - configuration: configJson, - generatorName, - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Playground server error (${response.status}): ${errorText}`); - } - - const result: { files: Array<{ path: string; content: string }> } = await response.json(); - - for (const file of result.files) { - await sdkContext.program.host.writeFile(resolvePath(outputFolder, file.path), file.content); - } -} - -/** - * Runs the .NET generator locally via subprocess. - * Uses dynamic imports for Node.js modules (fs, path, url) to keep the - * emitter module loadable in browser environments. - */ -async function runLocalGenerator( - sdkContext: CSharpEmitterContext, - diagnostics: ReturnType, - options: { - outputFolder: string; - packageName: string; - generatorName: string; - newProject: boolean; - debug: boolean; - emitterExtensionPath?: string; - logger: Logger; - }, -): Promise { - const fs = await import("fs"); - const { dirname } = await import("path"); - const { fileURLToPath } = await import("url"); - - const generatedFolder = resolvePath(options.outputFolder, "src", "Generated"); - - if (!fs.existsSync(generatedFolder)) { - fs.mkdirSync(generatedFolder, { recursive: true }); - } - - const csProjFile = resolvePath( - options.outputFolder, - "src", - `${options.packageName}.csproj`, - ); - options.logger.info(`Checking if ${csProjFile} exists`); - - const emitterPath = options.emitterExtensionPath ?? import.meta.url; - const projectRoot = findProjectRoot(dirname(fileURLToPath(emitterPath)), fs.statSync); - const generatorPath = resolvePath( - projectRoot + "/dist/generator/Microsoft.TypeSpec.Generator.dll", - ); - - const checkFile = (path: string) => { - try { - return fs.statSync(path); - } catch { - return undefined; - } - }; - - try { - const result = await execCSharpGenerator(sdkContext, { - generatorPath: generatorPath, - outputFolder: options.outputFolder, - generatorName: options.generatorName, - newProject: options.newProject || !checkFile(csProjFile), - debug: options.debug, - }); - if (result.exitCode !== 0) { - const isValid = diagnostics.pipe( - await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), - ); - // if the dotnet sdk is valid, the error is not dependency issue, log it as normal - if (isValid) { - throw new Error( - `Failed to generate the library. Exit code: ${result.exitCode}.\nStackTrace: \n${result.stderr}`, - ); - } - } - } catch (error: any) { - const isValid = diagnostics.pipe( - await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), - ); - // if the dotnet sdk is valid, the error is not dependency issue, log it as normal - if (isValid) throw new Error(error, { cause: error }); - } -} diff --git a/packages/http-client-csharp/emitter/src/lib/utils.ts b/packages/http-client-csharp/emitter/src/lib/utils.ts index 8275ef27bf7..aa8af4d3889 100644 --- a/packages/http-client-csharp/emitter/src/lib/utils.ts +++ b/packages/http-client-csharp/emitter/src/lib/utils.ts @@ -9,7 +9,7 @@ import { } from "@azure-tools/typespec-client-generator-core"; import { getNamespaceFullName, Namespace, NoTarget, Type } from "@typespec/compiler"; import { Visibility } from "@typespec/http"; -import type { SpawnOptions } from "child_process"; +import { spawn, SpawnOptions } from "child_process"; import { CSharpEmitterContext } from "../sdk-context.js"; export async function execCSharpGenerator( @@ -22,7 +22,6 @@ export async function execCSharpGenerator( debug: boolean; }, ): Promise<{ exitCode: number; stderr: string; proc: any }> { - const { spawn } = await import("child_process"); const command = "dotnet"; const args = [ "--roll-forward", @@ -111,7 +110,6 @@ export async function execAsync( args: string[] = [], options: SpawnOptions = {}, ): Promise<{ exitCode: number; stdio: string; stdout: string; stderr: string; proc: any }> { - const { spawn } = await import("child_process"); const child = spawn(command, args, options); return new Promise((resolve, reject) => { diff --git a/packages/http-client-csharp/emitter/src/options.ts b/packages/http-client-csharp/emitter/src/options.ts index dfa82a09fb3..9ae08884a9e 100644 --- a/packages/http-client-csharp/emitter/src/options.ts +++ b/packages/http-client-csharp/emitter/src/options.ts @@ -22,7 +22,6 @@ export interface CSharpEmitterOptions { "generate-protocol-methods"?: boolean; "generate-convenience-methods"?: boolean; "package-name"?: string; - "playground-server-url"?: string; license?: { name: string; company?: string; @@ -134,12 +133,6 @@ export const CSharpEmitterOptionsSchema: JSONSchemaType = description: "The SDK context options that implement the `CreateSdkContextOptions` interface from the [`@azure-tools/typespec-client-generator-core`](https://www.npmjs.com/package/@azure-tools/typespec-client-generator-core) package to be used by the CSharp emitter.", }, - "playground-server-url": { - type: "string", - nullable: true, - description: - "URL of a playground server that runs the .NET generator. When set, the emitter sends the code model to this server instead of spawning a local dotnet process. Used for browser-based playground environments.", - }, }, required: [], }; diff --git a/packages/http-client-csharp/package.json b/packages/http-client-csharp/package.json index da59c45fd8a..d5d3cd22c5b 100644 --- a/packages/http-client-csharp/package.json +++ b/packages/http-client-csharp/package.json @@ -25,12 +25,15 @@ "default": "./dist/emitter/src/index.js" } }, + "browser": { + "./dist/emitter/src/emit-generate.js": "./dist/emitter/src/emit-generate.browser.js", + "./emitter/src/emit-generate.ts": "./emitter/src/emit-generate.browser.ts" + }, "scripts": { "clean": "rimraf ./dist ./emitter/temp && dotnet clean ./generator", "build:emitter": "tsc -p ./emitter/tsconfig.build.json", "build:generator": "dotnet build ./generator", "build": "npm run build:emitter && npm run build:generator && npm run extract-api", - "dev:playground": "npx concurrently -k -n playground,server -c cyan,green \"npx vite --config ../../packages/playground-website/vite.config.ts ../../packages/playground-website\" \"dotnet run --project ./playground-server\"", "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", "watch": "tsc -p ./emitter/tsconfig.build.json --watch", "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", From 7b533fbd98a0d40bbe568c6d961be9e0698d1f5c Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 10:14:52 -0700 Subject: [PATCH 013/103] fix: update test to import _validateDotNetSdk from emit-generate Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/emitter/test/Unit/emitter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts index d3078c9dc59..b2b0d0c8ed8 100644 --- a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts @@ -265,7 +265,7 @@ describe("Test _validateDotNetSdk", () => { // dynamically import the module to get the $onEmit function // we avoid importing it at the top to allow mocking of dependencies - _validateDotNetSdk = (await import("../../src/emitter.js"))._validateDotNetSdk; + _validateDotNetSdk = (await import("../../src/emit-generate.js"))._validateDotNetSdk; }); it("should return false and report diagnostic when dotnet SDK is not installed.", async () => { From 1f7259716eccb6ac514a445e9aa86ba22a7def76 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 10:29:41 -0700 Subject: [PATCH 014/103] fix: update emitter tests for browser override refactor Update tests to verify generate() is called with correct options instead of execCSharpGenerator (which moved to emit-generate.ts). Re-export _validateDotNetSdk from emitter.ts for test compatibility. Mock emit-generate.js with importOriginal to preserve _validateDotNetSdk. All 195 emitter tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../http-client-csharp/emitter/src/emitter.ts | 2 + .../emitter/test/Unit/emitter.test.ts | 88 +++++++++---------- 2 files changed, 43 insertions(+), 47 deletions(-) diff --git a/packages/http-client-csharp/emitter/src/emitter.ts b/packages/http-client-csharp/emitter/src/emitter.ts index fba12988b45..eae96670dc2 100644 --- a/packages/http-client-csharp/emitter/src/emitter.ts +++ b/packages/http-client-csharp/emitter/src/emitter.ts @@ -9,6 +9,8 @@ import { Program, } from "@typespec/compiler"; import { generate } from "./emit-generate.js"; +// Re-export for backwards compatibility with tests +export { _validateDotNetSdk } from "./emit-generate.js"; import { createModel } from "./lib/client-model-builder.js"; import { LoggerLevel } from "./lib/logger-level.js"; import { Logger } from "./lib/logger.js"; diff --git a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts index b2b0d0c8ed8..b304065966c 100644 --- a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts @@ -5,6 +5,7 @@ import { TestHost } from "@typespec/compiler/testing"; import { strictEqual } from "assert"; import { statSync } from "fs"; import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; +import { generate } from "../../src/emit-generate.js"; import { execAsync, execCSharpGenerator } from "../../src/lib/utils.js"; import { CSharpEmitterOptions } from "../../src/options.js"; import { CodeModel } from "../../src/type/code-model.js"; @@ -61,6 +62,14 @@ describe("$onEmit tests", () => { execAsync: vi.fn(), })); + vi.mock("../../src/emit-generate.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + generate: vi.fn(), + }; + }); + vi.mock("../../src/lib/client-model-builder.js", () => ({ createModel: vi.fn().mockReturnValue([{ name: "TestNamespace" }, []]), })); @@ -119,68 +128,53 @@ describe("$onEmit tests", () => { ); }); - it("should set newProject to TRUE if .csproj file DOES NOT exist", async () => { - vi.mocked(statSync).mockImplementation(() => { - throw new Error("File not found"); - }); - - const context: EmitContext = createEmitterContext(program); - await $onEmit(context); - - expect(execCSharpGenerator).toHaveBeenCalledWith(expect.anything(), { - generatorPath: expect.any(String), - outputFolder: undefined, - generatorName: "ScmCodeModelGenerator", - newProject: true, // Ensure this is passed as true - debug: false, - }); - }); - - it("should set newProject to FALSE if .csproj file DOES exist", async () => { - vi.mocked(statSync).mockReturnValue({ isFile: () => true } as any); - + it("should pass newProject FALSE by default", async () => { const context: EmitContext = createEmitterContext(program); await $onEmit(context); - expect(execCSharpGenerator).toHaveBeenCalledWith(expect.anything(), { - generatorPath: expect.any(String), - outputFolder: undefined, - generatorName: "ScmCodeModelGenerator", - newProject: false, // Ensure this is passed as false - debug: false, - }); + expect(generate).toHaveBeenCalledWith( + expect.anything(), + expect.any(String), + expect.any(String), + expect.objectContaining({ + newProject: false, + generatorName: "ScmCodeModelGenerator", + }), + ); }); - it("should set newProject to TRUE if passed in options", async () => { - vi.mocked(statSync).mockReturnValue({ isFile: () => true } as any); - + it("should pass newProject TRUE when set in options", async () => { const context: EmitContext = createEmitterContext(program, { "new-project": true, }); await $onEmit(context); - expect(execCSharpGenerator).toHaveBeenCalledWith(expect.anything(), { - generatorPath: expect.any(String), - outputFolder: undefined, - generatorName: "ScmCodeModelGenerator", - newProject: true, // Ensure this is passed as true - debug: false, - }); - }); - it("should set newProject to FALSE if passed in options", async () => { - vi.mocked(statSync).mockReturnValue({ isFile: () => true } as any); + expect(generate).toHaveBeenCalledWith( + expect.anything(), + expect.any(String), + expect.any(String), + expect.objectContaining({ + newProject: true, + generatorName: "ScmCodeModelGenerator", + }), + ); + }); + it("should pass newProject FALSE when set in options", async () => { const context: EmitContext = createEmitterContext(program, { "new-project": false, }); await $onEmit(context); - expect(execCSharpGenerator).toHaveBeenCalledWith(expect.anything(), { - generatorPath: expect.any(String), - outputFolder: undefined, - generatorName: "ScmCodeModelGenerator", - newProject: false, // Ensure this is passed as true - debug: false, - }); + + expect(generate).toHaveBeenCalledWith( + expect.anything(), + expect.any(String), + expect.any(String), + expect.objectContaining({ + newProject: false, + generatorName: "ScmCodeModelGenerator", + }), + ); }); }); From 2b652f026b63c67c50f67797666702855101ea94 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 11:21:21 -0700 Subject: [PATCH 015/103] fix: use serializeCodeModel for proper / tracking The code model was being serialized with plain JSON.stringify, losing the reference tracking (/) and usage flag transformations that the .NET generator requires. Use serializeCodeModel() from code-model-writer which applies buildJson() and transformJSONProperties. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../emitter/src/code-model-writer.ts | 12 +++++++++++- packages/http-client-csharp/emitter/src/emitter.ts | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/emitter/src/code-model-writer.ts b/packages/http-client-csharp/emitter/src/code-model-writer.ts index 994d0bad2e7..e60360c32b5 100644 --- a/packages/http-client-csharp/emitter/src/code-model-writer.ts +++ b/packages/http-client-csharp/emitter/src/code-model-writer.ts @@ -8,6 +8,16 @@ import { CSharpEmitterContext } from "./sdk-context.js"; import { CodeModel } from "./type/code-model.js"; import { Configuration } from "./type/configuration.js"; +/** + * Serializes the code model to a JSON string with reference tracking. + * @param context - The CSharp emitter context + * @param codeModel - The code model to serialize + * @beta + */ +export function serializeCodeModel(context: CSharpEmitterContext, codeModel: CodeModel): string { + return prettierOutput(JSON.stringify(buildJson(context, codeModel), transformJSONProperties, 2)); +} + /** * Writes the code model to the output folder. Should only be used by autorest.csharp. * @param context - The CSharp emitter context @@ -22,7 +32,7 @@ export async function writeCodeModel( ) { await context.program.host.writeFile( resolvePath(outputFolder, tspOutputFileName), - prettierOutput(JSON.stringify(buildJson(context, codeModel), transformJSONProperties, 2)), + serializeCodeModel(context, codeModel), ); } diff --git a/packages/http-client-csharp/emitter/src/emitter.ts b/packages/http-client-csharp/emitter/src/emitter.ts index eae96670dc2..bcb5f4ec8e4 100644 --- a/packages/http-client-csharp/emitter/src/emitter.ts +++ b/packages/http-client-csharp/emitter/src/emitter.ts @@ -8,6 +8,7 @@ import { EmitContext, Program, } from "@typespec/compiler"; +import { serializeCodeModel } from "./code-model-writer.js"; import { generate } from "./emit-generate.js"; // Re-export for backwards compatibility with tests export { _validateDotNetSdk } from "./emit-generate.js"; @@ -79,7 +80,7 @@ export async function emitCodeModel( const configurations: Configuration = createConfiguration(options, namespace, sdkContext); // Serialize code model and configuration - const codeModelJson = JSON.stringify(updatedRoot, null, 2); + const codeModelJson = serializeCodeModel(sdkContext, updatedRoot); const configJson = JSON.stringify(configurations, null, 2) + "\n"; // Generate C# code via platform-specific implementation. From 09bc7f8f98733ab34ab96d00e738c3d06df21cba Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 12:17:33 -0700 Subject: [PATCH 016/103] fix: remove _validateDotNetSdk re-export from emitter.ts The browser bundle can't resolve this export since emit-generate.browser.ts doesn't export it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/emitter/src/emitter.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/http-client-csharp/emitter/src/emitter.ts b/packages/http-client-csharp/emitter/src/emitter.ts index bcb5f4ec8e4..8728b440f0a 100644 --- a/packages/http-client-csharp/emitter/src/emitter.ts +++ b/packages/http-client-csharp/emitter/src/emitter.ts @@ -10,8 +10,6 @@ import { } from "@typespec/compiler"; import { serializeCodeModel } from "./code-model-writer.js"; import { generate } from "./emit-generate.js"; -// Re-export for backwards compatibility with tests -export { _validateDotNetSdk } from "./emit-generate.js"; import { createModel } from "./lib/client-model-builder.js"; import { LoggerLevel } from "./lib/logger-level.js"; import { Logger } from "./lib/logger.js"; From bb5060ba982371d3d1cb24c436ed1c0c266b5742 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 12:52:33 -0700 Subject: [PATCH 017/103] fix: split _validateDotNetSdk tests to separate file Move _validateDotNetSdk tests to validate-dotnet-sdk.test.ts to avoid vitest module mock conflicts between tests (which mock emit-generate.js) and _validateDotNetSdk tests (which need the real emit-generate.js). All 195 emitter tests pass across 24 test files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../emitter/test/Unit/emitter.test.ts | 124 +----------------- .../test/Unit/validate-dotnet-sdk.test.ts | 118 +++++++++++++++++ 2 files changed, 122 insertions(+), 120 deletions(-) create mode 100644 packages/http-client-csharp/emitter/test/Unit/validate-dotnet-sdk.test.ts diff --git a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts index b304065966c..b3f99e4f38a 100644 --- a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts @@ -7,6 +7,7 @@ import { statSync } from "fs"; import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; import { generate } from "../../src/emit-generate.js"; import { execAsync, execCSharpGenerator } from "../../src/lib/utils.js"; +import { generate } from "../../src/emit-generate.js"; import { CSharpEmitterOptions } from "../../src/options.js"; import { CodeModel } from "../../src/type/code-model.js"; import { @@ -62,13 +63,9 @@ describe("$onEmit tests", () => { execAsync: vi.fn(), })); - vi.mock("../../src/emit-generate.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - generate: vi.fn(), - }; - }); + vi.mock("../../src/emit-generate.js", () => ({ + generate: vi.fn(), + })); vi.mock("../../src/lib/client-model-builder.js", () => ({ createModel: vi.fn().mockReturnValue([{ name: "TestNamespace" }, []]), @@ -231,116 +228,3 @@ describe("emitCodeModel tests", () => { }); }); -describe("Test _validateDotNetSdk", () => { - let runner: TestHost; - let program: Program; - const minVersion = 8; - let _validateDotNetSdk: (arg0: any, arg1: number) => Promise<[boolean, readonly Diagnostic[]]>; - - beforeEach(async () => { - vi.resetModules(); - runner = await createEmitterTestHost(); - program = await typeSpecCompile( - ` - op test( - @query - @encode(DurationKnownEncoding.ISO8601) - input: duration - ): NoContentResponse; - `, - runner, - ); - // Restore all mocks before each test - vi.restoreAllMocks(); - vi.mock("../../src/lib/utils.js", () => ({ - execCSharpGenerator: vi.fn(), - execAsync: vi.fn(), - })); - - // dynamically import the module to get the $onEmit function - // we avoid importing it at the top to allow mocking of dependencies - _validateDotNetSdk = (await import("../../src/emit-generate.js"))._validateDotNetSdk; - }); - - it("should return false and report diagnostic when dotnet SDK is not installed.", async () => { - /* mock the scenario that dotnet SDK is not installed, so execAsync will throw exception with error ENOENT */ - const error: any = new Error("ENOENT: no such file or directory"); - error.code = "ENOENT"; - (execAsync as Mock).mockRejectedValueOnce(error); - const context = createEmitterContext(program); - const sdkContext = await createCSharpSdkContext(context); - const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); - // Report collected diagnostics to program - program.reportDiagnostics(diagnostics); - expect(result).toBe(false); - strictEqual(program.diagnostics.length, 1); - strictEqual( - program.diagnostics[0].code, - "@typespec/http-client-csharp/invalid-dotnet-sdk-dependency", - ); - strictEqual( - program.diagnostics[0].message, - "The dotnet command was not found in the PATH. Please install the .NET SDK version 8 or above. Guidance for installing the .NET SDK can be found at https://dotnet.microsoft.com/.", - ); - }); - - it("should return true for installed SDK version whose major equals min supported version", async () => { - /* mock the scenario that the installed SDK version whose major equals min supported version */ - (execAsync as Mock).mockResolvedValueOnce({ - exitCode: 0, - stdio: "", - stdout: "8.0.204", - stderr: "", - proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" }, - }); - const context = createEmitterContext(program); - const sdkContext = await createCSharpSdkContext(context); - const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); - expect(result).toBe(true); - /* no diagnostics */ - strictEqual(diagnostics.length, 0); - }); - - it("should return true for installed SDK version whose major greaters than min supported version", async () => { - /* mock the scenario that the installed SDK version whose major greater than min supported version */ - (execAsync as Mock).mockResolvedValueOnce({ - exitCode: 0, - stdio: "", - stdout: "9.0.102", - stderr: "", - proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" }, - }); - const context = createEmitterContext(program); - const sdkContext = await createCSharpSdkContext(context); - const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); - expect(result).toBe(true); - /* no diagnostics */ - strictEqual(diagnostics.length, 0); - }); - - it("should return false and report diagnostic for invalid .NET SDK version", async () => { - /* mock the scenario that the installed SDK version whose major less than min supported version */ - (execAsync as Mock).mockResolvedValueOnce({ - exitCode: 0, - stdio: "", - stdout: "5.0.408", - stderr: "", - proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" }, - }); - const context = createEmitterContext(program); - const sdkContext = await createCSharpSdkContext(context); - const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); - // Report collected diagnostics to program - program.reportDiagnostics(diagnostics); - expect(result).toBe(false); - strictEqual(program.diagnostics.length, 1); - strictEqual( - program.diagnostics[0].code, - "@typespec/http-client-csharp/invalid-dotnet-sdk-dependency", - ); - strictEqual( - program.diagnostics[0].message, - "The .NET SDK found is version 5.0.408. Please install the .NET SDK 8 or above and ensure there is no global.json in the file system requesting a lower version. Guidance for installing the .NET SDK can be found at https://dotnet.microsoft.com/.", - ); - }); -}); diff --git a/packages/http-client-csharp/emitter/test/Unit/validate-dotnet-sdk.test.ts b/packages/http-client-csharp/emitter/test/Unit/validate-dotnet-sdk.test.ts new file mode 100644 index 00000000000..a3f0eb397fd --- /dev/null +++ b/packages/http-client-csharp/emitter/test/Unit/validate-dotnet-sdk.test.ts @@ -0,0 +1,118 @@ +vi.resetModules(); + +import { Diagnostic, Program } from "@typespec/compiler"; +import { TestHost } from "@typespec/compiler/testing"; +import { strictEqual } from "assert"; +import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; +import { execAsync } from "../../src/lib/utils.js"; +import { + createCSharpSdkContext, + createEmitterContext, + createEmitterTestHost, + typeSpecCompile, +} from "./utils/test-util.js"; + +describe("Test _validateDotNetSdk", () => { + let runner: TestHost; + let program: Program; + const minVersion = 8; + let _validateDotNetSdk: (arg0: any, arg1: number) => Promise<[boolean, readonly Diagnostic[]]>; + + beforeEach(async () => { + vi.resetModules(); + runner = await createEmitterTestHost(); + program = await typeSpecCompile( + ` + op test( + @query + @encode(DurationKnownEncoding.ISO8601) + input: duration + ): NoContentResponse; + `, + runner, + ); + // Restore all mocks before each test + vi.restoreAllMocks(); + vi.mock("../../src/lib/utils.js", () => ({ + execCSharpGenerator: vi.fn(), + execAsync: vi.fn(), + })); + + // dynamically import the module to get the _validateDotNetSdk function + _validateDotNetSdk = (await import("../../src/emit-generate.js"))._validateDotNetSdk; + }); + + it("should return false and report diagnostic when dotnet SDK is not installed.", async () => { + const error: any = new Error("ENOENT: no such file or directory"); + error.code = "ENOENT"; + (execAsync as Mock).mockRejectedValueOnce(error); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); + program.reportDiagnostics(diagnostics); + expect(result).toBe(false); + strictEqual(program.diagnostics.length, 1); + strictEqual( + program.diagnostics[0].code, + "@typespec/http-client-csharp/invalid-dotnet-sdk-dependency", + ); + strictEqual( + program.diagnostics[0].message, + "The dotnet command was not found in the PATH. Please install the .NET SDK version 8 or above. Guidance for installing the .NET SDK can be found at https://dotnet.microsoft.com/.", + ); + }); + + it("should return true for installed SDK version whose major equals min supported version", async () => { + (execAsync as Mock).mockResolvedValueOnce({ + exitCode: 0, + stdio: "", + stdout: "8.0.204", + stderr: "", + proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" }, + }); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); + expect(result).toBe(true); + strictEqual(diagnostics.length, 0); + }); + + it("should return true for installed SDK version whose major greaters than min supported version", async () => { + (execAsync as Mock).mockResolvedValueOnce({ + exitCode: 0, + stdio: "", + stdout: "9.0.102", + stderr: "", + proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" }, + }); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); + expect(result).toBe(true); + strictEqual(diagnostics.length, 0); + }); + + it("should return false and report diagnostic for invalid .NET SDK version", async () => { + (execAsync as Mock).mockResolvedValueOnce({ + exitCode: 0, + stdio: "", + stdout: "5.0.408", + stderr: "", + proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" }, + }); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); + program.reportDiagnostics(diagnostics); + expect(result).toBe(false); + strictEqual(program.diagnostics.length, 1); + strictEqual( + program.diagnostics[0].code, + "@typespec/http-client-csharp/invalid-dotnet-sdk-dependency", + ); + strictEqual( + program.diagnostics[0].message, + "The .NET SDK found is version 5.0.408. Please install the .NET SDK 8 or above and ensure there is no global.json in the file system requesting a lower version. Guidance for installing the .NET SDK can be found at https://dotnet.microsoft.com/.", + ); + }); +}); From 034c1e0e0d78f0664c3e3f797d35421aa5c755ce Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 14:58:03 -0700 Subject: [PATCH 018/103] fix: remove duplicate generate import Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/emitter/test/Unit/emitter.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts index b3f99e4f38a..4d0aec53f37 100644 --- a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts @@ -7,7 +7,6 @@ import { statSync } from "fs"; import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; import { generate } from "../../src/emit-generate.js"; import { execAsync, execCSharpGenerator } from "../../src/lib/utils.js"; -import { generate } from "../../src/emit-generate.js"; import { CSharpEmitterOptions } from "../../src/options.js"; import { CodeModel } from "../../src/type/code-model.js"; import { From d1ce21e9859cdbc1dab26795963db31fe2790d01 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 16:11:22 -0700 Subject: [PATCH 019/103] fix: sync version from published package before bundle upload Extract the version from the published tgz in build artifacts and update package.json before building and uploading the playground bundle. This ensures each publish produces a unique bundle version so the uploader doesn't skip it as already-exists. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../pipelines/templates/stages/emitter-stages.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index ae7e24dab43..dba0cc34daf 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -347,6 +347,15 @@ stages: - ${{ if parameters.UploadPlaygroundBundle }}: - script: npm install -g pnpm displayName: Install pnpm for playground bundle upload + - script: | + PUBLISHED_VERSION=$(ls $(buildArtifactsPath)/packages/*.tgz | head -1 | sed 's/.*-\([0-9].*\)\.tgz/\1/') + echo "Published version: $PUBLISHED_VERSION" + if [ -n "$PUBLISHED_VERSION" ]; then + node -e "const p=require('./package.json'); p.version='$PUBLISHED_VERSION'; require('fs').writeFileSync('./package.json', JSON.stringify(p, null, 2)+'\n')" + echo "Updated package.json version to $PUBLISHED_VERSION" + fi + displayName: Sync version from published package + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} - script: npm ci displayName: Install emitter dependencies for playground bundle workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} From 3f8ddc51995cfdea1e6a20849ec5fc7ca056df5d Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 16:23:26 -0700 Subject: [PATCH 020/103] feat: deploy playground server from publish pipeline Add pipeline parameters for Docker-based playground server deployment. After uploading the emitter bundle, builds the Docker image via az acr build and updates the Azure Container App. The C# publish pipeline is configured with placeholder resource names that need to be updated once the Azure resources are provisioned. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 48 +++++++++++++++++++ .../eng/pipeline/publish.yml | 4 ++ 2 files changed, 52 insertions(+) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index dba0cc34daf..d875bbf8c2c 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -93,6 +93,28 @@ parameters: type: string default: "build" + # Path to a Dockerfile (relative to PackagePath) for the playground server. + # When set alongside UploadPlaygroundBundle, the server container is built and + # deployed to Azure Container Apps after publishing. + - name: PlaygroundServerDockerfile + type: string + default: "" + + # Azure Container Registry name for the playground server image. + - name: PlaygroundServerRegistry + type: string + default: "" + + # Azure Container Apps name for the playground server. + - name: PlaygroundServerAppName + type: string + default: "" + + # Azure resource group for the playground server. + - name: PlaygroundServerResourceGroup + type: string + default: "" + stages: # Build stage # Responsible for building the autorest generator and typespec emitter packages @@ -376,6 +398,32 @@ stages: inlineScript: node ./eng/emitters/scripts/upload-bundled-emitter.js ${{ parameters.PackagePath }} workingDirectory: $(Build.SourcesDirectory) + - ${{ if and(parameters.UploadPlaygroundBundle, ne(parameters.PlaygroundServerDockerfile, '')) }}: + - task: AzureCLI@1 + displayName: Build and deploy playground server + inputs: + azureSubscription: "Azure SDK Engineering System" + scriptLocation: inlineScript + inlineScript: | + set -e + IMAGE="${{ parameters.PlaygroundServerRegistry }}.azurecr.io/${{ parameters.PlaygroundServerAppName }}:$(Build.BuildId)" + + echo "Building Docker image: $IMAGE" + az acr build \ + --registry ${{ parameters.PlaygroundServerRegistry }} \ + --image "${{ parameters.PlaygroundServerAppName }}:$(Build.BuildId)" \ + --file ${{ parameters.PlaygroundServerDockerfile }} \ + $(Build.SourcesDirectory)/${{ parameters.PackagePath }} + + echo "Deploying to Container Apps" + az containerapp update \ + --name ${{ parameters.PlaygroundServerAppName }} \ + --resource-group ${{ parameters.PlaygroundServerResourceGroup }} \ + --image "$IMAGE" + + echo "Deployed successfully" + workingDirectory: $(Build.SourcesDirectory) + templateContext: outputs: - output: pipelineArtifact diff --git a/packages/http-client-csharp/eng/pipeline/publish.yml b/packages/http-client-csharp/eng/pipeline/publish.yml index 07fedb5cb09..8551eb7eed2 100644 --- a/packages/http-client-csharp/eng/pipeline/publish.yml +++ b/packages/http-client-csharp/eng/pipeline/publish.yml @@ -66,6 +66,10 @@ extends: CadlRanchName: "@typespec/http-client-csharp" UploadPlaygroundBundle: true PlaygroundBundleBuildScript: "build:emitter" + PlaygroundServerDockerfile: "playground-server/Dockerfile" + PlaygroundServerRegistry: "typespec" + PlaygroundServerAppName: "csharp-playground-server" + PlaygroundServerResourceGroup: "typespec-playground" AdditionalInitializeSteps: - task: UseDotNet@2 inputs: From fecb3d8db710d9f902d824c694a8440d5211422f Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 16:27:22 -0700 Subject: [PATCH 021/103] fix: use shared infra constants for server deploy Hardcode registry and resource group ('typespec') in the pipeline step since they're infrastructure constants. Only the app name is per-emitter config. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 23 +++++++------------ .../eng/pipeline/publish.yml | 2 -- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index d875bbf8c2c..fb9439cd6f4 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -100,21 +100,11 @@ parameters: type: string default: "" - # Azure Container Registry name for the playground server image. - - name: PlaygroundServerRegistry - type: string - default: "" - # Azure Container Apps name for the playground server. - name: PlaygroundServerAppName type: string default: "" - # Azure resource group for the playground server. - - name: PlaygroundServerResourceGroup - type: string - default: "" - stages: # Build stage # Responsible for building the autorest generator and typespec emitter packages @@ -406,19 +396,22 @@ stages: scriptLocation: inlineScript inlineScript: | set -e - IMAGE="${{ parameters.PlaygroundServerRegistry }}.azurecr.io/${{ parameters.PlaygroundServerAppName }}:$(Build.BuildId)" + REGISTRY="typespec" + RESOURCE_GROUP="typespec" + APP_NAME="${{ parameters.PlaygroundServerAppName }}" + IMAGE="$REGISTRY.azurecr.io/$APP_NAME:$(Build.BuildId)" echo "Building Docker image: $IMAGE" az acr build \ - --registry ${{ parameters.PlaygroundServerRegistry }} \ - --image "${{ parameters.PlaygroundServerAppName }}:$(Build.BuildId)" \ + --registry "$REGISTRY" \ + --image "$APP_NAME:$(Build.BuildId)" \ --file ${{ parameters.PlaygroundServerDockerfile }} \ $(Build.SourcesDirectory)/${{ parameters.PackagePath }} echo "Deploying to Container Apps" az containerapp update \ - --name ${{ parameters.PlaygroundServerAppName }} \ - --resource-group ${{ parameters.PlaygroundServerResourceGroup }} \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ --image "$IMAGE" echo "Deployed successfully" diff --git a/packages/http-client-csharp/eng/pipeline/publish.yml b/packages/http-client-csharp/eng/pipeline/publish.yml index 8551eb7eed2..c4adb784e39 100644 --- a/packages/http-client-csharp/eng/pipeline/publish.yml +++ b/packages/http-client-csharp/eng/pipeline/publish.yml @@ -67,9 +67,7 @@ extends: UploadPlaygroundBundle: true PlaygroundBundleBuildScript: "build:emitter" PlaygroundServerDockerfile: "playground-server/Dockerfile" - PlaygroundServerRegistry: "typespec" PlaygroundServerAppName: "csharp-playground-server" - PlaygroundServerResourceGroup: "typespec-playground" AdditionalInitializeSteps: - task: UseDotNet@2 inputs: From c1012a0154982aeeb8b0712c2bd73c52b7daa216 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 17:00:00 -0700 Subject: [PATCH 022/103] fix: don't default to localhost in browser stub Instead of defaulting to http://localhost:5174 (which triggers OS permission prompts), throw a clear error if no server URL is configured. The server URL must be set via globalThis.__TYPESPEC_PLAYGROUND_SERVER_URL__. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../emitter/src/emit-generate.browser.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/emitter/src/emit-generate.browser.ts b/packages/http-client-csharp/emitter/src/emit-generate.browser.ts index 7ea955cc207..a45f5a230c7 100644 --- a/packages/http-client-csharp/emitter/src/emit-generate.browser.ts +++ b/packages/http-client-csharp/emitter/src/emit-generate.browser.ts @@ -13,8 +13,15 @@ export async function generate( configJson: string, options: GenerateOptions, ): Promise { - const serverUrl = - (globalThis as any).__TYPESPEC_PLAYGROUND_SERVER_URL__ ?? "http://localhost:5174"; + const serverUrl = (globalThis as any).__TYPESPEC_PLAYGROUND_SERVER_URL__; + + if (!serverUrl) { + throw new Error( + "C# code generation requires a playground server. " + + "No server URL is configured. Set globalThis.__TYPESPEC_PLAYGROUND_SERVER_URL__ " + + "to the URL of a running C# playground server.", + ); + } const response = await fetch(`${serverUrl}/generate`, { method: "POST", From 7909ea4f836797e1e7614f6b4a81ed77e88ae1ac Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 3 Apr 2026 17:01:35 -0700 Subject: [PATCH 023/103] feat: set playground server URL on the website Configure __TYPESPEC_PLAYGROUND_SERVER_URL__ in the website playground component so the C# emitter's browser stub knows where to send generation requests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/src/components/playground-component/playground.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/website/src/components/playground-component/playground.tsx b/website/src/components/playground-component/playground.tsx index 2b19e2f401b..2b4012e87e9 100644 --- a/website/src/components/playground-component/playground.tsx +++ b/website/src/components/playground-component/playground.tsx @@ -16,6 +16,11 @@ import { LoadingSpinner } from "./loading-spinner"; import "@typespec/playground-website/style.css"; import "@typespec/playground/styles.css"; +// Configure the playground server URL for the C# emitter's browser stub. +// This must be set before the emitter runs. +(globalThis as any).__TYPESPEC_PLAYGROUND_SERVER_URL__ = + "https://csharp-playground-server.typespec.io"; + export interface WebsitePlaygroundProps { versionData: VersionData; } From c9f0f9efffa44888642b60e4498e2f8a7784d2ca Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 13:36:02 -0700 Subject: [PATCH 024/103] fix: use correct ACR name (azsdkengsys) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index fb9439cd6f4..1669cd54cbe 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -396,7 +396,7 @@ stages: scriptLocation: inlineScript inlineScript: | set -e - REGISTRY="typespec" + REGISTRY="azsdkengsys" RESOURCE_GROUP="typespec" APP_NAME="${{ parameters.PlaygroundServerAppName }}" IMAGE="$REGISTRY.azurecr.io/$APP_NAME:$(Build.BuildId)" From 0b4a0d7fd8b6f4824d28be98470ac5223270791e Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 13:51:09 -0700 Subject: [PATCH 025/103] fix: use typespecacr registry in typespec resource group Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 1669cd54cbe..5c24a197fe7 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -396,7 +396,7 @@ stages: scriptLocation: inlineScript inlineScript: | set -e - REGISTRY="azsdkengsys" + REGISTRY="typespecacr" RESOURCE_GROUP="typespec" APP_NAME="${{ parameters.PlaygroundServerAppName }}" IMAGE="$REGISTRY.azurecr.io/$APP_NAME:$(Build.BuildId)" From 0a2eb5280b2f35dd6d3f74faa1ad9476c04dd5ee Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 13:53:28 -0700 Subject: [PATCH 026/103] fix: auto-provision Azure resources in pipeline Create ACR, Container Apps environment, and Container App automatically if they don't exist. On subsequent runs, only updates the container image. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 5c24a197fe7..1fed2ccfc7b 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -398,9 +398,17 @@ stages: set -e REGISTRY="typespecacr" RESOURCE_GROUP="typespec" + ENV_NAME="typespec-playground" APP_NAME="${{ parameters.PlaygroundServerAppName }}" IMAGE="$REGISTRY.azurecr.io/$APP_NAME:$(Build.BuildId)" + # Create ACR if it doesn't exist + if ! az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" &>/dev/null; then + echo "Creating container registry: $REGISTRY" + az acr create --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --sku Basic --admin-enabled true + fi + + # Build and push Docker image echo "Building Docker image: $IMAGE" az acr build \ --registry "$REGISTRY" \ @@ -408,13 +416,34 @@ stages: --file ${{ parameters.PlaygroundServerDockerfile }} \ $(Build.SourcesDirectory)/${{ parameters.PackagePath }} - echo "Deploying to Container Apps" - az containerapp update \ - --name "$APP_NAME" \ - --resource-group "$RESOURCE_GROUP" \ - --image "$IMAGE" - - echo "Deployed successfully" + # Create Container Apps environment if it doesn't exist + if ! az containerapp env show --name "$ENV_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then + echo "Creating Container Apps environment: $ENV_NAME" + az containerapp env create --name "$ENV_NAME" --resource-group "$RESOURCE_GROUP" + fi + + # Create or update Container App + if az containerapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then + echo "Updating Container App: $APP_NAME" + az containerapp update \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --image "$IMAGE" + else + echo "Creating Container App: $APP_NAME" + az containerapp create \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --environment "$ENV_NAME" \ + --image "$IMAGE" \ + --target-port 5174 \ + --ingress external \ + --min-replicas 1 \ + --registry-server "$REGISTRY.azurecr.io" + fi + + FQDN=$(az containerapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query properties.configuration.ingress.fqdn -o tsv) + echo "Deployed successfully at https://$FQDN" workingDirectory: $(Build.SourcesDirectory) templateContext: From 981d325796fd1bc1d5ef51d2e45df3a8d0c38e37 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 14:40:37 -0700 Subject: [PATCH 027/103] fix: disable ACR admin account per org policy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 1fed2ccfc7b..77ab2ddbf26 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -405,7 +405,7 @@ stages: # Create ACR if it doesn't exist if ! az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" &>/dev/null; then echo "Creating container registry: $REGISTRY" - az acr create --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --sku Basic --admin-enabled true + az acr create --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --sku Basic --admin-enabled false fi # Build and push Docker image From 3ca51907aa29d9f3f9bfd62e9ced8f3557995815 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 15:39:59 -0700 Subject: [PATCH 028/103] trigger pipeline with latest changes From c38b629fe9482cad5d36746b9a84f699cddb794c Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 15:45:25 -0700 Subject: [PATCH 029/103] fix: use absolute path for Dockerfile in az acr build Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 77ab2ddbf26..2e4d195a43f 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -410,11 +410,12 @@ stages: # Build and push Docker image echo "Building Docker image: $IMAGE" + CONTEXT="$(Build.SourcesDirectory)/${{ parameters.PackagePath }}" az acr build \ --registry "$REGISTRY" \ --image "$APP_NAME:$(Build.BuildId)" \ - --file ${{ parameters.PlaygroundServerDockerfile }} \ - $(Build.SourcesDirectory)/${{ parameters.PackagePath }} + --file "$CONTEXT/${{ parameters.PlaygroundServerDockerfile }}" \ + "$CONTEXT" # Create Container Apps environment if it doesn't exist if ! az containerapp env show --name "$ENV_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then From 57c13fbf16b3e8899819c07b2a4c0c342d07ef7e Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 15:46:28 -0700 Subject: [PATCH 030/103] fix: add .dockerignore to exclude node_modules from ACR upload Without this, az acr build tries to upload the entire context including node_modules/ which is huge and may cause issues. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/.dockerignore | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/http-client-csharp/.dockerignore diff --git a/packages/http-client-csharp/.dockerignore b/packages/http-client-csharp/.dockerignore new file mode 100644 index 00000000000..d2ba1a47b14 --- /dev/null +++ b/packages/http-client-csharp/.dockerignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +emitter/ +eng/ +.tspd/ +*.md +*.tsp +package-lock.json From 7da1f8b3e7e2dfa7d7714d0fe6cb1ef9e27f5986 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 16:13:37 -0700 Subject: [PATCH 031/103] fix: include eng/ for signing key and ruleset in Docker build Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/.dockerignore | 1 - packages/http-client-csharp/playground-server/Dockerfile | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/.dockerignore b/packages/http-client-csharp/.dockerignore index d2ba1a47b14..26ee17cadf1 100644 --- a/packages/http-client-csharp/.dockerignore +++ b/packages/http-client-csharp/.dockerignore @@ -1,7 +1,6 @@ node_modules/ dist/ emitter/ -eng/ .tspd/ *.md *.tsp diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index c6894a3ab17..acedcbbe4d8 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -5,6 +5,7 @@ WORKDIR /src # Build the generator (populates dist/generator/) COPY generator/ generator/ +COPY eng/ eng/ COPY global.json . RUN dotnet build generator -c Release From 026001bad78d24b23d3c651378da143db8dfa83a Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 18:10:50 -0700 Subject: [PATCH 032/103] fix: switch from Container Apps to App Service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Container Apps environment creation is blocked by org policy. App Service is simpler — no environment needed, predictable URL (csharp-playground-server.azurewebsites.net). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 46 +++++++++++-------- .../playground-component/playground.tsx | 2 +- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 2e4d195a43f..df406ff33f3 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -398,7 +398,7 @@ stages: set -e REGISTRY="typespecacr" RESOURCE_GROUP="typespec" - ENV_NAME="typespec-playground" + PLAN_NAME="typespec-playground-plan" APP_NAME="${{ parameters.PlaygroundServerAppName }}" IMAGE="$REGISTRY.azurecr.io/$APP_NAME:$(Build.BuildId)" @@ -417,33 +417,41 @@ stages: --file "$CONTEXT/${{ parameters.PlaygroundServerDockerfile }}" \ "$CONTEXT" - # Create Container Apps environment if it doesn't exist - if ! az containerapp env show --name "$ENV_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then - echo "Creating Container Apps environment: $ENV_NAME" - az containerapp env create --name "$ENV_NAME" --resource-group "$RESOURCE_GROUP" + # Create App Service plan if it doesn't exist + if ! az appservice plan show --name "$PLAN_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then + echo "Creating App Service plan: $PLAN_NAME" + az appservice plan create \ + --name "$PLAN_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --is-linux \ + --sku B1 fi - # Create or update Container App - if az containerapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then - echo "Updating Container App: $APP_NAME" - az containerapp update \ + # Create or update Web App + if az webapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then + echo "Updating Web App: $APP_NAME" + az webapp config container set \ --name "$APP_NAME" \ --resource-group "$RESOURCE_GROUP" \ - --image "$IMAGE" + --container-image-name "$IMAGE" \ + --container-registry-url "https://$REGISTRY.azurecr.io" else - echo "Creating Container App: $APP_NAME" - az containerapp create \ + echo "Creating Web App: $APP_NAME" + az webapp create \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --plan "$PLAN_NAME" \ + --container-image-name "$IMAGE" \ + --container-registry-url "https://$REGISTRY.azurecr.io" + + # Configure the app + az webapp config appsettings set \ --name "$APP_NAME" \ --resource-group "$RESOURCE_GROUP" \ - --environment "$ENV_NAME" \ - --image "$IMAGE" \ - --target-port 5174 \ - --ingress external \ - --min-replicas 1 \ - --registry-server "$REGISTRY.azurecr.io" + --settings WEBSITES_PORT=5174 fi - FQDN=$(az containerapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query properties.configuration.ingress.fqdn -o tsv) + FQDN="$APP_NAME.azurewebsites.net" echo "Deployed successfully at https://$FQDN" workingDirectory: $(Build.SourcesDirectory) diff --git a/website/src/components/playground-component/playground.tsx b/website/src/components/playground-component/playground.tsx index 2b4012e87e9..e69fce27198 100644 --- a/website/src/components/playground-component/playground.tsx +++ b/website/src/components/playground-component/playground.tsx @@ -19,7 +19,7 @@ import "@typespec/playground/styles.css"; // Configure the playground server URL for the C# emitter's browser stub. // This must be set before the emitter runs. (globalThis as any).__TYPESPEC_PLAYGROUND_SERVER_URL__ = - "https://csharp-playground-server.typespec.io"; + "https://csharp-playground-server.azurewebsites.net"; export interface WebsitePlaygroundProps { versionData: VersionData; From 35862beed280ed4e26a4f347a045cacaceb1ea0b Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 18:31:11 -0700 Subject: [PATCH 033/103] fix: add CORS origins for playground websites Add typespec.io and PR preview origins to CORS allowed list. Support comma-separated PLAYGROUND_URLS env var. Set it in the App Service config during deployment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 4 +++- .../playground-server/Program.cs | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index df406ff33f3..56ef3e7de72 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -448,7 +448,9 @@ stages: az webapp config appsettings set \ --name "$APP_NAME" \ --resource-group "$RESOURCE_GROUP" \ - --settings WEBSITES_PORT=5174 + --settings \ + WEBSITES_PORT=5174 \ + PLAYGROUND_URLS="https://typespec.io,https://tspwebsitepr.z22.web.core.windows.net,https://cadlplayground.z22.web.core.windows.net" fi FQDN="$APP_NAME.azurewebsites.net" diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 85bd18aff1f..c7c359c2463 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -14,11 +14,21 @@ "http://localhost:5173", // vite dev "http://localhost:4173", // vite preview "http://localhost:3000", + "https://typespec.io", + "https://www.typespec.io", }; -var playgroundUrl = Environment.GetEnvironmentVariable("PLAYGROUND_URL"); -if (!string.IsNullOrEmpty(playgroundUrl) && Uri.TryCreate(playgroundUrl, UriKind.Absolute, out var uri)) +// Add additional origins from PLAYGROUND_URLS (comma-separated) or PLAYGROUND_URL (single) +var playgroundUrls = Environment.GetEnvironmentVariable("PLAYGROUND_URLS") + ?? Environment.GetEnvironmentVariable("PLAYGROUND_URL"); +if (!string.IsNullOrEmpty(playgroundUrls)) { - allowedOrigins.Add(uri.GetLeftPart(UriPartial.Authority)); + foreach (var url in playgroundUrls.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + allowedOrigins.Add(uri.GetLeftPart(UriPartial.Authority)); + } + } } builder.Services.AddCors(); From 91c6849fd5f2086649c585a9692d66440720c876 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 18:32:58 -0700 Subject: [PATCH 034/103] fix: enable managed identity for ACR pull Assign system-managed identity to App Service and grant AcrPull role so it can pull images without admin credentials. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 56ef3e7de72..4721ef986d7 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -444,6 +444,13 @@ stages: --container-image-name "$IMAGE" \ --container-registry-url "https://$REGISTRY.azurecr.io" + # Enable managed identity and grant ACR pull access + az webapp identity assign --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" + PRINCIPAL_ID=$(az webapp identity show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query principalId -o tsv) + ACR_ID=$(az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --query id -o tsv) + az role assignment create --assignee "$PRINCIPAL_ID" --role AcrPull --scope "$ACR_ID" + az webapp config set --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --generic-configurations '{"acrUseManagedIdentityCreds": true}' + # Configure the app az webapp config appsettings set \ --name "$APP_NAME" \ From 591b78d619c0c0866506f76b42807f12bd587ba5 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 18:47:29 -0700 Subject: [PATCH 035/103] fix: rename loop variable to avoid scope conflict Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index c7c359c2463..b2ff671211c 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -22,9 +22,9 @@ ?? Environment.GetEnvironmentVariable("PLAYGROUND_URL"); if (!string.IsNullOrEmpty(playgroundUrls)) { - foreach (var url in playgroundUrls.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + foreach (var origin in playgroundUrls.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { - if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) + if (Uri.TryCreate(origin, UriKind.Absolute, out var uri)) { allowedOrigins.Add(uri.GetLeftPart(UriPartial.Authority)); } From b8f019d81a2cc44dcb880a7ad11a9ee216d447a7 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 19:03:58 -0700 Subject: [PATCH 036/103] fix: create webapp with placeholder image, then configure ACR Create the webapp with a public MCR image first (no auth needed), then set up managed identity and ACR pull, then switch to the actual image from our private ACR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../pipelines/templates/stages/emitter-stages.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 4721ef986d7..d0faaae9ebd 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -437,19 +437,27 @@ stages: --container-registry-url "https://$REGISTRY.azurecr.io" else echo "Creating Web App: $APP_NAME" + # Create webapp with a placeholder image first az webapp create \ --name "$APP_NAME" \ --resource-group "$RESOURCE_GROUP" \ --plan "$PLAN_NAME" \ - --container-image-name "$IMAGE" \ - --container-registry-url "https://$REGISTRY.azurecr.io" + --container-image-name "mcr.microsoft.com/dotnet/aspnet:10.0" # Enable managed identity and grant ACR pull access az webapp identity assign --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" PRINCIPAL_ID=$(az webapp identity show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query principalId -o tsv) ACR_ID=$(az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --query id -o tsv) az role assignment create --assignee "$PRINCIPAL_ID" --role AcrPull --scope "$ACR_ID" - az webapp config set --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --generic-configurations '{"acrUseManagedIdentityCreds": true}' + + # Configure to use managed identity for ACR and set the actual image + az webapp config set --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" \ + --generic-configurations '{"acrUseManagedIdentityCreds": true}' + az webapp config container set \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --container-image-name "$IMAGE" \ + --container-registry-url "https://$REGISTRY.azurecr.io" # Configure the app az webapp config appsettings set \ From bdc1a0880528530ecd4cb06363be544874100f6b Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 19:07:24 -0700 Subject: [PATCH 037/103] fix: ensure identity and ACR pull on every deploy Move managed identity and ACR pull role setup before the create/update branch so it runs on every deployment, not just the first one. Use || true to ignore already-exists errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index d0faaae9ebd..655c73f3c5a 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -427,6 +427,14 @@ stages: --sku B1 fi + # Ensure managed identity and ACR pull access are configured + az webapp identity assign --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" 2>/dev/null || true + PRINCIPAL_ID=$(az webapp identity show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query principalId -o tsv) + ACR_ID=$(az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --query id -o tsv) + az role assignment create --assignee "$PRINCIPAL_ID" --role AcrPull --scope "$ACR_ID" 2>/dev/null || true + az webapp config set --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" \ + --generic-configurations '{"acrUseManagedIdentityCreds": true}' + # Create or update Web App if az webapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then echo "Updating Web App: $APP_NAME" @@ -437,22 +445,13 @@ stages: --container-registry-url "https://$REGISTRY.azurecr.io" else echo "Creating Web App: $APP_NAME" - # Create webapp with a placeholder image first az webapp create \ --name "$APP_NAME" \ --resource-group "$RESOURCE_GROUP" \ --plan "$PLAN_NAME" \ --container-image-name "mcr.microsoft.com/dotnet/aspnet:10.0" - # Enable managed identity and grant ACR pull access - az webapp identity assign --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" - PRINCIPAL_ID=$(az webapp identity show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query principalId -o tsv) - ACR_ID=$(az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --query id -o tsv) - az role assignment create --assignee "$PRINCIPAL_ID" --role AcrPull --scope "$ACR_ID" - - # Configure to use managed identity for ACR and set the actual image - az webapp config set --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" \ - --generic-configurations '{"acrUseManagedIdentityCreds": true}' + # Set the actual image after identity is ready az webapp config container set \ --name "$APP_NAME" \ --resource-group "$RESOURCE_GROUP" \ From 3995e8c4f4c082b41016d983ba5eafb251171d7d Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 19:08:31 -0700 Subject: [PATCH 038/103] fix: set WEBSITES_PORT on every deploy, not just create The app settings were only configured in the create branch. Move them after the if/else so they run on every deployment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 655c73f3c5a..8aa3220a809 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -457,16 +457,16 @@ stages: --resource-group "$RESOURCE_GROUP" \ --container-image-name "$IMAGE" \ --container-registry-url "https://$REGISTRY.azurecr.io" - - # Configure the app - az webapp config appsettings set \ - --name "$APP_NAME" \ - --resource-group "$RESOURCE_GROUP" \ - --settings \ - WEBSITES_PORT=5174 \ - PLAYGROUND_URLS="https://typespec.io,https://tspwebsitepr.z22.web.core.windows.net,https://cadlplayground.z22.web.core.windows.net" fi + # Configure app settings (runs on every deploy) + az webapp config appsettings set \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --settings \ + WEBSITES_PORT=5174 \ + PLAYGROUND_URLS="https://typespec.io,https://tspwebsitepr.z22.web.core.windows.net,https://cadlplayground.z22.web.core.windows.net" + FQDN="$APP_NAME.azurewebsites.net" echo "Deployed successfully at https://$FQDN" workingDirectory: $(Build.SourcesDirectory) From cd1e7aa099c95c9d6b9e4619b5597645352b9bb7 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 19:34:39 -0700 Subject: [PATCH 039/103] fix: use PORT env var for App Service compatibility App Service sets PORT/WEBSITES_PORT. Remove hardcoded ASPNETCORE_URLS from Dockerfile and read PORT at runtime instead. Listen on 0.0.0.0 (http://+) not localhost. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 1 - packages/http-client-csharp/playground-server/Program.cs | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index acedcbbe4d8..1b9b1e91d9f 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -26,7 +26,6 @@ COPY --from=build /app . RUN groupadd -r playground && useradd -r -g playground playground USER playground -ENV ASPNETCORE_URLS=http://+:5174 ENV DOTNET_ENVIRONMENT=Production ENV GENERATOR_PATH=/app/generator/Microsoft.TypeSpec.Generator.dll diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index b2ff671211c..5466dffead3 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -154,7 +154,10 @@ } }).RequireRateLimiting("generate"); -var url = Environment.GetEnvironmentVariable("ASPNETCORE_URLS") ?? "http://localhost:5174"; +var port = Environment.GetEnvironmentVariable("PORT") + ?? Environment.GetEnvironmentVariable("WEBSITES_PORT") + ?? "5174"; +var url = Environment.GetEnvironmentVariable("ASPNETCORE_URLS") ?? $"http://+:{port}"; Console.WriteLine($"C# playground server listening on {url}"); app.Run(url); From 10e4e20ee2992158cafc6e5f5724e6828f8e16f6 Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 9 Apr 2026 15:10:03 -0700 Subject: [PATCH 040/103] fix: add logging for identity setup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../pipelines/templates/stages/emitter-stages.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 8aa3220a809..3221d373023 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -428,13 +428,20 @@ stages: fi # Ensure managed identity and ACR pull access are configured - az webapp identity assign --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" 2>/dev/null || true + echo "Configuring managed identity..." + az webapp identity assign --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" PRINCIPAL_ID=$(az webapp identity show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query principalId -o tsv) ACR_ID=$(az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --query id -o tsv) - az role assignment create --assignee "$PRINCIPAL_ID" --role AcrPull --scope "$ACR_ID" 2>/dev/null || true + echo "Principal ID: $PRINCIPAL_ID" + echo "ACR ID: $ACR_ID" + az role assignment create --assignee "$PRINCIPAL_ID" --role AcrPull --scope "$ACR_ID" || echo "Role assignment already exists" az webapp config set --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" \ --generic-configurations '{"acrUseManagedIdentityCreds": true}' + # Wait for role assignment to propagate + echo "Waiting 30s for role assignment propagation..." + sleep 30 + # Create or update Web App if az webapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then echo "Updating Web App: $APP_NAME" From a4b83af5cab7d609b15f9e5572b354fd7caf7e90 Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 9 Apr 2026 15:24:59 -0700 Subject: [PATCH 041/103] fix: add C# emitter back to playground packages Was lost during merge conflict resolution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/src/components/react-pages/playground.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/website/src/components/react-pages/playground.tsx b/website/src/components/react-pages/playground.tsx index 5ed019fabbb..326124bdf55 100644 --- a/website/src/components/react-pages/playground.tsx +++ b/website/src/components/react-pages/playground.tsx @@ -3,7 +3,10 @@ import { useEffect, useState, type ReactNode } from "react"; import { FluentLayout } from "../fluent/fluent-layout"; import { loadImportMap, type VersionData } from "../playground-component/import-map"; -const additionalPlaygroundPackages = ["@typespec/http-client-python"]; +const additionalPlaygroundPackages = [ + "@typespec/http-client-csharp", + "@typespec/http-client-python", +]; export const AsyncPlayground = ({ latestVersion, From c496f54f808bbd906dcb1fb1182a9ae9f0edd500 Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 9 Apr 2026 15:41:20 -0700 Subject: [PATCH 042/103] fix: install Node 22 for playground bundle upload The repo now requires Node >=22 but the publish pipeline defaults to Node 20. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 3221d373023..190c10bb479 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -374,6 +374,10 @@ stages: - script: npm run ${{ parameters.PlaygroundBundleBuildScript }} displayName: Build emitter for playground bundle workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} + - task: NodeTool@0 + displayName: Install Node.js for playground bundle + inputs: + versionSpec: "22.x" - script: pnpm install --filter "@typespec/bundle-uploader..." displayName: Install bundle-uploader dependencies workingDirectory: $(Build.SourcesDirectory) From a2ae6e975de9fae0da22bd0aa73f78d06dafc676 Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 9 Apr 2026 15:45:22 -0700 Subject: [PATCH 043/103] fix: upgrade App Service plan to B2 (2 vCPU, 3.5 GB) B1 was too small for the .NET generator + Roslyn, causing OOM crashes (exit code 139). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 190c10bb479..a8718f928c2 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -428,7 +428,7 @@ stages: --name "$PLAN_NAME" \ --resource-group "$RESOURCE_GROUP" \ --is-linux \ - --sku B1 + --sku B2 fi # Ensure managed identity and ACR pull access are configured From 89b8d8a4a481e6cae113404583b956d038453d0d Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 9 Apr 2026 17:19:38 -0700 Subject: [PATCH 044/103] fix: create writable temp dir for Roslyn persistent storage Roslyn's DefaultPersistentStorageConfiguration needs a writable temp directory. Create /tmp/roslyn owned by the playground user and set TMPDIR so Roslyn can write its cache files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 1b9b1e91d9f..ea92bd7bc23 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -23,11 +23,14 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime WORKDIR /app COPY --from=build /app . -RUN groupadd -r playground && useradd -r -g playground playground +RUN groupadd -r playground && useradd -r -g playground -m playground \ + && mkdir -p /tmp/roslyn && chown -R playground:playground /tmp/roslyn USER playground ENV DOTNET_ENVIRONMENT=Production ENV GENERATOR_PATH=/app/generator/Microsoft.TypeSpec.Generator.dll +# Roslyn needs a writable temp directory for persistent storage +ENV TMPDIR=/tmp/roslyn EXPOSE 5174 ENTRYPOINT ["dotnet", "playground-server.dll"] From 9aecc0662a734a0e5c8219381170893b59b38f5b Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 9 Apr 2026 17:28:41 -0700 Subject: [PATCH 045/103] fix: set no-cache on latest.json blobs Prevent browsers from caching latest.json so they always fetch the current version pointer. Versioned JS files are immutable and can be cached forever. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/bundle-uploader/src/upload-browser-package.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/bundle-uploader/src/upload-browser-package.ts b/packages/bundle-uploader/src/upload-browser-package.ts index c2815847b9f..7ecef6d34fd 100644 --- a/packages/bundle-uploader/src/upload-browser-package.ts +++ b/packages/bundle-uploader/src/upload-browser-package.ts @@ -120,6 +120,7 @@ export class TypeSpecBundledPackageUploader { await blob.upload(content, content.length, { blobHTTPHeaders: { blobContentType: "application/json; charset=utf-8", + blobCacheControl: "no-cache, max-age=0", }, }); } @@ -145,6 +146,7 @@ export class TypeSpecBundledPackageUploader { await blob.upload(content, content.length, { blobHTTPHeaders: { blobContentType: "application/json; charset=utf-8", + blobCacheControl: "no-cache, max-age=0", }, }); } From 416255c8369fe4f1ec0ae7925730b2c899d88dd2 Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 9 Apr 2026 18:04:52 -0700 Subject: [PATCH 046/103] fix: run container as root to debug segfault Remove non-root user to eliminate permission-related Roslyn crashes. Can be re-added once the segfault is resolved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index ea92bd7bc23..5bafbf205e8 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -23,14 +23,9 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime WORKDIR /app COPY --from=build /app . -RUN groupadd -r playground && useradd -r -g playground -m playground \ - && mkdir -p /tmp/roslyn && chown -R playground:playground /tmp/roslyn -USER playground - ENV DOTNET_ENVIRONMENT=Production ENV GENERATOR_PATH=/app/generator/Microsoft.TypeSpec.Generator.dll -# Roslyn needs a writable temp directory for persistent storage -ENV TMPDIR=/tmp/roslyn +ENV TMPDIR=/tmp EXPOSE 5174 ENTRYPOINT ["dotnet", "playground-server.dll"] From 237549e1c3f28631ff85276728ec139a8560392b Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 9 Apr 2026 18:51:52 -0700 Subject: [PATCH 047/103] fix: use Ubuntu Noble base image explicitly Try noble (Ubuntu 24.04) instead of default to rule out base image issues causing SIGSEGV. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../playground-server/Dockerfile | 4 ++-- .../playground-server/Program.cs | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 5bafbf205e8..fd3caa92457 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -1,6 +1,6 @@ # Build from the http-client-csharp package root: # docker build -f playground-server/Dockerfile -t csharp-playground-server . -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS build WORKDIR /src # Build the generator (populates dist/generator/) @@ -19,7 +19,7 @@ RUN dotnet publish playground-server -c Release -o /app RUN cp -r dist/generator /app/generator # Need full SDK (not just aspnet) because the server spawns `dotnet` to run the generator DLL -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime +FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS runtime WORKDIR /app COPY --from=build /app . diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 5466dffead3..22202475304 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -74,6 +74,21 @@ generatorPath })); +// Debug endpoint: returns the first 5000 chars of the code model and configuration +app.MapPost("/debug-generate", async (HttpRequest request) => +{ + var body = await JsonSerializer.DeserializeAsync( + request.Body, GenerateJsonContext.Default.GenerateRequest); + return Results.Ok(new + { + codeModelLength = body?.CodeModel?.Length ?? 0, + codeModelPreview = body?.CodeModel?.Substring(0, Math.Min(5000, body?.CodeModel?.Length ?? 0)), + configurationLength = body?.Configuration?.Length ?? 0, + configuration = body?.Configuration, + generatorName = body?.GeneratorName + }); +}); + app.MapPost("/generate", async (HttpRequest request) => { var body = await JsonSerializer.DeserializeAsync( From 4595959076c581c7164edbc0e449e5ec045e9f4a Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 9 Apr 2026 18:52:21 -0700 Subject: [PATCH 048/103] Revert "fix: use Ubuntu Noble base image explicitly" This reverts commit 237549e1c3f28631ff85276728ec139a8560392b. --- .../playground-server/Dockerfile | 4 ++-- .../playground-server/Program.cs | 15 --------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index fd3caa92457..5bafbf205e8 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -1,6 +1,6 @@ # Build from the http-client-csharp package root: # docker build -f playground-server/Dockerfile -t csharp-playground-server . -FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src # Build the generator (populates dist/generator/) @@ -19,7 +19,7 @@ RUN dotnet publish playground-server -c Release -o /app RUN cp -r dist/generator /app/generator # Need full SDK (not just aspnet) because the server spawns `dotnet` to run the generator DLL -FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS runtime +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime WORKDIR /app COPY --from=build /app . diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 22202475304..5466dffead3 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -74,21 +74,6 @@ generatorPath })); -// Debug endpoint: returns the first 5000 chars of the code model and configuration -app.MapPost("/debug-generate", async (HttpRequest request) => -{ - var body = await JsonSerializer.DeserializeAsync( - request.Body, GenerateJsonContext.Default.GenerateRequest); - return Results.Ok(new - { - codeModelLength = body?.CodeModel?.Length ?? 0, - codeModelPreview = body?.CodeModel?.Substring(0, Math.Min(5000, body?.CodeModel?.Length ?? 0)), - configurationLength = body?.Configuration?.Length ?? 0, - configuration = body?.Configuration, - generatorName = body?.GeneratorName - }); -}); - app.MapPost("/generate", async (HttpRequest request) => { var body = await JsonSerializer.DeserializeAsync( From d8e47262fa7e4a63dc3e564e9682a6eda23c7dcb Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 08:35:12 -0700 Subject: [PATCH 049/103] fix: stream generator stdout/stderr to App Service logs Log the full generator subprocess output so we can see where it crashes in the App Service log stream. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../playground-server/Program.cs | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 5466dffead3..7e3588d819b 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -103,6 +103,10 @@ var generatorName = body.GeneratorName ?? "ScmCodeModelGenerator"; // Run the .NET generator as a subprocess (same approach as the TypeSpec emitter) + Console.WriteLine($"Starting generator: dotnet --roll-forward Major {generatorPath} {tempDir} -g {generatorName} --new-project"); + Console.WriteLine($"Code model size: {body.CodeModel!.Length} chars"); + Console.WriteLine($"Configuration: {body.Configuration}"); + var psi = new ProcessStartInfo { FileName = "dotnet", @@ -114,14 +118,35 @@ }; using var process = Process.Start(psi)!; - var stdout = await process.StandardOutput.ReadToEndAsync(); - var stderr = await process.StandardError.ReadToEndAsync(); + + // Stream stdout/stderr to console for logging + var stdoutTask = Task.Run(async () => + { + string? line; + while ((line = await process.StandardOutput.ReadLineAsync()) != null) + { + Console.WriteLine($"[generator stdout] {line}"); + } + }); + var stderrTask = Task.Run(async () => + { + string? line; + while ((line = await process.StandardError.ReadLineAsync()) != null) + { + Console.Error.WriteLine($"[generator stderr] {line}"); + } + }); + await process.WaitForExitAsync(); + await Task.WhenAll(stdoutTask, stderrTask); + + var exitCode = process.ExitCode; + Console.WriteLine($"Generator exited with code {exitCode}"); - if (process.ExitCode != 0) + if (exitCode != 0) { return Results.Json( - new GenerateErrorResponse($"Generator failed with exit code {process.ExitCode}", stderr), + new GenerateErrorResponse($"Generator failed with exit code {exitCode}", "See server logs for details"), GenerateJsonContext.Default.GenerateErrorResponse, statusCode: 500); } From 03dd65c61e1fbc2834412194c05dc2d7914deca2 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 09:10:18 -0700 Subject: [PATCH 050/103] fix: disable .NET diagnostics to prevent container segfault Set COMPlus_EnableDiagnostics=0 and DOTNET_EnableDiagnostics=0. The .NET diagnostic infrastructure (EventPipe, IPC) can cause segfaults in containerized environments when it can't create diagnostic sockets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 5bafbf205e8..4f092afed4b 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -26,6 +26,9 @@ COPY --from=build /app . ENV DOTNET_ENVIRONMENT=Production ENV GENERATOR_PATH=/app/generator/Microsoft.TypeSpec.Generator.dll ENV TMPDIR=/tmp +# Disable .NET diagnostics to prevent segfaults in containers +ENV COMPlus_EnableDiagnostics=0 +ENV DOTNET_EnableDiagnostics=0 EXPOSE 5174 ENTRYPOINT ["dotnet", "playground-server.dll"] From 1727736dffbf02233857284e9ca3041b88cdd4bd Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 09:44:51 -0700 Subject: [PATCH 051/103] fix: disable server GC and increase stack size for generator Server GC and small stack sizes can cause segfaults in container environments. Set these for the generator subprocess. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Program.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 7e3588d819b..5d48f629297 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -116,6 +116,11 @@ UseShellExecute = false, CreateNoWindow = true, }; + // Set env vars to prevent segfaults in container environment + psi.Environment["DOTNET_DefaultStackSize"] = "0x200000"; // 2MB stack + psi.Environment["COMPlus_DefaultStackSize"] = "200000"; + psi.Environment["DOTNET_gcServer"] = "0"; // Disable server GC (can segfault in containers) + psi.Environment["COMPlus_EnableDiagnostics"] = "0"; using var process = Process.Start(psi)!; From 12815cc551ab34d1851d483a4f077c9e04252676 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 10:46:44 -0700 Subject: [PATCH 052/103] fix: upgrade to P1v3 (8 GB RAM) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index a8718f928c2..64eeeb8333e 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -428,7 +428,7 @@ stages: --name "$PLAN_NAME" \ --resource-group "$RESOURCE_GROUP" \ --is-linux \ - --sku B2 + --sku P1v3 fi # Ensure managed identity and ACR pull access are configured From bc698ba8e5d3befc984f0f8f0d587cbb922eb5ce Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 10:56:02 -0700 Subject: [PATCH 053/103] fix: add runtime info to health endpoint for debugging Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../playground-server/Program.cs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 5d48f629297..6b983799353 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -67,12 +67,29 @@ Console.WriteLine($"Generator DLL: {generatorPath}"); } -app.MapGet("/health", () => Results.Ok(new +app.MapGet("/health", () => { - status = "ok", - generatorFound = File.Exists(generatorPath), - generatorPath -})); + string dotnetVersion; + try + { + var psi = new ProcessStartInfo("dotnet", "--version") { RedirectStandardOutput = true, UseShellExecute = false }; + var proc = Process.Start(psi)!; + dotnetVersion = proc.StandardOutput.ReadToEnd().Trim(); + proc.WaitForExit(); + } + catch (Exception ex) { dotnetVersion = ex.Message; } + + return Results.Ok(new + { + status = "ok", + generatorFound = File.Exists(generatorPath), + generatorPath, + dotnetVersion, + runtime = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription, + os = System.Runtime.InteropServices.RuntimeInformation.OSDescription, + arch = System.Runtime.InteropServices.RuntimeInformation.OSArchitecture.ToString() + }); +}); app.MapPost("/generate", async (HttpRequest request) => { From cff843be6a4a79092bfbde8a9746445f16e694a6 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 12:01:46 -0700 Subject: [PATCH 054/103] fix: switch from App Service to Azure Container Instances App Service's container sandbox causes SIGSEGV in the .NET generator. ACI runs containers with fewer restrictions. 2 vCPU, 4 GB RAM, public IP, restart always. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 80 +++++-------------- 1 file changed, 22 insertions(+), 58 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 64eeeb8333e..5d595708817 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -402,7 +402,6 @@ stages: set -e REGISTRY="typespecacr" RESOURCE_GROUP="typespec" - PLAN_NAME="typespec-playground-plan" APP_NAME="${{ parameters.PlaygroundServerAppName }}" IMAGE="$REGISTRY.azurecr.io/$APP_NAME:$(Build.BuildId)" @@ -421,65 +420,30 @@ stages: --file "$CONTEXT/${{ parameters.PlaygroundServerDockerfile }}" \ "$CONTEXT" - # Create App Service plan if it doesn't exist - if ! az appservice plan show --name "$PLAN_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then - echo "Creating App Service plan: $PLAN_NAME" - az appservice plan create \ - --name "$PLAN_NAME" \ - --resource-group "$RESOURCE_GROUP" \ - --is-linux \ - --sku P1v3 - fi - - # Ensure managed identity and ACR pull access are configured - echo "Configuring managed identity..." - az webapp identity assign --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" - PRINCIPAL_ID=$(az webapp identity show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query principalId -o tsv) - ACR_ID=$(az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --query id -o tsv) - echo "Principal ID: $PRINCIPAL_ID" - echo "ACR ID: $ACR_ID" - az role assignment create --assignee "$PRINCIPAL_ID" --role AcrPull --scope "$ACR_ID" || echo "Role assignment already exists" - az webapp config set --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" \ - --generic-configurations '{"acrUseManagedIdentityCreds": true}' - - # Wait for role assignment to propagate - echo "Waiting 30s for role assignment propagation..." - sleep 30 - - # Create or update Web App - if az webapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then - echo "Updating Web App: $APP_NAME" - az webapp config container set \ - --name "$APP_NAME" \ - --resource-group "$RESOURCE_GROUP" \ - --container-image-name "$IMAGE" \ - --container-registry-url "https://$REGISTRY.azurecr.io" - else - echo "Creating Web App: $APP_NAME" - az webapp create \ - --name "$APP_NAME" \ - --resource-group "$RESOURCE_GROUP" \ - --plan "$PLAN_NAME" \ - --container-image-name "mcr.microsoft.com/dotnet/aspnet:10.0" - - # Set the actual image after identity is ready - az webapp config container set \ - --name "$APP_NAME" \ - --resource-group "$RESOURCE_GROUP" \ - --container-image-name "$IMAGE" \ - --container-registry-url "https://$REGISTRY.azurecr.io" - fi - - # Configure app settings (runs on every deploy) - az webapp config appsettings set \ + # Deploy to Azure Container Instances + echo "Deploying to ACI: $APP_NAME" + az container create \ --name "$APP_NAME" \ --resource-group "$RESOURCE_GROUP" \ - --settings \ - WEBSITES_PORT=5174 \ - PLAYGROUND_URLS="https://typespec.io,https://tspwebsitepr.z22.web.core.windows.net,https://cadlplayground.z22.web.core.windows.net" - - FQDN="$APP_NAME.azurewebsites.net" - echo "Deployed successfully at https://$FQDN" + --image "$IMAGE" \ + --registry-login-server "$REGISTRY.azurecr.io" \ + --acr-identity "[system]" \ + --assign-identity "[system]" \ + --cpu 2 \ + --memory 4 \ + --ports 5174 \ + --ip-address Public \ + --os-type Linux \ + --environment-variables \ + DOTNET_ENVIRONMENT=Production \ + TMPDIR=/tmp \ + COMPlus_EnableDiagnostics=0 \ + DOTNET_EnableDiagnostics=0 \ + --restart-policy Always + + FQDN=$(az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query ipAddress.fqdn -o tsv) + IP=$(az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query ipAddress.ip -o tsv) + echo "Deployed successfully at http://$IP:5174 (FQDN: $FQDN)" workingDirectory: $(Build.SourcesDirectory) templateContext: From 94d33eef5e6a2507ec4a80fb7a901d22f125718f Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 12:09:03 -0700 Subject: [PATCH 055/103] fix: add ReadyToRun and TieredCompilation workarounds for Roslyn segfault Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Program.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 6b983799353..4af45ef5cd7 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -136,8 +136,10 @@ // Set env vars to prevent segfaults in container environment psi.Environment["DOTNET_DefaultStackSize"] = "0x200000"; // 2MB stack psi.Environment["COMPlus_DefaultStackSize"] = "200000"; - psi.Environment["DOTNET_gcServer"] = "0"; // Disable server GC (can segfault in containers) + psi.Environment["DOTNET_gcServer"] = "0"; // Disable server GC psi.Environment["COMPlus_EnableDiagnostics"] = "0"; + psi.Environment["DOTNET_ReadyToRun"] = "0"; // Disable R2R, force JIT + psi.Environment["DOTNET_TieredCompilation"] = "0"; // Disable tiered compilation using var process = Process.Start(psi)!; From 833291b52d01b47bf827e3647b5faa00b709b897 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 14:02:21 -0700 Subject: [PATCH 056/103] fix: use ACR admin credentials for ACI image pull instead of managed identity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../pipelines/templates/stages/emitter-stages.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 5d595708817..3f7f16d8150 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -420,6 +420,11 @@ stages: --file "$CONTEXT/${{ parameters.PlaygroundServerDockerfile }}" \ "$CONTEXT" + # Enable ACR admin to get credentials for ACI pull + az acr update --name "$REGISTRY" --admin-enabled true + ACR_USERNAME=$(az acr credential show --name "$REGISTRY" --query username -o tsv) + ACR_PASSWORD=$(az acr credential show --name "$REGISTRY" --query "passwords[0].value" -o tsv) + # Deploy to Azure Container Instances echo "Deploying to ACI: $APP_NAME" az container create \ @@ -427,8 +432,8 @@ stages: --resource-group "$RESOURCE_GROUP" \ --image "$IMAGE" \ --registry-login-server "$REGISTRY.azurecr.io" \ - --acr-identity "[system]" \ - --assign-identity "[system]" \ + --registry-username "$ACR_USERNAME" \ + --registry-password "$ACR_PASSWORD" \ --cpu 2 \ --memory 4 \ --ports 5174 \ From 4332d9fbb8d355bfe49230b676dba1a1ec4023d0 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 14:03:47 -0700 Subject: [PATCH 057/103] fix: revert pipeline to App Service to test ReadyToRun/TieredCompilation workarounds Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 83 +++++++++++++------ 1 file changed, 57 insertions(+), 26 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 3f7f16d8150..64eeeb8333e 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -402,6 +402,7 @@ stages: set -e REGISTRY="typespecacr" RESOURCE_GROUP="typespec" + PLAN_NAME="typespec-playground-plan" APP_NAME="${{ parameters.PlaygroundServerAppName }}" IMAGE="$REGISTRY.azurecr.io/$APP_NAME:$(Build.BuildId)" @@ -420,35 +421,65 @@ stages: --file "$CONTEXT/${{ parameters.PlaygroundServerDockerfile }}" \ "$CONTEXT" - # Enable ACR admin to get credentials for ACI pull - az acr update --name "$REGISTRY" --admin-enabled true - ACR_USERNAME=$(az acr credential show --name "$REGISTRY" --query username -o tsv) - ACR_PASSWORD=$(az acr credential show --name "$REGISTRY" --query "passwords[0].value" -o tsv) + # Create App Service plan if it doesn't exist + if ! az appservice plan show --name "$PLAN_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then + echo "Creating App Service plan: $PLAN_NAME" + az appservice plan create \ + --name "$PLAN_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --is-linux \ + --sku P1v3 + fi + + # Ensure managed identity and ACR pull access are configured + echo "Configuring managed identity..." + az webapp identity assign --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" + PRINCIPAL_ID=$(az webapp identity show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query principalId -o tsv) + ACR_ID=$(az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --query id -o tsv) + echo "Principal ID: $PRINCIPAL_ID" + echo "ACR ID: $ACR_ID" + az role assignment create --assignee "$PRINCIPAL_ID" --role AcrPull --scope "$ACR_ID" || echo "Role assignment already exists" + az webapp config set --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" \ + --generic-configurations '{"acrUseManagedIdentityCreds": true}' + + # Wait for role assignment to propagate + echo "Waiting 30s for role assignment propagation..." + sleep 30 + + # Create or update Web App + if az webapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then + echo "Updating Web App: $APP_NAME" + az webapp config container set \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --container-image-name "$IMAGE" \ + --container-registry-url "https://$REGISTRY.azurecr.io" + else + echo "Creating Web App: $APP_NAME" + az webapp create \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --plan "$PLAN_NAME" \ + --container-image-name "mcr.microsoft.com/dotnet/aspnet:10.0" + + # Set the actual image after identity is ready + az webapp config container set \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --container-image-name "$IMAGE" \ + --container-registry-url "https://$REGISTRY.azurecr.io" + fi - # Deploy to Azure Container Instances - echo "Deploying to ACI: $APP_NAME" - az container create \ + # Configure app settings (runs on every deploy) + az webapp config appsettings set \ --name "$APP_NAME" \ --resource-group "$RESOURCE_GROUP" \ - --image "$IMAGE" \ - --registry-login-server "$REGISTRY.azurecr.io" \ - --registry-username "$ACR_USERNAME" \ - --registry-password "$ACR_PASSWORD" \ - --cpu 2 \ - --memory 4 \ - --ports 5174 \ - --ip-address Public \ - --os-type Linux \ - --environment-variables \ - DOTNET_ENVIRONMENT=Production \ - TMPDIR=/tmp \ - COMPlus_EnableDiagnostics=0 \ - DOTNET_EnableDiagnostics=0 \ - --restart-policy Always - - FQDN=$(az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query ipAddress.fqdn -o tsv) - IP=$(az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query ipAddress.ip -o tsv) - echo "Deployed successfully at http://$IP:5174 (FQDN: $FQDN)" + --settings \ + WEBSITES_PORT=5174 \ + PLAYGROUND_URLS="https://typespec.io,https://tspwebsitepr.z22.web.core.windows.net,https://cadlplayground.z22.web.core.windows.net" + + FQDN="$APP_NAME.azurewebsites.net" + echo "Deployed successfully at https://$FQDN" workingDirectory: $(Build.SourcesDirectory) templateContext: From 1afdac16ea9a1bce26589821062d06d8dc32886b Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 14:23:54 -0700 Subject: [PATCH 058/103] fix: stop copying global.json into Docker build to avoid SDK version pin mismatch The sdk:10.0 image updated to 10.0.201 (2xx band) but global.json pins to 10.0.103 (1xx band). Default rollForward policy won't cross bands. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 4f092afed4b..4d919d21328 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /src # Build the generator (populates dist/generator/) COPY generator/ generator/ COPY eng/ eng/ -COPY global.json . +# Skip global.json — it pins an SDK version that may not match the container RUN dotnet build generator -c Release # Build the server From e68f8657acb3656eccf5cc571346de6e826b444e Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 16:21:24 -0700 Subject: [PATCH 059/103] fix: switch to ACI deployment with ACR admin credentials App Service workarounds exhausted (disabled diagnostics, server GC, ReadyToRun, TieredCompilation, upgraded to P1v3). Generator still segfaults. ACI has fewer sandbox restrictions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 90 ++++++++----------- 1 file changed, 35 insertions(+), 55 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 64eeeb8333e..73156dc5f70 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -402,7 +402,6 @@ stages: set -e REGISTRY="typespecacr" RESOURCE_GROUP="typespec" - PLAN_NAME="typespec-playground-plan" APP_NAME="${{ parameters.PlaygroundServerAppName }}" IMAGE="$REGISTRY.azurecr.io/$APP_NAME:$(Build.BuildId)" @@ -421,65 +420,46 @@ stages: --file "$CONTEXT/${{ parameters.PlaygroundServerDockerfile }}" \ "$CONTEXT" - # Create App Service plan if it doesn't exist - if ! az appservice plan show --name "$PLAN_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then - echo "Creating App Service plan: $PLAN_NAME" - az appservice plan create \ - --name "$PLAN_NAME" \ - --resource-group "$RESOURCE_GROUP" \ - --is-linux \ - --sku P1v3 - fi + # Deploy to Azure Container Instances + echo "Deploying to ACI: $APP_NAME" + + # Enable ACR admin to get credentials for ACI pull + az acr update --name "$REGISTRY" --admin-enabled true + ACR_USERNAME=$(az acr credential show --name "$REGISTRY" --query username -o tsv) + ACR_PASSWORD=$(az acr credential show --name "$REGISTRY" --query "passwords[0].value" -o tsv) - # Ensure managed identity and ACR pull access are configured - echo "Configuring managed identity..." - az webapp identity assign --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" - PRINCIPAL_ID=$(az webapp identity show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query principalId -o tsv) - ACR_ID=$(az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --query id -o tsv) - echo "Principal ID: $PRINCIPAL_ID" - echo "ACR ID: $ACR_ID" - az role assignment create --assignee "$PRINCIPAL_ID" --role AcrPull --scope "$ACR_ID" || echo "Role assignment already exists" - az webapp config set --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" \ - --generic-configurations '{"acrUseManagedIdentityCreds": true}' - - # Wait for role assignment to propagate - echo "Waiting 30s for role assignment propagation..." - sleep 30 - - # Create or update Web App - if az webapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then - echo "Updating Web App: $APP_NAME" - az webapp config container set \ - --name "$APP_NAME" \ - --resource-group "$RESOURCE_GROUP" \ - --container-image-name "$IMAGE" \ - --container-registry-url "https://$REGISTRY.azurecr.io" - else - echo "Creating Web App: $APP_NAME" - az webapp create \ - --name "$APP_NAME" \ - --resource-group "$RESOURCE_GROUP" \ - --plan "$PLAN_NAME" \ - --container-image-name "mcr.microsoft.com/dotnet/aspnet:10.0" - - # Set the actual image after identity is ready - az webapp config container set \ - --name "$APP_NAME" \ - --resource-group "$RESOURCE_GROUP" \ - --container-image-name "$IMAGE" \ - --container-registry-url "https://$REGISTRY.azurecr.io" + # Delete existing container if present (ACI doesn't support in-place update) + if az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then + echo "Deleting existing container..." + az container delete --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --yes fi - # Configure app settings (runs on every deploy) - az webapp config appsettings set \ + az container create \ --name "$APP_NAME" \ --resource-group "$RESOURCE_GROUP" \ - --settings \ - WEBSITES_PORT=5174 \ - PLAYGROUND_URLS="https://typespec.io,https://tspwebsitepr.z22.web.core.windows.net,https://cadlplayground.z22.web.core.windows.net" - - FQDN="$APP_NAME.azurewebsites.net" - echo "Deployed successfully at https://$FQDN" + --image "$IMAGE" \ + --registry-login-server "$REGISTRY.azurecr.io" \ + --registry-username "$ACR_USERNAME" \ + --registry-password "$ACR_PASSWORD" \ + --cpu 2 \ + --memory 4 \ + --ports 5174 \ + --ip-address Public \ + --dns-name-label "$APP_NAME" \ + --os-type Linux \ + --environment-variables \ + DOTNET_ENVIRONMENT=Production \ + TMPDIR=/tmp \ + COMPlus_EnableDiagnostics=0 \ + DOTNET_EnableDiagnostics=0 \ + PLAYGROUND_URLS="https://typespec.io,https://tspwebsitepr.z22.web.core.windows.net,https://cadlplayground.z22.web.core.windows.net" \ + --restart-policy Always + + FQDN=$(az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query ipAddress.fqdn -o tsv) + IP=$(az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query ipAddress.ip -o tsv) + echo "Deployed successfully" + echo " FQDN: http://$FQDN:5174" + echo " IP: http://$IP:5174" workingDirectory: $(Build.SourcesDirectory) templateContext: From 852782761480d846838595ed5b28c92285ecd258 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 16:23:43 -0700 Subject: [PATCH 060/103] fix: add Azure Front Door for HTTPS termination in front of ACI ACI only provides HTTP. Front Door provides managed HTTPS with Microsoft certificate. Created idempotently - only on first deploy, then updates origin on subsequent deploys. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 73156dc5f70..ccdbb765d3b 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -457,9 +457,66 @@ stages: FQDN=$(az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query ipAddress.fqdn -o tsv) IP=$(az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query ipAddress.ip -o tsv) - echo "Deployed successfully" - echo " FQDN: http://$FQDN:5174" - echo " IP: http://$IP:5174" + echo "ACI deployed: http://$FQDN:5174" + + # Set up Azure Front Door for HTTPS termination (idempotent) + FD_PROFILE="$APP_NAME-fd" + FD_ENDPOINT="$APP_NAME" + if ! az afd profile show --profile-name "$FD_PROFILE" --resource-group "$RESOURCE_GROUP" &>/dev/null; then + echo "Creating Front Door profile: $FD_PROFILE" + az afd profile create \ + --profile-name "$FD_PROFILE" \ + --resource-group "$RESOURCE_GROUP" \ + --sku Standard_AzureFrontDoor + + az afd endpoint create \ + --profile-name "$FD_PROFILE" \ + --endpoint-name "$FD_ENDPOINT" \ + --resource-group "$RESOURCE_GROUP" + + az afd origin-group create \ + --profile-name "$FD_PROFILE" \ + --origin-group-name "$APP_NAME-og" \ + --resource-group "$RESOURCE_GROUP" \ + --probe-request-type GET \ + --probe-protocol Http \ + --probe-path "/health" + + az afd origin create \ + --profile-name "$FD_PROFILE" \ + --origin-group-name "$APP_NAME-og" \ + --origin-name "$APP_NAME-origin" \ + --resource-group "$RESOURCE_GROUP" \ + --host-name "$FQDN" \ + --origin-host-header "$FQDN" \ + --http-port 5174 \ + --priority 1 \ + --weight 1000 \ + --enabled-state Enabled + + az afd route create \ + --profile-name "$FD_PROFILE" \ + --endpoint-name "$FD_ENDPOINT" \ + --origin-group "$APP_NAME-og" \ + --route-name "$APP_NAME-route" \ + --resource-group "$RESOURCE_GROUP" \ + --forwarding-protocol HttpOnly \ + --https-redirect Enabled \ + --supported-protocols Https Http + else + # Update origin host if ACI FQDN changed + az afd origin update \ + --profile-name "$FD_PROFILE" \ + --origin-group-name "$APP_NAME-og" \ + --origin-name "$APP_NAME-origin" \ + --resource-group "$RESOURCE_GROUP" \ + --host-name "$FQDN" \ + --origin-host-header "$FQDN" \ + --http-port 5174 + fi + + FD_HOSTNAME=$(az afd endpoint show --profile-name "$FD_PROFILE" --endpoint-name "$FD_ENDPOINT" --resource-group "$RESOURCE_GROUP" --query hostName -o tsv) + echo "HTTPS endpoint: https://$FD_HOSTNAME" workingDirectory: $(Build.SourcesDirectory) templateContext: From 906295eb4f3d33b2d00ce71deaf9ecee6414a451 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 16:58:04 -0700 Subject: [PATCH 061/103] fix: use user-assigned managed identity for ACR pull (admin blocked by policy) Org policy disallows ACR admin accounts. Create a user-assigned managed identity, assign AcrPull role, and reference it in az container create. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index ccdbb765d3b..54e61f6da11 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -423,10 +423,25 @@ stages: # Deploy to Azure Container Instances echo "Deploying to ACI: $APP_NAME" - # Enable ACR admin to get credentials for ACI pull - az acr update --name "$REGISTRY" --admin-enabled true - ACR_USERNAME=$(az acr credential show --name "$REGISTRY" --query username -o tsv) - ACR_PASSWORD=$(az acr credential show --name "$REGISTRY" --query "passwords[0].value" -o tsv) + # Create user-assigned managed identity for ACR pull (avoids admin account) + IDENTITY_NAME="$APP_NAME-identity" + if ! az identity show --name "$IDENTITY_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then + echo "Creating managed identity: $IDENTITY_NAME" + az identity create --name "$IDENTITY_NAME" --resource-group "$RESOURCE_GROUP" + fi + IDENTITY_ID=$(az identity show --name "$IDENTITY_NAME" --resource-group "$RESOURCE_GROUP" --query id -o tsv) + IDENTITY_PRINCIPAL=$(az identity show --name "$IDENTITY_NAME" --resource-group "$RESOURCE_GROUP" --query principalId -o tsv) + ACR_ID=$(az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --query id -o tsv) + + # Assign AcrPull role (idempotent) + az role assignment create \ + --assignee "$IDENTITY_PRINCIPAL" \ + --role AcrPull \ + --scope "$ACR_ID" 2>/dev/null || echo "AcrPull role already assigned" + + # Wait for role propagation on first create + echo "Waiting 30s for role assignment propagation..." + sleep 30 # Delete existing container if present (ACI doesn't support in-place update) if az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then @@ -439,8 +454,8 @@ stages: --resource-group "$RESOURCE_GROUP" \ --image "$IMAGE" \ --registry-login-server "$REGISTRY.azurecr.io" \ - --registry-username "$ACR_USERNAME" \ - --registry-password "$ACR_PASSWORD" \ + --acr-identity "$IDENTITY_ID" \ + --assign-identity "$IDENTITY_ID" \ --cpu 2 \ --memory 4 \ --ports 5174 \ From bcdf04319f90d1c6d00f4559cbfebe50d5f2eb31 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 17:46:24 -0700 Subject: [PATCH 062/103] fix: use assignee-object-id for role assignment, increase propagation wait to 90s Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../pipelines/templates/stages/emitter-stages.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 54e61f6da11..3c921f62e8b 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -433,15 +433,17 @@ stages: IDENTITY_PRINCIPAL=$(az identity show --name "$IDENTITY_NAME" --resource-group "$RESOURCE_GROUP" --query principalId -o tsv) ACR_ID=$(az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --query id -o tsv) - # Assign AcrPull role (idempotent) + # Assign AcrPull role (idempotent - will succeed even if already exists) + echo "Assigning AcrPull role to identity..." az role assignment create \ - --assignee "$IDENTITY_PRINCIPAL" \ + --assignee-object-id "$IDENTITY_PRINCIPAL" \ + --assignee-principal-type ServicePrincipal \ --role AcrPull \ - --scope "$ACR_ID" 2>/dev/null || echo "AcrPull role already assigned" + --scope "$ACR_ID" || true - # Wait for role propagation on first create - echo "Waiting 30s for role assignment propagation..." - sleep 30 + # Wait for AAD role propagation (can take up to 2 minutes) + echo "Waiting 90s for role assignment propagation..." + sleep 90 # Delete existing container if present (ACI doesn't support in-place update) if az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then From 64e1e4938745d84b63dfbb227aa3fef0b06e8e41 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 18:04:31 -0700 Subject: [PATCH 063/103] fix: remove role assignment from pipeline (requires manual one-time setup) Pipeline SP lacks Microsoft.Authorization/roleAssignments/write. The AcrPull role must be assigned manually once by an Owner. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 3c921f62e8b..d72e4bcf7dd 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -433,17 +433,15 @@ stages: IDENTITY_PRINCIPAL=$(az identity show --name "$IDENTITY_NAME" --resource-group "$RESOURCE_GROUP" --query principalId -o tsv) ACR_ID=$(az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --query id -o tsv) - # Assign AcrPull role (idempotent - will succeed even if already exists) - echo "Assigning AcrPull role to identity..." - az role assignment create \ - --assignee-object-id "$IDENTITY_PRINCIPAL" \ - --assignee-principal-type ServicePrincipal \ - --role AcrPull \ - --scope "$ACR_ID" || true - - # Wait for AAD role propagation (can take up to 2 minutes) - echo "Waiting 90s for role assignment propagation..." - sleep 90 + # NOTE: AcrPull role must be assigned manually once by an Owner: + # az role assignment create \ + # --assignee-object-id \ + # --assignee-principal-type ServicePrincipal \ + # --role AcrPull \ + # --scope + echo "Identity: $IDENTITY_ID" + echo "Identity Principal: $IDENTITY_PRINCIPAL" + echo "ACR ID: $ACR_ID" # Delete existing container if present (ACI doesn't support in-place update) if az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then From d52f63050a4a6cbe4a580527b90796292fbcd11b Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 18:44:10 -0700 Subject: [PATCH 064/103] fix: add probe-interval-in-seconds to Front Door origin group Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index d72e4bcf7dd..e67cf991708 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -495,7 +495,8 @@ stages: --resource-group "$RESOURCE_GROUP" \ --probe-request-type GET \ --probe-protocol Http \ - --probe-path "/health" + --probe-path "/health" \ + --probe-interval-in-seconds 30 az afd origin create \ --profile-name "$FD_PROFILE" \ From 8f741d5001293cb4772c27af5008c27d09f1a28b Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 18:44:40 -0700 Subject: [PATCH 065/103] fix: update playground server URL to Front Door endpoint Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/src/components/playground-component/playground.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/components/playground-component/playground.tsx b/website/src/components/playground-component/playground.tsx index e69fce27198..c073f75ad69 100644 --- a/website/src/components/playground-component/playground.tsx +++ b/website/src/components/playground-component/playground.tsx @@ -19,7 +19,7 @@ import "@typespec/playground/styles.css"; // Configure the playground server URL for the C# emitter's browser stub. // This must be set before the emitter runs. (globalThis as any).__TYPESPEC_PLAYGROUND_SERVER_URL__ = - "https://csharp-playground-server.azurewebsites.net"; + "https://csharp-playground-server-hnhfbfcdc6fnc4gh.b02.azurefd.net"; export interface WebsitePlaygroundProps { versionData: VersionData; From 263b3f72cf1f64b890957b4909d8dd0a711103e1 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 19:11:06 -0700 Subject: [PATCH 066/103] fix: add load balancing settings to Front Door origin group Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/emitters/pipelines/templates/stages/emitter-stages.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index e67cf991708..275692f4845 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -496,7 +496,9 @@ stages: --probe-request-type GET \ --probe-protocol Http \ --probe-path "/health" \ - --probe-interval-in-seconds 30 + --probe-interval-in-seconds 30 \ + --sample-size 4 \ + --successful-samples-required 3 az afd origin create \ --profile-name "$FD_PROFILE" \ From 45ce88ed89622f6c47a591d24483f4c2d2b8e963 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 19:16:29 -0700 Subject: [PATCH 067/103] fix: complete Front Door config with all required properties from docs Added: additional-latency-in-milliseconds, https-port, enforce-certificate-name-check, link-to-default-domain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../pipelines/templates/stages/emitter-stages.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 275692f4845..2380aae4ca6 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -498,7 +498,8 @@ stages: --probe-path "/health" \ --probe-interval-in-seconds 30 \ --sample-size 4 \ - --successful-samples-required 3 + --successful-samples-required 3 \ + --additional-latency-in-milliseconds 50 az afd origin create \ --profile-name "$FD_PROFILE" \ @@ -508,9 +509,11 @@ stages: --host-name "$FQDN" \ --origin-host-header "$FQDN" \ --http-port 5174 \ + --https-port 443 \ --priority 1 \ --weight 1000 \ - --enabled-state Enabled + --enabled-state Enabled \ + --enforce-certificate-name-check false az afd route create \ --profile-name "$FD_PROFILE" \ @@ -520,7 +523,8 @@ stages: --resource-group "$RESOURCE_GROUP" \ --forwarding-protocol HttpOnly \ --https-redirect Enabled \ - --supported-protocols Https Http + --supported-protocols Http Https \ + --link-to-default-domain Enabled else # Update origin host if ACI FQDN changed az afd origin update \ From 3bc0338cc73dc60089128aafb1a1edbf7301b485 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 19:30:51 -0700 Subject: [PATCH 068/103] fix: enable core dump collection and show dump files in /health endpoint Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../http-client-csharp/playground-server/Program.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 4af45ef5cd7..dfd4b33eada 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -79,6 +79,11 @@ } catch (Exception ex) { dotnetVersion = ex.Message; } + // Check for core dumps + var dumpFiles = Directory.Exists("/tmp") + ? Directory.GetFiles("/tmp", "coredump.*").Select(Path.GetFileName).ToArray() + : Array.Empty(); + return Results.Ok(new { status = "ok", @@ -87,7 +92,8 @@ dotnetVersion, runtime = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription, os = System.Runtime.InteropServices.RuntimeInformation.OSDescription, - arch = System.Runtime.InteropServices.RuntimeInformation.OSArchitecture.ToString() + arch = System.Runtime.InteropServices.RuntimeInformation.OSArchitecture.ToString(), + coreDumps = dumpFiles }); }); @@ -140,6 +146,10 @@ psi.Environment["COMPlus_EnableDiagnostics"] = "0"; psi.Environment["DOTNET_ReadyToRun"] = "0"; // Disable R2R, force JIT psi.Environment["DOTNET_TieredCompilation"] = "0"; // Disable tiered compilation + // Collect mini dump on crash for diagnostics + psi.Environment["DOTNET_DbgEnableMiniDump"] = "1"; + psi.Environment["DOTNET_DbgMiniDumpType"] = "1"; // Mini dump + psi.Environment["DOTNET_DbgMiniDumpName"] = "/tmp/coredump.%p"; using var process = Process.Start(psi)!; From 3e0160ba3595a8bb358306b0c890cad68ff617c0 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 19:39:35 -0700 Subject: [PATCH 069/103] fix: revert to App Service deployment with core dump collection ACI deployment blocked by policy/auth issues. App Service has HTTPS built-in. Core dumps enabled in Program.cs for crash diagnostics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 153 +++++------------- .../playground-component/playground.tsx | 2 +- 2 files changed, 43 insertions(+), 112 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 2380aae4ca6..803ebda7291 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -420,125 +420,56 @@ stages: --file "$CONTEXT/${{ parameters.PlaygroundServerDockerfile }}" \ "$CONTEXT" - # Deploy to Azure Container Instances - echo "Deploying to ACI: $APP_NAME" - - # Create user-assigned managed identity for ACR pull (avoids admin account) - IDENTITY_NAME="$APP_NAME-identity" - if ! az identity show --name "$IDENTITY_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then - echo "Creating managed identity: $IDENTITY_NAME" - az identity create --name "$IDENTITY_NAME" --resource-group "$RESOURCE_GROUP" + # Deploy to Azure App Service + PLAN_NAME="typespec-playground-plan" + + # Create App Service plan if it doesn't exist + if ! az appservice plan show --name "$PLAN_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then + echo "Creating App Service plan: $PLAN_NAME" + az appservice plan create \ + --name "$PLAN_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --is-linux \ + --sku P1v3 fi - IDENTITY_ID=$(az identity show --name "$IDENTITY_NAME" --resource-group "$RESOURCE_GROUP" --query id -o tsv) - IDENTITY_PRINCIPAL=$(az identity show --name "$IDENTITY_NAME" --resource-group "$RESOURCE_GROUP" --query principalId -o tsv) - ACR_ID=$(az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --query id -o tsv) - # NOTE: AcrPull role must be assigned manually once by an Owner: - # az role assignment create \ - # --assignee-object-id \ - # --assignee-principal-type ServicePrincipal \ - # --role AcrPull \ - # --scope - echo "Identity: $IDENTITY_ID" - echo "Identity Principal: $IDENTITY_PRINCIPAL" - echo "ACR ID: $ACR_ID" - - # Delete existing container if present (ACI doesn't support in-place update) - if az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then - echo "Deleting existing container..." - az container delete --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --yes + # Create or update Web App + if az webapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then + echo "Updating Web App: $APP_NAME" + else + echo "Creating Web App: $APP_NAME" + az webapp create \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --plan "$PLAN_NAME" \ + --container-image-name "$IMAGE" \ + --container-registry-url "https://$REGISTRY.azurecr.io" fi - az container create \ + # Ensure managed identity and ACR pull access + az webapp identity assign --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" 2>/dev/null || true + PRINCIPAL_ID=$(az webapp identity show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query principalId -o tsv) + ACR_ID=$(az acr show --name "$REGISTRY" --resource-group "$RESOURCE_GROUP" --query id -o tsv) + az role assignment create --assignee-object-id "$PRINCIPAL_ID" --assignee-principal-type ServicePrincipal --role AcrPull --scope "$ACR_ID" 2>/dev/null || true + az webapp config set --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" \ + --generic-configurations '{"acrUseManagedIdentityCreds": true}' + + # Update container image + az webapp config container set \ --name "$APP_NAME" \ --resource-group "$RESOURCE_GROUP" \ - --image "$IMAGE" \ - --registry-login-server "$REGISTRY.azurecr.io" \ - --acr-identity "$IDENTITY_ID" \ - --assign-identity "$IDENTITY_ID" \ - --cpu 2 \ - --memory 4 \ - --ports 5174 \ - --ip-address Public \ - --dns-name-label "$APP_NAME" \ - --os-type Linux \ - --environment-variables \ - DOTNET_ENVIRONMENT=Production \ - TMPDIR=/tmp \ - COMPlus_EnableDiagnostics=0 \ - DOTNET_EnableDiagnostics=0 \ - PLAYGROUND_URLS="https://typespec.io,https://tspwebsitepr.z22.web.core.windows.net,https://cadlplayground.z22.web.core.windows.net" \ - --restart-policy Always - - FQDN=$(az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query ipAddress.fqdn -o tsv) - IP=$(az container show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" --query ipAddress.ip -o tsv) - echo "ACI deployed: http://$FQDN:5174" - - # Set up Azure Front Door for HTTPS termination (idempotent) - FD_PROFILE="$APP_NAME-fd" - FD_ENDPOINT="$APP_NAME" - if ! az afd profile show --profile-name "$FD_PROFILE" --resource-group "$RESOURCE_GROUP" &>/dev/null; then - echo "Creating Front Door profile: $FD_PROFILE" - az afd profile create \ - --profile-name "$FD_PROFILE" \ - --resource-group "$RESOURCE_GROUP" \ - --sku Standard_AzureFrontDoor - - az afd endpoint create \ - --profile-name "$FD_PROFILE" \ - --endpoint-name "$FD_ENDPOINT" \ - --resource-group "$RESOURCE_GROUP" + --container-image-name "$IMAGE" \ + --container-registry-url "https://$REGISTRY.azurecr.io" - az afd origin-group create \ - --profile-name "$FD_PROFILE" \ - --origin-group-name "$APP_NAME-og" \ - --resource-group "$RESOURCE_GROUP" \ - --probe-request-type GET \ - --probe-protocol Http \ - --probe-path "/health" \ - --probe-interval-in-seconds 30 \ - --sample-size 4 \ - --successful-samples-required 3 \ - --additional-latency-in-milliseconds 50 - - az afd origin create \ - --profile-name "$FD_PROFILE" \ - --origin-group-name "$APP_NAME-og" \ - --origin-name "$APP_NAME-origin" \ - --resource-group "$RESOURCE_GROUP" \ - --host-name "$FQDN" \ - --origin-host-header "$FQDN" \ - --http-port 5174 \ - --https-port 443 \ - --priority 1 \ - --weight 1000 \ - --enabled-state Enabled \ - --enforce-certificate-name-check false - - az afd route create \ - --profile-name "$FD_PROFILE" \ - --endpoint-name "$FD_ENDPOINT" \ - --origin-group "$APP_NAME-og" \ - --route-name "$APP_NAME-route" \ - --resource-group "$RESOURCE_GROUP" \ - --forwarding-protocol HttpOnly \ - --https-redirect Enabled \ - --supported-protocols Http Https \ - --link-to-default-domain Enabled - else - # Update origin host if ACI FQDN changed - az afd origin update \ - --profile-name "$FD_PROFILE" \ - --origin-group-name "$APP_NAME-og" \ - --origin-name "$APP_NAME-origin" \ - --resource-group "$RESOURCE_GROUP" \ - --host-name "$FQDN" \ - --origin-host-header "$FQDN" \ - --http-port 5174 - fi + # Configure app settings + az webapp config appsettings set \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --settings \ + WEBSITES_PORT=5174 \ + PLAYGROUND_URLS="https://typespec.io,https://tspwebsitepr.z22.web.core.windows.net,https://cadlplayground.z22.web.core.windows.net" - FD_HOSTNAME=$(az afd endpoint show --profile-name "$FD_PROFILE" --endpoint-name "$FD_ENDPOINT" --resource-group "$RESOURCE_GROUP" --query hostName -o tsv) - echo "HTTPS endpoint: https://$FD_HOSTNAME" + echo "Deployed to https://$APP_NAME.azurewebsites.net" workingDirectory: $(Build.SourcesDirectory) templateContext: diff --git a/website/src/components/playground-component/playground.tsx b/website/src/components/playground-component/playground.tsx index c073f75ad69..e69fce27198 100644 --- a/website/src/components/playground-component/playground.tsx +++ b/website/src/components/playground-component/playground.tsx @@ -19,7 +19,7 @@ import "@typespec/playground/styles.css"; // Configure the playground server URL for the C# emitter's browser stub. // This must be set before the emitter runs. (globalThis as any).__TYPESPEC_PLAYGROUND_SERVER_URL__ = - "https://csharp-playground-server-hnhfbfcdc6fnc4gh.b02.azurefd.net"; + "https://csharp-playground-server.azurewebsites.net"; export interface WebsitePlaygroundProps { versionData: VersionData; From cb1bf583fb38d2165a2912bb8e2d86aff6833f9e Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 20:09:01 -0700 Subject: [PATCH 070/103] fix: return stderr in error response, improve core dump file search Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../playground-server/Program.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index dfd4b33eada..7ae463e527d 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -79,10 +79,15 @@ } catch (Exception ex) { dotnetVersion = ex.Message; } - // Check for core dumps - var dumpFiles = Directory.Exists("/tmp") - ? Directory.GetFiles("/tmp", "coredump.*").Select(Path.GetFileName).ToArray() - : Array.Empty(); + // Check for core dumps in /tmp and subdirectories + var dumpFiles = new List(); + if (Directory.Exists("/tmp")) + { + foreach (var f in Directory.GetFiles("/tmp", "coredump*", SearchOption.TopDirectoryOnly)) + dumpFiles.Add(f); + foreach (var f in Directory.GetFiles("/tmp", "*.dmp", SearchOption.AllDirectories)) + dumpFiles.Add(f); + } return Results.Ok(new { @@ -154,6 +159,7 @@ using var process = Process.Start(psi)!; // Stream stdout/stderr to console for logging + var stderrLines = new List(); var stdoutTask = Task.Run(async () => { string? line; @@ -168,6 +174,7 @@ while ((line = await process.StandardError.ReadLineAsync()) != null) { Console.Error.WriteLine($"[generator stderr] {line}"); + stderrLines.Add(line); } }); @@ -180,7 +187,7 @@ if (exitCode != 0) { return Results.Json( - new GenerateErrorResponse($"Generator failed with exit code {exitCode}", "See server logs for details"), + new GenerateErrorResponse($"Generator failed with exit code {exitCode}", string.Join("\n", stderrLines.TakeLast(50))), GenerateJsonContext.Default.GenerateErrorResponse, statusCode: 500); } From 97e0f38123788367f18f219e88188d02a22e27d6 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 20:12:16 -0700 Subject: [PATCH 071/103] fix: add /coredump/{filename} endpoint to download crash dumps Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Program.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 7ae463e527d..8d9cc75c0fe 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -102,6 +102,14 @@ }); }); +app.MapGet("/coredump/{filename}", (string filename) => +{ + var path = Path.Combine("/tmp", filename); + if (!File.Exists(path) || !filename.StartsWith("coredump")) + return Results.NotFound(); + return Results.File(path, "application/octet-stream", filename); +}); + app.MapPost("/generate", async (HttpRequest request) => { var body = await JsonSerializer.DeserializeAsync( From 3f49098ca339d3420134bef73c553c10deba84bd Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 20:13:22 -0700 Subject: [PATCH 072/103] fix: write core dumps to /home for Kudu access (Kudu can't see /tmp) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../http-client-csharp/playground-server/Program.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 8d9cc75c0fe..650a67f5afc 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -81,11 +81,9 @@ // Check for core dumps in /tmp and subdirectories var dumpFiles = new List(); - if (Directory.Exists("/tmp")) + if (Directory.Exists("/home")) { - foreach (var f in Directory.GetFiles("/tmp", "coredump*", SearchOption.TopDirectoryOnly)) - dumpFiles.Add(f); - foreach (var f in Directory.GetFiles("/tmp", "*.dmp", SearchOption.AllDirectories)) + foreach (var f in Directory.GetFiles("/home", "coredump*", SearchOption.TopDirectoryOnly)) dumpFiles.Add(f); } @@ -104,7 +102,7 @@ app.MapGet("/coredump/{filename}", (string filename) => { - var path = Path.Combine("/tmp", filename); + var path = Path.Combine("/home", filename); if (!File.Exists(path) || !filename.StartsWith("coredump")) return Results.NotFound(); return Results.File(path, "application/octet-stream", filename); @@ -162,7 +160,7 @@ // Collect mini dump on crash for diagnostics psi.Environment["DOTNET_DbgEnableMiniDump"] = "1"; psi.Environment["DOTNET_DbgMiniDumpType"] = "1"; // Mini dump - psi.Environment["DOTNET_DbgMiniDumpName"] = "/tmp/coredump.%p"; + psi.Environment["DOTNET_DbgMiniDumpName"] = "/home/coredump.%p"; using var process = Process.Start(psi)!; From f848623273400a8e23c6c4cbc31c5060476f925d Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 20:15:15 -0700 Subject: [PATCH 073/103] fix: add SSH and dotnet-dump to container for crash diagnostics Enables 'az webapp ssh' and dotnet-dump for analyzing core dumps directly in the container. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../playground-server/Dockerfile | 15 +++++++++++++-- .../playground-server/entrypoint.sh | 6 ++++++ .../playground-server/sshd_config | 12 ++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 packages/http-client-csharp/playground-server/entrypoint.sh create mode 100644 packages/http-client-csharp/playground-server/sshd_config diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 4d919d21328..e2dd5698b95 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -23,6 +23,15 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime WORKDIR /app COPY --from=build /app . +# Install SSH for App Service remote access and dotnet-dump for crash analysis +RUN apt-get update && apt-get install -y openssh-server \ + && echo "root:Docker!" | chpasswd \ + && mkdir -p /run/sshd \ + && dotnet tool install -g dotnet-dump \ + && apt-get clean && rm -rf /var/lib/apt/lists/* +COPY playground-server/sshd_config /etc/ssh/ +ENV PATH="$PATH:/root/.dotnet/tools" + ENV DOTNET_ENVIRONMENT=Production ENV GENERATOR_PATH=/app/generator/Microsoft.TypeSpec.Generator.dll ENV TMPDIR=/tmp @@ -30,5 +39,7 @@ ENV TMPDIR=/tmp ENV COMPlus_EnableDiagnostics=0 ENV DOTNET_EnableDiagnostics=0 -EXPOSE 5174 -ENTRYPOINT ["dotnet", "playground-server.dll"] +EXPOSE 5174 2222 +COPY playground-server/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/packages/http-client-csharp/playground-server/entrypoint.sh b/packages/http-client-csharp/playground-server/entrypoint.sh new file mode 100644 index 00000000000..2d0a6893bff --- /dev/null +++ b/packages/http-client-csharp/playground-server/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Start SSH daemon for App Service remote access +/usr/sbin/sshd + +# Start the playground server +exec dotnet /app/playground-server.dll diff --git a/packages/http-client-csharp/playground-server/sshd_config b/packages/http-client-csharp/playground-server/sshd_config new file mode 100644 index 00000000000..27e95ecca7c --- /dev/null +++ b/packages/http-client-csharp/playground-server/sshd_config @@ -0,0 +1,12 @@ +Port 2222 +ListenAddress 0.0.0.0 +LoginGraceTime 180 +X11Forwarding yes +Ciphers aes128-cbc,3des-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr +MACs hmac-sha1,hmac-sha1-96 +StrictModes yes +SyslogFacility DAEMON +PasswordAuthentication yes +PermitEmptyPasswords no +PermitRootLogin yes +Subsystem sftp internal-sftp From c0d76ca6ced06a7f5e26d1009b7f17fd60c68885 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 20:47:23 -0700 Subject: [PATCH 074/103] fix: add lldb and switch to full dump for native crash analysis Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 4 ++-- packages/http-client-csharp/playground-server/Program.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index e2dd5698b95..43493251028 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -23,8 +23,8 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime WORKDIR /app COPY --from=build /app . -# Install SSH for App Service remote access and dotnet-dump for crash analysis -RUN apt-get update && apt-get install -y openssh-server \ +# Install SSH, lldb, and dotnet-dump for crash analysis +RUN apt-get update && apt-get install -y openssh-server lldb \ && echo "root:Docker!" | chpasswd \ && mkdir -p /run/sshd \ && dotnet tool install -g dotnet-dump \ diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 650a67f5afc..65db14f1a63 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -159,7 +159,7 @@ psi.Environment["DOTNET_TieredCompilation"] = "0"; // Disable tiered compilation // Collect mini dump on crash for diagnostics psi.Environment["DOTNET_DbgEnableMiniDump"] = "1"; - psi.Environment["DOTNET_DbgMiniDumpType"] = "1"; // Mini dump + psi.Environment["DOTNET_DbgMiniDumpType"] = "4"; // Full dump for lldb analysis psi.Environment["DOTNET_DbgMiniDumpName"] = "/home/coredump.%p"; using var process = Process.Start(psi)!; From 8feeea2c80fef85315b51602a815f139a94ea157 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 20:51:22 -0700 Subject: [PATCH 075/103] fix: switch to .NET 9 (GA) to test if crash is .NET 10 preview bug Build generator with -p:TargetFramework=net9.0 override. Server also targets net9.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 9 ++++----- .../playground-server/playground-server.csproj | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 43493251028..035ff2475da 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -1,13 +1,12 @@ # Build from the http-client-csharp package root: # docker build -f playground-server/Dockerfile -t csharp-playground-server . -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src -# Build the generator (populates dist/generator/) +# Build the generator targeting net9.0 (instead of net10.0) for runtime stability COPY generator/ generator/ COPY eng/ eng/ -# Skip global.json — it pins an SDK version that may not match the container -RUN dotnet build generator -c Release +RUN dotnet build generator -c Release -p:TargetFramework=net9.0 # Build the server COPY playground-server/playground-server.csproj playground-server/ @@ -19,7 +18,7 @@ RUN dotnet publish playground-server -c Release -o /app RUN cp -r dist/generator /app/generator # Need full SDK (not just aspnet) because the server spawns `dotnet` to run the generator DLL -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS runtime WORKDIR /app COPY --from=build /app . diff --git a/packages/http-client-csharp/playground-server/playground-server.csproj b/packages/http-client-csharp/playground-server/playground-server.csproj index 8c5ce456c81..2bd8cf88875 100644 --- a/packages/http-client-csharp/playground-server/playground-server.csproj +++ b/packages/http-client-csharp/playground-server/playground-server.csproj @@ -1,7 +1,7 @@ - net10.0 + net9.0 enable enable PlaygroundServer From 266acd0153313836eae26123351f144d663e307b Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 10 Apr 2026 21:35:27 -0700 Subject: [PATCH 076/103] fix: revert to .NET 10 (net9.0 can't build net10.0 dependencies) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 8 ++++---- .../playground-server/playground-server.csproj | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 035ff2475da..43abda005c8 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -1,12 +1,12 @@ # Build from the http-client-csharp package root: # docker build -f playground-server/Dockerfile -t csharp-playground-server . -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src -# Build the generator targeting net9.0 (instead of net10.0) for runtime stability +# Build the generator (populates dist/generator/) COPY generator/ generator/ COPY eng/ eng/ -RUN dotnet build generator -c Release -p:TargetFramework=net9.0 +RUN dotnet build generator -c Release # Build the server COPY playground-server/playground-server.csproj playground-server/ @@ -18,7 +18,7 @@ RUN dotnet publish playground-server -c Release -o /app RUN cp -r dist/generator /app/generator # Need full SDK (not just aspnet) because the server spawns `dotnet` to run the generator DLL -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS runtime +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime WORKDIR /app COPY --from=build /app . diff --git a/packages/http-client-csharp/playground-server/playground-server.csproj b/packages/http-client-csharp/playground-server/playground-server.csproj index 2bd8cf88875..8c5ce456c81 100644 --- a/packages/http-client-csharp/playground-server/playground-server.csproj +++ b/packages/http-client-csharp/playground-server/playground-server.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable PlaygroundServer From 614cdcf808fbc47a63d72247328bb6e1e8cf9c2b Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 11:44:49 -0700 Subject: [PATCH 077/103] fix: add test-generator.sh script to container for manual testing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../playground-server/Dockerfile | 3 ++- .../playground-server/test-generator.sh | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 packages/http-client-csharp/playground-server/test-generator.sh diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 43abda005c8..246a7e297ff 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -40,5 +40,6 @@ ENV DOTNET_EnableDiagnostics=0 EXPOSE 5174 2222 COPY playground-server/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +COPY playground-server/test-generator.sh /app/test-generator.sh +RUN chmod +x /entrypoint.sh /app/test-generator.sh ENTRYPOINT ["/entrypoint.sh"] diff --git a/packages/http-client-csharp/playground-server/test-generator.sh b/packages/http-client-csharp/playground-server/test-generator.sh new file mode 100644 index 00000000000..d2f5311df32 --- /dev/null +++ b/packages/http-client-csharp/playground-server/test-generator.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Test the generator manually in the App Service container +# Usage: bash /app/test-generator.sh + +set -e + +DIR=/tmp/test-gen-$$ +mkdir -p "$DIR" + +echo '{}' > "$DIR/tspCodeModel.json" +echo '{}' > "$DIR/Configuration.json" + +echo "Running generator in $DIR..." +dotnet --roll-forward Major /app/generator/Microsoft.TypeSpec.Generator.dll "$DIR" -g ScmCodeModelGenerator --new-project +EXIT=$? + +echo "Exit code: $EXIT" +ls -la "$DIR/" + +# Cleanup +rm -rf "$DIR" From e6438bbe34d5a2a52c1db74201ae660a2ea7ff3f Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 11:46:34 -0700 Subject: [PATCH 078/103] fix: keep temp dir on generator failure for manual SSH debugging Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../http-client-csharp/playground-server/Program.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 65db14f1a63..9c845fc90dd 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -222,7 +222,15 @@ finally { // Clean up temp directory - try { Directory.Delete(tempDir, recursive: true); } catch { } + // Keep temp dir on failure for manual debugging via SSH + if (exitCode == 0) + { + try { Directory.Delete(tempDir, recursive: true); } catch { } + } + else + { + Console.WriteLine($"Keeping temp dir for debugging: {tempDir}"); + } } }).RequireRateLimiting("generate"); From 1638fa53fe324748ecb6eaa81ba06b5df525b6fd Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 11:47:52 -0700 Subject: [PATCH 079/103] fix: bundle Spector routes test data and test script in container Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../playground-server/Dockerfile | 4 ++++ .../playground-server/test-generator.sh | 18 ++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 246a7e297ff..ea3d672d068 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -15,7 +15,11 @@ COPY playground-server/ playground-server/ RUN dotnet publish playground-server -c Release -o /app # Copy generator output +# Copy a test code model for manual debugging RUN cp -r dist/generator /app/generator +RUN mkdir -p /app/test-data && \ + cp generator/TestProjects/Spector/http/routes/tspCodeModel.json /app/test-data/ && \ + cp generator/TestProjects/Spector/http/routes/Configuration.json /app/test-data/ # Need full SDK (not just aspnet) because the server spawns `dotnet` to run the generator DLL FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime diff --git a/packages/http-client-csharp/playground-server/test-generator.sh b/packages/http-client-csharp/playground-server/test-generator.sh index d2f5311df32..878d221fe36 100644 --- a/packages/http-client-csharp/playground-server/test-generator.sh +++ b/packages/http-client-csharp/playground-server/test-generator.sh @@ -2,20 +2,22 @@ # Test the generator manually in the App Service container # Usage: bash /app/test-generator.sh -set -e - DIR=/tmp/test-gen-$$ mkdir -p "$DIR" -echo '{}' > "$DIR/tspCodeModel.json" -echo '{}' > "$DIR/Configuration.json" +# Copy test data from bundled Spector routes test +cp /app/test-data/tspCodeModel.json "$DIR/" +cp /app/test-data/Configuration.json "$DIR/" echo "Running generator in $DIR..." dotnet --roll-forward Major /app/generator/Microsoft.TypeSpec.Generator.dll "$DIR" -g ScmCodeModelGenerator --new-project EXIT=$? echo "Exit code: $EXIT" -ls -la "$DIR/" - -# Cleanup -rm -rf "$DIR" +if [ $EXIT -eq 0 ]; then + echo "Generated files:" + find "$DIR" -type f | head -20 + rm -rf "$DIR" +else + echo "FAILED - temp dir preserved at $DIR" +fi From 9ef3fbeeffe32b527c1908f8c0c92c8e6afd31c9 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 13:02:46 -0700 Subject: [PATCH 080/103] fix: move exitCode declaration before try block to fix scope error Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Program.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 9c845fc90dd..05892a4df1b 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -128,6 +128,8 @@ var generatedDir = Path.Combine(tempDir, "src", "Generated"); Directory.CreateDirectory(generatedDir); + var exitCode = -1; + try { // Write the input files the generator expects @@ -187,7 +189,7 @@ await process.WaitForExitAsync(); await Task.WhenAll(stdoutTask, stderrTask); - var exitCode = process.ExitCode; + exitCode = process.ExitCode; Console.WriteLine($"Generator exited with code {exitCode}"); if (exitCode != 0) From b96c44acc8522b8c087d60683ed9bc77ce0691c1 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 13:23:26 -0700 Subject: [PATCH 081/103] fix: run generator in-process instead of subprocess to avoid SIGSEGV Load generator assembly and invoke entry point directly via reflection instead of spawning a dotnet subprocess. The SIGSEGV appears to be triggered by process forking in the App Service container sandbox. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../playground-server/Program.cs | 84 +++++++++---------- 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 05892a4df1b..8f7a96ff99b 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -138,64 +138,62 @@ var generatorName = body.GeneratorName ?? "ScmCodeModelGenerator"; - // Run the .NET generator as a subprocess (same approach as the TypeSpec emitter) - Console.WriteLine($"Starting generator: dotnet --roll-forward Major {generatorPath} {tempDir} -g {generatorName} --new-project"); + // Run the generator in-process by loading the assembly and invoking its entry point + Console.WriteLine($"Starting generator in-process: {generatorPath}"); Console.WriteLine($"Code model size: {body.CodeModel!.Length} chars"); Console.WriteLine($"Configuration: {body.Configuration}"); + Console.WriteLine($"Temp dir: {tempDir}"); - var psi = new ProcessStartInfo + var args = new[] { tempDir, "-g", generatorName, "--new-project" }; + + var assembly = System.Reflection.Assembly.LoadFrom(generatorPath); + var entryPoint = assembly.EntryPoint; + if (entryPoint == null) { - FileName = "dotnet", - ArgumentList = { "--roll-forward", "Major", generatorPath, tempDir, "-g", generatorName, "--new-project" }, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - // Set env vars to prevent segfaults in container environment - psi.Environment["DOTNET_DefaultStackSize"] = "0x200000"; // 2MB stack - psi.Environment["COMPlus_DefaultStackSize"] = "200000"; - psi.Environment["DOTNET_gcServer"] = "0"; // Disable server GC - psi.Environment["COMPlus_EnableDiagnostics"] = "0"; - psi.Environment["DOTNET_ReadyToRun"] = "0"; // Disable R2R, force JIT - psi.Environment["DOTNET_TieredCompilation"] = "0"; // Disable tiered compilation - // Collect mini dump on crash for diagnostics - psi.Environment["DOTNET_DbgEnableMiniDump"] = "1"; - psi.Environment["DOTNET_DbgMiniDumpType"] = "4"; // Full dump for lldb analysis - psi.Environment["DOTNET_DbgMiniDumpName"] = "/home/coredump.%p"; - - using var process = Process.Start(psi)!; - - // Stream stdout/stderr to console for logging - var stderrLines = new List(); - var stdoutTask = Task.Run(async () => + return Results.Json( + new GenerateErrorResponse("Generator assembly has no entry point", ""), + GenerateJsonContext.Default.GenerateErrorResponse, + statusCode: 500); + } + + try { - string? line; - while ((line = await process.StandardOutput.ReadLineAsync()) != null) + var result = entryPoint.Invoke(null, new object[] { args }); + if (result is Task taskInt) { - Console.WriteLine($"[generator stdout] {line}"); + exitCode = await taskInt; } - }); - var stderrTask = Task.Run(async () => - { - string? line; - while ((line = await process.StandardError.ReadLineAsync()) != null) + else if (result is Task task) { - Console.Error.WriteLine($"[generator stderr] {line}"); - stderrLines.Add(line); + await task; + exitCode = 0; } - }); - - await process.WaitForExitAsync(); - await Task.WhenAll(stdoutTask, stderrTask); + else if (result is int intResult) + { + exitCode = intResult; + } + else + { + exitCode = 0; + } + } + catch (Exception ex) + { + var inner = ex.InnerException ?? ex; + Console.Error.WriteLine($"[generator error] {inner.Message}"); + Console.Error.WriteLine(inner.StackTrace); + return Results.Json( + new GenerateErrorResponse($"Generator threw exception: {inner.Message}", inner.StackTrace ?? ""), + GenerateJsonContext.Default.GenerateErrorResponse, + statusCode: 500); + } - exitCode = process.ExitCode; Console.WriteLine($"Generator exited with code {exitCode}"); if (exitCode != 0) { return Results.Json( - new GenerateErrorResponse($"Generator failed with exit code {exitCode}", string.Join("\n", stderrLines.TakeLast(50))), + new GenerateErrorResponse($"Generator failed with exit code {exitCode}", ""), GenerateJsonContext.Default.GenerateErrorResponse, statusCode: 500); } From 48bdd1234250520e8a168405e88c079201f9438d Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 13:43:01 -0700 Subject: [PATCH 082/103] fix: disable file locking for Roslyn mmap compatibility in App Service Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index ea3d672d068..219f75bc8ab 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -41,6 +41,8 @@ ENV TMPDIR=/tmp # Disable .NET diagnostics to prevent segfaults in containers ENV COMPlus_EnableDiagnostics=0 ENV DOTNET_EnableDiagnostics=0 +# Disable memory-mapped file locking (Roslyn uses mmap for assembly loading) +ENV DOTNET_SYSTEM_IO_DISABLEFILELOCKING=1 EXPOSE 5174 2222 COPY playground-server/entrypoint.sh /entrypoint.sh From 8ea10492bbbf2386a1c3c9f8e65a361dca5f0b04 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 13:49:01 -0700 Subject: [PATCH 083/103] fix: disable W^X enforcement and use /home for temp files App Service sandbox may enforce W^X on JIT pages, causing SIGSEGV when Roslyn's compiled code is executed. DOTNET_EnableWriteXorExecute=0 disables this protection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 8 +++++--- .../http-client-csharp/playground-server/entrypoint.sh | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 219f75bc8ab..52a2ad676e0 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -37,12 +37,14 @@ ENV PATH="$PATH:/root/.dotnet/tools" ENV DOTNET_ENVIRONMENT=Production ENV GENERATOR_PATH=/app/generator/Microsoft.TypeSpec.Generator.dll -ENV TMPDIR=/tmp -# Disable .NET diagnostics to prevent segfaults in containers +ENV TMPDIR=/home/tmp +ENV DOTNET_RUNNING_IN_CONTAINER=true +# Disable .NET diagnostics ENV COMPlus_EnableDiagnostics=0 ENV DOTNET_EnableDiagnostics=0 -# Disable memory-mapped file locking (Roslyn uses mmap for assembly loading) ENV DOTNET_SYSTEM_IO_DISABLEFILELOCKING=1 +# Use the interpreter instead of JIT (avoids potential JIT SIGSEGV) +ENV DOTNET_EnableWriteXorExecute=0 EXPOSE 5174 2222 COPY playground-server/entrypoint.sh /entrypoint.sh diff --git a/packages/http-client-csharp/playground-server/entrypoint.sh b/packages/http-client-csharp/playground-server/entrypoint.sh index 2d0a6893bff..713a548809e 100644 --- a/packages/http-client-csharp/playground-server/entrypoint.sh +++ b/packages/http-client-csharp/playground-server/entrypoint.sh @@ -2,5 +2,8 @@ # Start SSH daemon for App Service remote access /usr/sbin/sshd +# Create temp directory on /home (persistent, no mmap restrictions) +mkdir -p /home/tmp + # Start the playground server exec dotnet /app/playground-server.dll From 4eb58e85836e311af604b8c8e504e49f99b7b237 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 13:59:06 -0700 Subject: [PATCH 084/103] fix: revert to subprocess approach (in-process breaks MEF plugin discovery) The generator uses MEF DirectoryCatalog(AppContext.BaseDirectory) to find ScmCodeModelGenerator. In-process, AppContext.BaseDirectory points to the server dir, not the generator dir. Reverting to subprocess but with W^X and mmap fixes in the Dockerfile. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../playground-server/Program.cs | 72 ++++++++----------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 8f7a96ff99b..df50a1af034 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -138,62 +138,52 @@ var generatorName = body.GeneratorName ?? "ScmCodeModelGenerator"; - // Run the generator in-process by loading the assembly and invoking its entry point - Console.WriteLine($"Starting generator in-process: {generatorPath}"); + // Run the .NET generator as a subprocess + Console.WriteLine($"Starting generator: dotnet --roll-forward Major {generatorPath} {tempDir} -g {generatorName} --new-project"); Console.WriteLine($"Code model size: {body.CodeModel!.Length} chars"); Console.WriteLine($"Configuration: {body.Configuration}"); - Console.WriteLine($"Temp dir: {tempDir}"); - var args = new[] { tempDir, "-g", generatorName, "--new-project" }; - - var assembly = System.Reflection.Assembly.LoadFrom(generatorPath); - var entryPoint = assembly.EntryPoint; - if (entryPoint == null) + var psi = new ProcessStartInfo { - return Results.Json( - new GenerateErrorResponse("Generator assembly has no entry point", ""), - GenerateJsonContext.Default.GenerateErrorResponse, - statusCode: 500); - } - - try + FileName = "dotnet", + ArgumentList = { "--roll-forward", "Major", generatorPath, tempDir, "-g", generatorName, "--new-project" }, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using var process = Process.Start(psi)!; + + var stderrLines = new List(); + var stdoutTask = Task.Run(async () => { - var result = entryPoint.Invoke(null, new object[] { args }); - if (result is Task taskInt) - { - exitCode = await taskInt; - } - else if (result is Task task) - { - await task; - exitCode = 0; - } - else if (result is int intResult) + string? line; + while ((line = await process.StandardOutput.ReadLineAsync()) != null) { - exitCode = intResult; + Console.WriteLine($"[generator stdout] {line}"); } - else + }); + var stderrTask = Task.Run(async () => + { + string? line; + while ((line = await process.StandardError.ReadLineAsync()) != null) { - exitCode = 0; + Console.Error.WriteLine($"[generator stderr] {line}"); + stderrLines.Add(line); } - } - catch (Exception ex) - { - var inner = ex.InnerException ?? ex; - Console.Error.WriteLine($"[generator error] {inner.Message}"); - Console.Error.WriteLine(inner.StackTrace); - return Results.Json( - new GenerateErrorResponse($"Generator threw exception: {inner.Message}", inner.StackTrace ?? ""), - GenerateJsonContext.Default.GenerateErrorResponse, - statusCode: 500); - } + }); + + await process.WaitForExitAsync(); + await Task.WhenAll(stdoutTask, stderrTask); + exitCode = process.ExitCode; Console.WriteLine($"Generator exited with code {exitCode}"); if (exitCode != 0) { return Results.Json( - new GenerateErrorResponse($"Generator failed with exit code {exitCode}", ""), + new GenerateErrorResponse($"Generator failed with exit code {exitCode}", string.Join("\n", stderrLines.TakeLast(50))), GenerateJsonContext.Default.GenerateErrorResponse, statusCode: 500); } From 6b17cf97038641401c690b109a338b3a982d0fc9 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 14:36:39 -0700 Subject: [PATCH 085/103] fix: add --skip-post-processing flag to bypass Roslyn compilation Roslyn's Simplifier.ReduceAsync and GetCompilationAsync crash with SIGSEGV in App Service containers. The new flag writes raw generated code directly without Roslyn post-processing (simplification, formatting, unused type removal). Code is valid C# but uses fully-qualified type names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/CSharpGen.cs | 33 +++++++++++++++++++ .../src/CodeModelGenerator.cs | 1 + .../src/StartUp/CommandLineOptions.cs | 6 ++++ .../src/StartUp/GeneratorHandler.cs | 1 + .../playground-server/Program.cs | 2 +- .../playground-server/test-generator.sh | 2 +- 6 files changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 504f9c31e15..b5a036fc2b2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -76,6 +76,39 @@ await customCodeWorkspace.GetCompilationAsync(), LoggingHelpers.LogElapsedTime("All visitors have been applied"); + if (CodeModelGenerator.Instance.SkipPostProcessing) + { + // Playground mode: write raw generated code without Roslyn post-processing + foreach (var outputType in output.TypeProviders) + { + outputType.ProcessTypeForBackCompatibility(); + + var writer = CodeModelGenerator.Instance.GetWriter(outputType); + var file = writer.Write(); + if (!string.IsNullOrEmpty(file.Content)) + { + var filename = Path.Combine(outputPath, file.Name); + Directory.CreateDirectory(Path.GetDirectoryName(filename)!); + await File.WriteAllTextAsync(filename, file.Content); + } + + foreach (var serialization in outputType.SerializationProviders) + { + writer = CodeModelGenerator.Instance.GetWriter(serialization); + file = writer.Write(); + if (!string.IsNullOrEmpty(file.Content)) + { + var filename = Path.Combine(outputPath, file.Name); + Directory.CreateDirectory(Path.GetDirectoryName(filename)!); + await File.WriteAllTextAsync(filename, file.Content); + } + } + } + + LoggingHelpers.LogElapsedTime("All files have been written to disk (skip-post-processing)"); + return; + } + foreach (var outputType in output.TypeProviders) { // Ensure back-compatibility processing is done after all visitors have run diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs index df1ac4a27de..46a8e61e9d9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs @@ -72,6 +72,7 @@ protected CodeModelGenerator() } internal bool IsNewProject { get; set; } + internal bool SkipPostProcessing { get; set; } private InputLibrary _inputLibrary; public virtual Emitter Emitter { get; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/CommandLineOptions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/CommandLineOptions.cs index dfe6aeb1e3e..1d76f8b8766 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/CommandLineOptions.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/CommandLineOptions.cs @@ -35,5 +35,11 @@ internal class CommandLineOptions [Option(longName: NewProjectOptionName, shortName: 'n', Required = false, Default = false, Hidden = false, HelpText = CmdLineNewProjectOptionHelpText)] public bool IsNewProject { get; set; } + + private const string SkipPostProcessingOptionName = "skip-post-processing"; + private const string CmdLineSkipPostProcessingOptionHelpText = "Skip Roslyn post-processing (simplification, formatting, unused type removal). Used for playground scenarios."; + + [Option(longName: SkipPostProcessingOptionName, Required = false, Default = false, Hidden = true, HelpText = CmdLineSkipPostProcessingOptionHelpText)] + public bool SkipPostProcessing { get; set; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs index bee78c0034c..b78204ad6b7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs @@ -355,6 +355,7 @@ internal void SelectGenerator(CommandLineOptions options) { CodeModelGenerator.Instance = generator.Value; CodeModelGenerator.Instance.IsNewProject = options.IsNewProject; + CodeModelGenerator.Instance.SkipPostProcessing = options.SkipPostProcessing; // Apply discovered plugins (if any) if (Plugins != null) diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index df50a1af034..f4c229c398e 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -146,7 +146,7 @@ var psi = new ProcessStartInfo { FileName = "dotnet", - ArgumentList = { "--roll-forward", "Major", generatorPath, tempDir, "-g", generatorName, "--new-project" }, + ArgumentList = { "--roll-forward", "Major", generatorPath, tempDir, "-g", generatorName, "--new-project", "--skip-post-processing" }, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, diff --git a/packages/http-client-csharp/playground-server/test-generator.sh b/packages/http-client-csharp/playground-server/test-generator.sh index 878d221fe36..6919ae12c07 100644 --- a/packages/http-client-csharp/playground-server/test-generator.sh +++ b/packages/http-client-csharp/playground-server/test-generator.sh @@ -10,7 +10,7 @@ cp /app/test-data/tspCodeModel.json "$DIR/" cp /app/test-data/Configuration.json "$DIR/" echo "Running generator in $DIR..." -dotnet --roll-forward Major /app/generator/Microsoft.TypeSpec.Generator.dll "$DIR" -g ScmCodeModelGenerator --new-project +dotnet --roll-forward Major /app/generator/Microsoft.TypeSpec.Generator.dll "$DIR" -g ScmCodeModelGenerator --new-project --skip-post-processing EXIT=$? echo "Exit code: $EXIT" From 8ec3ec52ef892631856ab07757ae8858e83199c8 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 15:06:59 -0700 Subject: [PATCH 086/103] fix: add diagnostic log for SkipPostProcessing flag, fix log message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs | 1 + packages/http-client-csharp/playground-server/Program.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index b5a036fc2b2..374cae97227 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -75,6 +75,7 @@ await customCodeWorkspace.GetCompilationAsync(), FilterAllCustomizedMembers(output); LoggingHelpers.LogElapsedTime("All visitors have been applied"); + CodeModelGenerator.Instance.Emitter.Info($"SkipPostProcessing={CodeModelGenerator.Instance.SkipPostProcessing}"); if (CodeModelGenerator.Instance.SkipPostProcessing) { diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index f4c229c398e..43683647842 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -139,7 +139,7 @@ var generatorName = body.GeneratorName ?? "ScmCodeModelGenerator"; // Run the .NET generator as a subprocess - Console.WriteLine($"Starting generator: dotnet --roll-forward Major {generatorPath} {tempDir} -g {generatorName} --new-project"); + Console.WriteLine($"Starting generator: dotnet --roll-forward Major {generatorPath} {tempDir} -g {generatorName} --new-project --skip-post-processing"); Console.WriteLine($"Code model size: {body.CodeModel!.Length} chars"); Console.WriteLine($"Configuration: {body.Configuration}"); From 916bdab192f7d071713f67bcfd467ae3fc39f14b Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 15:14:33 -0700 Subject: [PATCH 087/103] fix: switch to Windows container to avoid Linux sandbox SIGSEGV Linux App Service sandbox causes SIGSEGV during Roslyn compilation. Windows containers run in Hyper-V with no such restriction. - Dockerfile: nanoserver-ltsc2022 base - Pipeline: --platform windows, --hyper-v plan - Reverted all generator skip-post-processing changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 11 +++--- .../src/CSharpGen.cs | 34 ---------------- .../src/CodeModelGenerator.cs | 1 - .../src/StartUp/CommandLineOptions.cs | 6 --- .../src/StartUp/GeneratorHandler.cs | 1 - .../playground-server/Dockerfile | 39 ++++--------------- .../playground-server/Program.cs | 4 +- .../playground-server/test-generator.sh | 2 +- 8 files changed, 17 insertions(+), 81 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 803ebda7291..ae82da06dfd 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -416,20 +416,21 @@ stages: CONTEXT="$(Build.SourcesDirectory)/${{ parameters.PackagePath }}" az acr build \ --registry "$REGISTRY" \ + --platform windows \ --image "$APP_NAME:$(Build.BuildId)" \ --file "$CONTEXT/${{ parameters.PlaygroundServerDockerfile }}" \ "$CONTEXT" - # Deploy to Azure App Service - PLAN_NAME="typespec-playground-plan" + # Deploy to Azure App Service (Windows container for Roslyn compatibility) + PLAN_NAME="typespec-playground-plan-win" - # Create App Service plan if it doesn't exist + # Create Windows App Service plan if it doesn't exist if ! az appservice plan show --name "$PLAN_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then - echo "Creating App Service plan: $PLAN_NAME" + echo "Creating Windows App Service plan: $PLAN_NAME" az appservice plan create \ --name "$PLAN_NAME" \ --resource-group "$RESOURCE_GROUP" \ - --is-linux \ + --hyper-v \ --sku P1v3 fi diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 374cae97227..504f9c31e15 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -75,40 +75,6 @@ await customCodeWorkspace.GetCompilationAsync(), FilterAllCustomizedMembers(output); LoggingHelpers.LogElapsedTime("All visitors have been applied"); - CodeModelGenerator.Instance.Emitter.Info($"SkipPostProcessing={CodeModelGenerator.Instance.SkipPostProcessing}"); - - if (CodeModelGenerator.Instance.SkipPostProcessing) - { - // Playground mode: write raw generated code without Roslyn post-processing - foreach (var outputType in output.TypeProviders) - { - outputType.ProcessTypeForBackCompatibility(); - - var writer = CodeModelGenerator.Instance.GetWriter(outputType); - var file = writer.Write(); - if (!string.IsNullOrEmpty(file.Content)) - { - var filename = Path.Combine(outputPath, file.Name); - Directory.CreateDirectory(Path.GetDirectoryName(filename)!); - await File.WriteAllTextAsync(filename, file.Content); - } - - foreach (var serialization in outputType.SerializationProviders) - { - writer = CodeModelGenerator.Instance.GetWriter(serialization); - file = writer.Write(); - if (!string.IsNullOrEmpty(file.Content)) - { - var filename = Path.Combine(outputPath, file.Name); - Directory.CreateDirectory(Path.GetDirectoryName(filename)!); - await File.WriteAllTextAsync(filename, file.Content); - } - } - } - - LoggingHelpers.LogElapsedTime("All files have been written to disk (skip-post-processing)"); - return; - } foreach (var outputType in output.TypeProviders) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs index 46a8e61e9d9..df1ac4a27de 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs @@ -72,7 +72,6 @@ protected CodeModelGenerator() } internal bool IsNewProject { get; set; } - internal bool SkipPostProcessing { get; set; } private InputLibrary _inputLibrary; public virtual Emitter Emitter { get; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/CommandLineOptions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/CommandLineOptions.cs index 1d76f8b8766..dfe6aeb1e3e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/CommandLineOptions.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/CommandLineOptions.cs @@ -35,11 +35,5 @@ internal class CommandLineOptions [Option(longName: NewProjectOptionName, shortName: 'n', Required = false, Default = false, Hidden = false, HelpText = CmdLineNewProjectOptionHelpText)] public bool IsNewProject { get; set; } - - private const string SkipPostProcessingOptionName = "skip-post-processing"; - private const string CmdLineSkipPostProcessingOptionHelpText = "Skip Roslyn post-processing (simplification, formatting, unused type removal). Used for playground scenarios."; - - [Option(longName: SkipPostProcessingOptionName, Required = false, Default = false, Hidden = true, HelpText = CmdLineSkipPostProcessingOptionHelpText)] - public bool SkipPostProcessing { get; set; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs index b78204ad6b7..bee78c0034c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs @@ -355,7 +355,6 @@ internal void SelectGenerator(CommandLineOptions options) { CodeModelGenerator.Instance = generator.Value; CodeModelGenerator.Instance.IsNewProject = options.IsNewProject; - CodeModelGenerator.Instance.SkipPostProcessing = options.SkipPostProcessing; // Apply discovered plugins (if any) if (Plugins != null) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 52a2ad676e0..52781538c7b 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -1,6 +1,6 @@ # Build from the http-client-csharp package root: # docker build -f playground-server/Dockerfile -t csharp-playground-server . -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0-nanoserver-ltsc2022 AS build WORKDIR /src # Build the generator (populates dist/generator/) @@ -14,40 +14,17 @@ RUN dotnet restore playground-server/playground-server.csproj COPY playground-server/ playground-server/ RUN dotnet publish playground-server -c Release -o /app -# Copy generator output -# Copy a test code model for manual debugging -RUN cp -r dist/generator /app/generator -RUN mkdir -p /app/test-data && \ - cp generator/TestProjects/Spector/http/routes/tspCodeModel.json /app/test-data/ && \ - cp generator/TestProjects/Spector/http/routes/Configuration.json /app/test-data/ +# Copy generator output and test data +RUN powershell -Command "Copy-Item -Path 'dist/generator' -Destination '/app/generator' -Recurse" +RUN powershell -Command "New-Item -ItemType Directory -Path '/app/test-data' -Force; Copy-Item 'generator/TestProjects/Spector/http/routes/tspCodeModel.json' '/app/test-data/'; Copy-Item 'generator/TestProjects/Spector/http/routes/Configuration.json' '/app/test-data/'" # Need full SDK (not just aspnet) because the server spawns `dotnet` to run the generator DLL -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime +FROM mcr.microsoft.com/dotnet/sdk:10.0-nanoserver-ltsc2022 AS runtime WORKDIR /app COPY --from=build /app . -# Install SSH, lldb, and dotnet-dump for crash analysis -RUN apt-get update && apt-get install -y openssh-server lldb \ - && echo "root:Docker!" | chpasswd \ - && mkdir -p /run/sshd \ - && dotnet tool install -g dotnet-dump \ - && apt-get clean && rm -rf /var/lib/apt/lists/* -COPY playground-server/sshd_config /etc/ssh/ -ENV PATH="$PATH:/root/.dotnet/tools" - ENV DOTNET_ENVIRONMENT=Production -ENV GENERATOR_PATH=/app/generator/Microsoft.TypeSpec.Generator.dll -ENV TMPDIR=/home/tmp -ENV DOTNET_RUNNING_IN_CONTAINER=true -# Disable .NET diagnostics -ENV COMPlus_EnableDiagnostics=0 -ENV DOTNET_EnableDiagnostics=0 -ENV DOTNET_SYSTEM_IO_DISABLEFILELOCKING=1 -# Use the interpreter instead of JIT (avoids potential JIT SIGSEGV) -ENV DOTNET_EnableWriteXorExecute=0 +ENV GENERATOR_PATH=C:\\app\\generator\\Microsoft.TypeSpec.Generator.dll -EXPOSE 5174 2222 -COPY playground-server/entrypoint.sh /entrypoint.sh -COPY playground-server/test-generator.sh /app/test-generator.sh -RUN chmod +x /entrypoint.sh /app/test-generator.sh -ENTRYPOINT ["/entrypoint.sh"] +EXPOSE 5174 +ENTRYPOINT ["dotnet", "playground-server.dll"] diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs index 43683647842..df50a1af034 100644 --- a/packages/http-client-csharp/playground-server/Program.cs +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -139,14 +139,14 @@ var generatorName = body.GeneratorName ?? "ScmCodeModelGenerator"; // Run the .NET generator as a subprocess - Console.WriteLine($"Starting generator: dotnet --roll-forward Major {generatorPath} {tempDir} -g {generatorName} --new-project --skip-post-processing"); + Console.WriteLine($"Starting generator: dotnet --roll-forward Major {generatorPath} {tempDir} -g {generatorName} --new-project"); Console.WriteLine($"Code model size: {body.CodeModel!.Length} chars"); Console.WriteLine($"Configuration: {body.Configuration}"); var psi = new ProcessStartInfo { FileName = "dotnet", - ArgumentList = { "--roll-forward", "Major", generatorPath, tempDir, "-g", generatorName, "--new-project", "--skip-post-processing" }, + ArgumentList = { "--roll-forward", "Major", generatorPath, tempDir, "-g", generatorName, "--new-project" }, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, diff --git a/packages/http-client-csharp/playground-server/test-generator.sh b/packages/http-client-csharp/playground-server/test-generator.sh index 6919ae12c07..878d221fe36 100644 --- a/packages/http-client-csharp/playground-server/test-generator.sh +++ b/packages/http-client-csharp/playground-server/test-generator.sh @@ -10,7 +10,7 @@ cp /app/test-data/tspCodeModel.json "$DIR/" cp /app/test-data/Configuration.json "$DIR/" echo "Running generator in $DIR..." -dotnet --roll-forward Major /app/generator/Microsoft.TypeSpec.Generator.dll "$DIR" -g ScmCodeModelGenerator --new-project --skip-post-processing +dotnet --roll-forward Major /app/generator/Microsoft.TypeSpec.Generator.dll "$DIR" -g ScmCodeModelGenerator --new-project EXIT=$? echo "Exit code: $EXIT" From 0d3d302e2a076722ee0dbfaea77242a683016a0a Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 15:21:50 -0700 Subject: [PATCH 088/103] fix: revert to Linux, add granular diagnostic logging to CSharpGen Add [diag] log lines between every Roslyn operation to pinpoint exactly which call triggers the SIGSEGV. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../templates/stages/emitter-stages.yml | 11 ++++--- .../src/CSharpGen.cs | 29 +++++++++++++++++-- .../playground-server/Dockerfile | 27 ++++++++++++----- .../playground-server/entrypoint.sh | 3 -- 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index ae82da06dfd..803ebda7291 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -416,21 +416,20 @@ stages: CONTEXT="$(Build.SourcesDirectory)/${{ parameters.PackagePath }}" az acr build \ --registry "$REGISTRY" \ - --platform windows \ --image "$APP_NAME:$(Build.BuildId)" \ --file "$CONTEXT/${{ parameters.PlaygroundServerDockerfile }}" \ "$CONTEXT" - # Deploy to Azure App Service (Windows container for Roslyn compatibility) - PLAN_NAME="typespec-playground-plan-win" + # Deploy to Azure App Service + PLAN_NAME="typespec-playground-plan" - # Create Windows App Service plan if it doesn't exist + # Create App Service plan if it doesn't exist if ! az appservice plan show --name "$PLAN_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then - echo "Creating Windows App Service plan: $PLAN_NAME" + echo "Creating App Service plan: $PLAN_NAME" az appservice plan create \ --name "$PLAN_NAME" \ --resource-group "$RESOURCE_GROUP" \ - --hyper-v \ + --is-linux \ --sku P1v3 fi diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 504f9c31e15..4ea3b3a0992 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -28,15 +28,21 @@ public async Task ExecuteAsync() CodeModelGenerator.Instance.Emitter.Info("Starting code generation"); CodeModelGenerator.Instance.Stopwatch.Start(); + CodeModelGenerator.Instance.Emitter.Info("[diag] Before GeneratedCodeWorkspace.Initialize()"); GeneratedCodeWorkspace.Initialize(); + CodeModelGenerator.Instance.Emitter.Info("[diag] After GeneratedCodeWorkspace.Initialize()"); var outputPath = CodeModelGenerator.Instance.Configuration.OutputDirectory; var generatedSourceOutputPath = CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory; // Resolve PackageReference items from the .csproj so custom code referencing // external NuGet types (e.g., Azure.Storage.Common) compiles correctly. + CodeModelGenerator.Instance.Emitter.Info("[diag] Before AddPackageReferencesFromProject"); await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); + CodeModelGenerator.Instance.Emitter.Info("[diag] After AddPackageReferencesFromProject"); + CodeModelGenerator.Instance.Emitter.Info("[diag] Before Create(customCode)"); GeneratedCodeWorkspace customCodeWorkspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: true); + CodeModelGenerator.Instance.Emitter.Info("[diag] After Create(customCode)"); // The generated attributes need to be added into the workspace before loading the custom code. Otherwise, // Roslyn doesn't load the attributes completely and we are unable to get the attribute arguments. @@ -47,12 +53,21 @@ public async Task ExecuteAsync() } await Task.WhenAll(generateAttributeTasks); + CodeModelGenerator.Instance.Emitter.Info("[diag] After AddInMemoryFile attributes"); - CodeModelGenerator.Instance.SourceInputModel = new SourceInputModel( - await customCodeWorkspace.GetCompilationAsync(), - await GeneratedCodeWorkspace.LoadBaselineContract()); + CodeModelGenerator.Instance.Emitter.Info("[diag] Before customCodeWorkspace.GetCompilationAsync()"); + var customCompilation = await customCodeWorkspace.GetCompilationAsync(); + CodeModelGenerator.Instance.Emitter.Info("[diag] After customCodeWorkspace.GetCompilationAsync()"); + CodeModelGenerator.Instance.Emitter.Info("[diag] Before LoadBaselineContract()"); + var baselineContract = await GeneratedCodeWorkspace.LoadBaselineContract(); + CodeModelGenerator.Instance.Emitter.Info("[diag] After LoadBaselineContract()"); + + CodeModelGenerator.Instance.SourceInputModel = new SourceInputModel(customCompilation, baselineContract); + + CodeModelGenerator.Instance.Emitter.Info("[diag] Before Create(generatedCode)"); GeneratedCodeWorkspace generatedCodeWorkspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: false); + CodeModelGenerator.Instance.Emitter.Info("[diag] After Create(generatedCode)"); var output = CodeModelGenerator.Instance.OutputLibrary; Directory.CreateDirectory(Path.Combine(generatedSourceOutputPath, "Models")); @@ -76,6 +91,7 @@ await customCodeWorkspace.GetCompilationAsync(), LoggingHelpers.LogElapsedTime("All visitors have been applied"); + int fileCount = 0; foreach (var outputType in output.TypeProviders) { // Ensure back-compatibility processing is done after all visitors have run @@ -83,14 +99,17 @@ await customCodeWorkspace.GetCompilationAsync(), var writer = CodeModelGenerator.Instance.GetWriter(outputType); generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); + fileCount++; foreach (var serialization in outputType.SerializationProviders) { writer = CodeModelGenerator.Instance.GetWriter(serialization); generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); + fileCount++; } } + CodeModelGenerator.Instance.Emitter.Info($"[diag] Before Task.WhenAll for {fileCount} AddGeneratedFile tasks"); // Add all the generated files to the workspace await Task.WhenAll(generateFilesTasks); @@ -101,9 +120,12 @@ await customCodeWorkspace.GetCompilationAsync(), LoggingHelpers.LogElapsedTime("All old generated files have been deleted"); + CodeModelGenerator.Instance.Emitter.Info("[diag] Before PostProcessAsync()"); await generatedCodeWorkspace.PostProcessAsync(); + CodeModelGenerator.Instance.Emitter.Info("[diag] After PostProcessAsync()"); // Write the generated files to the output directory + CodeModelGenerator.Instance.Emitter.Info("[diag] Before GetGeneratedFilesAsync()"); await foreach (var file in generatedCodeWorkspace.GetGeneratedFilesAsync()) { if (string.IsNullOrEmpty(file.Text)) @@ -115,6 +137,7 @@ await customCodeWorkspace.GetCompilationAsync(), Directory.CreateDirectory(Path.GetDirectoryName(filename)!); await File.WriteAllTextAsync(filename, file.Text); } + CodeModelGenerator.Instance.Emitter.Info("[diag] After GetGeneratedFilesAsync()"); // Write additional output files (e.g. configuration schemas, .targets files) await CodeModelGenerator.Instance.WriteAdditionalFiles(outputPath); diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 52781538c7b..0713180ef56 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -1,6 +1,6 @@ # Build from the http-client-csharp package root: # docker build -f playground-server/Dockerfile -t csharp-playground-server . -FROM mcr.microsoft.com/dotnet/sdk:10.0-nanoserver-ltsc2022 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src # Build the generator (populates dist/generator/) @@ -15,16 +15,29 @@ COPY playground-server/ playground-server/ RUN dotnet publish playground-server -c Release -o /app # Copy generator output and test data -RUN powershell -Command "Copy-Item -Path 'dist/generator' -Destination '/app/generator' -Recurse" -RUN powershell -Command "New-Item -ItemType Directory -Path '/app/test-data' -Force; Copy-Item 'generator/TestProjects/Spector/http/routes/tspCodeModel.json' '/app/test-data/'; Copy-Item 'generator/TestProjects/Spector/http/routes/Configuration.json' '/app/test-data/'" +RUN cp -r dist/generator /app/generator +RUN mkdir -p /app/test-data && \ + cp generator/TestProjects/Spector/http/routes/tspCodeModel.json /app/test-data/ && \ + cp generator/TestProjects/Spector/http/routes/Configuration.json /app/test-data/ # Need full SDK (not just aspnet) because the server spawns `dotnet` to run the generator DLL -FROM mcr.microsoft.com/dotnet/sdk:10.0-nanoserver-ltsc2022 AS runtime +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime WORKDIR /app COPY --from=build /app . +# Install SSH for remote debugging +RUN apt-get update && apt-get install -y openssh-server \ + && echo "root:Docker!" | chpasswd \ + && mkdir -p /run/sshd \ + && apt-get clean && rm -rf /var/lib/apt/lists/* +COPY playground-server/sshd_config /etc/ssh/ + ENV DOTNET_ENVIRONMENT=Production -ENV GENERATOR_PATH=C:\\app\\generator\\Microsoft.TypeSpec.Generator.dll +ENV GENERATOR_PATH=/app/generator/Microsoft.TypeSpec.Generator.dll +ENV TMPDIR=/tmp -EXPOSE 5174 -ENTRYPOINT ["dotnet", "playground-server.dll"] +EXPOSE 5174 2222 +COPY playground-server/entrypoint.sh /entrypoint.sh +COPY playground-server/test-generator.sh /app/test-generator.sh +RUN chmod +x /entrypoint.sh /app/test-generator.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/packages/http-client-csharp/playground-server/entrypoint.sh b/packages/http-client-csharp/playground-server/entrypoint.sh index 713a548809e..2d0a6893bff 100644 --- a/packages/http-client-csharp/playground-server/entrypoint.sh +++ b/packages/http-client-csharp/playground-server/entrypoint.sh @@ -2,8 +2,5 @@ # Start SSH daemon for App Service remote access /usr/sbin/sshd -# Create temp directory on /home (persistent, no mmap restrictions) -mkdir -p /home/tmp - # Start the playground server exec dotnet /app/playground-server.dll From 95bb043153cc11e5416010e6afddec602fda3072 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 15:46:40 -0700 Subject: [PATCH 089/103] fix: run AddGeneratedFile sequentially with per-file logging Run files one at a time instead of parallel Task.WhenAll to identify exactly which file/type triggers the SIGSEGV. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/CSharpGen.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 4ea3b3a0992..96d99618386 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -71,8 +71,6 @@ public async Task ExecuteAsync() var output = CodeModelGenerator.Instance.OutputLibrary; Directory.CreateDirectory(Path.Combine(generatedSourceOutputPath, "Models")); - List generateFilesTasks = new(); - // Build all TypeProviders foreach (var type in output.TypeProviders) { @@ -98,20 +96,24 @@ public async Task ExecuteAsync() outputType.ProcessTypeForBackCompatibility(); var writer = CodeModelGenerator.Instance.GetWriter(outputType); - generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); + var codeFile = writer.Write(); + CodeModelGenerator.Instance.Emitter.Info($"[diag] Adding file {fileCount}: {codeFile.Name} ({codeFile.Content.Length} chars)"); + await generatedCodeWorkspace.AddGeneratedFile(codeFile); + CodeModelGenerator.Instance.Emitter.Info($"[diag] Added file {fileCount}: {codeFile.Name}"); fileCount++; foreach (var serialization in outputType.SerializationProviders) { writer = CodeModelGenerator.Instance.GetWriter(serialization); - generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); + codeFile = writer.Write(); + CodeModelGenerator.Instance.Emitter.Info($"[diag] Adding file {fileCount}: {codeFile.Name} ({codeFile.Content.Length} chars)"); + await generatedCodeWorkspace.AddGeneratedFile(codeFile); + CodeModelGenerator.Instance.Emitter.Info($"[diag] Added file {fileCount}: {codeFile.Name}"); fileCount++; } } - CodeModelGenerator.Instance.Emitter.Info($"[diag] Before Task.WhenAll for {fileCount} AddGeneratedFile tasks"); - // Add all the generated files to the workspace - await Task.WhenAll(generateFilesTasks); + CodeModelGenerator.Instance.Emitter.Info($"[diag] All {fileCount} files added to workspace"); LoggingHelpers.LogElapsedTime("All generated types have been written into memory"); From 44003901b70aeb8bacd04584a452bbd1aa41184e Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 16:30:56 -0700 Subject: [PATCH 090/103] fix: use stderr with flush for diagnostic logs to avoid buffering Emitter.Info writes to stdout JSON-RPC which may buffer. Switch to Console.Error.WriteLine + Flush to ensure logs appear before crash. Also added logging inside PostProcessAsync and GetGeneratedFilesAsync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.TypeSpec.Generator/src/CSharpGen.cs | 15 ++++++++++----- .../src/PostProcessing/GeneratedCodeWorkspace.cs | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 96d99618386..e8f9b7cdf58 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -97,23 +97,28 @@ public async Task ExecuteAsync() var writer = CodeModelGenerator.Instance.GetWriter(outputType); var codeFile = writer.Write(); - CodeModelGenerator.Instance.Emitter.Info($"[diag] Adding file {fileCount}: {codeFile.Name} ({codeFile.Content.Length} chars)"); + Console.Error.WriteLine($"[diag] Adding file {fileCount}: {codeFile.Name} ({codeFile.Content.Length} chars)"); + Console.Error.Flush(); await generatedCodeWorkspace.AddGeneratedFile(codeFile); - CodeModelGenerator.Instance.Emitter.Info($"[diag] Added file {fileCount}: {codeFile.Name}"); + Console.Error.WriteLine($"[diag] Added file {fileCount}: {codeFile.Name}"); + Console.Error.Flush(); fileCount++; foreach (var serialization in outputType.SerializationProviders) { writer = CodeModelGenerator.Instance.GetWriter(serialization); codeFile = writer.Write(); - CodeModelGenerator.Instance.Emitter.Info($"[diag] Adding file {fileCount}: {codeFile.Name} ({codeFile.Content.Length} chars)"); + Console.Error.WriteLine($"[diag] Adding file {fileCount}: {codeFile.Name} ({codeFile.Content.Length} chars)"); + Console.Error.Flush(); await generatedCodeWorkspace.AddGeneratedFile(codeFile); - CodeModelGenerator.Instance.Emitter.Info($"[diag] Added file {fileCount}: {codeFile.Name}"); + Console.Error.WriteLine($"[diag] Added file {fileCount}: {codeFile.Name}"); + Console.Error.Flush(); fileCount++; } } - CodeModelGenerator.Instance.Emitter.Info($"[diag] All {fileCount} files added to workspace"); + Console.Error.WriteLine($"[diag] All {fileCount} files added to workspace"); + Console.Error.Flush(); LoggingHelpers.LogElapsedTime("All generated types have been written into memory"); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 431b5443059..c069be1763f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -104,8 +104,10 @@ internal static SyntaxTree GetTree(TypeProvider provider) public async IAsyncEnumerable<(string Name, string Text)> GetGeneratedFilesAsync() { + CodeModelGenerator.Instance.Emitter.Info($"[diag] GetGeneratedFilesAsync: processing {_project.Documents.Count()} documents"); List> documents = new List>(); var memberRemover = new MemberRemoverRewriter(); + int docIndex = 0; foreach (Document document in _project.Documents) { if (!IsGeneratedDocument(document)) @@ -113,9 +115,13 @@ internal static SyntaxTree GetTree(TypeProvider provider) continue; } + CodeModelGenerator.Instance.Emitter.Info($"[diag] GetGeneratedFilesAsync: queuing doc {docIndex}: {document.Name}"); documents.Add(ProcessDocument(document, memberRemover)); + docIndex++; } + CodeModelGenerator.Instance.Emitter.Info($"[diag] GetGeneratedFilesAsync: before Task.WhenAll for {docIndex} docs"); var docs = await Task.WhenAll(documents); + CodeModelGenerator.Instance.Emitter.Info($"[diag] GetGeneratedFilesAsync: after Task.WhenAll"); LoggingHelpers.LogElapsedTime("Roslyn post processing complete"); @@ -262,6 +268,7 @@ internal static Project AddDirectory(Project project, string directory, Func public async Task PostProcessAsync() { + CodeModelGenerator.Instance.Emitter.Info("[diag] PostProcessAsync: creating PostProcessor"); var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; var nonRootTypes = CodeModelGenerator.Instance.NonRootTypes; var postProcessor = new PostProcessor( @@ -269,18 +276,26 @@ public async Task PostProcessAsync() modelFactoryFullName: modelFactory.Type.FullyQualifiedName, additionalNonRootTypeNames: nonRootTypes); + CodeModelGenerator.Instance.Emitter.Info($"[diag] PostProcessAsync: UnreferencedTypesHandling={Configuration.UnreferencedTypesHandling}"); switch (Configuration.UnreferencedTypesHandling) { case Configuration.UnreferencedTypesHandlingOption.KeepAll: + CodeModelGenerator.Instance.Emitter.Info("[diag] PostProcessAsync: KeepAll - skipping"); break; case Configuration.UnreferencedTypesHandlingOption.Internalize: + CodeModelGenerator.Instance.Emitter.Info("[diag] PostProcessAsync: Before InternalizeAsync"); _project = await postProcessor.InternalizeAsync(_project); + CodeModelGenerator.Instance.Emitter.Info("[diag] PostProcessAsync: After InternalizeAsync"); break; case Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize: + CodeModelGenerator.Instance.Emitter.Info("[diag] PostProcessAsync: Before InternalizeAsync"); _project = await postProcessor.InternalizeAsync(_project); + CodeModelGenerator.Instance.Emitter.Info("[diag] PostProcessAsync: After InternalizeAsync, Before RemoveAsync"); _project = await postProcessor.RemoveAsync(_project); + CodeModelGenerator.Instance.Emitter.Info("[diag] PostProcessAsync: After RemoveAsync"); break; } + CodeModelGenerator.Instance.Emitter.Info("[diag] PostProcessAsync: complete"); } /// From c36b90a0a07b5a165ed8cabbf052d227554cca8a Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 16:33:03 -0700 Subject: [PATCH 091/103] fix: add per-step logging (ProcessTypeForBackCompat, Write, AddFile) Split logging to identify if crash is in ProcessTypeForBackCompatibility, writer.Write(), or AddGeneratedFile for the TypeFormatters type. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.TypeSpec.Generator/src/CSharpGen.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index e8f9b7cdf58..4201207cf00 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -92,10 +92,17 @@ public async Task ExecuteAsync() int fileCount = 0; foreach (var outputType in output.TypeProviders) { + Console.Error.WriteLine($"[diag] Processing type {fileCount}: {outputType.Name}"); + Console.Error.Flush(); + // Ensure back-compatibility processing is done after all visitors have run outputType.ProcessTypeForBackCompatibility(); + Console.Error.WriteLine($"[diag] BackCompat done for {outputType.Name}"); + Console.Error.Flush(); var writer = CodeModelGenerator.Instance.GetWriter(outputType); + Console.Error.WriteLine($"[diag] Writer created for {outputType.Name}, calling Write()"); + Console.Error.Flush(); var codeFile = writer.Write(); Console.Error.WriteLine($"[diag] Adding file {fileCount}: {codeFile.Name} ({codeFile.Content.Length} chars)"); Console.Error.Flush(); @@ -106,6 +113,8 @@ public async Task ExecuteAsync() foreach (var serialization in outputType.SerializationProviders) { + Console.Error.WriteLine($"[diag] Processing serialization for {outputType.Name}: {serialization.Name}"); + Console.Error.Flush(); writer = CodeModelGenerator.Instance.GetWriter(serialization); codeFile = writer.Write(); Console.Error.WriteLine($"[diag] Adding file {fileCount}: {codeFile.Name} ({codeFile.Content.Length} chars)"); From b6c01c605fe8072e299f9a7c7a95b3f8d2e558b6 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 16:53:22 -0700 Subject: [PATCH 092/103] fix: increase default stack size to 8MB for deep expression tree recursion The Widgets rest client type crashes during writer.Write() which recursively writes deeply nested expression trees. This may be a stack overflow in the App Service container's restricted stack. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 0713180ef56..d6afb048726 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -35,6 +35,9 @@ COPY playground-server/sshd_config /etc/ssh/ ENV DOTNET_ENVIRONMENT=Production ENV GENERATOR_PATH=/app/generator/Microsoft.TypeSpec.Generator.dll ENV TMPDIR=/tmp +# Increase default stack size to 8MB (may be restricted in App Service) +ENV DOTNET_DefaultStackSize=0x800000 +ENV COMPlus_DefaultStackSize=800000 EXPOSE 5174 2222 COPY playground-server/entrypoint.sh /entrypoint.sh From dcc11cf24dd5bbc49cf7fb9bd06eb0f339d7cc3e Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 16:55:28 -0700 Subject: [PATCH 093/103] fix: replace ArrayPool.Shared with private pool to avoid mmap in sandbox ArrayPool.Shared uses mmap-backed allocations on Linux for large segments. In App Service sandbox, this may cause SIGSEGV. Private pool uses simple managed arrays instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Writers/UnsafeBufferSequence.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.cs index 9e76e92afd2..996bd70c054 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.cs @@ -17,6 +17,9 @@ internal sealed partial class UnsafeBufferSequence : IBufferWriter, IDispo private volatile UnsafeBufferSegment[] _buffers; // this is an array so items can be accessed by ref private volatile int _count; private readonly int _segmentSize; + // Use a private pool instead of ArrayPool.Shared to avoid mmap-backed allocations + // that can cause SIGSEGV in sandboxed container environments (e.g., Azure App Service). + private static readonly ArrayPool _pool = ArrayPool.Create(); /// /// Initializes a new instance of . @@ -92,7 +95,7 @@ private void ExpandBuffers(int sizeToRent) _buffers.CopyTo(resized, 0); } _buffers = resized; - _buffers[_count].Array = ArrayPool.Shared.Rent(sizeToRent); + _buffers[_count].Array = _pool.Rent(sizeToRent); _count = bufferCount == 1 ? bufferCount : _count + 1; } } @@ -125,7 +128,7 @@ public void Dispose() for (int i = 0; i < bufferCountToFree; i++) { - ArrayPool.Shared.Return(buffersToFree[i].Array); + _pool.Return(buffersToFree[i].Array); } } From 3706ae53a9d47833a1f0f0f57e1ffac2d62d7769 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 17:42:02 -0700 Subject: [PATCH 094/103] fix: run Write() on dedicated thread with 64MB stack to diagnose crash If it's a stack overflow, the 64MB thread will survive. If the thread still crashes, codeFile will be null and we'll log it. Also logs type info (method count, property count, concrete type name). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/CSharpGen.cs | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 4201207cf00..2519c2fc906 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -5,8 +5,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.SourceInput; using Microsoft.TypeSpec.Generator.Utilities; @@ -92,7 +94,7 @@ public async Task ExecuteAsync() int fileCount = 0; foreach (var outputType in output.TypeProviders) { - Console.Error.WriteLine($"[diag] Processing type {fileCount}: {outputType.Name}"); + Console.Error.WriteLine($"[diag] Processing type {fileCount}: {outputType.Name} (type: {outputType.GetType().Name})"); Console.Error.Flush(); // Ensure back-compatibility processing is done after all visitors have run @@ -101,10 +103,41 @@ public async Task ExecuteAsync() Console.Error.Flush(); var writer = CodeModelGenerator.Instance.GetWriter(outputType); - Console.Error.WriteLine($"[diag] Writer created for {outputType.Name}, calling Write()"); + Console.Error.WriteLine($"[diag] Writer created for {outputType.Name} (methods: {outputType.Methods.Count}, props: {outputType.Properties.Count}), calling Write()"); Console.Error.Flush(); - var codeFile = writer.Write(); - Console.Error.WriteLine($"[diag] Adding file {fileCount}: {codeFile.Name} ({codeFile.Content.Length} chars)"); + + // Run Write on a new thread with a large stack to rule out stack overflow + CodeFile? codeFile = null; + Exception? writeError = null; + var writeThread = new Thread(() => + { + try + { + codeFile = writer.Write(); + } + catch (Exception ex) + { + writeError = ex; + } + }, 64 * 1024 * 1024); // 64MB stack + writeThread.Start(); + writeThread.Join(); + + if (writeError != null) + { + Console.Error.WriteLine($"[diag] Write() THREW: {writeError.GetType().Name}: {writeError.Message}"); + Console.Error.WriteLine(writeError.StackTrace); + Console.Error.Flush(); + throw writeError; + } + if (codeFile is null) + { + Console.Error.WriteLine($"[diag] Write() returned null - thread may have crashed"); + Console.Error.Flush(); + throw new InvalidOperationException($"Write() for {outputType.Name} returned null"); + } + + Console.Error.WriteLine($"[diag] Adding file {fileCount}: {codeFile!.Name} ({codeFile.Content.Length} chars)"); Console.Error.Flush(); await generatedCodeWorkspace.AddGeneratedFile(codeFile); Console.Error.WriteLine($"[diag] Added file {fileCount}: {codeFile.Name}"); From 85ce8cb941d81c1c05cfe8929036233f644869f1 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 18:19:50 -0700 Subject: [PATCH 095/103] fix: use ReadyToRun pre-compilation to minimize JIT at runtime Pre-compile generator assemblies with PublishReadyToRun to reduce runtime JIT compilation that may trigger SIGSEGV in App Service sandbox. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index d6afb048726..cdec3d5a646 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -3,10 +3,10 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src -# Build the generator (populates dist/generator/) +# Build the generator with ReadyToRun pre-compilation to minimize JIT at runtime COPY generator/ generator/ COPY eng/ eng/ -RUN dotnet build generator -c Release +RUN dotnet publish generator/Microsoft.TypeSpec.Generator/src -c Release -r linux-x64 --self-contained false -p:PublishReadyToRun=true -o dist/generator # Build the server COPY playground-server/playground-server.csproj playground-server/ From 7ce99c2acabb93f741846b5cfdb5445601bc3e3e Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 18:25:42 -0700 Subject: [PATCH 096/103] fix: replace UnsafeBufferSequence with safe List-based implementation Remove volatile/ref patterns, ArrayPool, and lock-based synchronization that may cause SIGSEGV in sandboxed container environments. Use simple List with direct char[] allocation instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Writers/UnsafeBufferSequence.Reader.cs | 6 - .../src/Writers/UnsafeBufferSequence.cs | 106 ++++-------------- 2 files changed, 20 insertions(+), 92 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.Reader.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.Reader.cs index 8bc79dfc54d..7f805a5b254 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.Reader.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.Reader.cs @@ -132,14 +132,8 @@ public void Dispose() { if (!_isDisposed) { - int buffersToReturnCount = _count; - var buffersToReturn = _buffers; _count = 0; _buffers = Array.Empty(); - for (int i = 0; i < buffersToReturnCount; i++) - { - ArrayPool.Shared.Return(buffersToReturn[i].Array); - } _isDisposed = true; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.cs index 996bd70c054..fbf2c8f60d7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.cs @@ -3,58 +3,35 @@ using System; using System.Buffers; +using System.Collections.Generic; namespace Microsoft.TypeSpec.Generator; /// -/// This class is a helper to write to a in a thread safe manner. -/// It uses the shared pool to allocate buffers and returns them to the pool when disposed. -/// Since there is no way to ensure someone didn't keep a reference to one of the buffers -/// it must be disposed of in the same context it was created and its referenced should not be stored or shared. +/// Simple buffer sequence writer that avoids volatile/ref patterns and ArrayPool +/// that can cause SIGSEGV in sandboxed container environments. /// internal sealed partial class UnsafeBufferSequence : IBufferWriter, IDisposable { - private volatile UnsafeBufferSegment[] _buffers; // this is an array so items can be accessed by ref - private volatile int _count; + private readonly List _segments = new(); private readonly int _segmentSize; - // Use a private pool instead of ArrayPool.Shared to avoid mmap-backed allocations - // that can cause SIGSEGV in sandboxed container environments (e.g., Azure App Service). - private static readonly ArrayPool _pool = ArrayPool.Create(); - /// - /// Initializes a new instance of . - /// - /// The size of each buffer segment. public UnsafeBufferSequence(int segmentSize = 16384) { - // we perf tested a very large and a small model and found that the performance - // for 4k, 8k, 16k, 32k, was negligible for the small model but had a 30% alloc improvement - // from 4k to 16k on the very large model. _segmentSize = segmentSize; - _buffers = Array.Empty(); } - /// - /// Notifies the that bytes bytes were written to the output or . - /// You must request a new buffer after calling to continue writing more data; you cannot write to a previously acquired buffer. - /// - /// The number of bytes written to the or . - /// public void Advance(int bytesWritten) { - ref UnsafeBufferSegment last = ref _buffers[_count - 1]; + var last = _segments[_segments.Count - 1]; last.Written += bytesWritten; + _segments[_segments.Count - 1] = last; if (last.Written > last.Array.Length) { throw new ArgumentOutOfRangeException(nameof(bytesWritten)); } } - /// - /// Returns a to write to that is at least the requested size, as specified by the parameter. - /// - /// The minimum length of the returned . If less than 256, a buffer of size 256 will be returned. - /// A memory buffer of at least bytes. If is less than 256, a buffer of size 256 will be returned. public Memory GetMemory(int sizeHint = 0) { if (sizeHint < 256) @@ -64,82 +41,39 @@ public Memory GetMemory(int sizeHint = 0) int sizeToRent = sizeHint > _segmentSize ? sizeHint : _segmentSize; - if (_buffers.Length == 0) + if (_segments.Count == 0) { - ExpandBuffers(sizeToRent); + _segments.Add(new UnsafeBufferSegment { Array = new char[sizeToRent], Written = 0 }); } - ref UnsafeBufferSegment last = ref _buffers[_count - 1]; - Memory free = last.Array.AsMemory(last.Written); - if (free.Length >= sizeHint) + var last = _segments[_segments.Count - 1]; + int remaining = last.Array.Length - last.Written; + if (remaining >= sizeHint) { - return free; + return last.Array.AsMemory(last.Written); } - // else allocate a new buffer: - ExpandBuffers(sizeToRent); - - return _buffers[_count - 1].Array; - } - - private readonly object _lock = new object(); - private void ExpandBuffers(int sizeToRent) - { - lock (_lock) - { - int bufferCount = _count == 0 ? 1 : _count * 2; - - UnsafeBufferSegment[] resized = new UnsafeBufferSegment[bufferCount]; - if (_count > 0) - { - _buffers.CopyTo(resized, 0); - } - _buffers = resized; - _buffers[_count].Array = _pool.Rent(sizeToRent); - _count = bufferCount == 1 ? bufferCount : _count + 1; - } + // Allocate a new segment + _segments.Add(new UnsafeBufferSegment { Array = new char[sizeToRent], Written = 0 }); + return _segments[_segments.Count - 1].Array; } - /// - /// Returns a to write to that is at least the requested size, as specified by the parameter. - /// - /// The minimum length of the returned . If less than 256, a buffer of size 256 will be returned. - /// A buffer of at least bytes. If is less than 256, a buffer of size 256 will be returned. public Span GetSpan(int sizeHint = 0) { Memory memory = GetMemory(sizeHint); return memory.Span; } - /// - /// Disposes the SequenceWriter and returns the underlying buffers to the pool. - /// public void Dispose() { - int bufferCountToFree; - UnsafeBufferSegment[] buffersToFree; - lock (_lock) - { - bufferCountToFree = _count; - buffersToFree = _buffers; - _count = 0; - _buffers = Array.Empty(); - } - - for (int i = 0; i < bufferCountToFree; i++) - { - _pool.Return(buffersToFree[i].Array); - } + _segments.Clear(); } public Reader ExtractReader() { - lock (_lock) - { - Reader reader = new ReaderInstance(_buffers, _count); - _count = 0; - _buffers = Array.Empty(); - return reader; - } + var buffers = _segments.ToArray(); + var count = buffers.Length; + _segments.Clear(); + return new ReaderInstance(buffers, count); } } From f3c69608ae9dded79a0904d3a81a186b55673d20 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 18:40:15 -0700 Subject: [PATCH 097/103] fix: revert to dotnet build (publish misses ClientModel plugin DLL) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index cdec3d5a646..d6afb048726 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -3,10 +3,10 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src -# Build the generator with ReadyToRun pre-compilation to minimize JIT at runtime +# Build the generator (populates dist/generator/) COPY generator/ generator/ COPY eng/ eng/ -RUN dotnet publish generator/Microsoft.TypeSpec.Generator/src -c Release -r linux-x64 --self-contained false -p:PublishReadyToRun=true -o dist/generator +RUN dotnet build generator -c Release # Build the server COPY playground-server/playground-server.csproj playground-server/ From 34ae6c5527ca63ab08b13a63d47090a3c7183e93 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 18:41:18 -0700 Subject: [PATCH 098/103] fix: remove stack size and JIT workaround env vars from Dockerfile Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index d6afb048726..0713180ef56 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -35,9 +35,6 @@ COPY playground-server/sshd_config /etc/ssh/ ENV DOTNET_ENVIRONMENT=Production ENV GENERATOR_PATH=/app/generator/Microsoft.TypeSpec.Generator.dll ENV TMPDIR=/tmp -# Increase default stack size to 8MB (may be restricted in App Service) -ENV DOTNET_DefaultStackSize=0x800000 -ENV COMPlus_DefaultStackSize=800000 EXPOSE 5174 2222 COPY playground-server/entrypoint.sh /entrypoint.sh From 41e62e50a1f1dbdc3e2f01d01698ea308b80ee08 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 19:01:35 -0700 Subject: [PATCH 099/103] fix: revert UnsafeBufferSequence, add per-method logging and dump collection UnsafeBufferSequence was not the cause. Add logging inside WriteMethod to identify which of the 22 Widgets methods triggers the crash. Re-enable core dump collection for lldb analysis. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Writers/CodeWriter.cs | 2 + .../Writers/UnsafeBufferSequence.Reader.cs | 6 + .../src/Writers/UnsafeBufferSequence.cs | 106 ++++++++++++++---- .../playground-server/Dockerfile | 4 + 4 files changed, 98 insertions(+), 20 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs index 1302db936ac..548f0628bc4 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs @@ -182,6 +182,8 @@ public CodeWriter Append(FormattableString formattableString) public void WriteMethod(MethodProvider method) { ArgumentNullException.ThrowIfNull(method, nameof(method)); + Console.Error.WriteLine($"[diag] WriteMethod: {method.Signature.Name}"); + Console.Error.Flush(); using (WriteXmlDocs(method.XmlDocs)) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.Reader.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.Reader.cs index 7f805a5b254..8bc79dfc54d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.Reader.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.Reader.cs @@ -132,8 +132,14 @@ public void Dispose() { if (!_isDisposed) { + int buffersToReturnCount = _count; + var buffersToReturn = _buffers; _count = 0; _buffers = Array.Empty(); + for (int i = 0; i < buffersToReturnCount; i++) + { + ArrayPool.Shared.Return(buffersToReturn[i].Array); + } _isDisposed = true; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.cs index fbf2c8f60d7..996bd70c054 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/UnsafeBufferSequence.cs @@ -3,35 +3,58 @@ using System; using System.Buffers; -using System.Collections.Generic; namespace Microsoft.TypeSpec.Generator; /// -/// Simple buffer sequence writer that avoids volatile/ref patterns and ArrayPool -/// that can cause SIGSEGV in sandboxed container environments. +/// This class is a helper to write to a in a thread safe manner. +/// It uses the shared pool to allocate buffers and returns them to the pool when disposed. +/// Since there is no way to ensure someone didn't keep a reference to one of the buffers +/// it must be disposed of in the same context it was created and its referenced should not be stored or shared. /// internal sealed partial class UnsafeBufferSequence : IBufferWriter, IDisposable { - private readonly List _segments = new(); + private volatile UnsafeBufferSegment[] _buffers; // this is an array so items can be accessed by ref + private volatile int _count; private readonly int _segmentSize; + // Use a private pool instead of ArrayPool.Shared to avoid mmap-backed allocations + // that can cause SIGSEGV in sandboxed container environments (e.g., Azure App Service). + private static readonly ArrayPool _pool = ArrayPool.Create(); + /// + /// Initializes a new instance of . + /// + /// The size of each buffer segment. public UnsafeBufferSequence(int segmentSize = 16384) { + // we perf tested a very large and a small model and found that the performance + // for 4k, 8k, 16k, 32k, was negligible for the small model but had a 30% alloc improvement + // from 4k to 16k on the very large model. _segmentSize = segmentSize; + _buffers = Array.Empty(); } + /// + /// Notifies the that bytes bytes were written to the output or . + /// You must request a new buffer after calling to continue writing more data; you cannot write to a previously acquired buffer. + /// + /// The number of bytes written to the or . + /// public void Advance(int bytesWritten) { - var last = _segments[_segments.Count - 1]; + ref UnsafeBufferSegment last = ref _buffers[_count - 1]; last.Written += bytesWritten; - _segments[_segments.Count - 1] = last; if (last.Written > last.Array.Length) { throw new ArgumentOutOfRangeException(nameof(bytesWritten)); } } + /// + /// Returns a to write to that is at least the requested size, as specified by the parameter. + /// + /// The minimum length of the returned . If less than 256, a buffer of size 256 will be returned. + /// A memory buffer of at least bytes. If is less than 256, a buffer of size 256 will be returned. public Memory GetMemory(int sizeHint = 0) { if (sizeHint < 256) @@ -41,39 +64,82 @@ public Memory GetMemory(int sizeHint = 0) int sizeToRent = sizeHint > _segmentSize ? sizeHint : _segmentSize; - if (_segments.Count == 0) + if (_buffers.Length == 0) { - _segments.Add(new UnsafeBufferSegment { Array = new char[sizeToRent], Written = 0 }); + ExpandBuffers(sizeToRent); } - var last = _segments[_segments.Count - 1]; - int remaining = last.Array.Length - last.Written; - if (remaining >= sizeHint) + ref UnsafeBufferSegment last = ref _buffers[_count - 1]; + Memory free = last.Array.AsMemory(last.Written); + if (free.Length >= sizeHint) { - return last.Array.AsMemory(last.Written); + return free; } - // Allocate a new segment - _segments.Add(new UnsafeBufferSegment { Array = new char[sizeToRent], Written = 0 }); - return _segments[_segments.Count - 1].Array; + // else allocate a new buffer: + ExpandBuffers(sizeToRent); + + return _buffers[_count - 1].Array; + } + + private readonly object _lock = new object(); + private void ExpandBuffers(int sizeToRent) + { + lock (_lock) + { + int bufferCount = _count == 0 ? 1 : _count * 2; + + UnsafeBufferSegment[] resized = new UnsafeBufferSegment[bufferCount]; + if (_count > 0) + { + _buffers.CopyTo(resized, 0); + } + _buffers = resized; + _buffers[_count].Array = _pool.Rent(sizeToRent); + _count = bufferCount == 1 ? bufferCount : _count + 1; + } } + /// + /// Returns a to write to that is at least the requested size, as specified by the parameter. + /// + /// The minimum length of the returned . If less than 256, a buffer of size 256 will be returned. + /// A buffer of at least bytes. If is less than 256, a buffer of size 256 will be returned. public Span GetSpan(int sizeHint = 0) { Memory memory = GetMemory(sizeHint); return memory.Span; } + /// + /// Disposes the SequenceWriter and returns the underlying buffers to the pool. + /// public void Dispose() { - _segments.Clear(); + int bufferCountToFree; + UnsafeBufferSegment[] buffersToFree; + lock (_lock) + { + bufferCountToFree = _count; + buffersToFree = _buffers; + _count = 0; + _buffers = Array.Empty(); + } + + for (int i = 0; i < bufferCountToFree; i++) + { + _pool.Return(buffersToFree[i].Array); + } } public Reader ExtractReader() { - var buffers = _segments.ToArray(); - var count = buffers.Length; - _segments.Clear(); - return new ReaderInstance(buffers, count); + lock (_lock) + { + Reader reader = new ReaderInstance(_buffers, _count); + _count = 0; + _buffers = Array.Empty(); + return reader; + } } } diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 0713180ef56..219f3dfd050 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -35,6 +35,10 @@ COPY playground-server/sshd_config /etc/ssh/ ENV DOTNET_ENVIRONMENT=Production ENV GENERATOR_PATH=/app/generator/Microsoft.TypeSpec.Generator.dll ENV TMPDIR=/tmp +# Collect full dump on crash +ENV DOTNET_DbgEnableMiniDump=1 +ENV DOTNET_DbgMiniDumpType=4 +ENV DOTNET_DbgMiniDumpName=/home/coredump.%p EXPOSE 5174 2222 COPY playground-server/entrypoint.sh /entrypoint.sh From c092798708d848eda9daed6343fb9716bd796d81 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 19:23:58 -0700 Subject: [PATCH 100/103] fix: add lldb-18 to Dockerfile Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/playground-server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile index 219f3dfd050..02453cab263 100644 --- a/packages/http-client-csharp/playground-server/Dockerfile +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -25,8 +25,8 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime WORKDIR /app COPY --from=build /app . -# Install SSH for remote debugging -RUN apt-get update && apt-get install -y openssh-server \ +# Install SSH and lldb for remote debugging +RUN apt-get update && apt-get install -y openssh-server lldb-18 \ && echo "root:Docker!" | chpasswd \ && mkdir -p /run/sshd \ && apt-get clean && rm -rf /var/lib/apt/lists/* From d6bd7ba0e67bfde48a8d2c77acf53619dd780e31 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 19:40:13 -0700 Subject: [PATCH 101/103] fix: remove dedicated thread so crash happens on threadpool (preserves dump state) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/CSharpGen.cs | 39 +------------------ 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 2519c2fc906..87cdd5da2f7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -5,10 +5,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.SourceInput; using Microsoft.TypeSpec.Generator.Utilities; @@ -99,45 +97,12 @@ public async Task ExecuteAsync() // Ensure back-compatibility processing is done after all visitors have run outputType.ProcessTypeForBackCompatibility(); - Console.Error.WriteLine($"[diag] BackCompat done for {outputType.Name}"); - Console.Error.Flush(); var writer = CodeModelGenerator.Instance.GetWriter(outputType); Console.Error.WriteLine($"[diag] Writer created for {outputType.Name} (methods: {outputType.Methods.Count}, props: {outputType.Properties.Count}), calling Write()"); Console.Error.Flush(); - - // Run Write on a new thread with a large stack to rule out stack overflow - CodeFile? codeFile = null; - Exception? writeError = null; - var writeThread = new Thread(() => - { - try - { - codeFile = writer.Write(); - } - catch (Exception ex) - { - writeError = ex; - } - }, 64 * 1024 * 1024); // 64MB stack - writeThread.Start(); - writeThread.Join(); - - if (writeError != null) - { - Console.Error.WriteLine($"[diag] Write() THREW: {writeError.GetType().Name}: {writeError.Message}"); - Console.Error.WriteLine(writeError.StackTrace); - Console.Error.Flush(); - throw writeError; - } - if (codeFile is null) - { - Console.Error.WriteLine($"[diag] Write() returned null - thread may have crashed"); - Console.Error.Flush(); - throw new InvalidOperationException($"Write() for {outputType.Name} returned null"); - } - - Console.Error.WriteLine($"[diag] Adding file {fileCount}: {codeFile!.Name} ({codeFile.Content.Length} chars)"); + var codeFile = writer.Write(); + Console.Error.WriteLine($"[diag] Adding file {fileCount}: {codeFile.Name} ({codeFile.Content.Length} chars)"); Console.Error.Flush(); await generatedCodeWorkspace.AddGeneratedFile(codeFile); Console.Error.WriteLine($"[diag] Added file {fileCount}: {codeFile.Name}"); From 464ed4c8d590a85558d13e8b1609f7dc2f45bf7e Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 20:15:50 -0700 Subject: [PATCH 102/103] fix: pre-warm ParameterProvider JIT compilation to avoid dispatch crash Force JIT compilation of IEquatable methods before Write path to ensure dispatch stubs are initialized. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.TypeSpec.Generator/src/CSharpGen.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 87cdd5da2f7..604c417fd7a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -28,6 +28,13 @@ public async Task ExecuteAsync() CodeModelGenerator.Instance.Emitter.Info("Starting code generation"); CodeModelGenerator.Instance.Stopwatch.Start(); + // Pre-warm JIT compilation of ParameterProvider interface methods + // to avoid potential dispatch issues in sandboxed container environments. + System.Runtime.CompilerServices.RuntimeHelpers.PrepareMethod( + typeof(Providers.ParameterProvider).GetMethod("Equals", new[] { typeof(Providers.ParameterProvider) })!.MethodHandle); + System.Runtime.CompilerServices.RuntimeHelpers.PrepareMethod( + typeof(Providers.ParameterProvider).GetMethod("GetHashCode", Type.EmptyTypes)!.MethodHandle); + CodeModelGenerator.Instance.Emitter.Info("[diag] Before GeneratedCodeWorkspace.Initialize()"); GeneratedCodeWorkspace.Initialize(); CodeModelGenerator.Instance.Emitter.Info("[diag] After GeneratedCodeWorkspace.Initialize()"); From b600c54eab7a30a4284c43afeab92e2acf8e11e0 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 11 Apr 2026 20:51:44 -0700 Subject: [PATCH 103/103] fix: log argument types in WriteArguments to identify crash target Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Writers/CodeWriter.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs index 548f0628bc4..5bf718fd838 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs @@ -1066,13 +1066,20 @@ public void WriteArguments(IEnumerable arguments, bool useSingl { AppendRaw("("); var iterator = arguments.GetEnumerator(); + int argIdx = 0; if (iterator.MoveNext()) { - iterator.Current.Write(this); + Console.Error.WriteLine($"[diag] WriteArguments arg {argIdx}: {iterator.Current?.GetType().FullName ?? "NULL"}"); + Console.Error.Flush(); + iterator.Current!.Write(this); + argIdx++; while (iterator.MoveNext()) { AppendRaw(", "); - iterator.Current.Write(this); + Console.Error.WriteLine($"[diag] WriteArguments arg {argIdx}: {iterator.Current?.GetType().FullName ?? "NULL"}"); + Console.Error.Flush(); + iterator.Current!.Write(this); + argIdx++; } } AppendRaw(")");