Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions src/Exceptionless.AppHost/Extensions/WorktreeScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System.Net;
using System.Net.Sockets;
using System.Text;

public sealed record WorktreePorts(
int DashboardHttps,
int DashboardHttp,
int DashboardOtlp,
int ResourceService,
int ApiHttp,
int ApiHttps,
int JobsHttp,
int OldAppHttp,
int OldAppHttps,
int OldAppLiveReload,
int AppHttps)
{
public string ApiHttpUrl => $"http://localhost:{ApiHttp}";
public string ApiHttpsUrl => $"https://localhost:{ApiHttps}";
public string OldAppHttpsUrl => $"https://angular-ex.dev.localhost:{OldAppHttps}";
}

public static class WorktreeScope
{
public static string? Resolve()
{
var explicitScope = Environment.GetEnvironmentVariable("Scope");
if (!String.IsNullOrWhiteSpace(explicitScope))
{
return Sanitize(explicitScope);
}

for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir != null; dir = dir.Parent)
{
var dotGit = Path.Combine(dir.FullName, ".git");
if (File.Exists(dotGit))
{
return Sanitize(ResolveGitWorktreeName(dotGit) ?? dir.Name);
}

if (Directory.Exists(dotGit))
{
return null;
}
}

return null;
}

public static WorktreePorts AssignFreePorts()
{
var ports = FreePorts(11);
var assignments = new WorktreePorts(
ports[0],
ports[1],
ports[2],
ports[3],
ports[4],
ports[5],
ports[6],
ports[7],
ports[8],
ports[9],
ports[10]);

Environment.SetEnvironmentVariable("ASPNETCORE_URLS", $"https://localhost:{assignments.DashboardHttps};http://localhost:{assignments.DashboardHttp}");
Environment.SetEnvironmentVariable("DOTNET_DASHBOARD_OTLP_ENDPOINT_URL", $"https://localhost:{assignments.DashboardOtlp}");
Environment.SetEnvironmentVariable("DOTNET_RESOURCE_SERVICE_ENDPOINT_URL", $"https://localhost:{assignments.ResourceService}");

return assignments;
}

private static int[] FreePorts(int count)
{
var listeners = new List<TcpListener>(count);
try
{
for (var i = 0; i < count; i++)
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
listeners.Add(listener);
}

return listeners.Select(l => ((IPEndPoint)l.LocalEndpoint).Port).ToArray();
}
finally
{
foreach (var listener in listeners)
{
listener.Stop();
}
}
}

private static string? ResolveGitWorktreeName(string dotGitPath)
{
var content = File.ReadAllText(dotGitPath).Trim();
const string gitDirPrefix = "gitdir:";
if (!content.StartsWith(gitDirPrefix, StringComparison.OrdinalIgnoreCase))
{
return null;
}

var gitDir = content[gitDirPrefix.Length..].Trim().TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
return Path.GetFileName(gitDir);
}

private static string Sanitize(string value)
{
var builder = new StringBuilder(value.Length);
foreach (var ch in value.ToLowerInvariant())
{
builder.Append(Char.IsLetterOrDigit(ch) ? ch : '-');
}

var cleaned = builder.ToString().Trim('-');
while (cleaned.Contains("--", StringComparison.Ordinal))
{
cleaned = cleaned.Replace("--", "-", StringComparison.Ordinal);
}

return cleaned.Length > 40 ? cleaned[..40].Trim('-') : cleaned;
}
}
118 changes: 86 additions & 32 deletions src/Exceptionless.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,42 @@
using Aspire.Hosting.JavaScript;
using Microsoft.Extensions.Hosting;

