Skip to content

Commit 7b47913

Browse files
hemanandrclaude
andcommitted
feat: Implement Issue #23 - Windows Service Host
Complete Windows Service hosting implementation for ThingConnect Pulse: **Key Features:** - Windows Service configuration using UseWindowsService() - Serilog integration with rolling file logging (30-day retention) - Proper service lifecycle management with error handling - PowerShell installation script for complete service management **Changes:** - Added Windows Service hosting packages - Configured Serilog with daily rolling logs to ProgramData - Fixed nullable warnings for Release builds - Created install-service.ps1 for service installation/management **Files Modified:** - Program.cs: Added Windows Service + Serilog configuration - ConfigurationService.cs: Fixed nullable type annotations - ThingConnect.Pulse.Server.csproj: Added required packages - install-service.ps1: New PowerShell service management script - DEVELOPMENT_PLAN.md: Updated Issue #23 status to complete 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 6eabc67 commit 7b47913

File tree

5 files changed

+218
-67
lines changed

5 files changed

+218
-67
lines changed

DEVELOPMENT_PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ You can effectively work on **up to 6 parallel worktrees** without conflicts:
143143

144144
| Issue | Priority | Time | Description | Worktree |
145145
|-------|----------|------|-------------|----------|
146-
| #23 | P1 | 1d | Windows service host wrapper | 1 |
146+
| #23 | P1 | 1d | **COMPLETE** - Windows service host wrapper | 1 |
147147
| ENV-15 | P1 | 1d | Installer conventions, paths | 1 |
148148
| #24 | P1 | 4-6h | Inno Setup installer script | 1 |
149149

Lines changed: 102 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11

