mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 07:28:45 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 503d838fd1 | |||
| e29707deeb | |||
| bafb380fc8 |
@@ -25,7 +25,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-dotnet@v4
|
- uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 10.0.x
|
dotnet-version: 9.0.x
|
||||||
|
|
||||||
- name: Install protobuf compiler
|
- name: Install protobuf compiler
|
||||||
run: |
|
run: |
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Start server
|
- name: Start server
|
||||||
run: |
|
run: |
|
||||||
dotnet DorkNet.Server/bin/Debug/net10.0/DorkNet.Server.dll > "$RUNNER_TEMP/dorknet-server.log" 2>&1 &
|
dotnet DorkNet.Server/bin/Debug/net9.0/DorkNet.Server.dll > "$RUNNER_TEMP/dorknet-server.log" 2>&1 &
|
||||||
echo "$!" > "$RUNNER_TEMP/dorknet-server.pid"
|
echo "$!" > "$RUNNER_TEMP/dorknet-server.pid"
|
||||||
|
|
||||||
- name: Wait for server readiness
|
- name: Wait for server readiness
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- name: Run endpoint contract
|
- name: Run endpoint contract
|
||||||
run: |
|
run: |
|
||||||
dotnet run --project tools/DorkNet.EndpointContract/DorkNet.EndpointContract.csproj --no-build -- \
|
dotnet run --project tools/DorkNet.EndpointContract/DorkNet.EndpointContract.csproj --no-build -- \
|
||||||
--server-assembly DorkNet.Server/bin/Debug/net10.0/DorkNet.Server.dll \
|
--server-assembly DorkNet.Server/bin/Debug/net9.0/DorkNet.Server.dll \
|
||||||
--base-url http://127.0.0.1:18080 \
|
--base-url http://127.0.0.1:18080 \
|
||||||
--report "$RUNNER_TEMP/endpoint-contract-report.json"
|
--report "$RUNNER_TEMP/endpoint-contract-report.json"
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-dotnet@v4
|
- uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: '10.0.x'
|
dotnet-version: '9.0.x'
|
||||||
|
|
||||||
# ── Server self-contained publish ────────────────────────────────
|
# ── Server self-contained publish ────────────────────────────────
|
||||||
- name: Publish DorkNet.Server (win-x64, self-contained)
|
- name: Publish DorkNet.Server (win-x64, self-contained)
|
||||||
|
|||||||
+2
-2
@@ -12,8 +12,8 @@ dist/
|
|||||||
# Local SQLite + uploaded blobs only — DON'T match the source-tree
|
# Local SQLite + uploaded blobs only — DON'T match the source-tree
|
||||||
# DorkNet.Server/Data/ folder (which holds DbContext + Entities/) on
|
# DorkNet.Server/Data/ folder (which holds DbContext + Entities/) on
|
||||||
# case-insensitive filesystems. Use the precise runtime data dirs.
|
# case-insensitive filesystems. Use the precise runtime data dirs.
|
||||||
DorkNet.Server/bin/Debug/net10.0/data/
|
DorkNet.Server/bin/Debug/net9.0/data/
|
||||||
DorkNet.Server/bin/Release/net10.0/data/
|
DorkNet.Server/bin/Release/net9.0/data/
|
||||||
# appsettings.Production.json is intentionally versioned — contains
|
# appsettings.Production.json is intentionally versioned — contains
|
||||||
# only the Postgres provider switch + Kestrel bind address. All real
|
# only the Postgres provider switch + Kestrel bind address. All real
|
||||||
# secrets come from Coolify env vars at runtime.
|
# secrets come from Coolify env vars at runtime.
|
||||||
|
|||||||
+4
-4
@@ -1,14 +1,14 @@
|
|||||||
# DorkNet.Server container image — Coolify deploy target.
|
# DorkNet.Server container image — Coolify deploy target.
|
||||||
# ────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────
|
||||||
# Multi-stage: SDK image builds + publishes, runtime image is the
|
# Multi-stage: SDK image builds + publishes, runtime image is the
|
||||||
# slimmer aspnet:10.0 with only the published output.
|
# slimmer aspnet:9.0 with only the published output.
|
||||||
#
|
#
|
||||||
# Build context: repo root (so Tools/ and DorkNet.Models/ are visible
|
# Build context: repo root (so Tools/ and DorkNet.Models/ are visible
|
||||||
# to ProjectReference + dotnet restore). The .dockerignore at the
|
# to ProjectReference + dotnet restore). The .dockerignore at the
|
||||||
# repo root keeps the bin/, obj/, and other dev junk out of the
|
# repo root keeps the bin/, obj/, and other dev junk out of the
|
||||||
# context.
|
# context.
|
||||||
|
|
||||||
ARG DOTNET_VERSION=10.0
|
ARG DOTNET_VERSION=9.0
|
||||||
ARG NODE_VERSION=22
|
ARG NODE_VERSION=22
|
||||||
|
|
||||||
# ── admin SPA build stage ──────────────────────────────────────────
|
# ── admin SPA build stage ──────────────────────────────────────────
|
||||||
@@ -76,13 +76,13 @@ WORKDIR /app
|
|||||||
|
|
||||||
# curl + wget for healthcheck probes — Coolify v4 defaults to wget for
|
# curl + wget for healthcheck probes — Coolify v4 defaults to wget for
|
||||||
# its own probe regardless of what the image's HEALTHCHECK uses, so
|
# its own probe regardless of what the image's HEALTHCHECK uses, so
|
||||||
# both have to be present. The aspnet:10.0 base image strips both by
|
# both have to be present. The aspnet:9.0 base image strips both by
|
||||||
# default to keep the surface area small.
|
# default to keep the surface area small.
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends curl wget \
|
&& apt-get install -y --no-install-recommends curl wget \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Run as a non-root user — aspnet:10.0 ships with the `app` UID 1654.
|
# Run as a non-root user — aspnet:9.0 ships with the `app` UID 1654.
|
||||||
# Coolify mounts persistent volumes with write permission for this UID.
|
# Coolify mounts persistent volumes with write permission for this UID.
|
||||||
USER app
|
USER app
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
ARG DOTNET_VERSION=10.0
|
ARG DOTNET_VERSION=9.0
|
||||||
ARG NODE_VERSION=22
|
ARG NODE_VERSION=22
|
||||||
|
|
||||||
FROM node:${NODE_VERSION}-alpine AS admin-ui
|
FROM node:${NODE_VERSION}-alpine AS admin-ui
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.13" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.2" />
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.13" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.13" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.15" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.13">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.13" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<!-- RenderedCompactJsonFormatter for stdout JSON logs in production
|
<!-- RenderedCompactJsonFormatter for stdout JSON logs in production
|
||||||
(Coolify-friendly). The host pipeline reads stdout line-by-line,
|
(Coolify-friendly). The host pipeline reads stdout line-by-line,
|
||||||
and structured JSON lets a future Loki/Datadog ingest parse fields
|
and structured JSON lets a future Loki/Datadog ingest parse fields
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
account-creation flow), and PlayerPresenceService heartbeats all
|
account-creation flow), and PlayerPresenceService heartbeats all
|
||||||
need shared state across replicas. -->
|
need shared state across replicas. -->
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
|
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="10.0.13" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.4" />
|
||||||
<!-- S3-compatible object storage. Production target: Garage in
|
<!-- S3-compatible object storage. Production target: Garage in
|
||||||
Coolify (one-click). Dev fallback: local MinIO for parity
|
Coolify (one-click). Dev fallback: local MinIO for parity
|
||||||
testing. The SDK speaks plain S3, so swapping endpoints
|
testing. The SDK speaks plain S3, so swapping endpoints
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
<!-- Password hashing for the real account auth flow. BCrypt embeds
|
<!-- Password hashing for the real account auth flow. BCrypt embeds
|
||||||
cost+salt in the output string so we only need one column. -->
|
cost+salt in the output string so we only need one column. -->
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.3.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" />
|
||||||
<!-- Server-side crop/resize for img.* CDN transforms (?cropSquare=1&width=N
|
<!-- Server-side crop/resize for img.* CDN transforms (?cropSquare=1&width=N
|
||||||
on profile thumbnails). The 2020 client treats this as part of the
|
on profile thumbnails). The 2020 client treats this as part of the
|
||||||
image CDN contract — it uploads a full-frame capture and expects the
|
image CDN contract — it uploads a full-frame capture and expects the
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ namespace DorkNet.Server.Startup;
|
|||||||
/// <item>SQLite — <c>Database.Migrate()</c> after
|
/// <item>SQLite — <c>Database.Migrate()</c> after
|
||||||
/// <see cref="BaselineExistingSchemaIfNeeded"/> reconciles any legacy
|
/// <see cref="BaselineExistingSchemaIfNeeded"/> reconciles any legacy
|
||||||
/// migration-history state.</item>
|
/// migration-history state.</item>
|
||||||
/// <item>Postgres — <c>EnsureCreated</c>, with the whole bootstrap
|
/// <item>Postgres — <c>EnsureCreated</c> behind a transaction-scoped
|
||||||
/// (schema and seeds) behind a session advisory lock so concurrent
|
/// advisory lock so concurrent replica boots don't race on
|
||||||
/// replica boots take turns instead of racing. Connect retries cover the
|
/// <c>CREATE TABLE</c>. Connect retries cover the
|
||||||
/// container-orchestrated boot window where Postgres DNS isn't
|
/// container-orchestrated boot window where Postgres DNS isn't
|
||||||
/// resolvable yet.</item>
|
/// resolvable yet.</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
@@ -40,110 +40,61 @@ public static class DatabaseBootstrap
|
|||||||
await ConfigureSqliteForLocalConcurrencyAsync(db);
|
await ConfigureSqliteForLocalConcurrencyAsync(db);
|
||||||
BaselineExistingSchemaIfNeeded(db);
|
BaselineExistingSchemaIfNeeded(db);
|
||||||
db.Database.Migrate();
|
db.Database.Migrate();
|
||||||
await RunUpgradesAndSeedsAsync(app, scope, db);
|
}
|
||||||
return;
|
else
|
||||||
|
{
|
||||||
|
// Postgres path: schema generated from the EF Core model via
|
||||||
|
// EnsureCreated. Wrapped in a transaction-scoped advisory lock
|
||||||
|
// so concurrent replicas booting against the same database
|
||||||
|
// don't race on CREATE TABLE — only the first replica to take
|
||||||
|
// the lock runs EnsureCreated, the rest see "schema already
|
||||||
|
// exists" and proceed.
|
||||||
|
//
|
||||||
|
// Lock key 0x444F524B (ascii "DORK") is hard-coded; it just
|
||||||
|
// needs to be the same int64 across replicas and not collide
|
||||||
|
// with anything else in the database. pg_advisory_xact_lock
|
||||||
|
// releases automatically on COMMIT, so a crashed replica can't
|
||||||
|
// wedge the next boot.
|
||||||
|
var conn = db.Database.GetDbConnection();
|
||||||
|
|
||||||
|
// Retry initial Postgres connect — on container-orchestrated boots
|
||||||
|
// (Coolify, docker-compose) the Postgres service's DNS often isn't
|
||||||
|
// resolvable yet when this app starts, surfacing as SocketException
|
||||||
|
// errno=11 EAGAIN from Npgsql's Dns.GetHostEntryOrAddressesCore.
|
||||||
|
// Postgres itself can also be up-but-not-accepting-connections for
|
||||||
|
// a few seconds while it replays WAL. 15 attempts × ~2s backoff
|
||||||
|
// covers the typical 5-30s warmup window without looping forever
|
||||||
|
// on a permanently broken config.
|
||||||
|
var connectLogger = app.Services.GetRequiredService<ILoggerFactory>()
|
||||||
|
.CreateLogger("DorkNet.Server.Bootstrap");
|
||||||
|
for (var attempt = 1; ; attempt++)
|
||||||
|
{
|
||||||
|
try { await conn.OpenAsync(); break; }
|
||||||
|
catch (Exception ex) when (attempt < 15 &&
|
||||||
|
(ex is System.Net.Sockets.SocketException
|
||||||
|
|| ex is Npgsql.NpgsqlException
|
||||||
|
|| ex is System.Data.Common.DbException))
|
||||||
|
{
|
||||||
|
var delay = TimeSpan.FromSeconds(Math.Min(attempt, 5));
|
||||||
|
connectLogger.LogWarning(
|
||||||
|
"[bootstrap] Postgres connect attempt {Attempt}/15 failed ({Type}: {Message}); retrying in {Delay}s",
|
||||||
|
attempt, ex.GetType().Name, ex.Message, delay.TotalSeconds);
|
||||||
|
await Task.Delay(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await using var tx = await conn.BeginTransactionAsync();
|
||||||
|
await using (var lockCmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
lockCmd.Transaction = tx;
|
||||||
|
lockCmd.CommandText = "SELECT pg_advisory_xact_lock(1146246987);";
|
||||||
|
await lockCmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
db.Database.EnsureCreated();
|
||||||
|
await tx.CommitAsync();
|
||||||
|
|
||||||
|
await conn.CloseAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Postgres path: schema generated from the EF Core model via
|
|
||||||
// EnsureCreated, then the same upgrades and seeds as SQLite. The
|
|
||||||
// whole bootstrap holds a session-level advisory lock, so replicas
|
|
||||||
// booting against one database take turns: the first creates the
|
|
||||||
// schema and seeds, the rest find everything in place. Holding it
|
|
||||||
// only around EnsureCreated left the seeders racing (duplicate keys
|
|
||||||
// on a fresh database).
|
|
||||||
//
|
|
||||||
// Lock key 0x444F524B (ascii "DORK") is hard-coded; it just needs
|
|
||||||
// to be the same int64 across replicas and not collide with
|
|
||||||
// anything else in the database. It is released in the finally
|
|
||||||
// below, and Postgres drops it if the replica's connection dies, so
|
|
||||||
// a crashed replica can't wedge the next boot.
|
|
||||||
var conn = db.Database.GetDbConnection();
|
|
||||||
|
|
||||||
// Retry initial Postgres connect — on container-orchestrated boots
|
|
||||||
// (Coolify, docker-compose) the Postgres service's DNS often isn't
|
|
||||||
// resolvable yet when this app starts, surfacing as SocketException
|
|
||||||
// errno=11 EAGAIN from Npgsql's Dns.GetHostEntryOrAddressesCore.
|
|
||||||
// Postgres itself can also be up-but-not-accepting-connections for
|
|
||||||
// a few seconds while it replays WAL. 15 attempts × ~2s backoff
|
|
||||||
// covers the typical 5-30s warmup window without looping forever
|
|
||||||
// on a permanently broken config.
|
|
||||||
var connectLogger = app.Services.GetRequiredService<ILoggerFactory>()
|
|
||||||
.CreateLogger("DorkNet.Server.Bootstrap");
|
|
||||||
for (var attempt = 1; ; attempt++)
|
|
||||||
{
|
|
||||||
try { await conn.OpenAsync(); break; }
|
|
||||||
catch (Exception ex) when (attempt < 15 &&
|
|
||||||
(ex is System.Net.Sockets.SocketException
|
|
||||||
|| ex is Npgsql.NpgsqlException
|
|
||||||
|| ex is System.Data.Common.DbException))
|
|
||||||
{
|
|
||||||
var delay = TimeSpan.FromSeconds(Math.Min(attempt, 5));
|
|
||||||
connectLogger.LogWarning(
|
|
||||||
"[bootstrap] Postgres connect attempt {Attempt}/15 failed ({Type}: {Message}); retrying in {Delay}s",
|
|
||||||
attempt, ex.GetType().Name, ex.Message, delay.TotalSeconds);
|
|
||||||
await Task.Delay(delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The connection stays open until the finally, so every step below
|
|
||||||
// (they all share this scope's DbContext) runs on the session that
|
|
||||||
// holds the lock. Waiting for it has no command timeout: another
|
|
||||||
// replica may be seeding a fresh database (thumbnail downloads
|
|
||||||
// included) for well over the default 30 s.
|
|
||||||
connectLogger.LogInformation("[bootstrap] waiting for the database bootstrap lock");
|
|
||||||
var previousTimeout = db.Database.GetCommandTimeout();
|
|
||||||
db.Database.SetCommandTimeout(0);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await db.Database.ExecuteSqlRawAsync("SELECT pg_advisory_lock(1146246987);");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
db.Database.SetCommandTimeout(previousTimeout);
|
|
||||||
}
|
|
||||||
connectLogger.LogInformation("[bootstrap] holding the database bootstrap lock");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Begin the transaction through EF, not on the raw connection:
|
|
||||||
// EnsureCreated runs its commands through the migration executor,
|
|
||||||
// which starts its own transaction unless EF already has one, and
|
|
||||||
// fails on a fresh database with "A transaction is already in
|
|
||||||
// progress" when the open one is invisible to it.
|
|
||||||
await using (var tx = await db.Database.BeginTransactionAsync())
|
|
||||||
{
|
|
||||||
db.Database.EnsureCreated();
|
|
||||||
await tx.CommitAsync();
|
|
||||||
}
|
|
||||||
await RunUpgradesAndSeedsAsync(app, scope, db);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
// Explicit: Npgsql pools connections, so closing alone would not
|
|
||||||
// end the session that holds the lock. If the connection is
|
|
||||||
// already broken the unlock fails too; that must not replace the
|
|
||||||
// real bootstrap error, and the connection still gets closed
|
|
||||||
// (Postgres then drops the lock with the session).
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await db.Database.ExecuteSqlRawAsync("SELECT pg_advisory_unlock(1146246987);");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
connectLogger.LogWarning(ex, "[bootstrap] releasing the database bootstrap lock failed");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
await conn.CloseAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>The idempotent post-schema sequence both providers run:
|
|
||||||
/// column/table upgrades, system account and backfills, legacy data
|
|
||||||
/// transforms, canonical rooms and the per-feature seeders. Flips the
|
|
||||||
/// <c>/healthz</c> readiness flag at the end.</summary>
|
|
||||||
private static async Task RunUpgradesAndSeedsAsync(WebApplication app, IServiceScope scope, DorkNetDbContext db)
|
|
||||||
{
|
|
||||||
// Signup-code + pending-device tables. These post-date the
|
// Signup-code + pending-device tables. These post-date the
|
||||||
// consolidated Initial migration, and the Postgres path is
|
// consolidated Initial migration, and the Postgres path is
|
||||||
// EnsureCreated-only (no migration replay), so neither provider
|
// EnsureCreated-only (no migration replay), so neither provider
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
Vendored
+21
-10
@@ -9,8 +9,9 @@
|
|||||||
// from the agent's environment (ARCHIVE_PC_REPO / ARCHIVE_QUEST_REPO), each
|
// from the agent's environment (ARCHIVE_PC_REPO / ARCHIVE_QUEST_REPO), each
|
||||||
// overridable per run. A build is picked by snapshot tags.
|
// overridable per run. A build is picked by snapshot tags.
|
||||||
//
|
//
|
||||||
// On a push, only the platforms whose code changed are rebuilt. A manual run
|
// A push builds only the platforms whose client code changed (never for
|
||||||
// picks the platforms with PLATFORMS. See ci/jenkins/README.md.
|
// docs or CI-only changes). A manual run picks the platforms with PLATFORMS.
|
||||||
|
// See ci/jenkins/README.md.
|
||||||
//
|
//
|
||||||
// Finished builds are renamed for players and published to the downloads bucket
|
// Finished builds are renamed for players and published to the downloads bucket
|
||||||
// (ci/downloads) unless PUBLISH_DOWNLOADS is off. The build scripts keep their
|
// (ci/downloads) unless PUBLISH_DOWNLOADS is off. The build scripts keep their
|
||||||
@@ -76,10 +77,15 @@ pipeline {
|
|||||||
for (cs in currentBuild.changeSets) {
|
for (cs in currentBuild.changeSets) {
|
||||||
for (entry in cs.items) changed.addAll(entry.affectedPaths)
|
for (entry in cs.items) changed.addAll(entry.affectedPaths)
|
||||||
}
|
}
|
||||||
def touches = { prefixes -> changed.any { p -> prefixes.any { p.startsWith(it) } } }
|
// Only client-facing changes rebuild (and so republish) a
|
||||||
def common = ['Jenkinsfile', 'ci/jenkins/']
|
// platform; Markdown (.md, .markdown) doesn't count. The job's Git path filter
|
||||||
buildPc = touches(common + ['DorkNet.ClientMod/', 'tools/build_pc.py'])
|
// uses the same paths, so other pushes don't start a run at all.
|
||||||
buildQuest = touches(common + ['DorkNet.Quest/'])
|
// A CI-only change (Jenkinsfile, ci/) takes a manual run.
|
||||||
|
def touches = { prefixes ->
|
||||||
|
changed.any { p -> !(p.toLowerCase() ==~ /.*\.(md|markdown)/) && prefixes.any { p.startsWith(it) } }
|
||||||
|
}
|
||||||
|
buildPc = touches(['DorkNet.ClientMod/', 'tools/build_pc.py'])
|
||||||
|
buildQuest = touches(['DorkNet.Quest/'])
|
||||||
echo "Push build; ${changed.size()} changed file(s)"
|
echo "Push build; ${changed.size()} changed file(s)"
|
||||||
} else {
|
} else {
|
||||||
buildPc = params.PLATFORMS in ['both', 'pc']
|
buildPc = params.PLATFORMS in ['both', 'pc']
|
||||||
@@ -220,10 +226,15 @@ pipeline {
|
|||||||
archiveArtifacts artifacts: 'out/downloads/**', allowEmptyArchive: true, fingerprint: true
|
archiveArtifacts artifacts: 'out/downloads/**', allowEmptyArchive: true, fingerprint: true
|
||||||
}
|
}
|
||||||
cleanup {
|
cleanup {
|
||||||
// Runs after archiving. Keeps the restic cache and restored stock
|
// Runs after archiving. Drops the build output and scratch (several
|
||||||
// builds ($DORKNET_CACHE on the agent, outside the workspace); drops
|
// GB) but keeps the Git checkout: the job's path filter compares
|
||||||
// the workspace's build output and scratch.
|
// pushes against it, and without a workspace Jenkins treats every
|
||||||
cleanWs(deleteDirs: true, notFailBuild: true)
|
// push as a change and starts a run. The restic cache and restored
|
||||||
|
// stock builds live outside the workspace ($DORKNET_CACHE).
|
||||||
|
cleanWs(deleteDirs: true, notFailBuild: true, patterns: [
|
||||||
|
[pattern: 'out/**', type: 'INCLUDE'],
|
||||||
|
[pattern: 'quest-work/**', type: 'INCLUDE'],
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ What the 2023 client gets here that the 2020 branches don't have:
|
|||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
**1. Install .NET 10 SDK** and (optionally) `dotnet-ef`:
|
**1. Install .NET 9 SDK** and (optionally) `dotnet-ef`:
|
||||||
|
|
||||||
```pwsh
|
```pwsh
|
||||||
dotnet tool install --global dotnet-ef
|
dotnet tool install --global dotnet-ef
|
||||||
@@ -101,7 +101,7 @@ cd DorkNet
|
|||||||
dotnet run --project DorkNet.Server
|
dotnet run --project DorkNet.Server
|
||||||
```
|
```
|
||||||
|
|
||||||
First boot creates `bin/Debug/net10.0/data/dorknet.db`, runs every
|
First boot creates `bin/Debug/net9.0/data/dorknet.db`, runs every
|
||||||
`LegacyUpgrades` pass, seeds canonical rooms / store catalog / club
|
`LegacyUpgrades` pass, seeds canonical rooms / store catalog / club
|
||||||
categories / playlists, and starts listening. You should see
|
categories / playlists, and starts listening. You should see
|
||||||
`Application started.` in the log; `curl http://localhost:8080/healthz`
|
`Application started.` in the log; `curl http://localhost:8080/healthz`
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ RUN apt-get update \
|
|||||||
&& wget -q "https://packages.microsoft.com/config/debian/${VERSION_ID}/packages-microsoft-prod.deb" -O /tmp/ms.deb \
|
&& wget -q "https://packages.microsoft.com/config/debian/${VERSION_ID}/packages-microsoft-prod.deb" -O /tmp/ms.deb \
|
||||||
&& dpkg -i /tmp/ms.deb && rm /tmp/ms.deb \
|
&& dpkg -i /tmp/ms.deb && rm /tmp/ms.deb \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends dotnet-sdk-10.0 \
|
&& apt-get install -y --no-install-recommends dotnet-sdk-8.0 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# ---- Python with LIEF (DT_NEEDED injection into libmain.so) ---------------
|
# ---- Python with LIEF (DT_NEEDED injection into libmain.so) ---------------
|
||||||
|
|||||||
+14
-3
@@ -60,10 +60,21 @@ Resolve a table for another build locally with `build.py build --dump-cs`.
|
|||||||
|
|
||||||
## Triggers
|
## Triggers
|
||||||
|
|
||||||
- **Push** to the branch (GitHub webhook) rebuilds only what changed:
|
- **Push** to the branch (GitHub webhook) builds only for client-facing
|
||||||
- `DorkNet.ClientMod/` and the PC scripts rebuild PC;
|
changes, and only the platform they touch:
|
||||||
|
- `DorkNet.ClientMod/` and `tools/build_pc.py` rebuild PC;
|
||||||
- `DorkNet.Quest/` rebuilds Quest;
|
- `DorkNet.Quest/` rebuilds Quest;
|
||||||
- `Jenkinsfile` and `ci/jenkins/` rebuild both.
|
- Markdown files (`.md`, `.markdown`) never count; server, docs and CI-only pushes (including
|
||||||
|
the `Jenkinsfile` and `ci/`) build nothing. Run those manually.
|
||||||
|
|
||||||
|
Set the same filter on the job so other pushes don't start a run at all:
|
||||||
|
under the job's **Git** source, add **Additional Behaviours → Polling
|
||||||
|
ignores commits in certain paths** with included regions
|
||||||
|
`DorkNet[.]ClientMod/.*`, `DorkNet[.]Quest/.*`, `tools/build_pc[.]py` (one
|
||||||
|
per line) and excluded region `.*[.](md|markdown)`. The filter compares against the
|
||||||
|
last build's checkout, so the pipeline keeps it and deletes only `out/`
|
||||||
|
and its scratch folders after each run; without a checkout, Jenkins treats
|
||||||
|
every push as a change.
|
||||||
- **Build with Parameters** runs a manual build:
|
- **Build with Parameters** runs a manual build:
|
||||||
- `PLATFORMS` (both / pc / quest), and `PC_FULL_CLIENT` for the full PC client;
|
- `PLATFORMS` (both / pc / quest), and `PC_FULL_CLIENT` for the full PC client;
|
||||||
- snapshot tags, and per-run repository overrides;
|
- snapshot tags, and per-run repository overrides;
|
||||||
|
|||||||
+14
-14
@@ -35,20 +35,20 @@ DorkNet.sln
|
|||||||
|
|
||||||
| Project | Responsibility | Target |
|
| Project | Responsibility | Target |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `DorkNet.Server` | Shared REST/controller/service stack, database bootstrap, standalone fallback, monolith fallback | .NET 10 |
|
| `DorkNet.Server` | Shared REST/controller/service stack, database bootstrap, standalone fallback, monolith fallback | .NET 9 |
|
||||||
| `DorkNet.Models` | DTOs the wire protocol speaks. No project dependencies. | .NET 10 |
|
| `DorkNet.Models` | DTOs the wire protocol speaks. No project dependencies. | .NET 9 |
|
||||||
| `DorkNet.Contracts` | Service names, route ownership, service-map options, health responses, probe responses | .NET 10 |
|
| `DorkNet.Contracts` | Service names, route ownership, service-map options, health responses, probe responses | .NET 9 |
|
||||||
| `DorkNet.ServiceDefaults` | Common service health checks, JSON defaults, HTTP clients, service identity, route guard | .NET 10 |
|
| `DorkNet.ServiceDefaults` | Common service health checks, JSON defaults, HTTP clients, service identity, route guard | .NET 9 |
|
||||||
| `DorkNet.Gateway` | Edge reverse proxy; routes host/path slices to services and exposes service health probes | .NET 10 |
|
| `DorkNet.Gateway` | Edge reverse proxy; routes host/path slices to services and exposes service health probes | .NET 9 |
|
||||||
| `DorkNet.Services.Identity` | Accounts, auth, platform login, JWT issuance route slice | .NET 10 |
|
| `DorkNet.Services.Identity` | Accounts, auth, platform login, JWT issuance route slice | .NET 9 |
|
||||||
| `DorkNet.Services.Rooms` | Rooms, room keys, matchmaking, discovery route slice | .NET 10 |
|
| `DorkNet.Services.Rooms` | Rooms, room keys, matchmaking, discovery route slice | .NET 9 |
|
||||||
| `DorkNet.Services.Notify` | SignalR edge, messages/chat, notification fan-out route slice | .NET 10 |
|
| `DorkNet.Services.Notify` | SignalR edge, messages/chat, notification fan-out route slice | .NET 9 |
|
||||||
| `DorkNet.Services.Content` | CDN paths, image/photo APIs, uploads, S3-backed blobs | .NET 10 |
|
| `DorkNet.Services.Content` | CDN paths, image/photo APIs, uploads, S3-backed blobs | .NET 9 |
|
||||||
| `DorkNet.Services.Social` | Clubs, groups, announcements, player events, subscriptions | .NET 10 |
|
| `DorkNet.Services.Social` | Clubs, groups, announcements, player events, subscriptions | .NET 9 |
|
||||||
| `DorkNet.Services.Commerce` | Catalog, storefronts, econ, inventory, inventions | .NET 10 |
|
| `DorkNet.Services.Commerce` | Catalog, storefronts, econ, inventory, inventions | .NET 9 |
|
||||||
| `DorkNet.Services.Platform` | Service URLs, config, version checks, geo, strings, telemetry-style surfaces | .NET 10 |
|
| `DorkNet.Services.Platform` | Service URLs, config, version checks, geo, strings, telemetry-style surfaces | .NET 9 |
|
||||||
| `DorkNet.Services.Moderation` | Bug reporting, player reporting, sanitize, path-routed admin API, testcase routes | .NET 10 |
|
| `DorkNet.Services.Moderation` | Bug reporting, player reporting, sanitize, path-routed admin API, testcase routes | .NET 9 |
|
||||||
| `DorkNet.Services.Web` | Public site, admin static host, same-origin admin browser API, feed static host, site API | .NET 10 |
|
| `DorkNet.Services.Web` | Public site, admin static host, same-origin admin browser API, feed static host, site API | .NET 9 |
|
||||||
| `DorkNet.ClientMod` | MelonLoader 0.6.x IL2CPP mod — the client patcher, JSON config | .NET 6 |
|
| `DorkNet.ClientMod` | MelonLoader 0.6.x IL2CPP mod — the client patcher, JSON config | .NET 6 |
|
||||||
| `DorkNet.Quest` | native hook library + build scripts for the Meta Quest client | C++/arm64 |
|
| `DorkNet.Quest` | native hook library + build scripts for the Meta Quest client | C++/arm64 |
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user