Update packages, add dynamic service start/stop logic WIP (#113)

This commit is contained in:
Leendert de Borst
2024-07-26 00:07:51 +02:00
parent 99cc429779
commit 2f7a5acf42
32 changed files with 1243 additions and 99 deletions

View File

@@ -49,6 +49,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Logging", "src\U
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.RazorComponents", "src\Utilities\AliasVault.RazorComponents\AliasVault.RazorComponents.csproj", "{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.WorkerStatus", "src\Utilities\AliasVault.WorkerStatus\AliasVault.WorkerStatus.csproj", "{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -127,6 +129,10 @@ Global
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC}.Release|Any CPU.Build.0 = Release|Any CPU
{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -146,6 +152,7 @@ Global
{857BCD0E-753F-437A-AF75-B995B4D9A5FE} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
{FF0B0E64-1AE2-415C-A404-0EB78010821A} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FEE82475-C009-4762-8113-A6563D9DC49E}

View File

@@ -19,10 +19,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.6"/>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.6"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6"/>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -20,13 +20,13 @@
<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.6.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.6.0" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.0.1" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -16,7 +16,7 @@ using Microsoft.EntityFrameworkCore;
/// </summary>
[ApiController]
[Route("/")]
public class RootController : ControllerBase
public class RootController(AliasServerDbContext context) : ControllerBase
{
/// <summary>
/// Root endpoint that returns a 200 OK if the database connection is successful
@@ -30,20 +30,17 @@ public class RootController : ControllerBase
{
try
{
using (var dbContext = new AliasServerDbContext())
var appliedMigrations = context.Database.GetAppliedMigrations();
var allMigrations = context.Database.GetMigrations();
if (allMigrations.Except(appliedMigrations).Any())
{
var appliedMigrations = dbContext.Database.GetAppliedMigrations();
var allMigrations = dbContext.Database.GetMigrations();
if (allMigrations.Except(appliedMigrations).Any())
{
// There are pending migrations
return StatusCode(500, "There are pending migrations. Please run 'dotnet ef database update' to apply them.");
}
// Database is up to date
return Ok("OK");
// There are pending migrations
return StatusCode(500, "There are pending migrations. Please run 'dotnet ef database update' to apply them.");
}
// Database is up to date
return Ok("OK");
}
catch
{

View File

@@ -48,10 +48,10 @@
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.6" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.7" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>

View File

@@ -17,15 +17,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>

View File

@@ -16,17 +16,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
@@ -41,4 +41,8 @@
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Utilities\AliasVault.WorkerStatus\AliasVault.WorkerStatus.csproj" />
</ItemGroup>
</Project>

View File

@@ -7,6 +7,7 @@
namespace AliasServerDb;
using AliasVault.WorkerStatus.Database;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -16,7 +17,7 @@ using Microsoft.Extensions.Configuration;
/// we have two separate user objects, one for the admin panel and one for the vault. We manually
/// define the Identity tables in the OnModelCreating method.
/// </summary>
public class AliasServerDbContext : DbContext
public class AliasServerDbContext : WorkerStatusDbContext
{
/// <summary>
/// Initializes a new instance of the <see cref="AliasServerDbContext"/> class.

View File

@@ -1,4 +1,5 @@
using System;
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,5 @@
using System;
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -0,0 +1,565 @@
// <auto-generated />
using System;
using AliasServerDb;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace AliasServerDb.Migrations
{
[DbContext(typeof(AliasServerDbContext))]
[Migration("20240725202058_WorkerStatusTable")]
partial class WorkerStatusTable
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.7")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true);
modelBuilder.Entity("AliasServerDb.AdminRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AdminRoles");
});
modelBuilder.Entity("AliasServerDb.AdminUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastPasswordChanged")
.HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AdminUsers");
});
modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AliasVaultRoles");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("Salt")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasColumnType("TEXT");
b.Property<string>("Verifier")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AliasVaultUsers");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DeviceIdentifier")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("ExpireDate")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AliasVaultUserRefreshTokens");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Date")
.HasColumnType("TEXT");
b.Property<DateTime>("DateSystem")
.HasColumnType("TEXT");
b.Property<string>("From")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("FromDomain")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("FromLocal")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("MessageHtml")
.HasColumnType("TEXT");
b.Property<string>("MessagePlain")
.HasColumnType("TEXT");
b.Property<string>("MessagePreview")
.HasColumnType("TEXT");
b.Property<string>("MessageSource")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("PushNotificationSent")
.HasColumnType("INTEGER");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("To")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ToDomain")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ToLocal")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("Visible")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Date");
b.HasIndex("DateSystem");
b.HasIndex("PushNotificationSent");
b.HasIndex("ToLocal");
b.HasIndex("Visible");
b.ToTable("Emails");
});
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<byte[]>("Bytes")
.IsRequired()
.HasColumnType("BLOB");
b.Property<DateTime>("Date")
.HasColumnType("TEXT");
b.Property<int>("EmailId")
.HasColumnType("INTEGER");
b.Property<string>("Filename")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Filesize")
.HasColumnType("INTEGER");
b.Property<string>("MimeType")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("EmailId");
b.ToTable("EmailAttachments");
});
modelBuilder.Entity("AliasServerDb.Log", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Application")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Exception")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Level")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("LogEvent")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("LogEvent");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("MessageTemplate")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Properties")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("TimeStamp")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Application");
b.HasIndex("TimeStamp");
b.ToTable("Logs", (string)null);
});
modelBuilder.Entity("AliasServerDb.Vault", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("VaultBlob")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Vaults");
});
modelBuilder.Entity("AliasVault.WorkerStatus.WorkerServiceStatus", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CurrentStatus")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("DesiredStatus")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime>("Heartbeat")
.HasColumnType("TEXT");
b.Property<string>("ServiceName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar");
b.HasKey("Id");
b.ToTable("WorkerServiceStatuses");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("RoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("UserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.ToTable("UserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.ToTable("UserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("UserTokens", (string)null);
});
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
{
b.HasOne("AliasServerDb.Email", "Email")
.WithMany("Attachments")
.HasForeignKey("EmailId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Email");
});
modelBuilder.Entity("AliasServerDb.Vault", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.Navigation("Attachments");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,39 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AliasServerDb.Migrations
{
/// <inheritdoc />
public partial class WorkerStatusTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "WorkerServiceStatuses",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ServiceName = table.Column<string>(type: "varchar", maxLength: 255, nullable: false),
CurrentStatus = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
DesiredStatus = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
Heartbeat = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WorkerServiceStatuses", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "WorkerServiceStatuses");
}
}
}