22
using Microsoft.EntityFrameworkCore;
3+
using Serilog;
4+
using Serilog.Events;
35
using ThingConnect.Pulse.Server.Data;
46
using ThingConnect.Pulse.Server.Infrastructure;
57
using ThingConnect.Pulse.Server.Services;
@@ -13,75 +15,110 @@ public class Program
1315
{
1416
public static void Main(string[] args)
1517
{
16-
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
17-
18-
// Add services to the container.
19-
builder.Services.AddDbContext<PulseDbContext>(options =>
20-
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
21-
22-
// Add memory cache for settings service
23-
builder.Services.AddMemoryCache();
24-
25-
// Add HTTP client for probes
26-
builder.Services.AddHttpClient();
27-
28-
// Add configuration services
29-
builder.Services.AddSingleton<ConfigParser>();
30-
builder.Services.AddScoped<IConfigurationService, ConfigurationService>();
31-
builder.Services.AddScoped<ISettingsService, SettingsService>();
32-
33-
// Add monitoring services
34-
builder.Services.AddScoped<IProbeService, ProbeService>();
35-
builder.Services.AddScoped<IOutageDetectionService, OutageDetectionService>();
36-
builder.Services.AddScoped<IDiscoveryService, DiscoveryService>();
37-
builder.Services.AddScoped<IStatusService, StatusService>();
38-
builder.Services.AddScoped<IHistoryService, HistoryService>();
39-
builder.Services.AddHostedService<MonitoringBackgroundService>();
40-
41-
// Add rollup services
42-
builder.Services.AddScoped<IRollupService, RollupService>();
43-
builder.Services.AddHostedService<RollupBackgroundService>();
44-
45-
// Add prune services
46-
builder.Services.AddScoped<IPruneService, PruneService>();
47-
48-
builder.Services.AddControllers(options =>
18+
// Configure Serilog for rolling file logging
19+
Log.Logger = new LoggerConfiguration()
20+
.MinimumLevel.Information()
21+
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
22+
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
23+
.Enrich.FromLogContext()
24+
.WriteTo.Console()
25+
.WriteTo.File(
26+
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
27+
"ThingConnect.Pulse", "logs", "pulse-.log"),
28+
rollingInterval: RollingInterval.Day,
29+
retainedFileCountLimit: 30,
30+
shared: true)
31+
.CreateLogger();
32+
33+
try
4934
{
50-
options.InputFormatters.Insert(0, new PlainTextInputFormatter());
51-
});
52-
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
53-
builder.Services.AddEndpointsApiExplorer();
54-
builder.Services.AddSwaggerGen();
55-
56-
WebApplication app = builder.Build();
57-
58-
// Initialize database with seed data in development
59-
if (app.Environment.IsDevelopment())
35+
Log.Information("Starting ThingConnect Pulse Server");
36+
37+
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
38+
39+
// Use Serilog as the logging provider
40+
builder.Host.UseSerilog();
41+
42+
// Configure Windows Service hosting
43+
builder.Host.UseWindowsService();
44+
45+
// Add services to the container.
46+
builder.Services.AddDbContext<PulseDbContext>(options =>
47+
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
48+
49+
// Add memory cache for settings service
50+
builder.Services.AddMemoryCache();
51+
52+
// Add HTTP client for probes
53+
builder.Services.AddHttpClient();
54+
55+
// Add configuration services
56+
builder.Services.AddSingleton<ConfigParser>();
57+
builder.Services.AddScoped<IConfigurationService, ConfigurationService>();
58+
builder.Services.AddScoped<ISettingsService, SettingsService>();
59+
60+
// Add monitoring services
61+
builder.Services.AddScoped<IProbeService, ProbeService>();
62+
builder.Services.AddScoped<IOutageDetectionService, OutageDetectionService>();
63+
builder.Services.AddScoped<IDiscoveryService, DiscoveryService>();
64+
builder.Services.AddScoped<IStatusService, StatusService>();
65+
builder.Services.AddScoped<IHistoryService, HistoryService>();
66+
builder.Services.AddHostedService<MonitoringBackgroundService>();
67+
68+
// Add rollup services
69+
builder.Services.AddScoped<IRollupService, RollupService>();
70+
builder.Services.AddHostedService<RollupBackgroundService>();
71+
72+
// Add prune services
73+
builder.Services.AddScoped<IPruneService, PruneService>();
74+
75+
builder.Services.AddControllers(options =>
76+
{
77+
options.InputFormatters.Insert(0, new PlainTextInputFormatter());
78+
});
79+
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
80+
builder.Services.AddEndpointsApiExplorer();
81+
builder.Services.AddSwaggerGen();
82+
83+
WebApplication app = builder.Build();
84+
85+
// Initialize database with seed data in development
86+
if (app.Environment.IsDevelopment())
87+
{
88+
using IServiceScope scope = app.Services.CreateScope();
89+
PulseDbContext context = scope.ServiceProvider.GetRequiredService<PulseDbContext>();
90+
SeedData.Initialize(context);
91+
}
92+
93+
app.UseDefaultFiles();
94+
app.UseStaticFiles();
95+
96+
// Configure the HTTP request pipeline.
97+
if (app.Environment.IsDevelopment())
98+
{
99+
app.UseSwagger();
100+
app.UseSwaggerUI();
101+
}
102+
103+
app.UseHttpsRedirection();
104+
105+
app.UseAuthorization();
106+
107+
app.MapControllers();
108+
109+
app.MapFallbackToFile("/index.html");
110+
111+
Log.Information("ThingConnect Pulse Server configured successfully");
112+
app.Run();
113+
}
114+
catch (Exception ex)
60115
{
61-
using IServiceScope scope = app.Services.CreateScope();
62-
PulseDbContext context = scope.ServiceProvider.GetRequiredService<PulseDbContext>();
63-
SeedData.Initialize(context);
116+
Log.Fatal(ex, "ThingConnect Pulse Server terminated unexpectedly");
64117
}
65-
66-
app.UseDefaultFiles();
67-
app.UseStaticFiles();
68-
69-
// Configure the HTTP request pipeline.
70-
if (app.Environment.IsDevelopment())
118+
finally
71119
{
72-
app.UseSwagger();
73-
app.UseSwaggerUI();
120+
Log.Information("ThingConnect Pulse Server stopped");
121+
Log.CloseAndFlush();
74122
}
75-
76-
app.UseHttpsRedirection();
77-
78-
app.UseAuthorization();
79-
80-
81-
app.MapControllers();
82-
83-
app.MapFallbackToFile("/index.html");
84-
85-
app.Run();
86123
}
87124
}

ThingConnect.Pulse.Server/Services/ConfigurationService.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,16 @@ public ConfigurationService(PulseDbContext context, ConfigParser parser)
2929

