Add project files.
This commit is contained in:
parent
d1b8af5fd8
commit
6d996ae3c8
153
README.md
Normal file
153
README.md
Normal file
@ -0,0 +1,153 @@
|
||||
# Unified Scheduler Service
|
||||
|
||||
Is'a completelly rewritten in .NET8 version of **PowerShell Scrip Service** realized in .Net Framework 4.8
|
||||
|
||||
As previously, this project still has an aim to allow **System Administrators** and also to who **Thinks to be System Administrator** to launch **Power Shell** scripts and **Console Programs** as **Windows Service**.
|
||||
|
||||
## Latest builds
|
||||
|
||||
## How to Install and Uninstall Service
|
||||
|
||||
### Service Install
|
||||
|
||||
```powershell
|
||||
sc.exe create "Unified Scheduler Service" binpath="C:\Path\To\UScheduler.exe"
|
||||
```
|
||||
|
||||
with providing custom `contentRoot`:
|
||||
|
||||
```powershell
|
||||
sc.exe create "Unified Scheduler Service" binpath="C:\Path\To\UScheduler.exe --contentRoot C:\Other\Path"
|
||||
```
|
||||
|
||||
Edit `appsettings.json`` according your needs. Differently from previuos version it doesn't scans a folders for scripts and same for programs, but you have explicitly set what should be launched. Also, when changes are made, you have to restart service. This will improve security of your environment.
|
||||
|
||||
Then **start** your **Unified Scheduler Service**
|
||||
|
||||
I have also prepared ***.cmd** file to simplify service system integration:
|
||||
|
||||
Install.cmd
|
||||
|
||||
```bat
|
||||
sc.exe create "Unified Scheduler Service" binpath="%~dp0UScheduler.exe
|
||||
pause
|
||||
```
|
||||
|
||||
>These ***.cmd** files have to be launched with **Admin** privileges.
|
||||
|
||||
After installation you have to start your newly created windows service: Win+R -> services.msc -> Enter -> Search by DisplayName.
|
||||
|
||||
### Service Uninstall
|
||||
|
||||
```powershell
|
||||
sc.exe "Unified Scheduler Service"
|
||||
```
|
||||
|
||||
Uninstall.cmd
|
||||
|
||||
```bat
|
||||
sc.exe "Unified Scheduler Service"
|
||||
pause
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
Here is a short explanation of two functional parts currently available.
|
||||
|
||||
### Processes
|
||||
|
||||
> Warning: For the moment I haven't realized any scheduling functionality for `console applications`, so be carefull, if your program is not a service kind, like `node derver`, `syncthing` ecc... it will execute it continuously every 10 senconds after completes.
|
||||
|
||||
This functionality is aimed to execute `console app services` which do not provide any windows service integration, and keeps it always alive.
|
||||
|
||||
### Powershell
|
||||
|
||||
Executes scripts whith following command parameters every 10 seconds:
|
||||
|
||||
```C#
|
||||
myCommand.Parameters.Add(new CommandParameter("Automated", true));
|
||||
myCommand.Parameters.Add(new CommandParameter("CurrentDateTimeUtc", DateTime.UtcNow.ToString("o")));
|
||||
```
|
||||
|
||||
Retrieve parameters this way:
|
||||
|
||||
```PowerShell
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[switch]$Automated,
|
||||
[string]$CurrentDateTime
|
||||
)
|
||||
|
||||
if($CurrentDateTime) {
|
||||
[datetime]$CurrentDateTime = [datetime]::parseexact($CurrentDateTime, 'dd/MM/yyyy HH:mm:ss', $null)
|
||||
}
|
||||
|
||||
Write-Host "Automated: $Automated" -ForegroundColor Green
|
||||
Write-Host "CurrentDateTime: $CurrentDateTime" -ForegroundColor Green
|
||||
```
|
||||
|
||||
Thanks to that, it's possible to create standalone scripts or automated scheduled scripts, which will be executed according to the script managed schedule logic.
|
||||
|
||||
### Thread organization
|
||||
|
||||
Every script and program is launched in its **own thread**, so if one crashes, others are able to continue:
|
||||
|
||||
```
|
||||
Unified Scheduler Service Thread
|
||||
├── Powershell
|
||||
│ ├── /Scripts/SomeStuff_1/StartScript.ps1 Thread
|
||||
│ ├── /Scripts/SomeStuff_2/StartScript.ps1 Thread
|
||||
│ └── ...
|
||||
└── Processes
|
||||
├── /Programs/SomeStuff_1/Program.exe
|
||||
├── /Programs/SomeStuff_2/Program.exe
|
||||
└── ...
|
||||
```
|
||||
|
||||
> By default It's set to execute only **signed** scrips, but if you don't care about your environment security, it's possible to launch them in **unrestricted** mode.
|
||||
>
|
||||
> Continue to read to see other possible settings...
|
||||
|
||||
## Configurations
|
||||
|
||||
Here are all currently available configurations inside `appsettings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
//...
|
||||
|
||||
"Configurations": {
|
||||
"ServiceName": "UScheduler",
|
||||
"Description": "Windows service, which allows you to invoke PowerShell Scripts and Processes",
|
||||
"DisplayName": "Unified Scheduler Service",
|
||||
|
||||
"Powershell": [
|
||||
{
|
||||
"Path": "C:\\UScheduler\\Scripts\\Demo\\StartScript.ps1",
|
||||
"Signed": true
|
||||
}
|
||||
],
|
||||
|
||||
"Processes": [
|
||||
{
|
||||
"Path": "C:\\UScheduler\\Programs\\syncthing-windows-amd64-v1.27.1\\syncthing.exe",
|
||||
"Args": [],
|
||||
"RestartOnFailure": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's see each one:
|
||||
|
||||
* ServiceName - System service name. I suggest to use short names without spaces or other strange characters. See [What are valid characters in a Windows service (key) name?](https://stackoverflow.com/questions/801280/what-are-valid-characters-in-a-windows-service-key-name).
|
||||
* Description - Description you wants to give to this service. Just put something very serious and technically complex to admire what kind of DUDE you are!
|
||||
* DisplayName - Same thing like for ServiceName, but you are free to use spaces.
|
||||
* Powershell:
|
||||
* ScriptsPath - Specify script to launch.
|
||||
* SignedScripts - **true** for **AllSigned** or **false** for **Unrestricted**.
|
||||
* Processes:
|
||||
* Path - Specify program to launch.
|
||||
* Args - Program command line arguments
|
||||
* RestartOnFailure - Allows to restart if something went wrong with program.
|
||||
25
src/UScheduler.sln
Normal file
25
src/UScheduler.sln
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.8.34330.188
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UScheduler", "UScheduler\UScheduler.csproj", "{DE1F347C-D201-42E2-8D22-924508FD30AA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{DE1F347C-D201-42E2-8D22-924508FD30AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DE1F347C-D201-42E2-8D22-924508FD30AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DE1F347C-D201-42E2-8D22-924508FD30AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DE1F347C-D201-42E2-8D22-924508FD30AA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {3C81929E-84E5-4648-9FC6-C73902D7E58C}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@ -0,0 +1,77 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using UScheduler.Services;
|
||||
|
||||
namespace UScheduler.BackgroundServices {
|
||||
|
||||
public sealed class PSScriptBackgroundService : BackgroundService {
|
||||
|
||||
private readonly ILogger<PSScriptBackgroundService> _logger;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly PSScriptService _psScriptService;
|
||||
|
||||
public PSScriptBackgroundService(
|
||||
ILogger<PSScriptBackgroundService> logger,
|
||||
IOptions<Configuration> options,
|
||||
PSScriptService psScriptService
|
||||
) {
|
||||
_logger = logger;
|
||||
_configuration = options.Value;
|
||||
_psScriptService = psScriptService;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
|
||||
_logger.LogInformation("Starting PSScriptBackgroundService");
|
||||
|
||||
try {
|
||||
var psScripts = _configuration.PowershellOrDefault;
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested) {
|
||||
_logger.LogInformation("Checking for PowerShell scripts to run");
|
||||
|
||||
//stop background service if there are no PowerShell scripts to run
|
||||
if (psScripts.Count == 0) {
|
||||
_logger.LogInformation("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");
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Running PowerShell script {scriptPath}");
|
||||
_psScriptService.RunScript(scriptPath, psScript.GetIsSignedOrDefault);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
|
||||
}
|
||||
}
|
||||
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 PSScriptBackgroundService due to cancellation request");
|
||||
|
||||
_psScriptService.TerminateAllScripts();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogError(ex, "{Message}", ex.Message);
|
||||
|
||||
// Terminates this process and returns an exit code to the operating system.
|
||||
// This is required to avoid the 'BackgroundServiceExceptionBehavior', which
|
||||
// performs one of two scenarios:
|
||||
// 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
|
||||
// 2. When set to "StopHost": will cleanly stop the host, and log errors.
|
||||
//
|
||||
// In order for the Windows Service Management system to leverage configured
|
||||
// recovery options, we need to terminate the process with a non-zero exit code.
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using UScheduler.Services;
|
||||
|
||||
namespace UScheduler.BackgroundServices;
|
||||
|
||||
public sealed class ProcessBackgroundService : BackgroundService {
|
||||
|
||||
private readonly ILogger<ProcessBackgroundService> _logger;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly ProcessService _processService;
|
||||
|
||||
public ProcessBackgroundService(
|
||||
ILogger<ProcessBackgroundService> logger,
|
||||
IOptions<Configuration> options,
|
||||
ProcessService processService
|
||||
) {
|
||||
_logger = logger;
|
||||
_configuration = options.Value;
|
||||
_processService = processService;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
|
||||
_logger.LogInformation("Starting ProcessBackgroundService");
|
||||
|
||||
try {
|
||||
var processes = _configuration.ProcessesOrDefault;
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested) {
|
||||
_logger.LogInformation("Checking for processes to run");
|
||||
|
||||
//stop background service if there are no processes to run
|
||||
if (processes.Count == 0) {
|
||||
_logger.LogInformation("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");
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Running process {processPath} with arguments {string.Join(", ", processArgs)}");
|
||||
_processService.RunProcess(processPath, processArgs, stoppingToken);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
|
||||
}
|
||||
}
|
||||
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 ProcessBackgroundService due to cancellation request");
|
||||
|
||||
_processService.TerminateAllProcesses();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogError(ex, "{Message}", ex.Message);
|
||||
|
||||
// Terminates this process and returns an exit code to the operating system.
|
||||
// This is required to avoid the 'BackgroundServiceExceptionBehavior', which
|
||||
// performs one of two scenarios:
|
||||
// 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
|
||||
// 2. When set to "StopHost": will cleanly stop the host, and log errors.
|
||||
//
|
||||
// In order for the Windows Service Management system to leverage configured
|
||||
// recovery options, we need to terminate the process with a non-zero exit code.
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/UScheduler/Configuration.cs
Normal file
46
src/UScheduler/Configuration.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UScheduler {
|
||||
|
||||
public class PowershellScript {
|
||||
public string? Path { get; set; }
|
||||
public bool? IsSigned { get; set; }
|
||||
|
||||
public string GetPathOrDefault => Path ?? string.Empty;
|
||||
public bool GetIsSignedOrDefault => IsSigned ?? false;
|
||||
}
|
||||
|
||||
public class ProcessConfiguration {
|
||||
public string? Path { get; set; }
|
||||
public string[]? Args { get; set; }
|
||||
public bool? RestartOnFailure { get; set; }
|
||||
|
||||
public string GetPathOrDefault => Path ?? string.Empty;
|
||||
public string[] GetArgsOrDefault => Args ?? [];
|
||||
public bool GetRestartOnFailureOrDefault => RestartOnFailure ?? false;
|
||||
}
|
||||
|
||||
public class Configuration {
|
||||
|
||||
public string? ServiceName { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
public List<PowershellScript>? Powershell { get; set; }
|
||||
|
||||
public List<ProcessConfiguration>? Processes { get; set; }
|
||||
|
||||
public string ServiceNameOrDefault => ServiceName ?? string.Empty;
|
||||
public string DescriptionOrDefault => Description ?? string.Empty;
|
||||
|
||||
public string DisplayNameOrDefault => DisplayName ?? string.Empty;
|
||||
|
||||
public List<PowershellScript> PowershellOrDefault => Powershell ?? new List<PowershellScript>();
|
||||
|
||||
public List<ProcessConfiguration> ProcessesOrDefault => Processes ?? new List<ProcessConfiguration>();
|
||||
}
|
||||
}
|
||||
5
src/UScheduler/Install.cmd
Normal file
5
src/UScheduler/Install.cmd
Normal file
@ -0,0 +1,5 @@
|
||||
"%~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"
|
||||
pause
|
||||
38
src/UScheduler/Program.cs
Normal file
38
src/UScheduler/Program.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using Microsoft.Extensions.Logging.Configuration;
|
||||
using Microsoft.Extensions.Logging.EventLog;
|
||||
using UScheduler;
|
||||
using UScheduler.BackgroundServices;
|
||||
using UScheduler.Services;
|
||||
|
||||
// read configuration from appsettings.json
|
||||
var configurationRoot = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.Build();
|
||||
|
||||
// bind Configuration section inside configuration to a new instance of Settings
|
||||
var configuration = new Configuration();
|
||||
configurationRoot.GetSection("Configurations").Bind(configuration);
|
||||
|
||||
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Services.AddWindowsService(options => {
|
||||
options.ServiceName = configuration.ServiceNameOrDefault;
|
||||
});
|
||||
|
||||
LoggerProviderOptions.RegisterProviderOptions<
|
||||
EventLogSettings, EventLogLoggerProvider>(builder.Services);
|
||||
|
||||
|
||||
|
||||
|
||||
// register configuration as IOptions<Configuration>
|
||||
builder.Services.Configure<Configuration>(configurationRoot.GetSection("Configurations"));
|
||||
|
||||
builder.Services.AddSingleton<ProcessService>();
|
||||
builder.Services.AddHostedService<ProcessBackgroundService>();
|
||||
|
||||
builder.Services.AddSingleton<PSScriptService>();
|
||||
builder.Services.AddHostedService<PSScriptBackgroundService>();
|
||||
|
||||
IHost host = builder.Build();
|
||||
host.Run();
|
||||
12
src/UScheduler/Properties/launchSettings.json
Normal file
12
src/UScheduler/Properties/launchSettings.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"UScheduler": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
src/UScheduler/Services/PSScriptService.cs
Normal file
97
src/UScheduler/Services/PSScriptService.cs
Normal file
@ -0,0 +1,97 @@
|
||||
using System.Management.Automation;
|
||||
using System.Management.Automation.Runspaces;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace UScheduler.Services {
|
||||
public sealed class PSScriptService {
|
||||
|
||||
private readonly ILogger<PSScriptService> _logger;
|
||||
private readonly ConcurrentDictionary<string, PowerShell> _runningScripts = new ConcurrentDictionary<string, PowerShell>();
|
||||
private readonly Runspace _rs = RunspaceFactory.CreateRunspace();
|
||||
|
||||
public PSScriptService(ILogger<PSScriptService> logger) {
|
||||
_logger = logger;
|
||||
if (_rs.RunspaceStateInfo.State != RunspaceState.Opened) {
|
||||
_rs.Open();
|
||||
_logger.LogInformation($"Runspace opened");
|
||||
}
|
||||
}
|
||||
|
||||
public Task RunScript(string scriptPath, bool signed) {
|
||||
_logger.LogInformation($"Preparing to run script {scriptPath}");
|
||||
|
||||
if (!File.Exists(scriptPath)) {
|
||||
_logger.LogError($"Script file {scriptPath} does not exist");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var ps = PowerShell.Create();
|
||||
ps.Runspace = _rs;
|
||||
_runningScripts.TryAdd(scriptPath, ps);
|
||||
|
||||
try {
|
||||
var scriptPolicy = "Unrestricted";
|
||||
if (signed)
|
||||
scriptPolicy = "AllSigned";
|
||||
|
||||
ps.AddScript($"Set-ExecutionPolicy -Scope Process -ExecutionPolicy {scriptPolicy}");
|
||||
ps.Invoke();
|
||||
|
||||
ps.AddScript($"Get-AuthenticodeSignature \"{scriptPath}\"");
|
||||
|
||||
foreach (var result in ps.Invoke()) {
|
||||
if (signed) {
|
||||
if (((Signature)result.BaseObject).Status != SignatureStatus.Valid) {
|
||||
_logger.LogWarning($"Script {Directory.GetParent(scriptPath)?.Name} Signature Error! Correct, and restart the service.");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Invoking: {scriptPath}");
|
||||
|
||||
var myCommand = new Command(scriptPath);
|
||||
|
||||
// Pass -Automated switch and -CuttrentDateTimeUtc, as UTC ISO 8601 string
|
||||
myCommand.Parameters.Add(new CommandParameter("Automated", true));
|
||||
myCommand.Parameters.Add(new CommandParameter("CurrentDateTimeUtc", DateTime.UtcNow.ToString("o")));
|
||||
|
||||
ps.Commands.Commands.Add(myCommand);
|
||||
ps.Invoke();
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogError($"Error running script {scriptPath}: {ex.Message}");
|
||||
}
|
||||
finally {
|
||||
_runningScripts.TryRemove(scriptPath, out _);
|
||||
_logger.LogInformation($"Script {scriptPath} completed and removed from running scripts");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public List<string> GetRunningScriptTasks() {
|
||||
_logger.LogInformation($"Retrieving running script tasks. Current count: {_runningScripts.Count}");
|
||||
return _runningScripts.Keys.ToList();
|
||||
}
|
||||
|
||||
public void TerminateScript(string scriptPath) {
|
||||
_logger.LogInformation($"Attempting to terminate script {scriptPath}");
|
||||
|
||||
if (_runningScripts.TryRemove(scriptPath, out var ps)) {
|
||||
ps.Stop();
|
||||
_logger.LogInformation($"Script {scriptPath} terminated");
|
||||
}
|
||||
else {
|
||||
_logger.LogWarning($"Failed to terminate script {scriptPath}. Script not found.");
|
||||
}
|
||||
}
|
||||
|
||||
public void TerminateAllScripts() {
|
||||
foreach (var script in _runningScripts) {
|
||||
TerminateScript(script.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/UScheduler/Services/ProcessService.cs
Normal file
79
src/UScheduler/Services/ProcessService.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace UScheduler.Services {
|
||||
public sealed class ProcessService {
|
||||
private readonly ILogger<ProcessService> _logger;
|
||||
private readonly ConcurrentDictionary<int, Process> _runningProcesses = new();
|
||||
|
||||
public ProcessService(ILogger<ProcessService> logger) {
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RunProcess(string processPath, string[] args, CancellationToken stoppingToken) {
|
||||
_logger.LogInformation($"Starting process {processPath} with arguments {string.Join(", ", args)}");
|
||||
|
||||
Process? process = null;
|
||||
|
||||
try {
|
||||
process = new Process();
|
||||
process.StartInfo.FileName = processPath;
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.StartInfo.RedirectStandardOutput = true;
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
|
||||
foreach (var arg in args)
|
||||
process.StartInfo.ArgumentList.Add(arg);
|
||||
|
||||
process.Start();
|
||||
_runningProcesses.TryAdd(process.Id, process);
|
||||
|
||||
_logger.LogInformation($"Process {processPath} started with ID {process.Id}");
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (process.ExitCode != 0 && !stoppingToken.IsCancellationRequested) {
|
||||
_logger.LogWarning($"Process {processPath} exited with code {process.ExitCode}");
|
||||
await RunProcess(processPath, args, stoppingToken);
|
||||
}
|
||||
else {
|
||||
_logger.LogInformation($"Process {processPath} completed successfully");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) {
|
||||
_logger.LogInformation($"Process {processPath} was cancelled");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogError($"Error running process {processPath}: {ex.Message}");
|
||||
}
|
||||
finally {
|
||||
if (process != null && _runningProcesses.ContainsKey(process.Id)) {
|
||||
_runningProcesses.TryRemove(process.Id, out _);
|
||||
_logger.LogInformation($"Process {processPath} with ID {process.Id} removed from running processes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ConcurrentDictionary<int, Process> GetRunningProcesses() {
|
||||
_logger.LogInformation($"Retrieving running processes. Current count: {_runningProcesses.Count}");
|
||||
return _runningProcesses;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
else {
|
||||
_logger.LogWarning($"Failed to terminate process with ID {processId}. Process not found.");
|
||||
}
|
||||
}
|
||||
|
||||
public void TerminateAllProcesses() {
|
||||
foreach (var process in _runningProcesses) {
|
||||
TerminateProcessById(process.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/UScheduler/UScheduler.csproj
Normal file
29
src/UScheduler/UScheduler.csproj
Normal file
@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-UScheduler-040d8105-9e07-4024-a632-cbe091387b66</UserSecretsId>
|
||||
<OutputType>exe</OutputType>
|
||||
<PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.PowerShell.Commands.Diagnostics" Version="7.4.0" />
|
||||
<PackageReference Include="Microsoft.PowerShell.Commands.Management" Version="7.4.0" />
|
||||
<PackageReference Include="Microsoft.PowerShell.Commands.Utility" Version="7.4.0" />
|
||||
<PackageReference Include="Microsoft.PowerShell.ConsoleHost" Version="7.4.0" />
|
||||
<PackageReference Include="Microsoft.WSMan.Management" Version="7.4.0" />
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="8.0.0" />
|
||||
<PackageReference Include="System.Management.Automation" Version="7.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Abstractions\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
5
src/UScheduler/Uninstall.cmd
Normal file
5
src/UScheduler/Uninstall.cmd
Normal file
@ -0,0 +1,5 @@
|
||||
"%~dp0PSScriptsService.exe" uninstall
|
||||
|
||||
sc.exe delete ".NET Joke Service"
|
||||
|
||||
pause
|
||||
8
src/UScheduler/appsettings.Development.json
Normal file
8
src/UScheduler/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/UScheduler/appsettings.json
Normal file
37
src/UScheduler/appsettings.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning"
|
||||
},
|
||||
"EventLog": {
|
||||
"SourceName": "UScheduler",
|
||||
"LogName": "Application",
|
||||
"LogLevel": {
|
||||
"Microsoft": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user