View File

@@ -16,7 +16,7 @@ namespace AliasServerDb.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.6")
.HasAnnotation("ProductVersion", "8.0.7")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true);
@@ -399,6 +399,35 @@ namespace AliasServerDb.Migrations
b.ToTable("Vaults");
});
modelBuilder.Entity("AliasVault.WorkerStatus.WorkerServiceStatus", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CurrentStatus")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("DesiredStatus")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime>("Heartbeat")
.HasColumnType("TEXT");
b.Property<string>("ServiceName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar");
b.HasKey("Id");
b.ToTable("WorkerServiceStatuses");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")

View File

@@ -15,7 +15,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="MimeKit" Version="4.7.1" />
<PackageReference Include="NUglify" Version="1.21.9" />
<PackageReference Include="SmtpServer" Version="10.0.1" />

View File

@@ -6,6 +6,7 @@
//-----------------------------------------------------------------------
using System.Data.Common;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using AliasServerDb;
using AliasVault.SmtpService;
@@ -15,11 +16,14 @@ using Microsoft.EntityFrameworkCore;
using SmtpServer;
using SmtpServer.Storage;
using AliasVault.Logging;
using AliasVault.SmtpService.Workers;
using AliasVault.WorkerStatus;
using AliasVault.WorkerStatus.Database;
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
builder.Configuration.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);
builder.Services.ConfigureLogging(builder.Configuration, "SmtpService");
builder.Services.ConfigureLogging(builder.Configuration, Assembly.GetExecutingAssembly().GetName().Name!);
// Create global config object, get values from environment variables.
Config config = new Config();
@@ -118,7 +122,33 @@ builder.Services.AddSingleton(
}
);
builder.Services.AddHostedService<Worker>();
// -----------------------------------------------------------------------
// Worker status service registration.
// -----------------------------------------------------------------------
var globalServiceStatus = new GlobalServiceStatus();
builder.Services.AddSingleton(globalServiceStatus);
builder.Services.AddSingleton<Func<IWorkerStatusDbContext>>(sp =>
{
var factory = sp.GetRequiredService<IDbContextFactory<AliasServerDbContext>>();
return () => factory.CreateDbContext();
});
builder.Services.AddSingleton(sp => new WorkerStatusConfiguration
{
ServiceName = Assembly.GetExecutingAssembly().GetName().Name!,
GlobalServiceStatus = sp.GetRequiredService<GlobalServiceStatus>(),
});
builder.Services.AddHostedService<StatusWorker>();
// Register the names of the various worker services here so their status can be monitored.
globalServiceStatus.RegisterWorker(nameof(SmtpServerWorker));
// -----------------------------------------------------------------------
builder.Services.AddHostedService<SmtpServerWorker>();
var host = builder.Build();
using (var scope = host.Services.CreateScope())