var scope = WorktreeScope.Resolve();
var isScoped = !String.IsNullOrWhiteSpace(scope);
var worktreePorts = isScoped ? WorktreeScope.AssignFreePorts() : null;
var builder = DistributedApplication.CreateBuilder(args);
var servicesOnly = args.Any(arg => StringComparer.OrdinalIgnoreCase.Equals(arg, "--services-only") || StringComparer.OrdinalIgnoreCase.Equals(arg, "services-only"));
var oldAppHttpPort = worktreePorts?.OldAppHttp ?? 7120;
var oldAppPort = worktreePorts?.OldAppHttps ?? 7121;
var oldAppLiveReloadPort = worktreePorts?.OldAppLiveReload ?? 35729;
var oldAppAspNetCoreUrls = String.Concat("http://localhost:", oldAppHttpPort);
var appPort = worktreePorts?.AppHttps ?? 7131;
const string SharedEmailConnectionString = "smtp://localhost:1025";

var elastic = builder.AddElasticsearch("Elasticsearch", port: 9200)
.WithDataVolume(servicesOnly ? null : "exceptionless.data.v1");
.WithDataVolume("exceptionless.data.v1")
.WithEndpointProxySupport(false);

var storage = builder.AddAzureStorage("Storage")
.RunAsEmulator(c =>
{
c.WithEndpointProxySupport(false);
c.WithUrlForEndpoint("blob", u => { u.DisplayText = "Blobs"; u.DisplayLocation = UrlDisplayLocation.DetailsOnly; });
c.WithUrlForEndpoint("queue", u => { u.DisplayText = "Queues"; u.DisplayLocation = UrlDisplayLocation.DetailsOnly; });
c.WithUrlForEndpoint("table", u => { u.DisplayText = "Tables"; u.DisplayLocation = UrlDisplayLocation.DetailsOnly; });

if (!servicesOnly)
{
c.WithLifetime(ContainerLifetime.Persistent);
c.WithContainerName("Exceptionless-Storage");
c.WithDataVolume();
}
c.WithLifetime(ContainerLifetime.Persistent);
c.WithContainerName("Exceptionless-Storage");
c.WithDataVolume("exceptionless.storage.data.v1");
});

var storageBlobs = storage.AddBlobs("StorageBlobs");
var storageQueues = storage.AddQueues("StorageQueues");

