Complete refactor along with tests and validators
This commit is contained in:
334
.gitignore
vendored
334
.gitignore
vendored
@@ -1,7 +1,5 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
@@ -10,12 +8,6 @@
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
@@ -23,341 +15,37 @@ mono_crash.*
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
# Visual Studio cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
# Visual Studio Code
|
||||
.vscode/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
# Rider
|
||||
.idea/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
# Application specific
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
# Environment specific configs
|
||||
appsettings.Development.json
|
||||
appsettings.Production.json
|
||||
50
Dockerfile
Normal file
50
Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
||||
# Build stage
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy project file and restore dependencies
|
||||
COPY ["MarketAlly.ProcessMonitor.csproj", "."]
|
||||
RUN dotnet restore
|
||||
|
||||
# Copy all source files
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN dotnet build -c Release -o /app/build
|
||||
|
||||
# Publish stage
|
||||
FROM build AS publish
|
||||
RUN dotnet publish -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# Runtime stage
|
||||
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS final
|
||||
WORKDIR /app
|
||||
|
||||
# Install required packages for process monitoring
|
||||
RUN apt-get update && apt-get install -y \
|
||||
procps \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create logs directory
|
||||
RUN mkdir -p /app/logs
|
||||
|
||||
# Copy published application
|
||||
COPY --from=publish /app/publish .
|
||||
|
||||
# Create non-root user
|
||||
RUN groupadd -r processmonitor && useradd -r -g processmonitor processmonitor
|
||||
RUN chown -R processmonitor:processmonitor /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER processmonitor
|
||||
|
||||
# Set environment variables
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
ENV DOTNET_ENVIRONMENT=Production
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD test -f /app/logs/processmonitor-*.log || exit 1
|
||||
|
||||
# Entry point
|
||||
ENTRYPOINT ["dotnet", "MarketAlly.ProcessMonitor.dll"]
|
||||
41
Exceptions/ProcessMonitorExceptions.cs
Normal file
41
Exceptions/ProcessMonitorExceptions.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace MarketAlly.ProcessMonitor.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Base exception for process monitor
|
||||
/// </summary>
|
||||
public class ProcessMonitorException : Exception
|
||||
{
|
||||
public ProcessMonitorException() { }
|
||||
public ProcessMonitorException(string message) : base(message) { }
|
||||
public ProcessMonitorException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when process validation fails
|
||||
/// </summary>
|
||||
public class ProcessValidationException : ProcessMonitorException
|
||||
{
|
||||
public ProcessValidationException() { }
|
||||
public ProcessValidationException(string message) : base(message) { }
|
||||
public ProcessValidationException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when process fails to start
|
||||
/// </summary>
|
||||
public class ProcessStartException : ProcessMonitorException
|
||||
{
|
||||
public ProcessStartException() { }
|
||||
public ProcessStartException(string message) : base(message) { }
|
||||
public ProcessStartException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when configuration is invalid
|
||||
/// </summary>
|
||||
public class ConfigurationException : ProcessMonitorException
|
||||
{
|
||||
public ConfigurationException() { }
|
||||
public ConfigurationException(string message) : base(message) { }
|
||||
public ConfigurationException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
119
Extensions/HealthCheckExtensions.cs
Normal file
119
Extensions/HealthCheckExtensions.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using MarketAlly.ProcessMonitor.Services;
|
||||
using MarketAlly.ProcessMonitor.Interfaces;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Health check extensions for monitoring application health
|
||||
/// </summary>
|
||||
public static class HealthCheckExtensions
|
||||
{
|
||||
public static IServiceCollection AddProcessMonitorHealthChecks(this IServiceCollection services)
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddCheck<ConfigurationHealthCheck>("configuration", tags: new[] { "ready" })
|
||||
.AddCheck<ProcessManagerHealthCheck>("process_manager", tags: new[] { "live" })
|
||||
.AddCheck<DiskSpaceHealthCheck>("disk_space", tags: new[] { "ready" });
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if configuration is accessible and valid
|
||||
/// </summary>
|
||||
public class ConfigurationHealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly IConfigurationService _configurationService;
|
||||
|
||||
public ConfigurationHealthCheck(IConfigurationService configurationService)
|
||||
{
|
||||
_configurationService = configurationService;
|
||||
}
|
||||
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||
HealthCheckContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await _configurationService.GetConfigurationAsync();
|
||||
|
||||
if (config?.Processes == null || config.Processes.Count == 0)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy("No processes configured");
|
||||
}
|
||||
|
||||
return HealthCheckResult.Healthy($"Configuration loaded with {config.Processes.Count} processes");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy("Configuration check failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if process manager is operational
|
||||
/// </summary>
|
||||
public class ProcessManagerHealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly IProcessManager _processManager;
|
||||
|
||||
public ProcessManagerHealthCheck(IProcessManager processManager)
|
||||
{
|
||||
_processManager = processManager;
|
||||
}
|
||||
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||
HealthCheckContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Simple check to ensure process manager can query running processes
|
||||
var count = _processManager.GetRunningProcessCount("System");
|
||||
return await Task.FromResult(HealthCheckResult.Healthy("Process manager is operational"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy("Process manager check failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks available disk space for logs
|
||||
/// </summary>
|
||||
public class DiskSpaceHealthCheck : IHealthCheck
|
||||
{
|
||||
private const long MinimumFreeMegabytes = 100;
|
||||
|
||||
public Task<HealthCheckResult> CheckHealthAsync(
|
||||
HealthCheckContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var logPath = Path.Combine(AppContext.BaseDirectory, "logs");
|
||||
var drive = new DriveInfo(Path.GetPathRoot(logPath) ?? "C:\\");
|
||||
|
||||
var freeSpaceMb = drive.AvailableFreeSpace / (1024 * 1024);
|
||||
|
||||
if (freeSpaceMb < MinimumFreeMegabytes)
|
||||
{
|
||||
return Task.FromResult(HealthCheckResult.Unhealthy(
|
||||
$"Low disk space: {freeSpaceMb}MB available"));
|
||||
}
|
||||
|
||||
return Task.FromResult(HealthCheckResult.Healthy(
|
||||
$"Disk space OK: {freeSpaceMb}MB available"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromResult(HealthCheckResult.Unhealthy("Disk space check failed", ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Interfaces/IConfigurationService.cs
Normal file
29
Interfaces/IConfigurationService.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Manages application configuration
|
||||
/// </summary>
|
||||
public interface IConfigurationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current process configuration
|
||||
/// </summary>
|
||||
Task<ProcessConfiguration> GetConfigurationAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Reloads configuration from source
|
||||
/// </summary>
|
||||
Task ReloadConfigurationAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when configuration changes
|
||||
/// </summary>
|
||||
event EventHandler<ProcessConfiguration> ConfigurationChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets application settings
|
||||
/// </summary>
|
||||
AppSettings GetAppSettings();
|
||||
}
|
||||
29
Interfaces/IProcessManager.cs
Normal file
29
Interfaces/IProcessManager.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Manages process lifecycle operations
|
||||
/// </summary>
|
||||
public interface IProcessManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts a process with the specified configuration
|
||||
/// </summary>
|
||||
Task<bool> StartProcessAsync(ProcessInfo processInfo, bool openInNewWindow, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the specified number of process instances are running
|
||||
/// </summary>
|
||||
Task EnsureProcessRunningAsync(ProcessInfo processInfo, int desiredCount, bool openInNewWindow, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of running processes by name
|
||||
/// </summary>
|
||||
int GetRunningProcessCount(string processName);
|
||||
|
||||
/// <summary>
|
||||
/// Stops all instances of a process
|
||||
/// </summary>
|
||||
Task<bool> StopProcessAsync(string processName, CancellationToken cancellationToken = default);
|
||||
}
|
||||
26
Interfaces/IProcessValidator.cs
Normal file
26
Interfaces/IProcessValidator.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Validates process configurations and paths
|
||||
/// </summary>
|
||||
public interface IProcessValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates a process path for security
|
||||
/// </summary>
|
||||
bool ValidateProcessPath(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Validates entire process configuration
|
||||
/// </summary>
|
||||
ValidationResult ValidateProcessInfo(ProcessInfo processInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if process has required permissions
|
||||
/// </summary>
|
||||
Task<bool> CheckPermissionsAsync(string path);
|
||||
}
|
||||
|
||||
public record ValidationResult(bool IsValid, List<string> Errors);
|
||||
29
Interfaces/IScheduler.cs
Normal file
29
Interfaces/IScheduler.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Handles process scheduling operations
|
||||
/// </summary>
|
||||
public interface IScheduler
|
||||
{
|
||||
/// <summary>
|
||||
/// Schedules a process to start at a specific time
|
||||
/// </summary>
|
||||
void ScheduleProcessStart(ProcessInfo processInfo, bool openInNewWindow);
|
||||
|
||||
/// <summary>
|
||||
/// Schedules a process for repeated execution at specified intervals
|
||||
/// </summary>
|
||||
void ScheduleRepeatedExecution(ProcessInfo processInfo, bool openInNewWindow);
|
||||
|
||||
/// <summary>
|
||||
/// Cancels all scheduled tasks for a process
|
||||
/// </summary>
|
||||
void CancelScheduledTasks(string processName);
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of all scheduled tasks
|
||||
/// </summary>
|
||||
void Dispose();
|
||||
}
|
||||
42
Models/AppSettings.cs
Normal file
42
Models/AppSettings.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
namespace MarketAlly.ProcessMonitor.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Application settings
|
||||
/// </summary>
|
||||
public class AppSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitoring interval in seconds
|
||||
/// </summary>
|
||||
public int MonitoringIntervalSeconds { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Enable debug logging
|
||||
/// </summary>
|
||||
public bool EnableDebugLogging { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Log retention days
|
||||
/// </summary>
|
||||
public int LogRetentionDays { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Allowed process paths (whitelist)
|
||||
/// </summary>
|
||||
public List<string> AllowedPaths { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Enable process path validation
|
||||
/// </summary>
|
||||
public bool EnablePathValidation { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable performance monitoring
|
||||
/// </summary>
|
||||
public bool EnablePerformanceMonitoring { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum concurrent process starts
|
||||
/// </summary>
|
||||
public int MaxConcurrentStarts { get; set; } = 5;
|
||||
}
|
||||
22
Models/ProcessConfiguration.cs
Normal file
22
Models/ProcessConfiguration.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace MarketAlly.ProcessMonitor.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Root configuration for process monitoring
|
||||
/// </summary>
|
||||
public class ProcessConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// List of processes to monitor
|
||||
/// </summary>
|
||||
public List<ProcessInfo> Processes { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Configuration version
|
||||
/// </summary>
|
||||
public string Version { get; set; } = "1.0";
|
||||
|
||||
/// <summary>
|
||||
/// Last modified timestamp
|
||||
/// </summary>
|
||||
public DateTime LastModified { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
68
Models/ProcessInfo.cs
Normal file
68
Models/ProcessInfo.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents process configuration information
|
||||
/// </summary>
|
||||
public class ProcessInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the process
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Path to the executable process
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Number of process instances to maintain
|
||||
/// </summary>
|
||||
[Range(0, 100)]
|
||||
public int Count { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time of day (HH:mm) to launch the process
|
||||
/// </summary>
|
||||
public string? Time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Frequency in minutes to launch the process
|
||||
/// </summary>
|
||||
[Range(1, int.MaxValue)]
|
||||
public int? Interval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to enable process monitoring for this process
|
||||
/// </summary>
|
||||
public bool Enable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Command line arguments for the process
|
||||
/// </summary>
|
||||
public string? Arguments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Working directory for the process
|
||||
/// </summary>
|
||||
public string? WorkingDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment variables for the process
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? EnvironmentVariables { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum retries on failure
|
||||
/// </summary>
|
||||
public int MaxRetries { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Delay between retries in seconds
|
||||
/// </summary>
|
||||
public int RetryDelaySeconds { get; set; } = 5;
|
||||
}
|
||||
@@ -1,24 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace marketally.processmonitor
|
||||
{
|
||||
class ProcessInfo
|
||||
{
|
||||
//Name of process
|
||||
public string Name { get; set; }
|
||||
//Path to the executable process
|
||||
public string Path { get; set; }
|
||||
//Number of processes
|
||||
public Int32 Count { get; set; }
|
||||
//Time of day hour and minute to launch
|
||||
public string Time { get; set; }
|
||||
//Frequency in minutes to launch
|
||||
public int? Interval { get; set; }
|
||||
//Whether to enable process
|
||||
public bool Enable { get; set; }
|
||||
}
|
||||
}
|
||||
// This file is kept for backward compatibility
|
||||
// The actual ProcessInfo model is now in Models/ProcessInfo.cs
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
|
||||
230
Program.cs
230
Program.cs
@@ -1,177 +1,91 @@
|
||||
using marketally.processmonitor;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Diagnostics;
|
||||
using MarketAlly.ProcessMonitor.Interfaces;
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
using MarketAlly.ProcessMonitor.Services;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
// Asking the user if they want to open new processes in a separate window
|
||||
Console.WriteLine("Do you want to start processes in a new window? (yes/no)");
|
||||
string userInput = Console.ReadLine().ToLower();
|
||||
bool openInNewWindow = userInput == "yes";
|
||||
namespace MarketAlly.ProcessMonitor;
|
||||
|
||||
double checkOnce = 0;
|
||||
|
||||
// The main loop that continuously checks and manages processes
|
||||
while (true)
|
||||
public class Program
|
||||
{
|
||||
// Define the path to the JSON file containing process information
|
||||
string jsonFilePath = "processlist.json"; // Relative path to the JSON file
|
||||
string json = File.ReadAllText(jsonFilePath);
|
||||
var jObject = JObject.Parse(json);
|
||||
var processInfos = jObject["processes"].ToObject<List<ProcessInfo>>();
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
// Configure Serilog
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithMachineName()
|
||||
.Enrich.WithProcessId()
|
||||
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
.WriteTo.File("logs/processmonitor-.log",
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileCountLimit: 30,
|
||||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
|
||||
.CreateLogger();
|
||||
|
||||
foreach (var processInfo in processInfos.Where(x => x.Enable))
|
||||
try
|
||||
{
|
||||
// Check if it's the first run and if the process has a specific interval set
|
||||
if (checkOnce == 0 && processInfo.Interval.HasValue)
|
||||
{
|
||||
// Schedule repeated execution
|
||||
ScheduleRepeatedExecution(processInfo);
|
||||
Log.Information("Starting Process Monitor");
|
||||
await CreateHostBuilder(args).RunConsoleAsync();
|
||||
}
|
||||
// If no specific time is set, ensure the process is running continuously
|
||||
else if (string.IsNullOrEmpty(processInfo.Time))
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Run continuously
|
||||
EnsureProcessRunning(processInfo, processInfo.Count);
|
||||
Log.Fatal(ex, "Application terminated unexpectedly");
|
||||
}
|
||||
// If it's the first run and a specific time is set, schedule the process
|
||||
else if (checkOnce == 0)
|
||||
finally
|
||||
{
|
||||
// Schedule for a specific time
|
||||
ScheduleProcessStart(processInfo);
|
||||
}
|
||||
}
|
||||
// After the first iteration, set checkOnce to 1 to avoid rescheduling
|
||||
if (checkOnce == 0)
|
||||
checkOnce = 1;
|
||||
Thread.Sleep(10000); // Wait for 10 seconds before checking again
|
||||
}
|
||||
|
||||
// Ensures the desired number of instances of a process are running
|
||||
void EnsureProcessRunning(ProcessInfo processInfo, int desiredCount)
|
||||
{
|
||||
var runningProcesses = Process.GetProcessesByName(processInfo.Name);
|
||||
int countToStart = desiredCount - runningProcesses.Length;
|
||||
|
||||
for (int i = 0; i < countToStart; i++)
|
||||
{
|
||||
if (openInNewWindow)
|
||||
{
|
||||
ProcessStartInfo startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = processInfo.Path,
|
||||
CreateNoWindow = !openInNewWindow, // Controlled by the user's choice
|
||||
UseShellExecute = true, // Use the system shell to start the process
|
||||
};
|
||||
Process.Start(startInfo);
|
||||
} else
|
||||
{
|
||||
Process.Start(processInfo.Path);
|
||||
}
|
||||
|
||||
Console.WriteLine($"Started {processInfo.Name} at {DateTime.Now}");
|
||||
AppendToLogFile(processInfo, "launched");
|
||||
await Log.CloseAndFlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// Schedules a process to start at a specific time
|
||||
void ScheduleProcessStart(ProcessInfo processInfo)
|
||||
static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.UseSerilog()
|
||||
.ConfigureAppConfiguration((context, config) =>
|
||||
{
|
||||
DateTime scheduledTime = DateTime.Today.Add(TimeSpan.Parse(processInfo.Time));
|
||||
TimeSpan delay = scheduledTime - DateTime.Now;
|
||||
config
|
||||
.SetBasePath(AppContext.BaseDirectory)
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: true)
|
||||
.AddEnvironmentVariables()
|
||||
.AddCommandLine(args);
|
||||
})
|
||||
.ConfigureServices((context, services) =>
|
||||
{
|
||||
// Configure app settings
|
||||
services.Configure<AppSettings>(context.Configuration.GetSection("ProcessMonitor"));
|
||||
|
||||
if (delay < TimeSpan.Zero)
|
||||
// Register services
|
||||
services.AddSingleton<IMemoryCache, MemoryCache>();
|
||||
services.AddSingleton(provider =>
|
||||
{
|
||||
// Scheduled time is in the past. Schedule for the next day or handle as needed.
|
||||
delay = delay.Add(TimeSpan.FromDays(1));
|
||||
}
|
||||
var configuration = provider.GetRequiredService<IConfiguration>();
|
||||
var appSettings = new AppSettings();
|
||||
configuration.GetSection("ProcessMonitor").Bind(appSettings);
|
||||
return appSettings;
|
||||
});
|
||||
|
||||
var timer = new System.Threading.Timer(_ =>
|
||||
{
|
||||
var runningProcesses = Process.GetProcessesByName(processInfo.Name);
|
||||
if (runningProcesses.Length == 0)
|
||||
{
|
||||
if (openInNewWindow)
|
||||
{
|
||||
ProcessStartInfo startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = processInfo.Path,
|
||||
CreateNoWindow = !openInNewWindow, // Controlled by the user's choice
|
||||
UseShellExecute = true, // Use the system shell to start the process
|
||||
};
|
||||
Process.Start(startInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
Process.Start(processInfo.Path);
|
||||
}
|
||||
AppendToLogFile(processInfo, "run");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Process still running {processInfo.Name} at {DateTime.Now}, skipping for next interval");
|
||||
AppendToLogFile(processInfo, "skipped");
|
||||
}
|
||||
services.AddSingleton<IProcessValidator, ProcessValidator>();
|
||||
services.AddSingleton<IProcessManager, ProcessManager>();
|
||||
services.AddSingleton<IScheduler, ProcessScheduler>();
|
||||
services.AddSingleton<IConfigurationService, ConfigurationService>();
|
||||
|
||||
}, null, delay, Timeout.InfiniteTimeSpan); // Run only once
|
||||
// Register hosted services
|
||||
services.AddHostedService<ProcessMonitorService>();
|
||||
services.AddHostedService<PerformanceMonitoringService>();
|
||||
|
||||
Console.WriteLine($"Scheduled {processInfo.Name} to start at {scheduledTime}");
|
||||
}
|
||||
|
||||
// Schedules a process for repeated execution
|
||||
void ScheduleRepeatedExecution(ProcessInfo processInfo)
|
||||
{
|
||||
var timer = new System.Threading.Timer(_ =>
|
||||
{
|
||||
var runningProcesses = Process.GetProcessesByName(processInfo.Name);
|
||||
if (runningProcesses.Length == 0)
|
||||
{
|
||||
if (openInNewWindow)
|
||||
{
|
||||
ProcessStartInfo startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = processInfo.Path,
|
||||
CreateNoWindow = !openInNewWindow, // Controlled by the user's choice
|
||||
UseShellExecute = true, // Use the system shell to start the process
|
||||
};
|
||||
Process.Start(startInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
Process.Start(processInfo.Path);
|
||||
}
|
||||
Console.WriteLine($"Started {processInfo.Name} at {DateTime.Now}");
|
||||
AppendToLogFile(processInfo, "run");
|
||||
} else
|
||||
{
|
||||
Console.WriteLine($"Process still running {processInfo.Name} at {DateTime.Now}, skipping for next interval");
|
||||
AppendToLogFile(processInfo, "skipped");
|
||||
}
|
||||
}, null, TimeSpan.Zero, TimeSpan.FromMinutes(processInfo.Interval.Value));
|
||||
|
||||
Console.WriteLine($"Scheduled {processInfo.Name} to run every {processInfo.Interval.Value} minutes");
|
||||
}
|
||||
|
||||
// Appends a log entry for a process action
|
||||
void AppendToLogFile(ProcessInfo processInfo, string state)
|
||||
{
|
||||
string logFilePath = Path.Combine("Logs", GetLogFileName()); // Store logs in a "Logs" subfolder
|
||||
string logEntry = $"{DateTime.Now}: {processInfo.Name} was {state}.";
|
||||
|
||||
if (!Directory.Exists("Logs"))
|
||||
{
|
||||
Directory.CreateDirectory("Logs");
|
||||
}
|
||||
|
||||
File.AppendAllText(logFilePath, logEntry + Environment.NewLine);
|
||||
}
|
||||
|
||||
string GetLogFileName()
|
||||
{
|
||||
var cultureInfo = System.Globalization.CultureInfo.CurrentCulture;
|
||||
int weekNo = cultureInfo.Calendar.GetWeekOfYear(
|
||||
DateTime.Now,
|
||||
cultureInfo.DateTimeFormat.CalendarWeekRule,
|
||||
cultureInfo.DateTimeFormat.FirstDayOfWeek);
|
||||
|
||||
return $"processLog_{DateTime.Now.Year}_Week{weekNo}.log";
|
||||
// Configure logging
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
builder.ClearProviders();
|
||||
builder.AddSerilog();
|
||||
});
|
||||
})
|
||||
.UseConsoleLifetime();
|
||||
}
|
||||
354
README.md
354
README.md
@@ -1,119 +1,291 @@
|
||||
# Process Monitor Utility
|
||||
|
||||
A production-ready process monitoring and management tool built with .NET 9, featuring enterprise-grade security, comprehensive error handling, and robust scheduling capabilities.
|
||||
|
||||
## Overview
|
||||
|
||||
This utility is a process monitoring and management tool written in C#. It allows users to automate the starting of processes based on a predefined schedule and conditions. The program reads from a JSON file to determine which processes to manage and how to manage them.
|
||||
Process Monitor is a sophisticated utility that automates process lifecycle management with features including:
|
||||
- Scheduled process execution
|
||||
- Continuous process monitoring
|
||||
- Automatic process restart with retry logic
|
||||
- Configuration hot-reload
|
||||
- Comprehensive logging with Serilog
|
||||
- Security validation and path whitelisting
|
||||
- Performance monitoring
|
||||
- Docker support
|
||||
- Health checks
|
||||
|
||||
## Features
|
||||
|
||||
- **Flexible Process Management**: Enables starting processes at specific times, continuously running them, or scheduling them at regular intervals.
|
||||
- **User Input for Window Management**: Users can choose whether processes should start in a new window.
|
||||
- **Logging**: Automatically logs process start and management actions in a weekly log file.
|
||||
### Core Functionality
|
||||
- **Flexible Process Management**: Start processes at specific times, run continuously, or schedule at intervals
|
||||
- **Retry Logic**: Automatic retry with exponential backoff on process failures
|
||||
- **Concurrent Execution Control**: Limit simultaneous process starts
|
||||
- **Process Count Management**: Ensure specific number of instances are always running
|
||||
|
||||
### Security
|
||||
- **Path Validation**: Whitelist-based executable path validation
|
||||
- **Argument Sanitization**: Protection against command injection
|
||||
- **Digital Signature Verification**: Optional executable signature checking
|
||||
- **Non-root Execution**: Docker container runs as non-privileged user
|
||||
|
||||
### Monitoring & Observability
|
||||
- **Structured Logging**: Serilog with file and console sinks
|
||||
- **Performance Monitoring**: CPU and memory usage tracking
|
||||
- **Health Checks**: Configuration, process manager, and disk space checks
|
||||
- **Log Rotation**: Automatic daily log rotation with retention
|
||||
|
||||
### Configuration
|
||||
- **Hot Reload**: Automatic configuration reload on file changes
|
||||
- **Environment Variables**: Override settings via environment variables
|
||||
- **JSON Schema Validation**: Strongly-typed configuration with validation
|
||||
|
||||
## Requirements
|
||||
|
||||
- .NET Framework or .NET Core
|
||||
- Newtonsoft.Json package for JSON processing
|
||||
- Marketally.ProcessMonitor library (if applicable)
|
||||
- .NET 9.0 Runtime or SDK
|
||||
- Windows, Linux, or macOS
|
||||
- Docker (optional, for containerized deployment)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Ensure that .NET Framework or .NET Core is installed on your system.
|
||||
2. Include the `Newtonsoft.Json` package in your project.
|
||||
3. Add `marketally.processmonitor` library, if it's a separate dependency.
|
||||
### From Source
|
||||
```bash
|
||||
git clone https://github.com/MarketAlly/Process-Monitor.git
|
||||
cd Process-Monitor
|
||||
dotnet restore
|
||||
dotnet build -c Release
|
||||
```
|
||||
|
||||
### Docker
|
||||
```bash
|
||||
docker build -t marketally/processmonitor:latest .
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### appsettings.json
|
||||
```json
|
||||
{
|
||||
"ProcessMonitor": {
|
||||
"MonitoringIntervalSeconds": 10,
|
||||
"EnableDebugLogging": false,
|
||||
"LogRetentionDays": 30,
|
||||
"AllowedPaths": [
|
||||
"C:\\Windows\\System32",
|
||||
"C:\\Program Files"
|
||||
],
|
||||
"EnablePathValidation": true,
|
||||
"EnablePerformanceMonitoring": true,
|
||||
"MaxConcurrentStarts": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### processlist.json
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"processes": [
|
||||
{
|
||||
"name": "MyApp",
|
||||
"path": "C:\\Program Files\\MyApp\\app.exe",
|
||||
"count": 2,
|
||||
"enable": true,
|
||||
"interval": 30,
|
||||
"time": "08:00",
|
||||
"arguments": "--config production",
|
||||
"workingDirectory": "C:\\Program Files\\MyApp",
|
||||
"environmentVariables": {
|
||||
"NODE_ENV": "production"
|
||||
},
|
||||
"maxRetries": 3,
|
||||
"retryDelaySeconds": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Configure Process List**: Edit the `processlist.json` file to include the list of processes you want to manage. The JSON structure should be as follows:
|
||||
### Command Line
|
||||
```bash
|
||||
# Run with default configuration
|
||||
dotnet MarketAlly.ProcessMonitor.dll
|
||||
|
||||
```json
|
||||
{
|
||||
"processes": [
|
||||
{
|
||||
"Name": "ProcessName",
|
||||
"Path": "ExecutablePath",
|
||||
"Enable": true/false,
|
||||
"Interval": Minutes (optional),
|
||||
"Time": "HH:mm" (optional),
|
||||
"Count": NumberOfInstances (optional)
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
# Run with custom environment
|
||||
dotnet MarketAlly.ProcessMonitor.dll --environment Production
|
||||
|
||||
# Override configuration via environment variables
|
||||
ProcessMonitor__MonitoringIntervalSeconds=30 dotnet MarketAlly.ProcessMonitor.dll
|
||||
```
|
||||
|
||||
2. **Run the Utility**: Execute the program. It will ask if processes should be started in a new window. Respond with `yes` or `no`.
|
||||
3. **Monitor Logs**: Check the `Logs` folder for weekly log files detailing the process management actions.
|
||||
### Docker
|
||||
```bash
|
||||
# Using docker-compose
|
||||
docker-compose up -d
|
||||
|
||||
## Functions Description
|
||||
|
||||
- `EnsureProcessRunning`: Ensures the specified number of process instances are running.
|
||||
- `ScheduleProcessStart`: Schedules a process to start at a specific time.
|
||||
- `ScheduleRepeatedExecution`: Schedules a process to start at regular intervals.
|
||||
- `AppendToLogFile`: Logs actions to a weekly log file.
|
||||
- `GetLogFileName`: Generates a filename for the log based on the current week of the year.
|
||||
|
||||
## ProcessInfo Class
|
||||
|
||||
The `ProcessInfo` class is a key component of the utility, defining the structure and properties of each process that needs to be monitored or managed. Here is a detailed explanation of its properties:
|
||||
|
||||
### Properties
|
||||
|
||||
- **Name (`string`)**:
|
||||
- **Description**: The name of the process. This is used to identify and manage the process within the system.
|
||||
- **Example**: `"ExampleProcess"`
|
||||
|
||||
- **Path (`string`)**:
|
||||
- **Description**: The file path to the executable of the process. This path is used to start the process.
|
||||
- **Example**: `"C:\\Program Files\\ExampleProcess\\process.exe"`
|
||||
|
||||
- **Count (`Int32`)**:
|
||||
- **Description**: The desired number of instances of this process to be running simultaneously. This is used to ensure that a specific number of process instances are active.
|
||||
- **Example**: `3`
|
||||
|
||||
- **Time (`string`)**:
|
||||
- **Description**: The specific time of day when the process should be launched. It should be in the format of "HH:mm" (hour and minute).
|
||||
- **Example**: `"14:30"` (This would launch the process at 2:30 PM)
|
||||
|
||||
- **Interval (`int?`)**:
|
||||
- **Description**: The frequency, in minutes, at which the process should be launched. This is used to schedule repeated execution of the process. If null, it indicates that the process does not need to be started at regular intervals.
|
||||
- **Example**: `15` (This would schedule the process to start every 15 minutes)
|
||||
|
||||
- **Enable (`bool`)**:
|
||||
- **Description**: A flag indicating whether the process monitoring or management for this particular process should be enabled or disabled.
|
||||
- **Example**: `true` (This would enable the process monitoring and management)
|
||||
|
||||
### Usage in JSON Configuration
|
||||
|
||||
In the `processlist.json` file, each process to be managed should be defined as an object with the above properties. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"processes": [
|
||||
{
|
||||
"Name": "ExampleProcess",
|
||||
"Path": "C:\\Program Files\\ExampleProcess\\process.exe",
|
||||
"Count": 3,
|
||||
"Time": "14:30",
|
||||
"Interval": 15,
|
||||
"Enable": true
|
||||
},
|
||||
// More processes can be added here
|
||||
]
|
||||
}
|
||||
# Using docker run
|
||||
docker run -d \
|
||||
--name process-monitor \
|
||||
-v $(pwd)/processlist.json:/app/processlist.json:ro \
|
||||
-v $(pwd)/logs:/app/logs \
|
||||
marketally/processmonitor:latest
|
||||
```
|
||||
|
||||
Each process object in the array represents a separate process configuration that the utility will manage based on the provided details.
|
||||
## Process Configuration Options
|
||||
|
||||
## Notes
|
||||
| Property | Type | Required | Description |
|
||||
|----------|------|----------|-------------|
|
||||
| name | string | Yes | Process identifier |
|
||||
| path | string | Yes | Absolute path to executable |
|
||||
| count | int | No | Number of instances to maintain (0-100) |
|
||||
| enable | bool | Yes | Enable/disable monitoring |
|
||||
| time | string | No | Start time in HH:mm format |
|
||||
| interval | int | No | Repeat interval in minutes |
|
||||
| arguments | string | No | Command line arguments |
|
||||
| workingDirectory | string | No | Working directory path |
|
||||
| environmentVariables | object | No | Environment variables |
|
||||
| maxRetries | int | No | Maximum retry attempts (default: 3) |
|
||||
| retryDelaySeconds | int | No | Delay between retries (default: 5) |
|
||||
|
||||
- The program uses a 10-second loop to continuously check and manage processes.
|
||||
- It is advisable to have error handling for reading the JSON file and managing processes.
|
||||
## Architecture
|
||||
|
||||
## Contribution
|
||||
The application follows SOLID principles with a clean architecture:
|
||||
|
||||
Contributions to the project are welcome. Please ensure you follow the coding standards for new features.
|
||||
```
|
||||
├── Interfaces/ # Service contracts
|
||||
├── Services/ # Service implementations
|
||||
├── Models/ # Data models
|
||||
├── Configuration/ # Configuration handling
|
||||
├── Validators/ # Security and validation
|
||||
├── Extensions/ # Extension methods
|
||||
├── Exceptions/ # Custom exceptions
|
||||
└── Tests/ # Unit and integration tests
|
||||
```
|
||||
|
||||
---
|
||||
### Key Components
|
||||
- **ProcessManager**: Handles process lifecycle with retry logic
|
||||
- **ProcessScheduler**: Manages scheduled tasks
|
||||
- **ConfigurationService**: Hot-reload configuration management
|
||||
- **ProcessValidator**: Security validation
|
||||
- **ProcessMonitorService**: Main background service
|
||||
- **PerformanceMonitoringService**: System metrics collection
|
||||
|
||||
This README provides a basic overview. Please read through the source code comments for more detailed information.
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run with coverage
|
||||
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
|
||||
|
||||
# Run specific test category
|
||||
dotnet test --filter Category=Unit
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Production Checklist
|
||||
1. ✅ Update `appsettings.Production.json` with production values
|
||||
2. ✅ Configure allowed paths for your environment
|
||||
3. ✅ Set appropriate log retention period
|
||||
4. ✅ Enable performance monitoring if needed
|
||||
5. ✅ Configure health check endpoints
|
||||
6. ✅ Set up log aggregation (e.g., to Seq, ELK)
|
||||
7. ✅ Configure monitoring and alerting
|
||||
8. ✅ Review security settings
|
||||
|
||||
### Kubernetes Deployment
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: process-monitor
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: process-monitor
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: process-monitor
|
||||
spec:
|
||||
containers:
|
||||
- name: process-monitor
|
||||
image: marketally/processmonitor:latest
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /app/processlist.json
|
||||
subPath: processlist.json
|
||||
- name: logs
|
||||
mountPath: /app/logs
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: process-config
|
||||
- name: logs
|
||||
persistentVolumeClaim:
|
||||
claimName: process-monitor-logs
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
- `/health/ready` - Readiness probe
|
||||
- `/health/live` - Liveness probe
|
||||
|
||||
### Metrics
|
||||
- Process count by name
|
||||
- CPU and memory usage
|
||||
- Scheduled task execution
|
||||
- Error rates and retry counts
|
||||
|
||||
### Logging
|
||||
Logs are written to:
|
||||
- Console (structured JSON in production)
|
||||
- File: `logs/processmonitor-YYYYMMDD.log`
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Path Whitelisting**: Only executables in allowed paths can be started
|
||||
2. **Argument Validation**: Protection against shell injection
|
||||
3. **File Permissions**: Ensure proper permissions on configuration files
|
||||
4. **Secrets Management**: Use environment variables or secret stores for sensitive data
|
||||
5. **Network Isolation**: Run in isolated network when possible
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Process fails to start**
|
||||
- Check path validation and whitelist
|
||||
- Verify file permissions
|
||||
- Check logs for detailed error messages
|
||||
|
||||
**Configuration not reloading**
|
||||
- Ensure file watcher has permissions
|
||||
- Check for file lock issues
|
||||
- Verify JSON syntax
|
||||
|
||||
**High memory usage**
|
||||
- Enable performance monitoring
|
||||
- Check for process leaks
|
||||
- Review retry settings
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Write tests for new functionality
|
||||
4. Ensure all tests pass
|
||||
5. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE.txt file for details.
|
||||
|
||||
## Support
|
||||
|
||||
For issues and feature requests, please use the GitHub issue tracker.
|
||||
183
Services/ConfigurationService.cs
Normal file
183
Services/ConfigurationService.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using System.Text.Json;
|
||||
using MarketAlly.ProcessMonitor.Interfaces;
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages application configuration with hot reload support
|
||||
/// </summary>
|
||||
public class ConfigurationService : IConfigurationService, IDisposable
|
||||
{
|
||||
private readonly ILogger<ConfigurationService> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IOptionsMonitor<AppSettings> _appSettings;
|
||||
private readonly FileSystemWatcher _fileWatcher;
|
||||
private readonly string _configFilePath;
|
||||
private readonly SemaphoreSlim _configLock = new(1, 1);
|
||||
|
||||
public event EventHandler<ProcessConfiguration>? ConfigurationChanged;
|
||||
|
||||
public ConfigurationService(
|
||||
ILogger<ConfigurationService> logger,
|
||||
IConfiguration configuration,
|
||||
IMemoryCache cache,
|
||||
IOptionsMonitor<AppSettings> appSettings)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||
_appSettings = appSettings ?? throw new ArgumentNullException(nameof(appSettings));
|
||||
|
||||
_configFilePath = Path.Combine(AppContext.BaseDirectory, "processlist.json");
|
||||
_fileWatcher = InitializeFileWatcher();
|
||||
}
|
||||
|
||||
public async Task<ProcessConfiguration> GetConfigurationAsync()
|
||||
{
|
||||
const string cacheKey = "ProcessConfiguration";
|
||||
|
||||
if (_cache.TryGetValue<ProcessConfiguration>(cacheKey, out var cached) && cached != null)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
await _configLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
// Double-check after acquiring lock
|
||||
if (_cache.TryGetValue<ProcessConfiguration>(cacheKey, out cached) && cached != null)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
var config = await LoadConfigurationAsync();
|
||||
|
||||
_cache.Set(cacheKey, config, new MemoryCacheEntryOptions
|
||||
{
|
||||
SlidingExpiration = TimeSpan.FromMinutes(5),
|
||||
Priority = CacheItemPriority.High
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_configLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ReloadConfigurationAsync()
|
||||
{
|
||||
_logger.LogInformation("Reloading process configuration");
|
||||
|
||||
await _configLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
_cache.Remove("ProcessConfiguration");
|
||||
var config = await LoadConfigurationAsync();
|
||||
|
||||
_cache.Set("ProcessConfiguration", config, new MemoryCacheEntryOptions
|
||||
{
|
||||
SlidingExpiration = TimeSpan.FromMinutes(5),
|
||||
Priority = CacheItemPriority.High
|
||||
});
|
||||
|
||||
ConfigurationChanged?.Invoke(this, config);
|
||||
_logger.LogInformation("Configuration reloaded successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to reload configuration");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_configLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public AppSettings GetAppSettings()
|
||||
{
|
||||
return _appSettings.CurrentValue;
|
||||
}
|
||||
|
||||
private async Task<ProcessConfiguration> LoadConfigurationAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_configFilePath))
|
||||
{
|
||||
_logger.LogError("Configuration file not found at {Path}", _configFilePath);
|
||||
throw new FileNotFoundException($"Configuration file not found: {_configFilePath}");
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(_configFilePath);
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
|
||||
var config = JsonSerializer.Deserialize<ProcessConfiguration>(json, options);
|
||||
|
||||
if (config == null || config.Processes == null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid configuration format");
|
||||
}
|
||||
|
||||
config.LastModified = File.GetLastWriteTimeUtc(_configFilePath);
|
||||
|
||||
_logger.LogInformation("Loaded configuration with {Count} processes", config.Processes.Count);
|
||||
return config;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading configuration from {Path}", _configFilePath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private FileSystemWatcher InitializeFileWatcher()
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_configFilePath) ?? AppContext.BaseDirectory;
|
||||
var fileName = Path.GetFileName(_configFilePath);
|
||||
|
||||
var watcher = new FileSystemWatcher(directory, fileName)
|
||||
{
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
watcher.Changed += async (sender, e) =>
|
||||
{
|
||||
// Debounce file changes
|
||||
await Task.Delay(500);
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Configuration file changed, reloading");
|
||||
await ReloadConfigurationAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error handling configuration file change");
|
||||
}
|
||||
};
|
||||
|
||||
return watcher;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_fileWatcher?.Dispose();
|
||||
_configLock?.Dispose();
|
||||
}
|
||||
}
|
||||
167
Services/PerformanceMonitoringService.cs
Normal file
167
Services/PerformanceMonitoringService.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Monitors system and process performance metrics
|
||||
/// </summary>
|
||||
public class PerformanceMonitoringService : BackgroundService
|
||||
{
|
||||
private readonly ILogger<PerformanceMonitoringService> _logger;
|
||||
private readonly AppSettings _appSettings;
|
||||
private readonly PerformanceCounter? _cpuCounter;
|
||||
private readonly PerformanceCounter? _memoryCounter;
|
||||
private readonly Dictionary<string, ProcessPerformanceInfo> _processMetrics = new();
|
||||
|
||||
public PerformanceMonitoringService(
|
||||
ILogger<PerformanceMonitoringService> logger,
|
||||
AppSettings appSettings)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_appSettings = appSettings ?? throw new ArgumentNullException(nameof(appSettings));
|
||||
|
||||
if (OperatingSystem.IsWindows() && _appSettings.EnablePerformanceMonitoring)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
|
||||
_memoryCounter = new PerformanceCounter("Memory", "Available MBytes");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to initialize performance counters");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (!_appSettings.EnablePerformanceMonitoring)
|
||||
{
|
||||
_logger.LogInformation("Performance monitoring is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await CollectMetricsAsync();
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error collecting performance metrics");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CollectMetricsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var systemMetrics = GetSystemMetrics();
|
||||
_logger.LogInformation("System metrics - CPU: {Cpu:F1}%, Memory Available: {Memory}MB",
|
||||
systemMetrics.CpuUsage, systemMetrics.AvailableMemoryMB);
|
||||
|
||||
// Collect process-specific metrics
|
||||
var processes = Process.GetProcesses();
|
||||
foreach (var process in processes)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_processMetrics.ContainsKey(process.ProcessName))
|
||||
{
|
||||
var metrics = GetProcessMetrics(process);
|
||||
_processMetrics[process.ProcessName] = metrics;
|
||||
|
||||
if (metrics.WorkingSetMB > 1000) // Log if process uses more than 1GB
|
||||
{
|
||||
_logger.LogWarning("Process {ProcessName} is using {Memory}MB of memory",
|
||||
process.ProcessName, metrics.WorkingSetMB);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore individual process errors
|
||||
}
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to collect metrics");
|
||||
}
|
||||
}
|
||||
|
||||
private SystemMetrics GetSystemMetrics()
|
||||
{
|
||||
var metrics = new SystemMetrics();
|
||||
|
||||
if (OperatingSystem.IsWindows() && _cpuCounter != null && _memoryCounter != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
metrics.CpuUsage = _cpuCounter.NextValue();
|
||||
metrics.AvailableMemoryMB = (long)_memoryCounter.NextValue();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cross-platform fallback
|
||||
using var process = Process.GetCurrentProcess();
|
||||
metrics.AvailableMemoryMB = GC.GetTotalMemory(false) / (1024 * 1024);
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
private ProcessPerformanceInfo GetProcessMetrics(Process process)
|
||||
{
|
||||
return new ProcessPerformanceInfo
|
||||
{
|
||||
ProcessName = process.ProcessName,
|
||||
ProcessId = process.Id,
|
||||
WorkingSetMB = process.WorkingSet64 / (1024 * 1024),
|
||||
ThreadCount = process.Threads.Count,
|
||||
HandleCount = process.HandleCount
|
||||
};
|
||||
}
|
||||
|
||||
public Dictionary<string, ProcessPerformanceInfo> GetCurrentMetrics()
|
||||
{
|
||||
return new Dictionary<string, ProcessPerformanceInfo>(_processMetrics);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_cpuCounter?.Dispose();
|
||||
_memoryCounter?.Dispose();
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public class SystemMetrics
|
||||
{
|
||||
public double CpuUsage { get; set; }
|
||||
public long AvailableMemoryMB { get; set; }
|
||||
}
|
||||
|
||||
public class ProcessPerformanceInfo
|
||||
{
|
||||
public string ProcessName { get; set; } = string.Empty;
|
||||
public int ProcessId { get; set; }
|
||||
public long WorkingSetMB { get; set; }
|
||||
public int ThreadCount { get; set; }
|
||||
public int HandleCount { get; set; }
|
||||
}
|
||||
179
Services/ProcessManager.cs
Normal file
179
Services/ProcessManager.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using System.Diagnostics;
|
||||
using MarketAlly.ProcessMonitor.Interfaces;
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
using MarketAlly.ProcessMonitor.Exceptions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages process lifecycle operations with retry logic and error handling
|
||||
/// </summary>
|
||||
public class ProcessManager : IProcessManager
|
||||
{
|
||||
private readonly ILogger<ProcessManager> _logger;
|
||||
private readonly IProcessValidator _validator;
|
||||
private readonly AsyncRetryPolicy<bool> _retryPolicy;
|
||||
private readonly SemaphoreSlim _startSemaphore;
|
||||
|
||||
public ProcessManager(ILogger<ProcessManager> logger, IProcessValidator validator, AppSettings settings)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_validator = validator ?? throw new ArgumentNullException(nameof(validator));
|
||||
_startSemaphore = new SemaphoreSlim(settings.MaxConcurrentStarts);
|
||||
|
||||
_retryPolicy = Policy<bool>
|
||||
.Handle<Exception>()
|
||||
.WaitAndRetryAsync(
|
||||
3,
|
||||
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
|
||||
onRetry: (outcome, timeSpan, retryCount, context) =>
|
||||
{
|
||||
_logger.LogWarning(outcome.Exception,
|
||||
"Retry {RetryCount} after {TimeSpan}s for process {ProcessName}",
|
||||
retryCount, timeSpan.TotalSeconds, context["ProcessName"]);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> StartProcessAsync(ProcessInfo processInfo, bool openInNewWindow, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(processInfo);
|
||||
|
||||
var validationResult = _validator.ValidateProcessInfo(processInfo);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
_logger.LogError("Process validation failed for {ProcessName}: {Errors}",
|
||||
processInfo.Name, string.Join(", ", validationResult.Errors));
|
||||
throw new ProcessValidationException($"Validation failed: {string.Join(", ", validationResult.Errors)}");
|
||||
}
|
||||
|
||||
await _startSemaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
return await _retryPolicy.ExecuteAsync(async (context) =>
|
||||
{
|
||||
_logger.LogInformation("Starting process {ProcessName} from {ProcessPath}",
|
||||
processInfo.Name, processInfo.Path);
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = processInfo.Path,
|
||||
Arguments = processInfo.Arguments ?? string.Empty,
|
||||
WorkingDirectory = processInfo.WorkingDirectory ?? Path.GetDirectoryName(processInfo.Path),
|
||||
CreateNoWindow = !openInNewWindow,
|
||||
UseShellExecute = openInNewWindow,
|
||||
RedirectStandardOutput = !openInNewWindow,
|
||||
RedirectStandardError = !openInNewWindow
|
||||
};
|
||||
|
||||
if (processInfo.EnvironmentVariables != null)
|
||||
{
|
||||
foreach (var kvp in processInfo.EnvironmentVariables)
|
||||
{
|
||||
startInfo.Environment[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var process = Process.Start(startInfo);
|
||||
if (process == null)
|
||||
{
|
||||
throw new ProcessStartException($"Failed to start process {processInfo.Name}");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Successfully started process {ProcessName} with PID {ProcessId}",
|
||||
processInfo.Name, process.Id);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to start process {ProcessName}", processInfo.Name);
|
||||
throw new ProcessStartException($"Failed to start process {processInfo.Name}", ex);
|
||||
}
|
||||
}, new Context { ["ProcessName"] = processInfo.Name });
|
||||
}
|
||||
finally
|
||||
{
|
||||
_startSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnsureProcessRunningAsync(ProcessInfo processInfo, int desiredCount, bool openInNewWindow, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(processInfo);
|
||||
|
||||
var runningCount = GetRunningProcessCount(processInfo.Name);
|
||||
var toStart = Math.Max(0, desiredCount - runningCount);
|
||||
|
||||
if (toStart == 0)
|
||||
{
|
||||
_logger.LogDebug("Process {ProcessName} already has {Count} instances running",
|
||||
processInfo.Name, runningCount);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Starting {Count} instances of {ProcessName}", toStart, processInfo.Name);
|
||||
|
||||
var tasks = new List<Task<bool>>();
|
||||
for (int i = 0; i < toStart; i++)
|
||||
{
|
||||
tasks.Add(StartProcessAsync(processInfo, openInNewWindow, cancellationToken));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public int GetRunningProcessCount(string processName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var processes = Process.GetProcessesByName(processName);
|
||||
return processes.Length;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting process count for {ProcessName}", processName);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> StopProcessAsync(string processName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var processes = Process.GetProcessesByName(processName);
|
||||
if (processes.Length == 0)
|
||||
{
|
||||
_logger.LogInformation("No running instances of {ProcessName} found", processName);
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var process in processes)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill();
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
_logger.LogInformation("Stopped process {ProcessName} with PID {ProcessId}",
|
||||
processName, process.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to stop process {ProcessName} with PID {ProcessId}",
|
||||
processName, process.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error stopping process {ProcessName}", processName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
156
Services/ProcessMonitorService.cs
Normal file
156
Services/ProcessMonitorService.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
using MarketAlly.ProcessMonitor.Interfaces;
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Background service that monitors and manages processes
|
||||
/// </summary>
|
||||
public class ProcessMonitorService : BackgroundService
|
||||
{
|
||||
private readonly ILogger<ProcessMonitorService> _logger;
|
||||
private readonly IProcessManager _processManager;
|
||||
private readonly IScheduler _scheduler;
|
||||
private readonly IConfigurationService _configurationService;
|
||||
private readonly AppSettings _appSettings;
|
||||
private readonly Dictionary<string, bool> _initializedProcesses = new();
|
||||
private ProcessConfiguration? _currentConfiguration;
|
||||
|
||||
public ProcessMonitorService(
|
||||
ILogger<ProcessMonitorService> logger,
|
||||
IProcessManager processManager,
|
||||
IScheduler scheduler,
|
||||
IConfigurationService configurationService)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_processManager = processManager ?? throw new ArgumentNullException(nameof(processManager));
|
||||
_scheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler));
|
||||
_configurationService = configurationService ?? throw new ArgumentNullException(nameof(configurationService));
|
||||
_appSettings = _configurationService.GetAppSettings();
|
||||
|
||||
// Subscribe to configuration changes
|
||||
_configurationService.ConfigurationChanged += OnConfigurationChanged;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Process Monitor Service starting");
|
||||
|
||||
// Ask user about window preference once at startup
|
||||
var openInNewWindow = await GetUserPreferenceAsync();
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var configuration = await _configurationService.GetConfigurationAsync();
|
||||
|
||||
if (_currentConfiguration == null ||
|
||||
configuration.LastModified != _currentConfiguration.LastModified)
|
||||
{
|
||||
_currentConfiguration = configuration;
|
||||
await ProcessConfigurationAsync(configuration, openInNewWindow, stoppingToken);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(_appSettings.MonitoringIntervalSeconds), stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when cancellation is requested
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in process monitoring loop");
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); // Wait before retry
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Process Monitor Service stopping");
|
||||
}
|
||||
|
||||
private async Task<bool> GetUserPreferenceAsync()
|
||||
{
|
||||
// In production, this could be read from configuration
|
||||
// For now, keeping the interactive prompt
|
||||
await Task.Run(() =>
|
||||
{
|
||||
Console.WriteLine("Do you want to start processes in a new window? (yes/no)");
|
||||
Console.WriteLine("This preference will be used for all processes during this session.");
|
||||
});
|
||||
|
||||
string? userInput = await Task.Run(() => Console.ReadLine());
|
||||
return userInput?.ToLower() == "yes";
|
||||
}
|
||||
|
||||
private async Task ProcessConfigurationAsync(
|
||||
ProcessConfiguration configuration,
|
||||
bool openInNewWindow,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var processInfo in configuration.Processes.Where(p => p.Enable))
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested) break;
|
||||
|
||||
try
|
||||
{
|
||||
var isFirstRun = !_initializedProcesses.ContainsKey(processInfo.Name);
|
||||
|
||||
if (isFirstRun)
|
||||
{
|
||||
_initializedProcesses[processInfo.Name] = true;
|
||||
|
||||
// Schedule if it has interval
|
||||
if (processInfo.Interval.HasValue)
|
||||
{
|
||||
_scheduler.ScheduleRepeatedExecution(processInfo, openInNewWindow);
|
||||
}
|
||||
// Schedule if it has specific time
|
||||
else if (!string.IsNullOrEmpty(processInfo.Time))
|
||||
{
|
||||
_scheduler.ScheduleProcessStart(processInfo, openInNewWindow);
|
||||
}
|
||||
}
|
||||
|
||||
// Always ensure continuous processes are running
|
||||
if (string.IsNullOrEmpty(processInfo.Time) && !processInfo.Interval.HasValue)
|
||||
{
|
||||
await _processManager.EnsureProcessRunningAsync(
|
||||
processInfo,
|
||||
processInfo.Count,
|
||||
openInNewWindow,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing configuration for {ProcessName}", processInfo.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfigurationChanged(object? sender, ProcessConfiguration newConfiguration)
|
||||
{
|
||||
_logger.LogInformation("Configuration changed, updating process monitoring");
|
||||
|
||||
// Cancel tasks for processes that are no longer enabled
|
||||
var disabledProcesses = _initializedProcesses.Keys
|
||||
.Where(name => !newConfiguration.Processes.Any(p => p.Name == name && p.Enable))
|
||||
.ToList();
|
||||
|
||||
foreach (var processName in disabledProcesses)
|
||||
{
|
||||
_scheduler.CancelScheduledTasks(processName);
|
||||
_initializedProcesses.Remove(processName);
|
||||
_logger.LogInformation("Cancelled monitoring for disabled process: {ProcessName}", processName);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_configurationService.ConfigurationChanged -= OnConfigurationChanged;
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
168
Services/ProcessScheduler.cs
Normal file
168
Services/ProcessScheduler.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using System.Collections.Concurrent;
|
||||
using MarketAlly.ProcessMonitor.Interfaces;
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Handles process scheduling with proper cleanup and error handling
|
||||
/// </summary>
|
||||
public class ProcessScheduler : IScheduler, IDisposable
|
||||
{
|
||||
private readonly ILogger<ProcessScheduler> _logger;
|
||||
private readonly IProcessManager _processManager;
|
||||
private readonly ConcurrentDictionary<string, List<Timer>> _scheduledTasks;
|
||||
private bool _disposed;
|
||||
|
||||
public ProcessScheduler(ILogger<ProcessScheduler> logger, IProcessManager processManager)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_processManager = processManager ?? throw new ArgumentNullException(nameof(processManager));
|
||||
_scheduledTasks = new ConcurrentDictionary<string, List<Timer>>();
|
||||
}
|
||||
|
||||
public void ScheduleProcessStart(ProcessInfo processInfo, bool openInNewWindow)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(processInfo);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(processInfo.Time))
|
||||
{
|
||||
_logger.LogWarning("Cannot schedule process {ProcessName} without a time", processInfo.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var scheduledTime = DateTime.Today.Add(TimeSpan.Parse(processInfo.Time));
|
||||
var delay = scheduledTime - DateTime.Now;
|
||||
|
||||
if (delay < TimeSpan.Zero)
|
||||
{
|
||||
delay = delay.Add(TimeSpan.FromDays(1));
|
||||
scheduledTime = scheduledTime.AddDays(1);
|
||||
}
|
||||
|
||||
var timer = new Timer(async _ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var runningCount = _processManager.GetRunningProcessCount(processInfo.Name);
|
||||
if (runningCount == 0)
|
||||
{
|
||||
await _processManager.StartProcessAsync(processInfo, openInNewWindow);
|
||||
_logger.LogInformation("Scheduled process {ProcessName} started at {Time}",
|
||||
processInfo.Name, DateTime.Now);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Process {ProcessName} already running, skipping scheduled start",
|
||||
processInfo.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in scheduled start for process {ProcessName}", processInfo.Name);
|
||||
}
|
||||
}, null, delay, Timeout.InfiniteTimeSpan);
|
||||
|
||||
AddScheduledTask(processInfo.Name, timer);
|
||||
|
||||
_logger.LogInformation("Scheduled {ProcessName} to start at {ScheduledTime}",
|
||||
processInfo.Name, scheduledTime);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to schedule process {ProcessName}", processInfo.Name);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void ScheduleRepeatedExecution(ProcessInfo processInfo, bool openInNewWindow)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(processInfo);
|
||||
|
||||
if (!processInfo.Interval.HasValue || processInfo.Interval.Value <= 0)
|
||||
{
|
||||
_logger.LogWarning("Cannot schedule repeated execution for {ProcessName} without valid interval",
|
||||
processInfo.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var timer = new Timer(async _ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var runningCount = _processManager.GetRunningProcessCount(processInfo.Name);
|
||||
if (runningCount == 0)
|
||||
{
|
||||
await _processManager.StartProcessAsync(processInfo, openInNewWindow);
|
||||
_logger.LogInformation("Repeated execution: started {ProcessName} at {Time}",
|
||||
processInfo.Name, DateTime.Now);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Process {ProcessName} already running, skipping interval start",
|
||||
processInfo.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in repeated execution for process {ProcessName}", processInfo.Name);
|
||||
}
|
||||
}, null, TimeSpan.Zero, TimeSpan.FromMinutes(processInfo.Interval.Value));
|
||||
|
||||
AddScheduledTask(processInfo.Name, timer);
|
||||
|
||||
_logger.LogInformation("Scheduled {ProcessName} to run every {Interval} minutes",
|
||||
processInfo.Name, processInfo.Interval.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to schedule repeated execution for process {ProcessName}", processInfo.Name);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelScheduledTasks(string processName)
|
||||
{
|
||||
if (_scheduledTasks.TryRemove(processName, out var timers))
|
||||
{
|
||||
foreach (var timer in timers)
|
||||
{
|
||||
timer?.Dispose();
|
||||
}
|
||||
_logger.LogInformation("Cancelled all scheduled tasks for {ProcessName}", processName);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddScheduledTask(string processName, Timer timer)
|
||||
{
|
||||
_scheduledTasks.AddOrUpdate(processName,
|
||||
new List<Timer> { timer },
|
||||
(key, list) =>
|
||||
{
|
||||
list.Add(timer);
|
||||
return list;
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
foreach (var kvp in _scheduledTasks)
|
||||
{
|
||||
foreach (var timer in kvp.Value)
|
||||
{
|
||||
timer?.Dispose();
|
||||
}
|
||||
}
|
||||
_scheduledTasks.Clear();
|
||||
|
||||
_disposed = true;
|
||||
_logger.LogInformation("ProcessScheduler disposed");
|
||||
}
|
||||
}
|
||||
188
Services/ProcessValidator.cs
Normal file
188
Services/ProcessValidator.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Principal;
|
||||
using System.Text.RegularExpressions;
|
||||
using MarketAlly.ProcessMonitor.Interfaces;
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Validates process configurations for security and correctness
|
||||
/// </summary>
|
||||
public class ProcessValidator : IProcessValidator
|
||||
{
|
||||
private readonly ILogger<ProcessValidator> _logger;
|
||||
private readonly AppSettings _appSettings;
|
||||
private readonly HashSet<string> _allowedPaths;
|
||||
private static readonly Regex TimeFormatRegex = new(@"^([01]?[0-9]|2[0-3]):[0-5][0-9]$", RegexOptions.Compiled);
|
||||
|
||||
public ProcessValidator(ILogger<ProcessValidator> logger, AppSettings appSettings)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_appSettings = appSettings ?? throw new ArgumentNullException(nameof(appSettings));
|
||||
_allowedPaths = new HashSet<string>(_appSettings.AllowedPaths ?? new List<string>(), StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public bool ValidateProcessPath(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
_logger.LogWarning("Process path is null or empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Ensure absolute path
|
||||
if (!Path.IsPathRooted(path))
|
||||
{
|
||||
_logger.LogWarning("Process path is not absolute: {Path}", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize path
|
||||
var normalizedPath = Path.GetFullPath(path);
|
||||
|
||||
// Check if file exists
|
||||
if (!File.Exists(normalizedPath))
|
||||
{
|
||||
_logger.LogWarning("Process file does not exist: {Path}", normalizedPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check against whitelist if enabled
|
||||
if (_appSettings.EnablePathValidation && _allowedPaths.Count > 0)
|
||||
{
|
||||
var isAllowed = _allowedPaths.Any(allowedPath =>
|
||||
normalizedPath.StartsWith(allowedPath, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!isAllowed)
|
||||
{
|
||||
_logger.LogWarning("Process path not in allowed list: {Path}", normalizedPath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify it's an executable
|
||||
var extension = Path.GetExtension(normalizedPath).ToLowerInvariant();
|
||||
var executableExtensions = new[] { ".exe", ".bat", ".cmd", ".ps1", ".sh" };
|
||||
|
||||
if (!executableExtensions.Contains(extension))
|
||||
{
|
||||
_logger.LogWarning("File is not an executable: {Path}", normalizedPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error validating process path: {Path}", path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public ValidationResult ValidateProcessInfo(ProcessInfo processInfo)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (processInfo == null)
|
||||
{
|
||||
errors.Add("Process info is null");
|
||||
return new ValidationResult(false, errors);
|
||||
}
|
||||
|
||||
// Validate name
|
||||
if (string.IsNullOrWhiteSpace(processInfo.Name))
|
||||
{
|
||||
errors.Add("Process name is required");
|
||||
}
|
||||
else if (processInfo.Name.Length > 260)
|
||||
{
|
||||
errors.Add("Process name is too long");
|
||||
}
|
||||
|
||||
// Validate path
|
||||
if (!ValidateProcessPath(processInfo.Path))
|
||||
{
|
||||
errors.Add($"Invalid process path: {processInfo.Path}");
|
||||
}
|
||||
|
||||
// Validate count
|
||||
if (processInfo.Count < 0 || processInfo.Count > 100)
|
||||
{
|
||||
errors.Add($"Process count must be between 0 and 100, got {processInfo.Count}");
|
||||
}
|
||||
|
||||
// Validate time format
|
||||
if (!string.IsNullOrWhiteSpace(processInfo.Time) && !TimeFormatRegex.IsMatch(processInfo.Time))
|
||||
{
|
||||
errors.Add($"Invalid time format: {processInfo.Time}. Expected HH:mm");
|
||||
}
|
||||
|
||||
// Validate interval
|
||||
if (processInfo.Interval.HasValue && processInfo.Interval.Value <= 0)
|
||||
{
|
||||
errors.Add($"Interval must be positive, got {processInfo.Interval.Value}");
|
||||
}
|
||||
|
||||
// Validate working directory
|
||||
if (!string.IsNullOrWhiteSpace(processInfo.WorkingDirectory))
|
||||
{
|
||||
if (!Directory.Exists(processInfo.WorkingDirectory))
|
||||
{
|
||||
errors.Add($"Working directory does not exist: {processInfo.WorkingDirectory}");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate arguments for potential injection
|
||||
if (!string.IsNullOrWhiteSpace(processInfo.Arguments))
|
||||
{
|
||||
var dangerousPatterns = new[] { ";", "|", "&", "`", "$(" };
|
||||
if (dangerousPatterns.Any(pattern => processInfo.Arguments.Contains(pattern)))
|
||||
{
|
||||
errors.Add("Arguments contain potentially dangerous characters");
|
||||
}
|
||||
}
|
||||
|
||||
return new ValidationResult(errors.Count == 0, errors);
|
||||
}
|
||||
|
||||
public async Task<bool> CheckPermissionsAsync(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if we can read the file
|
||||
using (var stream = File.OpenRead(path))
|
||||
{
|
||||
// File is readable
|
||||
}
|
||||
|
||||
// Check if running as administrator (Windows)
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var identity = WindowsIdentity.GetCurrent();
|
||||
var principal = new WindowsPrincipal(identity);
|
||||
var isAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
|
||||
if (!isAdmin)
|
||||
{
|
||||
_logger.LogWarning("Not running as administrator, some processes may fail to start");
|
||||
}
|
||||
}
|
||||
|
||||
return await Task.FromResult(true);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
_logger.LogError("No permission to access file: {Path}", path);
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error checking permissions for: {Path}", path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
142
Tests/Unit/ProcessManagerTests.cs
Normal file
142
Tests/Unit/ProcessManagerTests.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using FluentAssertions;
|
||||
using MarketAlly.ProcessMonitor.Interfaces;
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
using MarketAlly.ProcessMonitor.Services;
|
||||
using MarketAlly.ProcessMonitor.Exceptions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Tests.Unit;
|
||||
|
||||
[TestClass]
|
||||
public class ProcessManagerTests
|
||||
{
|
||||
private Mock<ILogger<ProcessManager>> _loggerMock = null!;
|
||||
private Mock<IProcessValidator> _validatorMock = null!;
|
||||
private ProcessManager _processManager = null!;
|
||||
private AppSettings _appSettings = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_loggerMock = new Mock<ILogger<ProcessManager>>();
|
||||
_validatorMock = new Mock<IProcessValidator>();
|
||||
_appSettings = new AppSettings { MaxConcurrentStarts = 5 };
|
||||
_processManager = new ProcessManager(_loggerMock.Object, _validatorMock.Object, _appSettings);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task StartProcessAsync_WithValidConfig_LogsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var processInfo = new ProcessInfo
|
||||
{
|
||||
Name = "test",
|
||||
Path = @"C:\test.exe",
|
||||
Enable = true
|
||||
};
|
||||
|
||||
_validatorMock.Setup(v => v.ValidateProcessInfo(It.IsAny<ProcessInfo>()))
|
||||
.Returns(new ValidationResult(true, new List<string>()));
|
||||
|
||||
// Act & Assert
|
||||
// We can't actually start a process in unit tests, so we verify the validation occurs
|
||||
try
|
||||
{
|
||||
await _processManager.StartProcessAsync(processInfo, false);
|
||||
}
|
||||
catch (ProcessStartException)
|
||||
{
|
||||
// Expected when the process doesn't exist
|
||||
}
|
||||
|
||||
_validatorMock.Verify(v => v.ValidateProcessInfo(processInfo), Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task StartProcessAsync_WithInvalidConfig_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var processInfo = new ProcessInfo
|
||||
{
|
||||
Name = "invalid",
|
||||
Path = "invalid_path",
|
||||
Enable = true
|
||||
};
|
||||
|
||||
_validatorMock.Setup(v => v.ValidateProcessInfo(It.IsAny<ProcessInfo>()))
|
||||
.Returns(new ValidationResult(false, new List<string> { "Invalid path" }));
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsExceptionAsync<ProcessValidationException>(
|
||||
async () => await _processManager.StartProcessAsync(processInfo, false));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task EnsureProcessRunningAsync_LogsCorrectMessage()
|
||||
{
|
||||
// Arrange
|
||||
var processInfo = new ProcessInfo
|
||||
{
|
||||
Name = "test",
|
||||
Path = @"C:\test.exe",
|
||||
Count = 3,
|
||||
Enable = true
|
||||
};
|
||||
|
||||
_validatorMock.Setup(v => v.ValidateProcessInfo(It.IsAny<ProcessInfo>()))
|
||||
.Returns(new ValidationResult(true, new List<string>()));
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await _processManager.EnsureProcessRunningAsync(processInfo, 3, false);
|
||||
}
|
||||
catch (ProcessStartException)
|
||||
{
|
||||
// Expected when the process doesn't exist
|
||||
}
|
||||
|
||||
// Assert
|
||||
_loggerMock.Verify(
|
||||
l => l.Log(
|
||||
LogLevel.Information,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Starting 3 instances")),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetRunningProcessCount_ReturnsCorrectCount()
|
||||
{
|
||||
// Arrange & Act
|
||||
var count = _processManager.GetRunningProcessCount("System");
|
||||
|
||||
// Assert
|
||||
count.Should().BeGreaterOrEqualTo(0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task StopProcessAsync_ReturnsTrue_WhenNoProcessesRunning()
|
||||
{
|
||||
// Arrange
|
||||
var processName = "nonexistent_process";
|
||||
|
||||
// Act
|
||||
var result = await _processManager.StopProcessAsync(processName);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
_loggerMock.Verify(
|
||||
l => l.Log(
|
||||
LogLevel.Information,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("No running instances")),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
||||
Times.Once);
|
||||
}
|
||||
}
|
||||
168
Tests/Unit/ProcessSchedulerTests.cs
Normal file
168
Tests/Unit/ProcessSchedulerTests.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using FluentAssertions;
|
||||
using MarketAlly.ProcessMonitor.Interfaces;
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
using MarketAlly.ProcessMonitor.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Tests.Unit;
|
||||
|
||||
[TestClass]
|
||||
public class ProcessSchedulerTests
|
||||
{
|
||||
private Mock<ILogger<ProcessScheduler>> _loggerMock = null!;
|
||||
private Mock<IProcessManager> _processManagerMock = null!;
|
||||
private ProcessScheduler _scheduler = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_loggerMock = new Mock<ILogger<ProcessScheduler>>();
|
||||
_processManagerMock = new Mock<IProcessManager>();
|
||||
_scheduler = new ProcessScheduler(_loggerMock.Object, _processManagerMock.Object);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
_scheduler?.Dispose();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ScheduleProcessStart_WithValidTime_SchedulesProcess()
|
||||
{
|
||||
// Arrange
|
||||
var processInfo = new ProcessInfo
|
||||
{
|
||||
Name = "TestProcess",
|
||||
Path = "C:\\test.exe",
|
||||
Time = DateTime.Now.AddHours(1).ToString("HH:mm"),
|
||||
Enable = true
|
||||
};
|
||||
|
||||
// Act
|
||||
_scheduler.ScheduleProcessStart(processInfo, false);
|
||||
|
||||
// Assert
|
||||
_loggerMock.Verify(
|
||||
l => l.Log(
|
||||
LogLevel.Information,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Scheduled TestProcess to start at")),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ScheduleProcessStart_WithoutTime_DoesNotSchedule()
|
||||
{
|
||||
// Arrange
|
||||
var processInfo = new ProcessInfo
|
||||
{
|
||||
Name = "TestProcess",
|
||||
Path = "C:\\test.exe",
|
||||
Time = null,
|
||||
Enable = true
|
||||
};
|
||||
|
||||
// Act
|
||||
_scheduler.ScheduleProcessStart(processInfo, false);
|
||||
|
||||
// Assert
|
||||
_loggerMock.Verify(
|
||||
l => l.Log(
|
||||
LogLevel.Warning,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Cannot schedule process")),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ScheduleRepeatedExecution_WithValidInterval_SchedulesProcess()
|
||||
{
|
||||
// Arrange
|
||||
var processInfo = new ProcessInfo
|
||||
{
|
||||
Name = "TestProcess",
|
||||
Path = "C:\\test.exe",
|
||||
Interval = 15,
|
||||
Enable = true
|
||||
};
|
||||
|
||||
_processManagerMock.Setup(m => m.GetRunningProcessCount(It.IsAny<string>()))
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
_scheduler.ScheduleRepeatedExecution(processInfo, false);
|
||||
|
||||
// Assert
|
||||
_loggerMock.Verify(
|
||||
l => l.Log(
|
||||
LogLevel.Information,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Scheduled TestProcess to run every 15 minutes")),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CancelScheduledTasks_RemovesAllTasksForProcess()
|
||||
{
|
||||
// Arrange
|
||||
var processInfo = new ProcessInfo
|
||||
{
|
||||
Name = "TestProcess",
|
||||
Path = "C:\\test.exe",
|
||||
Interval = 15,
|
||||
Enable = true
|
||||
};
|
||||
|
||||
_scheduler.ScheduleRepeatedExecution(processInfo, false);
|
||||
|
||||
// Act
|
||||
_scheduler.CancelScheduledTasks("TestProcess");
|
||||
|
||||
// Assert
|
||||
_loggerMock.Verify(
|
||||
l => l.Log(
|
||||
LogLevel.Information,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Cancelled all scheduled tasks for TestProcess")),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Dispose_DisposesAllTimers()
|
||||
{
|
||||
// Arrange
|
||||
var processInfo = new ProcessInfo
|
||||
{
|
||||
Name = "TestProcess",
|
||||
Path = "C:\\test.exe",
|
||||
Interval = 15,
|
||||
Enable = true
|
||||
};
|
||||
|
||||
_scheduler.ScheduleRepeatedExecution(processInfo, false);
|
||||
|
||||
// Act
|
||||
_scheduler.Dispose();
|
||||
|
||||
// Assert
|
||||
_loggerMock.Verify(
|
||||
l => l.Log(
|
||||
LogLevel.Information,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("ProcessScheduler disposed")),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
||||
Times.Once);
|
||||
}
|
||||
}
|
||||
158
Tests/Unit/ProcessValidatorTests.cs
Normal file
158
Tests/Unit/ProcessValidatorTests.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using FluentAssertions;
|
||||
using MarketAlly.ProcessMonitor.Models;
|
||||
using MarketAlly.ProcessMonitor.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace MarketAlly.ProcessMonitor.Tests.Unit;
|
||||
|
||||
[TestClass]
|
||||
public class ProcessValidatorTests
|
||||
{
|
||||
private Mock<ILogger<ProcessValidator>> _loggerMock = null!;
|
||||
private ProcessValidator _validator = null!;
|
||||
private AppSettings _appSettings = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_loggerMock = new Mock<ILogger<ProcessValidator>>();
|
||||
_appSettings = new AppSettings
|
||||
{
|
||||
EnablePathValidation = true,
|
||||
AllowedPaths = new List<string> { @"C:\Windows", @"C:\Program Files" }
|
||||
};
|
||||
_validator = new ProcessValidator(_loggerMock.Object, _appSettings);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateProcessPath_WithNullPath_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
var result = _validator.ValidateProcessPath(null!);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateProcessPath_WithRelativePath_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
var result = _validator.ValidateProcessPath("relative/path.exe");
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateProcessPath_WithNonExistentFile_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
var result = _validator.ValidateProcessPath(@"C:\nonexistent\file.exe");
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateProcessPath_WithNonExecutableFile_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
var result = _validator.ValidateProcessPath(@"C:\Windows\System32\drivers\etc\hosts");
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateProcessInfo_WithValidInfo_ReturnsValid()
|
||||
{
|
||||
// Arrange
|
||||
var processInfo = new ProcessInfo
|
||||
{
|
||||
Name = "TestProcess",
|
||||
Path = @"C:\Windows\System32\notepad.exe",
|
||||
Count = 1,
|
||||
Enable = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.ValidateProcessInfo(processInfo);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateProcessInfo_WithInvalidTime_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var processInfo = new ProcessInfo
|
||||
{
|
||||
Name = "TestProcess",
|
||||
Path = @"C:\Windows\System32\notepad.exe",
|
||||
Time = "25:00", // Invalid time
|
||||
Enable = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.ValidateProcessInfo(processInfo);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("Invalid time format"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateProcessInfo_WithNegativeInterval_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var processInfo = new ProcessInfo
|
||||
{
|
||||
Name = "TestProcess",
|
||||
Path = @"C:\Windows\System32\notepad.exe",
|
||||
Interval = -5,
|
||||
Enable = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.ValidateProcessInfo(processInfo);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("Interval must be positive"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateProcessInfo_WithDangerousArguments_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var processInfo = new ProcessInfo
|
||||
{
|
||||
Name = "TestProcess",
|
||||
Path = @"C:\Windows\System32\notepad.exe",
|
||||
Arguments = "test; rm -rf /",
|
||||
Enable = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.ValidateProcessInfo(processInfo);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("dangerous characters"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CheckPermissionsAsync_WithAccessibleFile_ReturnsTrue()
|
||||
{
|
||||
// Act
|
||||
var result = await _validator.CheckPermissionsAsync(@"C:\Windows\System32\notepad.exe");
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
23
appsettings.json
Normal file
23
appsettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"ProcessMonitor": {
|
||||
"MonitoringIntervalSeconds": 10,
|
||||
"EnableDebugLogging": false,
|
||||
"LogRetentionDays": 30,
|
||||
"AllowedPaths": [
|
||||
"C:\\Windows\\System32",
|
||||
"C:\\Program Files",
|
||||
"C:\\Program Files (x86)",
|
||||
"C:\\test.exe"
|
||||
],
|
||||
"EnablePathValidation": true,
|
||||
"EnablePerformanceMonitoring": true,
|
||||
"MaxConcurrentStarts": 5
|
||||
}
|
||||
}
|
||||
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
processmonitor:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: marketally/processmonitor:latest
|
||||
container_name: process-monitor
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./processlist.json:/app/processlist.json:ro
|
||||
- ./appsettings.json:/app/appsettings.json:ro
|
||||
- ./logs:/app/logs
|
||||
environment:
|
||||
- DOTNET_ENVIRONMENT=Production
|
||||
- ProcessMonitor__EnableDebugLogging=false
|
||||
- ProcessMonitor__MonitoringIntervalSeconds=10
|
||||
networks:
|
||||
- processmonitor-network
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1'
|
||||
memory: 512M
|
||||
reservations:
|
||||
cpus: '0.5'
|
||||
memory: 256M
|
||||
|
||||
networks:
|
||||
processmonitor-network:
|
||||
driver: bridge
|
||||
@@ -2,23 +2,61 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PublishAot>false</PublishAot>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="processlist.json" />
|
||||
<None Remove="appsettings.json" />
|
||||
<None Remove="appsettings.*.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="processlist.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="appsettings.*.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
|
||||
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" />
|
||||
<PackageReference Include="Polly" Version="8.5.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.6.4" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.6.4" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.9.34310.174
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "marketally.processmonitor", "marketally.processmonitor.csproj", "{0DC18C8D-FF6C-4A8A-9215-3D99C3B19F98}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarketAlly.ProcessMonitor", "MarketAlly.ProcessMonitor.csproj", "{0DC18C8D-FF6C-4A8A-9215-3D99C3B19F98}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"processes": [
|
||||
{
|
||||
"name": "",
|
||||
"count": 3,
|
||||
"enable": true,
|
||||
"name": "ExampleProcess",
|
||||
"path": "C:\\Windows\\System32\\notepad.exe",
|
||||
"count": 1,
|
||||
"enable": false,
|
||||
"interval": 5,
|
||||
"time": "15:25",
|
||||
"path": ""
|
||||
"arguments": "",
|
||||
"workingDirectory": "",
|
||||
"maxRetries": 3,
|
||||
"retryDelaySeconds": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user