View File

@@ -1,45 +0,0 @@
//-----------------------------------------------------------------------
// <copyright file="Worker.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.SmtpService;
public class Worker(ILogger<Worker> logger, SmtpServer.SmtpServer smtpServer) : BackgroundService
{
/// <inheritdoc />
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
logger.LogWarning("AliasVault.SmtpService starting at: {Time}", DateTimeOffset.Now);
// Start the SMTP server
await smtpServer.StartAsync(stoppingToken);
// Wait for the cancellation token to be triggered
await Task.Delay(Timeout.Infinite, stoppingToken);
}
catch (OperationCanceledException ex)
{
// This exception is thrown when the stoppingToken is canceled
// It's expected behavior, so we can just log it
logger.LogWarning(ex, "AliasVault.SmtpService is stopping due to a cancellation request.");
}
catch (Exception ex)
{
// Log any unexpected exceptions
logger.LogError(ex, "An error occurred in AliasVault.SmtpService");
}
finally
{
// Log that the service is stopping, whether it's due to cancellation or an error
logger.LogWarning("AliasVault.SmtpService stopped at: {Time}", DateTimeOffset.Now);
// Ensure the SMTP server is stopped
smtpServer.Shutdown();
}
}
}

View File

@@ -0,0 +1,112 @@
//-----------------------------------------------------------------------
// <copyright file="SmtpServerWorker.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.SmtpService.Workers;
using AliasVault.WorkerStatus;
public class SmtpServerWorker(ILogger<SmtpServerWorker> logger, GlobalServiceStatus globalServiceStatus, SmtpServer.SmtpServer smtpServer) : BackgroundService
{
private Task? _workerTask;
private readonly object _taskLock = new object();
/// <inheritdoc />
protected override async Task ExecuteAsync(CancellationToken serviceCancellationToken)
{
logger.LogInformation("AliasVault.SmtpService.SmtpServerWorker ExecuteAsync called.");
// Create a new cancellation token source for worker.
using var workerCancellationTokenSource = new CancellationTokenSource();
var workerCancellationToken = workerCancellationTokenSource.Token;
while (!serviceCancellationToken.IsCancellationRequested)
{
if (globalServiceStatus.CurrentStatus == "Started" || globalServiceStatus.CurrentStatus == "Starting")
{
// Set worker status to true for acknowledgement.
globalServiceStatus.SetWorkerStatus(nameof(SmtpServerWorker), true);
// Start the worker in the background.
lock (_taskLock)
{
if (_workerTask == null)
{
// Reset the worker cancellation token if it was canceled before
_workerTask = Task.Run(() => WorkerLogic(workerCancellationToken), workerCancellationToken);
}
}
}
else if (globalServiceStatus.CurrentStatus == "Stopping")
{
// Request the worker to stop.
await workerCancellationTokenSource.CancelAsync();
}
else if (globalServiceStatus.CurrentStatus == "Stopped")
{
// Ensure worker task is completed and reset it so it can be started again.
if (_workerTask != null)
{
try
{
await _workerTask;
}
catch (OperationCanceledException)
{
// Task was cancelled, handle if needed.
}
_workerTask = null;
}
}
await Task.Delay(1000, serviceCancellationToken);
}
// If we reach this point, the service is hard stopping: not in software but on OS level.
// Request the actual worker to stop.
await workerCancellationTokenSource.CancelAsync();
}
/// <summary>
/// Actual worker logic.
/// </summary>
/// <param name="stoppingToken"></param>
private async Task WorkerLogic(CancellationToken stoppingToken)
{
try
{
logger.LogWarning("AliasVault.SmtpService starting at: {Time}", DateTimeOffset.Now);
// Start the SMTP server
await smtpServer.StartAsync(stoppingToken);
// Wait for the cancellation token to be triggered
await Task.Delay(Timeout.Infinite, stoppingToken);
}
catch (OperationCanceledException ex)
{
// This exception is thrown when the stoppingToken is canceled
// It's expected behavior, so we can just log it
logger.LogWarning(ex, "AliasVault.SmtpService is stopping due to a cancellation request.");
}
catch (Exception ex)
{
// Log any unexpected exceptions
logger.LogError(ex, "An error occurred in AliasVault.SmtpService");
}
finally
{
// Log that the service is stopping, whether it's due to cancellation or an error
logger.LogWarning("AliasVault.SmtpService stopped at: {Time}", DateTimeOffset.Now);
// Ensure the SMTP server is stopped
smtpServer.Shutdown();
// Set worker status to false for acknowledgement.
globalServiceStatus.SetWorkerStatus(nameof(SmtpServerWorker), false);
}
}
}

