diff --git a/.gitignore b/.gitignore index 23fd924..91e033c 100644 --- a/.gitignore +++ b/.gitignore @@ -257,3 +257,19 @@ paket-files/ *.binlog *.lscache + +# Aspire inner-loop +.azurite/ +samples/*/bin/sample/ +bin/index/ + + +# Aspire CLI / tooling state +.agents/ +.playwright/ +.modules/ + + +# HtmlGenerator drops Errors.txt into its working directory on startup. +src/SourceBrowser/src/HtmlGenerator/Errors.txt + diff --git a/Directory.Packages.props b/Directory.Packages.props index e17e5e9..0849208 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,3 +1,13 @@ - + + + + + + + + + + + \ No newline at end of file diff --git a/NuGet.config b/NuGet.config index 8ec79f9..2724eaf 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,11 +1,20 @@ - + + - + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index e64b2d7..19ed0dd 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ This repo contains the code for building http://source.dot.net ## Documentation +- [Local inner loop (Aspire)](docs/inner-loop.md) - **Recommended for development.** Runs the full pipeline (sample build → BinLogToSln → HtmlGenerator → blob → SourceIndexServer) locally against a tiny sample library. - [Source Selection Algorithm](docs/source-selection-algorithm.md) - How the indexer chooses the best implementation when multiple builds exist for the same assembly ## Build Status @@ -10,17 +11,27 @@ This repo contains the code for building http://source.dot.net ## What Is It? This repo uses https://github.com/KirillOsenkov/SourceBrowser (with a few additions here https://github.com/dotnet/SourceBrowser/tree/source-indexer) to index the dotnet sources and produce a navigatable and searchable website containing the full source code. This includes code from the runtime, winforms, wpf, aspnetcore, and msbuild, among others. For a full list see here https://github.com/dotnet/source-indexer/blob/main/src/index/repositories.props. -## Build Prerequsites -The build requires .NET 8.0 and Visual Studio 2022 to build. +## Local development (Aspire) +The repository ships with an [Aspire](https://aspire.dev) AppHost (`src/source-indexer.AppHost/`) that emulates the full production pipeline locally against a tiny sample library — **this is the recommended way to work on the repo**. + +```pwsh +aspire start +``` + +The dashboard URL (with login token) is printed to the console. From there you can run the `bootstrap-all` step to build the sample, run `BinLogToSln`, run `HtmlGenerator`, upload to Azurite, and start the web app. See [docs/inner-loop.md](docs/inner-loop.md) for the full resource graph and walkthroughs. + +You can also open the repo in **Visual Studio** or **VS Code** to set breakpoints and debug individual components (e.g., `HtmlGenerator`, `SourceIndexServer`, `BinLogToSln`) while the rest of the pipeline runs under the AppHost. + +## Building the production index (Windows-only) +The official pipeline build only works on Windows because `HtmlGenerator` is a .NET Framework executable. For local dev prefer the Aspire flow above; use this path only if you need to reproduce the exact pipeline build. + +**Prerequisites:** .NET 8.0 and Visual Studio 2022. -## Build -The build will only work on windows because the source indexer executable is a .net framework executable. 1. `git clone https://github.com/dotnet/source-indexer.git` 2. For each *.sln file `dotnet restore` 3. Find VS 2022 msbuild.exe on your machine, typically found at `C:\Program Files (x86)\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe` 4. `msbuild build.proj` -## Running the built index After the build is finished the index will exist in `bin\index` and can be run by running `dotnet Microsoft.SourceBrowser.SourceIndexServer.dll` in that folder. The index will be served on `http://localhost:5000` ## Deployment diff --git a/aspire.config.json b/aspire.config.json new file mode 100644 index 0000000..38a0f70 --- /dev/null +++ b/aspire.config.json @@ -0,0 +1,5 @@ +{ + "appHost": { + "language": "csharp" + } +} \ No newline at end of file diff --git a/docs/images/bootstrap-menu.png b/docs/images/bootstrap-menu.png new file mode 100644 index 0000000..e195777 Binary files /dev/null and b/docs/images/bootstrap-menu.png differ diff --git a/docs/inner-loop.md b/docs/inner-loop.md new file mode 100644 index 0000000..5e1b871 --- /dev/null +++ b/docs/inner-loop.md @@ -0,0 +1,112 @@ +# Local inner loop (Aspire) + +`src/source-indexer.AppHost/` is an [Aspire](https://aspire.dev) AppHost that +emulates the production indexing pipeline end-to-end against a tiny sample +library (`samples/MiniRuntime/`). It lets you exercise the full +binlog → HtmlGenerator → blob → SourceIndexServer chain locally without +pushing to ADO. + +## Prerequisites + +- **.NET SDK 10** (pinned in root `global.json`). +- **Docker / Podman** running locally (Azurite emulators run as containers). +- **Aspire CLI** — see . +- **Azure CLI** (`az`) on PATH — used by the `publish-index` step. + +Then from the repo root: + +```pwsh +aspire start +``` + +The dashboard URL (with login token) is printed to the console. + +## Resource graph + +Auto-started (running as soon as the AppHost is up): + +| Resource | Type | Purpose | +|---|---|---| +| `stage1Storage` | Azurite emulator | Models `netsourceindexstage1` (V2 upstream upload destination). | +| `prodStorage` | Azurite emulator | Models `netsourceindexprod` (final HTML index destination). | +| `stage1` | Blob container under `stage1Storage` | Where `upload-stage1` drops the `.tar.gz`. | +| `index-local` | Blob container under `prodStorage` | Where the generated HTML index is uploaded. The web app reads from here. | +| `web` | `SourceIndexServer` project | ASP.NET Core app. `SOURCE_BROWSER_INDEX_PROXY_URL` is wired to `index-local` so it serves the indexed HTML out of Azurite. | + +Explicit-start (stopped by default — click **Start** in the dashboard): + +| Resource | Type | What it does | +|---|---|---| +| `step1-sample-build` | Executable: `dotnet build /bl:` | Builds `samples/MiniRuntime` and produces `samples/MiniRuntime/bin/sample/msbuild.binlog`. | +| `step2-upload-stage1` | Project: `UploadIndexStage1` | Tars+gzips the sample folder + binlog and uploads it to `stage1`. Real V2 upstream tool — fully dogfooded. | +| `step3-htmlgenerator` | Project: `HtmlGenerator` | Runs `HtmlGenerator` on the binlog from `step1-sample-build` to produce static HTML under `bin/index/`. | +| `step4-publish-index` | Executable: `az storage blob upload-batch` | Uploads `bin/index/index/` to the `index-local` container in `prodStorage`. | + +## One-click bootstrap + +The `prodStorage` resource exposes a custom **Bootstrap full pipeline** +command (internal id `bootstrap-all`) that runs the four pipeline +resources in order (`step1-sample-build` → `step2-upload-stage1` → +`step3-htmlgenerator` → `step4-publish-index`) and waits for each one to +finish. This is the easy first-run path: + +1. `aspire start` +2. Open the dashboard. +3. On the `prodStorage` row, click the **⋯** button in the Actions + column and choose **Bootstrap full pipeline**. +4. Wait for it to finish (watch the logs). +5. Open the `web` URL — you should see `MiniRuntime`'s indexed HTML. + +![Bootstrap full pipeline menu on prodStorage](images/bootstrap-menu.png) + +Re-running any individual resource regenerates just that stage. + +## How this maps to prod + +| Prod | Local inner loop | +|---|---| +| V2 upstream repo publishes `.tar.gz` to `netsourceindexstage1/stage1//.tar.gz` (`UploadIndexStage1`) | `step2-upload-stage1` resource → `stage1Storage/stage1` | +| `HtmlGenerator.exe` reads binlog + writes HTML to `bin/index/` (`src/index/index.proj`) | `step3-htmlgenerator` resource | +| `AzureFileCopy@6` uploads `bin/index/index/*` to `netsourceindexprod/index-/` (`azure-pipelines.yml`) | `step4-publish-index` resource (`az storage blob upload-batch`) → `prodStorage/index-local` | +| App Service slot setting `SOURCE_BROWSER_INDEX_PROXY_URL` flipped to the new container (`deployment/deploy-storage-proxy.ps1`) | `web` resource started with `SOURCE_BROWSER_INDEX_PROXY_URL` env var pointing at `prodStorage/index-local` | + +See `docs/handoff/03-indexing-pipeline.md` and +`docs/handoff/05-azure-pipeline.md` for the full prod mechanics. + +## Auth: Azurite vs prod + +Production uses managed identity (`TokenCredential`) to talk to Azure +Storage. Azurite only speaks shared-key / connection-string auth. To keep +both paths working without a fork, `SourceIndexServer/Models/AzureBlobFileSystem.cs` +and `UploadIndexStage1/Program.cs` check for `AZURE_STORAGE_CONNECTION_STRING` +in the environment: + +- If set → use `BlobServiceClient(connectionString)` (Azurite-friendly). +- If not set → use the original `BlobServiceClient(uri, TokenCredential)` + code path (prod-friendly). + +Aspire injects the connection string automatically via `WithReference(...)` +on the blob resources in the AppHost. Prod is unaffected. + +## Persistence + +The two Azurite emulators use `ContainerLifetime.Persistent` and bind-mount +their data directories to `.azurite/stage1` and `.azurite/prod` at the repo +root. Blobs uploaded during a session survive `aspire start` restarts and +container recreations — to wipe state, just delete the `.azurite/` folder. +The folder is gitignored. + +## Debugging individual components + +The AppHost runs each stage as its own resource, but the underlying projects are normal .NET projects you can debug directly in **Visual Studio** or **VS Code**: + +- **`HtmlGenerator`** (`src/SourceBrowser/src/HtmlGenerator/`) — set breakpoints, then either start it from the IDE pointing at an existing binlog under `samples/MiniRuntime/bin/sample/`, or attach to the `step3-htmlgenerator` process after kicking it off from the dashboard. +- **`SourceIndexServer`** (`src/SourceBrowser/src/SourceIndexServer/`) — F5 from the IDE for fast inner-loop on the web UI. To debug against Azurite data, set `AZURE_STORAGE_CONNECTION_STRING` and `SOURCE_BROWSER_INDEX_PROXY_URL` from the running AppHost (visible on the `web` resource's env vars panel in the dashboard) in your launch profile. +- **`UploadIndexStage1`** (`src/UploadIndexStage1/`) — same pattern: copy the env vars off the AppHost's `step2-upload-stage1` resource and run from the IDE. +- **`BinLogToSln`** (`src/SourceBrowser/src/BinLogToSln/`) — runnable directly against any `.binlog` produced by `step1-sample-build`. + +The Aspire dashboard's per-resource "Environment" tab is the source of truth for the env vars Aspire injects — copy those into your `launchSettings.json` to reproduce the AppHost environment under a debugger. + +## Open follow-ups + +- **End-to-end OTel validation**: ServiceDefaults are wired into `SourceIndexServer` and `UploadIndexStage1`, but flowing traces across `HtmlGenerator` (net472) into the dashboard hasn't been validated yet. diff --git a/samples/MiniRuntime/MiniArray.cs b/samples/MiniRuntime/MiniArray.cs new file mode 100644 index 0000000..c5ee1c7 --- /dev/null +++ b/samples/MiniRuntime/MiniArray.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace MiniRuntime; + +/// +/// A minimal heap-backed array wrapper used to demonstrate generics and +/// indexer documentation in the indexed HTML output. +/// +/// The element type stored in the array. +public sealed class MiniArray : IReadOnlyList +{ + private readonly T[] _items; + + /// + /// Initializes a new with the given length. + /// + /// The number of elements the array will hold. + /// + /// Thrown if is negative. + /// + public MiniArray(int length) + { + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + _items = new T[length]; + } + + /// + /// Gets or sets the element at the given index. + /// + /// The zero-based index. + public T this[int index] + { + get => _items[index]; + set => _items[index] = value; + } + + /// + /// Gets the number of elements in the array. + /// + public int Count => _items.Length; + + /// + public IEnumerator GetEnumerator() + { + for (int i = 0; i < _items.Length; i++) + { + yield return _items[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + internal T[] UnsafeBuffer => _items; +} diff --git a/samples/MiniRuntime/MiniRuntime.csproj b/samples/MiniRuntime/MiniRuntime.csproj new file mode 100644 index 0000000..e2760ac --- /dev/null +++ b/samples/MiniRuntime/MiniRuntime.csproj @@ -0,0 +1,16 @@ + + + + net9.0 + MiniRuntime + MiniRuntime + true + enable + enable + $(NoWarn);CS1591 + + false + + + diff --git a/samples/MiniRuntime/MiniRuntimeInfo.cs b/samples/MiniRuntime/MiniRuntimeInfo.cs new file mode 100644 index 0000000..6404938 --- /dev/null +++ b/samples/MiniRuntime/MiniRuntimeInfo.cs @@ -0,0 +1,33 @@ +namespace MiniRuntime; + +/// +/// Provides static information about the MiniRuntime fixture library. +/// +/// +/// This type exists so the source indexer has something with XML doc +/// comments, public surface, and internal helpers to render. +/// +public static class MiniRuntimeInfo +{ + /// + /// Gets the human-readable name of this runtime fixture. + /// + public static string Name => "MiniRuntime"; + + /// + /// Gets the version string for this runtime fixture. + /// + public static string Version => "0.1.0-inner-loop"; + + /// + /// Returns a banner suitable for logging at startup. + /// + /// A formatted banner string. + public static string GetBanner() => Banners.Format(Name, Version); + + internal static class Banners + { + internal static string Format(string name, string version) + => $"=== {name} v{version} ==="; + } +} diff --git a/samples/MiniRuntime/MiniSpan.cs b/samples/MiniRuntime/MiniSpan.cs new file mode 100644 index 0000000..1f24d31 --- /dev/null +++ b/samples/MiniRuntime/MiniSpan.cs @@ -0,0 +1,61 @@ +using System; + +namespace MiniRuntime; + +/// +/// A trivial slice-like view over a . +/// +/// The element type. +public readonly struct MiniSpan +{ + private readonly MiniArray _source; + private readonly int _start; + private readonly int _length; + + /// + /// Initializes a new over the given array slice. + /// + /// The backing array. + /// The inclusive start index. + /// The slice length. + public MiniSpan(MiniArray source, int start, int length) + { + ArgumentNullException.ThrowIfNull(source); + + if ((uint)start > (uint)source.Count) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + + if ((uint)length > (uint)(source.Count - start)) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + _source = source; + _start = start; + _length = length; + } + + /// Gets the length of the span. + public int Length => _length; + + /// Gets a reference to the element at the given offset. + /// The zero-based offset within the span. + public T this[int index] + { + get + { + if ((uint)index >= (uint)_length) + { + throw new IndexOutOfRangeException(); + } + + return _source[_start + index]; + } + } + + /// Slices this span further. + public MiniSpan Slice(int start, int length) => + new MiniSpan(_source, _start + start, length); +} diff --git a/samples/MiniRuntime/README.md b/samples/MiniRuntime/README.md new file mode 100644 index 0000000..704fc9f --- /dev/null +++ b/samples/MiniRuntime/README.md @@ -0,0 +1,8 @@ +# MiniRuntime + +A tiny stand-in for a "real" upstream repo (think `dotnet/runtime`) used as a +fixture by the local Aspire inner loop in `src/source-indexer.AppHost`. + +It exists purely so the inner loop has something small to build, capture a +binlog from, push to Azurite, and index with `HtmlGenerator`. It is not +published anywhere and is not part of the source-indexer product. diff --git a/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs b/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs index 5b380a7..e502aec 100644 --- a/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs +++ b/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs @@ -16,6 +16,17 @@ public class AzureBlobFileSystem : IFileSystem public AzureBlobFileSystem(string uri) { + // If AZURE_STORAGE_CONNECTION_STRING is set, initialize the client from + // the connection string. The container name still comes from `uri` so + // the existing SOURCE_BROWSER_INDEX_PROXY_URL contract is preserved. + string? connectionString = Environment.GetEnvironmentVariable("AZURE_STORAGE_CONNECTION_STRING"); + if (!string.IsNullOrEmpty(connectionString)) + { + string containerName = GetContainerNameFromUri(uri); + container = new BlobContainerClient(connectionString, containerName); + return; + } + var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID"); credential = string.IsNullOrEmpty(clientId) ? new AzureCliCredential() @@ -25,6 +36,39 @@ public AzureBlobFileSystem(string uri) credential); } + private static string GetContainerNameFromUri(string uri) + { + // The connection-string form of `uri` may include a ContainerName= + // segment (e.g. "DefaultEndpointsProtocol=...;ContainerName=index"). + // Honor that segment first so callers can pass a full connection + // string instead of a URL. + if (uri.Contains("ContainerName=", StringComparison.OrdinalIgnoreCase)) + { + foreach (var segment in uri.Split(';', StringSplitOptions.RemoveEmptyEntries)) + { + if (segment.StartsWith("ContainerName=", StringComparison.OrdinalIgnoreCase)) + { + return segment["ContainerName=".Length..].Trim(); + } + } + } + + var parsed = new Uri(uri); + // Azurite URLs are http://host:port/devstoreaccount1/, real + // Azure URLs are https://.blob.core.windows.net/. + // In both cases the last non-empty path segment is the container. + string[] segments = parsed.AbsolutePath + .Split('/', StringSplitOptions.RemoveEmptyEntries); + if (segments.Length == 0) + { + throw new ArgumentException( + $"URI '{uri}' has no path segments; cannot derive container name.", + nameof(uri)); + } + + return segments[segments.Length - 1]; + } + public bool DirectoryExists(string name) { return true; diff --git a/src/SourceBrowser/src/SourceIndexServer/Program.cs b/src/SourceBrowser/src/SourceIndexServer/Program.cs index 97afa57..4ccdaaa 100644 --- a/src/SourceBrowser/src/SourceIndexServer/Program.cs +++ b/src/SourceBrowser/src/SourceIndexServer/Program.cs @@ -1,30 +1,85 @@ -using Microsoft.AspNetCore.Hosting; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileProviders.Physical; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.SourceBrowser.SourceIndexServer.Models; namespace Microsoft.SourceBrowser.SourceIndexServer { public class Program { + public static ILogger Logger { get; set; } + public static void Main(string[] args) { - BuildWebHost(args).Run(); - } + var builder = WebApplication.CreateBuilder(args); - public static ILogger Logger { get; set; } + builder.AddServiceDefaults(); + + builder.Logging.ClearProviders(); + builder.Logging.AddConsole(); + builder.Logging.AddAzureWebAppDiagnostics(); + + builder.Host.UseWindowsService(); + + var rootPath = Path.Combine(builder.Environment.ContentRootPath, "index"); + var subfolder = Path.Combine(rootPath, "index"); + if (File.Exists(Path.Combine(subfolder, "Projects.txt"))) + { + rootPath = subfolder; + } + + builder.Services.AddSingleton(new Index(rootPath)); + builder.Services.AddControllersWithViews(); + builder.Services.AddRazorPages(); + + var app = builder.Build(); - public static IHost BuildWebHost(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults( - builder => { builder - .UseStartup(); }) - .ConfigureLogging(logging => - { - logging.ClearProviders(); - logging.AddConsole(); - logging.AddAzureWebAppDiagnostics(); - }) - .UseWindowsService() - .Build(); + app.MapDefaultEndpoints(); + + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost, + KnownNetworks = { }, + KnownProxies = { } + }); + + app.Use(async (context, next) => + { + context.Response.Headers["X-UA-Compatible"] = "IE=edge"; + await next(); + }); + + if (app.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.Use(Helpers.ServeProxiedIndex); + + app.UseDefaultFiles(); + if (Directory.Exists(rootPath)) + { + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(rootPath, ExclusionFilters.Sensitive & ~ExclusionFilters.DotPrefixed), + }); + } + app.UseStaticFiles(); + app.UseRouting(); + + app.MapRazorPages(); + app.MapControllers(); + + Logger = app.Services.GetService>(); + + app.Run(); + } } } diff --git a/src/SourceBrowser/src/SourceIndexServer/SourceIndexServer.csproj b/src/SourceBrowser/src/SourceIndexServer/SourceIndexServer.csproj index d2fe663..3c1b620 100644 --- a/src/SourceBrowser/src/SourceIndexServer/SourceIndexServer.csproj +++ b/src/SourceBrowser/src/SourceIndexServer/SourceIndexServer.csproj @@ -16,6 +16,7 @@ + diff --git a/src/SourceBrowser/src/SourceIndexServer/Startup.cs b/src/SourceBrowser/src/SourceIndexServer/Startup.cs deleted file mode 100644 index 782b8af..0000000 --- a/src/SourceBrowser/src/SourceIndexServer/Startup.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System.Collections; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.FileProviders.Physical; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.SourceBrowser.SourceIndexServer.Models; - -namespace Microsoft.SourceBrowser.SourceIndexServer -{ - public class Startup - { - public Startup(IWebHostEnvironment env) - { - Environment = env; - } - - public IWebHostEnvironment Environment { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - RootPath = Path.Combine(Environment.ContentRootPath, "index"); - - var subfolder = Path.Combine(RootPath, "index"); - if (File.Exists(Path.Combine(subfolder, "Projects.txt"))) - { - RootPath = subfolder; - } - - services.AddSingleton(new Index(RootPath)); - services.AddControllersWithViews(); - services.AddRazorPages(); - - // Add health checks - //services.AddHealthChecks() - //.AddCheck( - //name: "storage", - //tags: ["ready"]) - //.AddCheck( - //name: "startup", - //check: () => HealthCheckResult.Healthy("Application is running"), - //tags: ["alive"]); - } - - public string RootPath { get; set; } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - // Configure forwarded headers for Azure Front Door - app.UseForwardedHeaders(new ForwardedHeadersOptions - { - ForwardedHeaders = ForwardedHeaders.XForwardedFor | - ForwardedHeaders.XForwardedProto | - ForwardedHeaders.XForwardedHost, - KnownNetworks = { }, - KnownProxies = { } - }); - - app.Use(async (context, next) => - { - context.Response.Headers["X-UA-Compatible"] = "IE=edge"; - await next(); - }); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.Use(Helpers.ServeProxiedIndex); - - app.UseDefaultFiles(); - if (Directory.Exists(RootPath)) - { - app.UseStaticFiles(new StaticFileOptions - { - FileProvider = new PhysicalFileProvider(RootPath, ExclusionFilters.Sensitive & ~ExclusionFilters.DotPrefixed), - }); - } - app.UseStaticFiles(); - app.UseRouting(); - - app.UseEndpoints(endPoints => - { - //const int healthCacheSeconds = 30; - - //static Task CacheableMinimalResponse(HttpContext context, HealthReport report) - //{ - //context.Response.Headers.CacheControl = $"public,max-age={healthCacheSeconds}"; - //context.Response.Headers.Pragma = "public"; - //context.Response.Headers.Expires = "0"; - //return HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse(context, report); - //} - - //// Health check endpoints - //// Basic health check with minimal information (cached by default) - //endPoints.MapHealthChecks("/health", new HealthCheckOptions - //{ - //Predicate = _ => true, - //ResponseWriter = CacheableMinimalResponse - //}); - - //// Liveness probe (always healthy if app is running) - //endPoints.MapHealthChecks("/health/alive", new HealthCheckOptions - //{ - //Predicate = check => check.Tags.Contains("alive"), - //ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse - //}); - - //if (env.IsDevelopment() || Helpers.DebugLoggingEnabled) - //{ - //// Detailed health check with full diagnostics - //endPoints.MapHealthChecks("/health/detailed", new HealthCheckOptions - //{ - //Predicate = _ => true, - //ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteResponse - //}); - - //// Readiness probe (checks storage) - //endPoints.MapHealthChecks("/health/ready", new HealthCheckOptions - //{ - //Predicate = check => check.Tags.Contains("ready"), - //ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse - //}); - //} - - endPoints.MapRazorPages(); - endPoints.MapControllers(); - }); - - // Retrieve and store the logger - Program.Logger = app.ApplicationServices.GetService>(); - } - } -} diff --git a/src/UploadIndexStage1/Program.cs b/src/UploadIndexStage1/Program.cs index 3b3d82a..6b0dfab 100644 --- a/src/UploadIndexStage1/Program.cs +++ b/src/UploadIndexStage1/Program.cs @@ -12,6 +12,7 @@ using Azure.Storage.Blobs.Models; using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.Tar; +using Microsoft.Extensions.Hosting; using Mono.Options; namespace UploadIndexStage1 @@ -19,12 +20,29 @@ namespace UploadIndexStage1 class Program { static async Task Main(string[] args) + { + var hostBuilder = Host.CreateApplicationBuilder(args); + hostBuilder.AddServiceDefaults(); + using var host = hostBuilder.Build(); + await host.StartAsync(); + try + { + await RunAsync(args); + } + finally + { + await host.StopAsync(); + } + } + + static async Task RunAsync(string[] args) { string sourceFolder = null; string repoName = null; string clientId = null; string storageAccount = null; string blobContainer = null; + string connectionString = null; var options = new OptionSet { {"i=", "The source folder", i => sourceFolder = i}, @@ -32,6 +50,7 @@ static async Task Main(string[] args) {"c=", "The Azure Client ID (optional)", c => clientId = c}, {"s=", "The destination storage account name or URL", s => storageAccount = s}, {"b=", "The destination storage account container", b => blobContainer = b}, + {"connection-string=", "Optional connection string for blob auth. When set, -s/-c are ignored.", cs => connectionString = cs}, }; List extra = options.Parse(args); @@ -51,9 +70,9 @@ static async Task Main(string[] args) Fatal("Missing argument -n"); } - if (string.IsNullOrEmpty(storageAccount)) + if (string.IsNullOrEmpty(connectionString)) { - Fatal("Missing argument -s"); + connectionString = Environment.GetEnvironmentVariable("AZURE_STORAGE_CONNECTION_STRING"); } if (string.IsNullOrEmpty(blobContainer)) @@ -61,37 +80,52 @@ static async Task Main(string[] args) Fatal("Missing argument -b"); } - if (!storageAccount.StartsWith("https://")) - { - storageAccount = "https://" + storageAccount + ".blob.core.windows.net"; - } - + BlobServiceClient blobServiceClient; using AzureEventSourceListener listener = AzureEventSourceListener.CreateConsoleLogger(); - TokenCredential credential; - - if (string.IsNullOrEmpty(clientId) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ARM_CLIENT_ID"))) + if (!string.IsNullOrEmpty(connectionString)) { - clientId = Environment.GetEnvironmentVariable("ARM_CLIENT_ID"); - System.Console.WriteLine("Found client ID in environment variable; using it"); - } - - if (string.IsNullOrEmpty(clientId)) - { - credential = new AzureCliCredential(); - System.Console.WriteLine("Trying to use managed identity without default identity"); + Console.WriteLine("Using connection-string for blob auth."); + blobServiceClient = new BlobServiceClient(connectionString); } else { - System.Console.WriteLine("Trying to use ManagedIdentityCredential with ClientID"); - credential = new ManagedIdentityCredential(clientId); - } + if (string.IsNullOrEmpty(storageAccount)) + { + Fatal("Missing argument -s (or supply --connection-string / AZURE_STORAGE_CONNECTION_STRING)"); + } + + if (!storageAccount.StartsWith("https://")) + { + storageAccount = "https://" + storageAccount + ".blob.core.windows.net"; + } + + TokenCredential credential; - BlobServiceClient blobServiceClient = new( - new Uri(storageAccount), - credential); + if (string.IsNullOrEmpty(clientId) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ARM_CLIENT_ID"))) + { + clientId = Environment.GetEnvironmentVariable("ARM_CLIENT_ID"); + System.Console.WriteLine("Found client ID in environment variable; using it"); + } + + if (string.IsNullOrEmpty(clientId)) + { + credential = new AzureCliCredential(); + System.Console.WriteLine("Trying to use managed identity without default identity"); + } + else + { + System.Console.WriteLine("Trying to use ManagedIdentityCredential with ClientID"); + credential = new ManagedIdentityCredential(clientId); + } + + blobServiceClient = new BlobServiceClient( + new Uri(storageAccount), + credential); + } var containerClient = blobServiceClient.GetBlobContainerClient(blobContainer); + await containerClient.CreateIfNotExistsAsync(); string newBlobName = $"{repoName}/{DateTime.UtcNow:O}.tar.gz"; BlobClient newBlobClient = containerClient.GetBlobClient(newBlobName); diff --git a/src/UploadIndexStage1/UploadIndexStage1.csproj b/src/UploadIndexStage1/UploadIndexStage1.csproj index 1af973c..739b06b 100644 --- a/src/UploadIndexStage1/UploadIndexStage1.csproj +++ b/src/UploadIndexStage1/UploadIndexStage1.csproj @@ -16,4 +16,8 @@ + + + + diff --git a/src/source-indexer.AppHost/AppHost.cs b/src/source-indexer.AppHost/AppHost.cs new file mode 100644 index 0000000..7438345 --- /dev/null +++ b/src/source-indexer.AppHost/AppHost.cs @@ -0,0 +1,224 @@ +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Azure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +var builder = DistributedApplication.CreateBuilder(args); + +// ============================================================================= +// Storage — emulates the two prod storage accounts via Azurite. +// +// Prod has TWO storage accounts: +// netsourceindexstage1 ← V2 upstream repos push /.tar.gz here +// netsourceindexprod ← HtmlGenerator's output is uploaded as index-/ +// +// We model each as its own AddAzureStorage().RunAsEmulator() (= two Azurite +// containers) so the boundary is visually distinct in the dashboard. +// ============================================================================= + +var stage1Storage = builder.AddAzureStorage("stage1Storage") + .RunAsEmulator(azurite => + { + azurite.WithLifetime(ContainerLifetime.Persistent) + .WithDataBindMount(".azurite/stage1"); + }); + +var stage1Blobs = stage1Storage.AddBlobs("stage1-blobs"); +var stage1Container = stage1Storage.AddBlobContainer("stage1", blobContainerName: "stage1"); + +var prodStorage = builder.AddAzureStorage("prodStorage") + .RunAsEmulator(azurite => + { + azurite.WithLifetime(ContainerLifetime.Persistent) + .WithDataBindMount(".azurite/prod"); + }); + +var prodBlobs = prodStorage.AddBlobs("prod-blobs"); +var indexContainer = prodStorage.AddBlobContainer("index-local", blobContainerName: "index-local"); + +// ============================================================================= +// Pipeline resources — all WithExplicitStart() so they only run when the user +// clicks "Start" in the dashboard (or via the bootstrap-all composite command). +// They emulate the prod stages from azure-pipelines.yml / src/index/index.proj. +// ============================================================================= + +string repoRoot = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, "..", "..")); +string sampleProj = Path.Combine(repoRoot, "samples", "MiniRuntime", "MiniRuntime.csproj"); +string sampleBinDir = Path.Combine(repoRoot, "samples", "MiniRuntime", "bin", "sample"); +string sampleBinlog = Path.Combine(sampleBinDir, "msbuild.binlog"); +string sampleSourceDir = Path.Combine(repoRoot, "samples", "MiniRuntime"); +string indexOutDir = Path.Combine(repoRoot, "bin", "index"); +string indexUploadDir = Path.Combine(indexOutDir, "index"); + +// 1. sample-build — `dotnet build /bl:` on MiniRuntime, producing a binlog. +// Emulates: Arcade-driven V1 repo build that produces a binlog. +// Use /t:Rebuild so MSBuild always re-invokes the C# compiler — otherwise an +// incremental no-op build produces a binlog with zero Csc invocations and +// HtmlGenerator finds nothing to index. +var sampleBuild = builder.AddExecutable( + "step1-sample-build", + "dotnet", + repoRoot, + "build", + sampleProj, + $"/bl:{sampleBinlog}", + "/t:Rebuild", + "-c", "Debug") + .WithExplicitStart(); + +// 2. upload-stage1 — runs the real UploadIndexStage1 tool against Azurite. +// Emulates: V2 upstream repos uploading their source bundle to +// netsourceindexstage1/stage1//.tar.gz. +// This is the same .NET console app that runs in upstream pipelines, so +// you can attach a debugger to it directly from the Aspire dashboard. +var uploadStage1 = builder.AddProject("step2-upload-stage1") + .WithExplicitStart() + .WithReference(stage1Blobs) + .WaitFor(stage1Container) + .WithEnvironment("AZURE_STORAGE_CONNECTION_STRING", stage1Blobs.Resource.ConnectionStringExpression) + .WithArgs( + "-i", sampleSourceDir, + "-n", "MiniRuntime", + "-b", "stage1"); + +// 3. htmlgenerator — runs HtmlGenerator (net472) over the binlog from step 1 +// and produces static HTML under bin/index/. HtmlGenerator targets net472 +// so we can't link its output assembly into the net10 AppHost, but +// `ReferenceOutputAssembly="false" SkipGetTargetFrameworkProperties="true"` +// on the ProjectReference still gets us the Projects.HtmlGenerator +// metadata, so we can use AddProject and get debugging / restart-on- +// rebuild for free. +var htmlGenerator = builder.AddProject("step3-htmlgenerator", launchProfileName: null) + .WithExplicitStart() + .WithArgs( + sampleBinlog, + $"/out:{indexOutDir}", + "/force", + $"/serverPath:{sampleSourceDir}=https://github.com/dotnet/source-indexer/tree/main/samples/MiniRuntime/"); + +// 4. normalize-case — lowercase every filename under bin/index/index/. +// SourceIndexServer.Helpers.ServeProxiedIndex lowercases incoming request +// paths before querying the blob (case-sensitive in Azure Storage), so any +// PascalCase output from HtmlGenerator (Projects.txt, results.html, etc.) +// would 404 if uploaded as-is. Prod runs the same script — see +// azure-pipelines.yml line ~165 (deployment/normalize-case.ps1). +var normalizeCase = builder.AddExecutable( + "step4-normalize-case", + "pwsh", + repoRoot, + "-NoProfile", + "-File", Path.Combine(repoRoot, "deployment", "normalize-case.ps1"), + "-Root", indexUploadDir) + .WithExplicitStart(); + +// 5. publish-index — wraps the Azure CLI (matching prod's AzureFileCopy@6 +// task). Uploads bin/index/index/* to prodStorage/index-local. +var publishIndex = builder.AddExecutable( + "step5-publish-index", + "az", + repoRoot, + "storage", "blob", "upload-batch", + "-s", indexUploadDir, + "-d", "index-local", + "--overwrite", + "true") + .WithExplicitStart() + .WithReference(prodBlobs) + .WaitFor(indexContainer) + .WithEnvironment(ctx => + { + // The az CLI reads AZURE_STORAGE_CONNECTION_STRING natively. + ctx.EnvironmentVariables["AZURE_STORAGE_CONNECTION_STRING"] = + prodBlobs.Resource.ConnectionStringExpression; + }); + +// ============================================================================= +// Web — SourceIndexServer, the real app. Auto-starts and serves indexed HTML +// out of prodStorage/index-local via SOURCE_BROWSER_INDEX_PROXY_URL (the same +// env var contract prod uses; see deployment/deploy-storage-proxy.ps1). +// ============================================================================= + +var web = builder.AddProject("web") + .WithReference(prodBlobs) + .WaitFor(indexContainer) + .WithEnvironment("AZURE_STORAGE_CONNECTION_STRING", prodBlobs.Resource.ConnectionStringExpression) + .WithEnvironment("SOURCE_BROWSER_INDEX_PROXY_URL", indexContainer.Resource.ConnectionStringExpression) + .WithExternalHttpEndpoints(); + +// ============================================================================= +// bootstrap-all — composite command that runs the full pipeline end-to-end. +// Attached to prodStorage (the "end of the pipeline" resource) since +// app-host-level commands aren't yet available in this Aspire SDK. +// ============================================================================= + +prodStorage.WithCommand( + "bootstrap-all", + "Bootstrap full pipeline", + async context => + { + var ct = context.CancellationToken; + var commandService = context.ServiceProvider.GetRequiredService(); + var notifications = context.ServiceProvider.GetRequiredService(); + var logger = context.Logger; + + var stages = new (string Name, IResource Resource)[] + { + ("step1-sample-build", sampleBuild.Resource), + ("step2-upload-stage1", uploadStage1.Resource), + ("step3-htmlgenerator", htmlGenerator.Resource), + ("step4-normalize-case", normalizeCase.Resource), + ("step5-publish-index", publishIndex.Resource), + }; + + foreach (var (name, resource) in stages) + { + logger.LogInformation("[bootstrap-all] Starting {Stage}...", name); + + var start = await commandService.ExecuteCommandAsync( + resource: resource, + commandName: "resource-start", + cancellationToken: ct); + + if (!start.Success) + { + return CommandResults.Failure($"Failed to start {name}: {start.Message}"); + } + + // Wait until the resource reaches a terminal state. + var snapshot = await notifications.WaitForResourceAsync( + resource.Name, + e => e.Snapshot.State?.Text is "Finished" or "Exited" or "FailedToStart" or "RuntimeUnhealthy", + ct); + + var state = snapshot.Snapshot.State?.Text; + var exitCode = snapshot.Snapshot.ExitCode; + + if (state != "Finished" && state != "Exited" || (exitCode is int code && code != 0)) + { + return CommandResults.Failure( + $"{name} ended in state '{state}' with exit code {exitCode?.ToString() ?? ""}."); + } + + logger.LogInformation("[bootstrap-all] {Stage} finished cleanly.", name); + } + + // The web's IndexLoader only runs once at startup (Index ctor → Task.Run). + // In prod the app-service slot-setting flip restarts the app after publish; + // locally we mirror that by restarting `web` so it re-reads the freshly + // published container. + logger.LogInformation("[bootstrap-all] Restarting web to pick up the freshly published index..."); + var restart = await commandService.ExecuteCommandAsync( + resource: web.Resource, + commandName: "resource-restart", + cancellationToken: ct); + if (!restart.Success) + { + return CommandResults.Failure($"Failed to restart web: {restart.Message}"); + } + await notifications.WaitForResourceHealthyAsync(web.Resource.Name, ct); + + return CommandResults.Success(); + }); + +builder.Build().Run(); + diff --git a/src/source-indexer.AppHost/Properties/launchSettings.json b/src/source-indexer.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..33d3c81 --- /dev/null +++ b/src/source-indexer.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17258;http://localhost:15161", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21208", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22007" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15161", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19226", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20243" + } + } + } +} diff --git a/src/source-indexer.AppHost/appsettings.Development.json b/src/source-indexer.AppHost/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/source-indexer.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/source-indexer.AppHost/appsettings.json b/src/source-indexer.AppHost/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/src/source-indexer.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/source-indexer.AppHost/aspire.config.json b/src/source-indexer.AppHost/aspire.config.json new file mode 100644 index 0000000..0e212ac --- /dev/null +++ b/src/source-indexer.AppHost/aspire.config.json @@ -0,0 +1,5 @@ +{ + "appHost": { + "path": "source-indexer.AppHost.csproj" + } +} diff --git a/src/source-indexer.AppHost/source-indexer.AppHost.csproj b/src/source-indexer.AppHost/source-indexer.AppHost.csproj new file mode 100644 index 0000000..eb5012e --- /dev/null +++ b/src/source-indexer.AppHost/source-indexer.AppHost.csproj @@ -0,0 +1,27 @@ + + + + Exe + net10.0 + enable + enable + fc6b83b7-dced-49f7-aa1c-78458fd03ee3 + + false + + + + + + + + + + + + + diff --git a/src/source-indexer.ServiceDefaults/Extensions.cs b/src/source-indexer.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..4fdcccf --- /dev/null +++ b/src/source-indexer.ServiceDefaults/Extensions.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj b/src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj new file mode 100644 index 0000000..09110f1 --- /dev/null +++ b/src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/src/source-indexer.sln b/src/source-indexer.sln index a9dce76..45070de 100644 --- a/src/source-indexer.sln +++ b/src/source-indexer.sln @@ -7,24 +7,109 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.SourceIndexer.Tas EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UploadIndexStage1", "UploadIndexStage1\UploadIndexStage1.csproj", "{72B1789D-B80E-4073-BDB4-672E3E2E45DA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "source-indexer.AppHost", "source-indexer.AppHost\source-indexer.AppHost.csproj", "{8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceBrowser", "SourceBrowser", "{F69B90C1-642B-7466-139B-A7D79630D179}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{61FFE273-1437-3EAF-DA5A-B4D8CAE2E20F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceIndexServer", "SourceBrowser\src\SourceIndexServer\SourceIndexServer.csproj", "{EB2C8F73-63CD-4727-B11D-773E9E0A1E40}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "SourceBrowser\src\Common\Common.csproj", "{623DF7AB-9F20-41CA-B1A9-10C100028A17}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniRuntime", "..\samples\MiniRuntime\MiniRuntime.csproj", "{1F66CD86-D2AD-40C2-846A-292FBB020CF3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|x64.ActiveCfg = Debug|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|x64.Build.0 = Debug|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|x86.ActiveCfg = Debug|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|x86.Build.0 = Debug|Any CPU {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|Any CPU.ActiveCfg = Release|Any CPU {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|Any CPU.Build.0 = Release|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|x64.ActiveCfg = Release|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|x64.Build.0 = Release|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|x86.ActiveCfg = Release|Any CPU + {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|x86.Build.0 = Release|Any CPU {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|x64.ActiveCfg = Debug|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|x64.Build.0 = Debug|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|x86.ActiveCfg = Debug|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|x86.Build.0 = Debug|Any CPU {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|Any CPU.ActiveCfg = Release|Any CPU {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|Any CPU.Build.0 = Release|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|x64.ActiveCfg = Release|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|x64.Build.0 = Release|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|x86.ActiveCfg = Release|Any CPU + {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|x86.Build.0 = Release|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|x64.ActiveCfg = Debug|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|x64.Build.0 = Debug|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|x86.ActiveCfg = Debug|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|x86.Build.0 = Debug|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|Any CPU.Build.0 = Release|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|x64.ActiveCfg = Release|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|x64.Build.0 = Release|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|x86.ActiveCfg = Release|Any CPU + {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|x86.Build.0 = Release|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|x64.ActiveCfg = Debug|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|x64.Build.0 = Debug|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|x86.ActiveCfg = Debug|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|x86.Build.0 = Debug|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|Any CPU.Build.0 = Release|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|x64.ActiveCfg = Release|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|x64.Build.0 = Release|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|x86.ActiveCfg = Release|Any CPU + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|x86.Build.0 = Release|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|x64.ActiveCfg = Debug|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|x64.Build.0 = Debug|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|x86.ActiveCfg = Debug|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|x86.Build.0 = Debug|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|Any CPU.Build.0 = Release|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|x64.ActiveCfg = Release|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|x64.Build.0 = Release|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|x86.ActiveCfg = Release|Any CPU + {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|x86.Build.0 = Release|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|x64.ActiveCfg = Debug|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|x64.Build.0 = Debug|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|x86.Build.0 = Debug|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|Any CPU.Build.0 = Release|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|x64.ActiveCfg = Release|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|x64.Build.0 = Release|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|x86.ActiveCfg = Release|Any CPU + {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {61FFE273-1437-3EAF-DA5A-B4D8CAE2E20F} = {F69B90C1-642B-7466-139B-A7D79630D179} + {EB2C8F73-63CD-4727-B11D-773E9E0A1E40} = {61FFE273-1437-3EAF-DA5A-B4D8CAE2E20F} + {623DF7AB-9F20-41CA-B1A9-10C100028A17} = {61FFE273-1437-3EAF-DA5A-B4D8CAE2E20F} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CBA6774C-91B0-4634-94B4-B6C12A4519C8} EndGlobalSection