diff --git a/README.md b/README.md index a5e24b8..ea816e6 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ I have also prepared ***.cmd** file to simplify service system integration: Install.cmd ```bat - sc.exe create "Unified Scheduler Service" binpath="%~dp0UScheduler.exe + sc.exe create "Unified Scheduler Service" binpath="%~dp0UScheduler.exe" pause ``` diff --git a/src/UScheduler/BackgroundServices/PSScriptBackgroundService.cs b/src/UScheduler/BackgroundServices/PSScriptBackgroundService.cs index af39767..bee0e63 100644 --- a/src/UScheduler/BackgroundServices/PSScriptBackgroundService.cs +++ b/src/UScheduler/BackgroundServices/PSScriptBackgroundService.cs @@ -30,23 +30,18 @@ namespace UScheduler.BackgroundServices { //stop background service if there are no PowerShell scripts to run if (psScripts.Count == 0) { - _logger.LogInformation("No PowerShell scripts to run, stopping PSScriptBackgroundService"); + _logger.LogWarning("No PowerShell scripts to run, stopping PSScriptBackgroundService"); break; } foreach (var psScript in psScripts) { - if (psScript.GetPathOrDefault == string.Empty) - continue; - var scriptPath = psScript.GetPathOrDefault; - if (_psScriptService.GetRunningScriptTasks().Contains(scriptPath)) { - _logger.LogInformation($"PowerShell script {scriptPath} is already running"); + if (scriptPath == string.Empty) continue; - } _logger.LogInformation($"Running PowerShell script {scriptPath}"); - _psScriptService.RunScript(scriptPath, psScript.GetIsSignedOrDefault); + _psScriptService.RunScript(scriptPath, psScript.GetIsSignedOrDefault, stoppingToken); } await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); @@ -56,8 +51,6 @@ namespace UScheduler.BackgroundServices { // When the stopping token is canceled, for example, a call made from services.msc, // we shouldn't exit with a non-zero exit code. In other words, this is expected... _logger.LogInformation("Stopping PSScriptBackgroundService due to cancellation request"); - - _psScriptService.TerminateAllScripts(); } catch (Exception ex) { _logger.LogError(ex, "{Message}", ex.Message); @@ -73,5 +66,17 @@ namespace UScheduler.BackgroundServices { Environment.Exit(1); } } + + + public override Task StopAsync(CancellationToken stoppingToken) { + // Perform cleanup tasks here + _logger.LogInformation("Stopping PSScriptBackgroundService"); + + _psScriptService.TerminateAllScripts(); + + _logger.LogInformation("PSScriptBackgroundService stopped"); + + return Task.CompletedTask; + } } } diff --git a/src/UScheduler/BackgroundServices/ProcessBackgroundService.cs b/src/UScheduler/BackgroundServices/ProcessBackgroundService.cs index 8d02811..5037542 100644 --- a/src/UScheduler/BackgroundServices/ProcessBackgroundService.cs +++ b/src/UScheduler/BackgroundServices/ProcessBackgroundService.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Options; +using System.Text.Json; using UScheduler.Services; namespace UScheduler.BackgroundServices; @@ -30,25 +31,22 @@ public sealed class ProcessBackgroundService : BackgroundService { //stop background service if there are no processes to run if (processes.Count == 0) { - _logger.LogInformation("No processes to run, stopping ProcessBackgroundService"); + _logger.LogWarning("No processes to run, stopping ProcessBackgroundService"); break; } foreach (var process in processes) { - if (process.GetPathOrDefault == string.Empty) - continue; - var processPath = process.GetPathOrDefault; var processArgs = process.GetArgsOrDefault; - if (_processService.GetRunningProcesses().Any(x => x.Value.StartInfo.FileName == processPath)) { - _logger.LogInformation($"Process {processPath} is already running"); + if (processPath == string.Empty) continue; - } _logger.LogInformation($"Running process {processPath} with arguments {string.Join(", ", processArgs)}"); _processService.RunProcess(processPath, processArgs, stoppingToken); + } + await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); } @@ -57,8 +55,6 @@ public sealed class ProcessBackgroundService : BackgroundService { // When the stopping token is canceled, for example, a call made from services.msc, // we shouldn't exit with a non-zero exit code. In other words, this is expected... _logger.LogInformation("Stopping ProcessBackgroundService due to cancellation request"); - - _processService.TerminateAllProcesses(); } catch (Exception ex) { _logger.LogError(ex, "{Message}", ex.Message); @@ -74,4 +70,15 @@ public sealed class ProcessBackgroundService : BackgroundService { Environment.Exit(1); } } + + public override Task StopAsync(CancellationToken stoppingToken) { + // Perform cleanup tasks here + _logger.LogInformation("Stopping ProcessBackgroundService"); + + _processService.TerminateAllProcesses(); + + _logger.LogInformation("All processes terminated"); + + return Task.CompletedTask; + } } diff --git a/src/UScheduler/Configuration.cs b/src/UScheduler/Configuration.cs index 70fe755..4b1e09e 100644 --- a/src/UScheduler/Configuration.cs +++ b/src/UScheduler/Configuration.cs @@ -27,20 +27,15 @@ namespace UScheduler { public class Configuration { public string? ServiceName { get; set; } - public string? Description { get; set; } - public string? DisplayName { get; set; } public List? Powershell { get; set; } public List? Processes { get; set; } public string ServiceNameOrDefault => ServiceName ?? string.Empty; - public string DescriptionOrDefault => Description ?? string.Empty; - public string DisplayNameOrDefault => DisplayName ?? string.Empty; + public List PowershellOrDefault => Powershell ?? []; - public List PowershellOrDefault => Powershell ?? new List(); - - public List ProcessesOrDefault => Processes ?? new List(); + public List ProcessesOrDefault => Processes ?? []; } } diff --git a/src/UScheduler/Install.cmd b/src/UScheduler/Install.cmd index cfb8e06..082c6f9 100644 --- a/src/UScheduler/Install.cmd +++ b/src/UScheduler/Install.cmd @@ -1,5 +1,3 @@ -"%~dp0PSScriptsService.exe" install - -sc.exe create ".NET Joke Service" binpath="C:\Path\To\App.WindowsService.exe" -sc.exe create "Svc Name" binpath="C:\Path\To\App.exe --contentRoot C:\Other\Path" +sc.exe create "Unified Scheduler Service" binpath="%~dp0UScheduler.exe" +sc description "Unified Scheduler Service" "Windows service, which allows you to invoke PowerShell Scripts and Processes" pause \ No newline at end of file diff --git a/src/UScheduler/Program.cs b/src/UScheduler/Program.cs index f28a4ca..3bc3075 100644 --- a/src/UScheduler/Program.cs +++ b/src/UScheduler/Program.cs @@ -1,13 +1,15 @@ using Microsoft.Extensions.Logging.Configuration; using Microsoft.Extensions.Logging.EventLog; +using System.Runtime.InteropServices; using UScheduler; using UScheduler.BackgroundServices; using UScheduler.Services; // read configuration from appsettings.json var configurationRoot = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) + .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) .AddJsonFile("appsettings.json", optional: true) + .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true) .Build(); // bind Configuration section inside configuration to a new instance of Settings @@ -19,13 +21,11 @@ builder.Services.AddWindowsService(options => { options.ServiceName = configuration.ServiceNameOrDefault; }); -LoggerProviderOptions.RegisterProviderOptions< - EventLogSettings, EventLogLoggerProvider>(builder.Services); +if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + LoggerProviderOptions.RegisterProviderOptions< + EventLogSettings, EventLogLoggerProvider>(builder.Services); +} - - - -// register configuration as IOptions builder.Services.Configure(configurationRoot.GetSection("Configurations")); builder.Services.AddSingleton(); diff --git a/src/UScheduler/Properties/PublishProfiles/FolderProfile.pubxml b/src/UScheduler/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..8766f85 --- /dev/null +++ b/src/UScheduler/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,18 @@ + + + + + Release + Any CPU + bin\Release\net8.0\win-x64\publish\win-x64\ + FileSystem + <_TargetId>Folder + net8.0 + win-x64 + true + true + false + + \ No newline at end of file diff --git a/src/UScheduler/Services/PSScriptService.cs b/src/UScheduler/Services/PSScriptService.cs index 59302ad..c27e615 100644 --- a/src/UScheduler/Services/PSScriptService.cs +++ b/src/UScheduler/Services/PSScriptService.cs @@ -17,9 +17,14 @@ namespace UScheduler.Services { } } - public Task RunScript(string scriptPath, bool signed) { + public Task RunScript(string scriptPath, bool signed, CancellationToken stoppingToken) { _logger.LogInformation($"Preparing to run script {scriptPath}"); + if (GetRunningScriptTasks().Contains(scriptPath)) { + _logger.LogInformation($"PowerShell script {scriptPath} is already running"); + return Task.CompletedTask; + } + if (!File.Exists(scriptPath)) { _logger.LogError($"Script file {scriptPath} does not exist"); return Task.CompletedTask; @@ -60,11 +65,17 @@ namespace UScheduler.Services { ps.Invoke(); } } + + catch (OperationCanceledException) { + // When the stopping token is canceled, for example, a call made from services.msc, + // we shouldn't exit with a non-zero exit code. In other words, this is expected... + _logger.LogInformation($"Stopping script {scriptPath} due to cancellation request"); + } catch (Exception ex) { _logger.LogError($"Error running script {scriptPath}: {ex.Message}"); } finally { - _runningScripts.TryRemove(scriptPath, out _); + TerminateScript(scriptPath); _logger.LogInformation($"Script {scriptPath} completed and removed from running scripts"); } diff --git a/src/UScheduler/Services/ProcessService.cs b/src/UScheduler/Services/ProcessService.cs index cd6f2b4..f0a2682 100644 --- a/src/UScheduler/Services/ProcessService.cs +++ b/src/UScheduler/Services/ProcessService.cs @@ -16,11 +16,20 @@ namespace UScheduler.Services { Process? process = null; try { + if (GetRunningProcesses().Any(x => x.Value.StartInfo.FileName == processPath)) { + _logger.LogInformation($"Process {processPath} is already running"); + return; + } + process = new Process(); - process.StartInfo.FileName = processPath; - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardOutput = true; - process.StartInfo.RedirectStandardError = true; + + process.StartInfo = new ProcessStartInfo { + FileName = processPath, + WorkingDirectory = Path.GetDirectoryName(processPath), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true + }; foreach (var arg in args) process.StartInfo.ArgumentList.Add(arg); @@ -41,14 +50,17 @@ namespace UScheduler.Services { } } catch (OperationCanceledException) { - _logger.LogInformation($"Process {processPath} was cancelled"); + // When the stopping token is canceled, for example, a call made from services.msc, + // we shouldn't exit with a non-zero exit code. In other words, this is expected... + _logger.LogWarning($"Process {processPath} was canceled"); } catch (Exception ex) { _logger.LogError($"Error running process {processPath}: {ex.Message}"); } finally { if (process != null && _runningProcesses.ContainsKey(process.Id)) { - _runningProcesses.TryRemove(process.Id, out _); + TerminateProcessById(process.Id); + _logger.LogInformation($"Process {processPath} with ID {process.Id} removed from running processes"); } } @@ -60,16 +72,29 @@ namespace UScheduler.Services { } public void TerminateProcessById(int processId) { - if (_runningProcesses.TryRemove(processId, out var process)) { - _logger.LogInformation($"Terminating process with ID {processId}"); - process.Kill(); - _logger.LogInformation($"Process with ID {processId} terminated"); + // Check if the process is in the running processes list + if (!_runningProcesses.TryGetValue(processId, out var processToTerminate)) { + _logger.LogWarning($"Failed to terminate process {processId}. Process not found."); + return; } - else { - _logger.LogWarning($"Failed to terminate process with ID {processId}. Process not found."); + + // Kill the process + try { + processToTerminate.Kill(true); + _logger.LogInformation($"Process {processId} terminated"); + } + catch (Exception ex) { + _logger.LogError($"Error terminating process {processId}: {ex.Message}"); + } + + // Check if the process has exited + if (!processToTerminate.HasExited) { + _logger.LogWarning($"Failed to terminate process {processId}. Process still running."); + TerminateProcessById(processId); } } + public void TerminateAllProcesses() { foreach (var process in _runningProcesses) { TerminateProcessById(process.Key); diff --git a/src/UScheduler/UScheduler.csproj b/src/UScheduler/UScheduler.csproj index e9f109a..0be443c 100644 --- a/src/UScheduler/UScheduler.csproj +++ b/src/UScheduler/UScheduler.csproj @@ -24,6 +24,15 @@ - + + PreserveNewest + + + PreserveNewest + + + + + diff --git a/src/UScheduler/Uninstall.cmd b/src/UScheduler/Uninstall.cmd index aee7b1f..b649321 100644 --- a/src/UScheduler/Uninstall.cmd +++ b/src/UScheduler/Uninstall.cmd @@ -1,5 +1,2 @@ -"%~dp0PSScriptsService.exe" uninstall - -sc.exe delete ".NET Joke Service" - +sc.exe delete "Unified Scheduler Service" pause \ No newline at end of file diff --git a/src/UScheduler/appsettings.Development.json b/src/UScheduler/appsettings.Development.json index b2dcdb6..5fbd560 100644 --- a/src/UScheduler/appsettings.Development.json +++ b/src/UScheduler/appsettings.Development.json @@ -1,8 +1,31 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Warning" + }, + "EventLog": { + "SourceName": "UScheduler", + "LogName": "Application", + "LogLevel": { + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } } + }, + "Configurations": { + "ServiceName": "UScheduler", + + "Powershell": [ + + ], + + "Processes": [ + { + "Path": "C:\\Users\\maksym\\Desktop\\Programs\\syncthing-windows-amd64-v1.27.1\\syncthing.exe", + "Args": ["--no-restart", "--home=C:\\Users\\maksym\\Desktop\\Data\\Syncthing"], + "RestartOnFailure": true + } + ] + } -} +} \ No newline at end of file diff --git a/src/UScheduler/appsettings.json b/src/UScheduler/appsettings.json index 938079e..22c8313 100644 --- a/src/UScheduler/appsettings.json +++ b/src/UScheduler/appsettings.json @@ -14,24 +14,14 @@ }, "Configurations": { "ServiceName": "UScheduler", - "Description": "Windows service, which allows you to invoke PowerShell Scripts and Processes", - "DisplayName": "Unified Scheduler Service", "Powershell": [ - { - "Path": "", - "StartScript": "", - "Signed": true - } + ], "Processes": [ - { - "Path": "C:\\Users\\maksym\\Desktop\\syncthing-windows-amd64-v1.27.1\\syncthing.exe", - "Args": [], - "RestartOnFailure": true - } + ] - + } } \ No newline at end of file