diff --git a/SysManager/SysManager/ServiceRegistration.cs b/SysManager/SysManager/ServiceRegistration.cs index 394d45d..2dccff7 100644 --- a/SysManager/SysManager/ServiceRegistration.cs +++ b/SysManager/SysManager/ServiceRegistration.cs @@ -18,8 +18,11 @@ public static IServiceCollection ConfigureServices(this IServiceCollection servi { // ── Core services ────────────────────────────────────────────── // PowerShellRunner is Transient — each consumer gets its own instance - // to avoid LineReceived event cross-talk between tabs. + // to avoid LineReceived event cross-talk between tabs. Registered under + // both the concrete type (legacy consumers) and the IPowerShellRunner + // seam (DNS / network repair / winget install — substitutable in tests). services.AddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/SysManager/SysManager/Services/BulkInstallerService.cs b/SysManager/SysManager/Services/BulkInstallerService.cs index 15ea12e..bf51203 100644 --- a/SysManager/SysManager/Services/BulkInstallerService.cs +++ b/SysManager/SysManager/Services/BulkInstallerService.cs @@ -12,9 +12,9 @@ namespace SysManager.Services; /// public sealed partial class BulkInstallerService { - private readonly PowerShellRunner _runner; + private readonly IPowerShellRunner _runner; - public BulkInstallerService(PowerShellRunner runner) => _runner = runner; + public BulkInstallerService(IPowerShellRunner runner) => _runner = runner; public event Action? LineReceived { diff --git a/SysManager/SysManager/Services/DnsService.cs b/SysManager/SysManager/Services/DnsService.cs index c4628a3..ec9fa34 100644 --- a/SysManager/SysManager/Services/DnsService.cs +++ b/SysManager/SysManager/Services/DnsService.cs @@ -14,10 +14,10 @@ namespace SysManager.Services; /// public sealed class DnsService : IDisposable { - private readonly PowerShellRunner _ps; + private readonly IPowerShellRunner _ps; private readonly SemaphoreSlim _gate = new(1, 1); - public DnsService(PowerShellRunner ps) => _ps = ps; + public DnsService(IPowerShellRunner ps) => _ps = ps; public void Dispose() => _gate.Dispose(); diff --git a/SysManager/SysManager/Services/IPowerShellRunner.cs b/SysManager/SysManager/Services/IPowerShellRunner.cs new file mode 100644 index 0000000..e7a40a3 --- /dev/null +++ b/SysManager/SysManager/Services/IPowerShellRunner.cs @@ -0,0 +1,61 @@ +// SysManager · IPowerShellRunner +// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager +// License: MIT + +using System.Collections.ObjectModel; +using System.Management.Automation; +using System.Text; +using SysManager.Models; + +namespace SysManager.Services; + +/// +/// Abstraction over — the single seam through which +/// services run PowerShell scripts and external processes. Extracting this interface +/// lets system-mutating services (DNS, network repair, winget install) be unit-tested +/// with a substituted runner instead of touching the live OS (Gate-ARCH: "external +/// process/PowerShell calls route through the single runner seam"). +/// +/// The same SECURITY CONTRACT as applies: +/// callers MUST only pass hard-coded script strings to and +/// . User input MUST NEVER be interpolated into +/// scripts. +/// +public interface IPowerShellRunner +{ + /// + /// Raised for each line of output from any stream. Fires on a thread-pool thread — + /// subscribers that update UI elements must marshal to the dispatcher. + /// + event Action? LineReceived; + + /// Raised with a 0-100 percentage as PowerShell progress records arrive. + event Action? ProgressChanged; + + /// + /// Execute a script in-process and return the collected PSObject results. + /// All streams are forwarded via for live UI display. + /// + Task> RunAsync( + string script, + IDictionary? parameters = null, + CancellationToken cancellationToken = default); + + /// + /// Run a PowerShell script via an external powershell.exe (Windows PS 5.1), + /// returning the process exit code. Output is streamed via . + /// + Task RunScriptViaPwshAsync( + string script, + CancellationToken cancellationToken = default); + + /// + /// Run an external process (winget, netsh, ipconfig, …) with live line streaming, + /// returning the process exit code. + /// + Task RunProcessAsync( + string fileName, + string arguments, + CancellationToken cancellationToken = default, + Encoding? outputEncoding = null); +} diff --git a/SysManager/SysManager/Services/NetworkRepairService.cs b/SysManager/SysManager/Services/NetworkRepairService.cs index f62e28e..6a3257a 100644 --- a/SysManager/SysManager/Services/NetworkRepairService.cs +++ b/SysManager/SysManager/Services/NetworkRepairService.cs @@ -12,10 +12,10 @@ namespace SysManager.Services; /// public sealed class NetworkRepairService : IDisposable { - private readonly PowerShellRunner _ps; + private readonly IPowerShellRunner _ps; private readonly SemaphoreSlim _gate = new(1, 1); - public NetworkRepairService(PowerShellRunner ps) => _ps = ps; + public NetworkRepairService(IPowerShellRunner ps) => _ps = ps; /// public void Dispose() => _gate.Dispose(); diff --git a/SysManager/SysManager/Services/PowerShellRunner.cs b/SysManager/SysManager/Services/PowerShellRunner.cs index 6b43978..0b4ba4b 100644 --- a/SysManager/SysManager/Services/PowerShellRunner.cs +++ b/SysManager/SysManager/Services/PowerShellRunner.cs @@ -23,7 +23,7 @@ namespace SysManager.Services; /// Violation of this contract creates a code injection vulnerability. The Bypass policy /// is safe ONLY because the script content is fully controlled by SysManager's source code. /// -public sealed class PowerShellRunner +public sealed class PowerShellRunner : IPowerShellRunner { /// /// Raised for each line of output from any stream (stdout, stderr, information,