View File

@@ -24,15 +24,15 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.44.0" />
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.45.1" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -15,7 +15,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="NUnit" Version="3.14.0"/>
<PackageReference Include="NUnit.Analyzers" Version="3.9.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -6,6 +6,7 @@
// -----------------------------------------------------------------------
using AliasVault.SmtpService.Handlers;
using AliasVault.SmtpService.Workers;
namespace AliasVault.IntegrationTests.SmtpServer;
@@ -19,6 +20,9 @@ using Microsoft.EntityFrameworkCore;
using global::SmtpServer;
using global::SmtpServer.Storage;
/// <summary>
/// Builder class for creating a test host for the SmtpServiceWorker in order to run integration tests against it.
/// </summary>
public class TestHostBuilder
{
/// <summary>
@@ -98,7 +102,7 @@ public class TestHostBuilder
}
);
services.AddHostedService<Worker>();
services.AddHostedService<SmtpServerWorker>();
// Ensure the in-memory database is populated with tables
var serviceProvider = services.BuildServiceProvider();

View File

@@ -33,7 +33,7 @@
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.7" />
</ItemGroup>
<ItemGroup>

View File

@@ -23,7 +23,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog" Version="4.0.1" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.2" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />

View File

@@ -12,7 +12,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.6"/>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.7" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json">
<Link>stylecop.json</Link>
</AdditionalFiles>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,37 @@
//-----------------------------------------------------------------------
// <copyright file="IWorkerStatusDbContext.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
using Microsoft.EntityFrameworkCore;
namespace AliasVault.WorkerStatus.Database;
/// <summary>
/// Interface for the WorkerStatusDbContext. Inherit from this interface to include the WorkerServiceStatus DbSet
/// which is used to store the status of worker services.
/// </summary>
public interface IWorkerStatusDbContext : IDisposable
{
/// <summary>
/// Gets or sets the WorkerServiceStatus DbSet.
/// </summary>
public DbSet<WorkerServiceStatus> WorkerServiceStatuses { get; set; }
/// <summary>
/// Save changes to the database.
/// </summary>
/// <returns>Count of records affected.</returns>
public int SaveChanges();
/// <summary>
/// Save changes to the database asynchronously.
/// </summary>
/// <param name="cancellationToken">CancellationToken instance.</param>
/// <returns>Task.</returns>
public Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,47 @@
//-----------------------------------------------------------------------
// <copyright file="WorkerServiceStatus.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AliasVault.WorkerStatus.Database;
/// <summary>
/// Represents the status of a worker service for monitoring and control.
/// </summary>
public class WorkerServiceStatus
{
/// <summary>
/// Gets or sets the unique identifier for the service status.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the name of the service.
/// </summary>
[Required]
[StringLength(255)]
[Column(TypeName = "varchar")]
public string ServiceName { get; set; } = null!;
/// <summary>
/// Gets or sets the current status of the service.
/// </summary>
[StringLength(50)]
public string CurrentStatus { get; set; } = null!;
/// <summary>
/// Gets or sets the desired status of the service.
/// </summary>
[StringLength(50)]
public string DesiredStatus { get; set; } = null!;
/// <summary>
/// Gets or sets the last heartbeat timestamp of the service.
/// </summary>
public DateTime Heartbeat { get; set; }
}

View File

@@ -0,0 +1,38 @@
//-----------------------------------------------------------------------
// <copyright file="WorkerStatusDbContext.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
using Microsoft.EntityFrameworkCore;
namespace AliasVault.WorkerStatus.Database;
/// <summary>
/// WorkerStatusDbContext class.
/// </summary>
public class WorkerStatusDbContext : DbContext, IWorkerStatusDbContext
{
/// <summary>
/// Initializes a new instance of the <see cref="WorkerStatusDbContext"/> class.
/// </summary>
public WorkerStatusDbContext()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WorkerStatusDbContext"/> class.
/// </summary>
/// <param name="options">DbContextOptions instance.</param>
public WorkerStatusDbContext(DbContextOptions options)
: base(options)
{
}
/// <summary>
/// Gets or sets the WorkerServiceStatus DbSet.
/// </summary>
public DbSet<WorkerServiceStatus> WorkerServiceStatuses { get; set; }
}

View File

@@ -0,0 +1,62 @@
//-----------------------------------------------------------------------
// <copyright file="GlobalServiceStatus.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.WorkerStatus;
using System.Collections.Concurrent;
/// <summary>
/// Global service status class for monitoring and control.
/// </summary>
public class GlobalServiceStatus
{
private readonly ConcurrentDictionary<string, bool> _workerStatuses = new();
/// <summary>
/// Gets or sets the status of the service.
/// </summary>
public string Status { get; set; } = "Stopped";
/// <summary>
/// Gets or sets the current status of the service.
/// </summary>
public string CurrentStatus { get; set; } = "Stopped";
/// <summary>
/// Register a worker with the service.
/// </summary>
/// <param name="workerName">Name of the worker</param>
public void RegisterWorker(string workerName)
{
_workerStatuses[workerName] = false;
}
/// <summary>
/// Set the status of a worker.
/// </summary>
/// <param name="workerName">Name of the worker.</param>
/// <param name="isRunning">Boolean which indicates if worker is currently running.</param>
public void SetWorkerStatus(string workerName, bool isRunning)
{
if (_workerStatuses.ContainsKey(workerName))
{
_workerStatuses[workerName] = isRunning;
}
}
/// <summary>
/// Returns boolean indicating if all workers are running.
/// </summary>
/// <returns>Boolean which indicates if all workers are started.</returns>
public bool AreAllWorkersRunning() => _workerStatuses.All(w => w.Value);
/// <summary>
/// Returns boolean indicating if all workers are stopped.
/// </summary>
/// <returns>Boolean which indicates if all workers are stopped.</returns>
public bool AreAllWorkersStopped() => _workerStatuses.All(w => !w.Value);
}

View File

@@ -0,0 +1,162 @@
//-----------------------------------------------------------------------
// <copyright file="StatusWorker.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
using AliasVault.WorkerStatus.Database;
namespace AliasVault.WorkerStatus;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
/// <summary>
/// StatusWorker class for monitoring and controlling the status of the worker services.
/// </summary>
public class StatusWorker(ILogger<StatusWorker> logger, WorkerStatusConfiguration config, Func<IWorkerStatusDbContext> createDbContext) : BackgroundService
{
private IWorkerStatusDbContext _dbContext = null!;
/// <summary>
/// Worker service execution method.
/// </summary>
/// <param name="stoppingToken">CancellationToken.</param>
/// <returns>Task.</returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_dbContext = createDbContext();
try
{
var statusEntry = await GetServiceStatus();
switch (statusEntry.CurrentStatus)
{
case "Starting":
await WaitForAllWorkersToStart(stoppingToken);
await SetServiceStatus(statusEntry, "Started");
logger.LogInformation("All workers started.");
break;
case "Stopping":
await WaitForAllWorkersToStop(stoppingToken);
await SetServiceStatus(statusEntry, "Stopped");
logger.LogInformation("All workers stopped.");
break;
case "Stopped":
logger.LogInformation("Service is (soft) stopped.");
break;
}
}
catch (Exception e)
{
logger.LogError(e, "Global main application exception");
}
await Task.Delay(5000, stoppingToken);
}
// If we reach this point, the service is hard stopping: not in software but on OS level.
// Mark the service as stopped.
_dbContext = createDbContext();
await SetServiceStatus(await GetServiceStatus(), "Stopped");
}
/// <summary>
/// Gets the current status record of the service from database.
/// </summary>
/// <returns>New current status.</returns>
private async Task<WorkerServiceStatus> GetServiceStatus()
{
var entry = await GetOrCreateInitialStatusRecord();
if (!string.IsNullOrEmpty(entry.DesiredStatus) && entry.CurrentStatus != entry.DesiredStatus)
{
entry.CurrentStatus = entry.DesiredStatus switch
{
"Started" => "Starting",
"Stopped" => "Stopping",
_ => entry.CurrentStatus,
};
}
config.GlobalServiceStatus.Status = entry.CurrentStatus;
config.GlobalServiceStatus.CurrentStatus = entry.CurrentStatus;
entry.Heartbeat = DateTime.Now;
await _dbContext.SaveChangesAsync();
return entry;
}
/// <summary>
/// Updates the status of the service.
/// </summary>
/// <param name="statusEntry">The WorkerServiceStatus entry to update.</param>
/// <param name="newStatus">The new status.</param>
/// <returns>New current status.</returns>
private async Task SetServiceStatus(WorkerServiceStatus statusEntry, string newStatus = "")
{
if (!string.IsNullOrEmpty(newStatus) && statusEntry.CurrentStatus != newStatus)
{
statusEntry.CurrentStatus = newStatus;
}
var status = statusEntry.CurrentStatus;
config.GlobalServiceStatus.Status = status;
config.GlobalServiceStatus.CurrentStatus = status;
statusEntry.Heartbeat = DateTime.Now;
await _dbContext.SaveChangesAsync();
}
/// <summary>
/// Waits for all workers to start.
/// </summary>
/// <param name="stoppingToken">CancellationToken.</param>
private async Task WaitForAllWorkersToStart(CancellationToken stoppingToken)
{
while (!config.GlobalServiceStatus.AreAllWorkersRunning())
{
logger.LogInformation("Waiting for all workers to start...");
await Task.Delay(1000, stoppingToken);
}
}
/// <summary>
/// Waits for all workers to stop.
/// </summary>
/// <param name="stoppingToken">CancellationToken.</param>
private async Task WaitForAllWorkersToStop(CancellationToken stoppingToken)
{
while (!config.GlobalServiceStatus.AreAllWorkersStopped())
{
logger.LogInformation("Waiting for all workers to stop...");
await Task.Delay(1000, stoppingToken);
}
}
/// <summary>
/// Retrieves status record or creates an initial status record if it does not exist.
/// </summary>
private async Task<WorkerServiceStatus> GetOrCreateInitialStatusRecord()
{
var entry = _dbContext.WorkerServiceStatuses.FirstOrDefault(x => x.ServiceName == config.ServiceName);
if (entry != null)
{
return entry;
}
entry = new WorkerServiceStatus
{
ServiceName = config.ServiceName,
CurrentStatus = "Started",
DesiredStatus = string.Empty,
};
await _dbContext.WorkerServiceStatuses.AddAsync(entry);
return entry;
}
}

View File

@@ -0,0 +1,26 @@
//-----------------------------------------------------------------------
// <copyright file="WorkerStatusConfiguration.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.WorkerStatus;
/// <summary>
/// Interface for the WorkerStatusDbContext.
/// </summary>
public class WorkerStatusConfiguration
{
/// <summary>
/// Gets or sets the GlobalServiceStatus for the WorkerStatusDbContext.
/// </summary>
public GlobalServiceStatus GlobalServiceStatus { get; set; } = null!;
/// <summary>
/// Gets or sets the ServiceName for the WorkerStatusDbContext.
/// </summary>
public string ServiceName { get; set; } = null!;
}

View File

@@ -22,7 +22,7 @@
<ItemGroup>
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" />
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.0" />
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
<PackageReference Include="srp" Version="1.0.7" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>