var cache = builder.AddRedis("Redis", port: 6379)
.WithImageTag("8.6")
.WithDataVolume("exceptionless.redis.data.v1")
.WithEndpointProxySupport(false)
.WithClearCommand()
.WithUrls(c =>
{
Expand All @@ -39,41 +49,54 @@

var mail = builder.AddContainer("Mail", "axllent/mailpit")
.WithImageTag("v1.27.10")
.WithEndpointProxySupport(false)
.WithHttpEndpoint(8025, 8025, "http")
.WithUrlForEndpoint("http", u => { u.DisplayText = "Mail"; u.DisplayOrder = 100; })
.WithHttpHealthCheck("/readyz")
.WithEndpoint(1025, 1025)
.WithUrlForEndpoint("tcp", u => u.DisplayLocation = UrlDisplayLocation.DetailsOnly);

var ownedElastic = elastic;
elastic = ownedElastic
.WithLifetime(ContainerLifetime.Persistent)
.WithContainerName("Exceptionless-Elasticsearch");

if (!servicesOnly)
{
elastic = elastic
elastic = elastic.WithKibana(b => b
.WithLifetime(ContainerLifetime.Persistent)
.WithContainerName("Exceptionless-Elasticsearch")
.WithKibana(b => b
.WithLifetime(ContainerLifetime.Persistent)
.WithContainerName("Exceptionless-Kibana")
.WithParentRelationship(elastic));
.WithEndpointProxySupport(false)
.WithContainerName("Exceptionless-Kibana")
.WithParentRelationship(ownedElastic));
}

cache = cache
.WithLifetime(ContainerLifetime.Persistent)
.WithContainerName("Exceptionless-Redis")
.WithRedisInsight(b => b
.WithLifetime(ContainerLifetime.Persistent)
.WithContainerName("Exceptionless-RedisInsight")
.WithUrlForEndpoint("http", u => u.DisplayText = "Redis")
.WithParentRelationship(cache), containerName: "Redis-insight");

mail = mail
var ownedCache = cache;
cache = ownedCache
.WithLifetime(ContainerLifetime.Persistent)
.WithContainerName("Exceptionless-Redis");

if (!servicesOnly)
{
cache = cache.WithRedisInsight(b => b
.WithLifetime(ContainerLifetime.Persistent)
.WithContainerName("Exceptionless-Mail");
.WithEndpointProxySupport(false)
.WithContainerName("Exceptionless-RedisInsight")
.WithUrlForEndpoint("http", u => u.DisplayText = "Redis")
.WithParentRelationship(ownedCache), containerName: "Redis-insight");
}

mail = mail
.WithLifetime(ContainerLifetime.Persistent)
.WithContainerName("Exceptionless-Mail");

if (!servicesOnly)
{
var api = builder.AddProject<Projects.Exceptionless_Web>("Api")
.WithReference(cache)
.WithReference(elastic)
.WithReference(storageBlobs, "AzureStorage")
.WithReference(storageQueues, "AzureQueues")
.WithEnvironment("ConnectionStrings:Email", "smtp://localhost:1025")
.WithEnvironment("ConnectionStrings:Email", SharedEmailConnectionString)
.WithEnvironment("RunJobsInProcess", "false")
.WaitFor(elastic)
.WaitFor(cache)
Expand All @@ -83,12 +106,20 @@
.WithUrlForEndpoint("http", u => u.DisplayLocation = UrlDisplayLocation.DetailsOnly)
.WithHttpHealthCheck("/health");

builder.AddProject<Projects.Exceptionless_Job>("Jobs", "AllJobs")
if (worktreePorts is not null)
{
api.WithEnvironment("Scope", scope!)
.WithEnvironment("AppScope", scope!)
.WithEndpoint("http", e => e.Port = worktreePorts.ApiHttp)
.WithEndpoint("https", e => e.Port = worktreePorts.ApiHttps);
}

var jobs = builder.AddProject<Projects.Exceptionless_Job>("Jobs", "AllJobs")
.WithReference(cache)
.WithReference(elastic)
.WithReference(storageBlobs, "AzureStorage")
.WithReference(storageQueues, "AzureQueues")
.WithEnvironment("ConnectionStrings:Email", "smtp://localhost:1025")
.WithEnvironment("ConnectionStrings:Email", SharedEmailConnectionString)
.WaitFor(elastic)
.WaitFor(cache)
.WaitFor(mail)
Expand All @@ -105,14 +136,22 @@
.WithHttpHealthCheck("/health")
.WithParentRelationship(api);

if (worktreePorts is not null)
{
jobs.WithEnvironment("Scope", scope!)
.WithEnvironment("AppScope", scope!)
.WithEndpoint("http", e => e.Port = worktreePorts.JobsHttp);
}

#pragma warning disable ASPIREBROWSERLOGS001
var oldApp = builder.AddJavaScriptApp("OldApp", "../../src/Exceptionless.Web/ClientApp.angular", "serve")
.WithBrowserLogs()
.WithReference(api)
.RemoveJavaScriptDebuggingAnnotation()
.WithEnvironment("ASPNETCORE_URLS", "http://localhost:7120")
.WithEnvironment("ASPNETCORE_URLS", oldAppAspNetCoreUrls)
.WithEnvironment("USE_HTTPS", "true")
.WithHttpEndpoint(port: 7121, targetPort: 7121, name: "https", env: "PORT", isProxied: false)
.WithEnvironment("LIVERELOAD_PORT", oldAppLiveReloadPort.ToString())
.WithHttpEndpoint(port: oldAppPort, targetPort: oldAppPort, name: "https", env: "PORT", isProxied: false)
.WithEndpoint("https", e =>
{
e.TargetHost = "angular-ex.dev.localhost";
Expand All @@ -125,19 +164,26 @@
u.DisplayOrder = 100;
})
.WithParentRelationship(api);

if (worktreePorts is not null)
{
oldApp.WithEnvironment("API_HTTP", worktreePorts.ApiHttpUrl)
.WithEnvironment("API_HTTPS", worktreePorts.ApiHttpsUrl);
}
#pragma warning restore ASPIREBROWSERLOGS001

#pragma warning disable ASPIREBROWSERLOGS001
builder.AddViteApp("App", "../Exceptionless.Web/ClientApp")
var app = builder.AddViteApp("App", "../Exceptionless.Web/ClientApp")
.WithBrowserLogs()
.WithReference(api)
.WithReference(oldApp)
.RemoveJavaScriptDebuggingAnnotation()
.WithEnvironment("PORT", appPort.ToString())
.WithEndpoint("http", e =>
{
// 7131 (HTTPS via Aspire dev cert) instead of Vite's default 5173 to avoid clashing with other local Vite projects.
e.Port = 7131;
e.TargetPort = 7131;
e.Port = appPort;
e.TargetPort = appPort;
e.TargetHost = "web-ex.dev.localhost";
e.IsProxied = false;
})
Expand All @@ -149,6 +195,14 @@
u.Url = $"{u.Url.TrimEnd('/')}/next/";
})
.WithParentRelationship(api);

if (worktreePorts is not null)
{
app.WithEnvironment("API_HTTP", worktreePorts.ApiHttpUrl)
.WithEnvironment("API_HTTPS", worktreePorts.ApiHttpsUrl)
.WithEnvironment("OLDAPP_HTTP", worktreePorts.OldAppHttpsUrl)
.WithEnvironment("OLDAPP_HTTPS", worktreePorts.OldAppHttpsUrl);
}
#pragma warning restore ASPIREBROWSERLOGS001
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ var path = require("path");
var fs = require("fs");
var s = require("child_process");
// eslint-disable-next-line import/no-extraneous-dependencies
var livereload = require("connect-livereload");
// eslint-disable-next-line import/no-extraneous-dependencies
var proxyRequest = require("grunt-connect-proxy2/lib/utils").proxyRequest;

module.exports = function () {
var target = getTarget();
var useHttps = String(process.env.USE_HTTPS || "").toLowerCase() === "true";
var port = Number(process.env.PORT) || 7121;
var liveReloadPort = Number(process.env.LIVERELOAD_PORT) || 35729;
var certs = useHttps ? generateCerts() : { cert: undefined, key: undefined };

return {
Expand All @@ -19,6 +22,7 @@ module.exports = function () {
cert: certs.cert,
middleware: function (connect, options, middlewares) {
middlewares.unshift(proxyRequest);
middlewares.splice(1, 0, livereload({ port: liveReloadPort }));
return middlewares;
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
module.exports = function (grunt) {
var liveReloadPort = Number(process.env.LIVERELOAD_PORT) || 35729;

return {
main: {
options: {
livereload: true,
livereload: liveReloadPort,
livereloadOnError: false,
spawn: false,
},
Expand Down
3 changes: 0 additions & 3 deletions src/Exceptionless.Web/ClientApp.angular/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,6 @@
data-remove="true"
></script>

<!-- Livereload script for development only (stripped during dist build) -->
<script src="http://localhost:35729/livereload.js" data-concat="false"></script>

<!-- JS from npm Components -->
<script src="node_modules/exceptionless/dist/exceptionless.js"></script>
<script type="application/javascript" data-concat="false" data-remove="true">
Expand Down
1 change: 1 addition & 0 deletions src/Exceptionless.Web/ClientApp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"scripts": {
"dev": "cross-env NODE_OPTIONS=--trace-warnings vite dev",
"dev:api": "cross-env API_HTTP=https://dev-collector.exceptionless.io OLDAPP_HTTP=https://dev-app.exceptionless.io vite dev",
"urls": "node scripts/resolve-url.mjs",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
Expand Down
Loading