3030
public async Task<ApplyResultDto> ApplyConfigurationAsync(string yamlContent, string? actor = null, string? note = null)
3131
{
32-
(ConfigYaml config, ValidationErrorsDto validationErrors) = _parser.ParseAndValidate(yamlContent);
32+
(ConfigYaml? config, ValidationErrorsDto? validationErrors) = _parser.ParseAndValidate(yamlContent);
3333
if (validationErrors != null)
3434
{
3535
throw new InvalidOperationException($"Validation failed: {validationErrors.Message}");
3636
}
37+
38+
if (config == null)
39+
{
40+
throw new InvalidOperationException("Configuration parsing returned null");
41+
}
3742

3843
string fileHash = ComputeHash(yamlContent);
3944
ConfigVersion? existingVersion = await _context.ConfigVersions

ThingConnect.Pulse.Server/ThingConnect.Pulse.Server.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
</PackageReference>
1616
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
1717
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
18+
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
1819
<PackageReference Include="NJsonSchema" Version="11.4.0" />
20+
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
21+
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
1922
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
2023
<PackageReference Include="YamlDotNet" Version="16.3.0" />
2124
</ItemGroup>

install-service.ps1

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# ThingConnect Pulse - Windows Service Installation Script
2+
# Run as Administrator
3+
4+
param(
5+
[string]$Action = "install"
6+
)
7+
8+
$ServiceName = "ThingConnectPulse"
9+
$ServiceDisplayName = "ThingConnect Pulse Server"
10+
$ServiceDescription = "Network availability monitoring system for manufacturing sites"
11+
12+
# Get the current script directory
13+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
14+
$BinaryPath = Join-Path $ScriptDir "ThingConnect.Pulse.Server\bin\Debug\net8.0\ThingConnect.Pulse.Server.exe"
15+
16+
# Ensure we have admin privileges
17+
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
18+
Write-Error "This script requires Administrator privileges. Please run as Administrator."
19+
exit 1
20+
}
21+
22+
switch ($Action.ToLower()) {
23+
"install" {
24+
Write-Host "Installing ThingConnect Pulse Windows Service..." -ForegroundColor Green
25+
26+
# Build the application first
27+
Write-Host "Building application..." -ForegroundColor Yellow
28+
dotnet build ThingConnect.Pulse.Server --configuration Release
29+
if ($LASTEXITCODE -ne 0) {
30+
Write-Error "Build failed!"
31+
exit 1
32+
}
33+
34+
# Update binary path for Release build
35+
$BinaryPath = Join-Path $ScriptDir "ThingConnect.Pulse.Server\bin\Release\net8.0\ThingConnect.Pulse.Server.exe"
36+
37+
# Stop service if it exists
38+
$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
39+
if ($service) {
40+
Write-Host "Stopping existing service..." -ForegroundColor Yellow
41+
Stop-Service -Name $ServiceName -Force
42+
43+
Write-Host "Removing existing service..." -ForegroundColor Yellow
44+
sc.exe delete $ServiceName
45+
Start-Sleep 2
46+
}
47+
48+
# Create the service
49+
Write-Host "Creating service..." -ForegroundColor Yellow
50+
New-Service -Name $ServiceName -BinaryPathName $BinaryPath -DisplayName $ServiceDisplayName -Description $ServiceDescription -StartupType Manual
51+
52+
Write-Host "Service '$ServiceDisplayName' installed successfully!" -ForegroundColor Green
53+
Write-Host "Use 'Start-Service $ServiceName' to start the service" -ForegroundColor Cyan
54+
Write-Host "Logs will be written to: C:\ProgramData\ThingConnect.Pulse\logs\" -ForegroundColor Cyan
55+
}
56+
57+
"uninstall" {
58+
Write-Host "Uninstalling ThingConnect Pulse Windows Service..." -ForegroundColor Red
59+
60+
$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
61+
if ($service) {
62+
Write-Host "Stopping service..." -ForegroundColor Yellow
63+
Stop-Service -Name $ServiceName -Force
64+
65+
Write-Host "Removing service..." -ForegroundColor Yellow
66+
sc.exe delete $ServiceName
67+
68+
Write-Host "Service '$ServiceDisplayName' uninstalled successfully!" -ForegroundColor Green
69+
} else {
70+
Write-Host "Service '$ServiceName' not found." -ForegroundColor Yellow
71+
}
72+
}
73+
74+
"start" {
75+
Write-Host "Starting ThingConnect Pulse Service..." -ForegroundColor Green
76+
Start-Service -Name $ServiceName
77+
Write-Host "Service started successfully!" -ForegroundColor Green
78+
}
79+
80+
"stop" {
81+
Write-Host "Stopping ThingConnect Pulse Service..." -ForegroundColor Red
82+
Stop-Service -Name $ServiceName
83+
Write-Host "Service stopped successfully!" -ForegroundColor Green
84+
}
85+
86+
"status" {
87+
$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
88+
if ($service) {
89+
Write-Host "Service Status: $($service.Status)" -ForegroundColor Cyan
90+
Write-Host "Startup Type: $($service.StartType)" -ForegroundColor Cyan
91+
} else {
92+
Write-Host "Service '$ServiceName' not installed." -ForegroundColor Yellow
93+
}
94+
}
95+
96+
default {
97+
Write-Host "Usage: .\install-service.ps1 [install|uninstall|start|stop|status]" -ForegroundColor Yellow
98+
Write-Host ""
99+
Write-Host "Commands:" -ForegroundColor Cyan
100+
Write-Host " install - Build and install the Windows Service" -ForegroundColor White
101+
Write-Host " uninstall - Remove the Windows Service" -ForegroundColor White
102+
Write-Host " start - Start the service" -ForegroundColor White
103+
Write-Host " stop - Stop the service" -ForegroundColor White
104+
Write-Host " status - Show service status" -ForegroundColor White
105+
}
106+
}

0 commit comments

Comments
 (0)