diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c31c68..f2a764b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.15.1 + +ARCH-1 step 1 from the audit — first collaborator extracted from the `TrayContext` partial. No user-visible changes; internal refactor only. + +Changed: + +- **New `RcloneClient` class** (`src/RcloneClient.cs`) owns the "invoke rclone, return `ProcessResult`" surface — `Run(args, timeoutMs)`, `Run(args, timeoutMs, envOverrides)`, and `RunWithStdin(args, timeoutMs, stdin)`. Constructor takes a `Func` path provider so it always sees the latest `rclonePath` (which mutates over the app's lifetime as the user downloads/installs rclone). +- **`TrayContext.RunRcloneCapture*` wrappers now delegate to `RcloneClient`.** Public API unchanged so no caller had to change; the legacy string-returning shim still works. New code should take `RcloneClient` as a dependency directly — that's testable without a TrayContext. +- **SEC-1 stdin path uses `RcloneInvoker.RunWithStdin`** instead of reaching into `RunCaptureCore`. The `obscure -` invocation no longer touches `ProcessStartInfo` directly. + +The remaining ARCH-1 sub-extractions (`SettingsStore`, `DependencyProbe`, `MountManager`) are intentionally **not** done here — the audit calls this work "incremental," and the rest can land in later releases as the natural shape of each collaborator emerges. + ## 0.15.0 GUI restructure from the audit — ProfileCard (GUI-1) and Edit-profile dialog (GUI-5). Layout-only changes; every field and action is preserved. diff --git a/src/Pixelpipe.Helpers.cs b/src/Pixelpipe.Helpers.cs index 16702a1..b25c167 100644 --- a/src/Pixelpipe.Helpers.cs +++ b/src/Pixelpipe.Helpers.cs @@ -224,32 +224,35 @@ private string RunProcessCapture(string fileName, string arguments, int timeoutM // pass secrets through environment variables instead of argv, where // any other user-level process can read them via Win32_Process. // CommandLine (SEC-1 fix). - private ProcessResult RunRcloneCaptureResult(string arguments, int timeoutMs, Dictionary envOverrides) - { - ProcessStartInfo psi = new ProcessStartInfo(); - psi.FileName = rclonePath; - psi.Arguments = arguments; - if (envOverrides != null) + // ARCH-1 step 1 (v0.15.1): a lazily-initialised RcloneClient owns + // the "invoke rclone, return ProcessResult" surface. The wrappers + // below stay so no caller has to change — new code should take + // RcloneClient directly to avoid the TrayContext dependency. + private RcloneClient _rcloneClient; + private RcloneClient RcloneInvoker + { + get { - foreach (KeyValuePair kv in envOverrides) - { - if (String.IsNullOrEmpty(kv.Key)) continue; - psi.EnvironmentVariables[kv.Key] = kv.Value ?? ""; - } + if (_rcloneClient == null) _rcloneClient = new RcloneClient(() => rclonePath); + return _rcloneClient; } - return RunCaptureCore(psi, timeoutMs); + } + + private ProcessResult RunRcloneCaptureResult(string arguments, int timeoutMs, Dictionary envOverrides) + { + return RcloneInvoker.Run(arguments, timeoutMs, envOverrides); } private ProcessResult RunRcloneCaptureResult(string arguments, int timeoutMs) { - return RunRcloneCaptureResult(arguments, timeoutMs, null); + return RcloneInvoker.Run(arguments, timeoutMs); } private string RunRcloneCapture(string arguments, int timeoutMs) { try { - ProcessResult r = RunRcloneCaptureResult(arguments, timeoutMs); + ProcessResult r = RcloneInvoker.Run(arguments, timeoutMs); if (!String.IsNullOrEmpty(r.LaunchError)) return r.LaunchError; return r.CombinedOutput; } diff --git a/src/Pixelpipe.SecretConfig.cs b/src/Pixelpipe.SecretConfig.cs index 115870f..f021264 100644 --- a/src/Pixelpipe.SecretConfig.cs +++ b/src/Pixelpipe.SecretConfig.cs @@ -151,10 +151,10 @@ private string ObscureSecretViaStdin(string plaintext, out string obscured) obscured = ""; try { - ProcessStartInfo psi = new ProcessStartInfo(); - psi.FileName = rclonePath; - psi.Arguments = "obscure -"; - ProcessResult res = RunCaptureCore(psi, 5000, plaintext); + // ARCH-1 (v0.15.1): SEC-1's stdin path now goes through + // RcloneClient so this method has no direct dependency on + // TrayContext's process-management internals. + ProcessResult res = RcloneInvoker.RunWithStdin("obscure -", 5000, plaintext); if (!res.Succeeded) { return res.TimedOut ? "obscure timed out" diff --git a/src/RcloneClient.cs b/src/RcloneClient.cs new file mode 100644 index 0000000..1f72200 --- /dev/null +++ b/src/RcloneClient.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Pixelpipe +{ + // ARCH-1 step 1 (v0.15.1, audit): first collaborator extracted from + // the TrayContext partial. Owns the "invoke rclone, return a structured + // result" surface. The legacy TrayContext.RunRcloneCapture wrappers + // continue to exist and delegate here so no caller has to change — but + // new code can take RcloneClient as a dependency directly and be unit- + // testable without spinning up a TrayContext. + // + // Path provider rather than a captured string because TrayContext.rclonePath + // mutates over the app's lifetime (first launch, "Download portable rclone" + // button, settings-driven path changes). The Func keeps us always-current + // without an explicit Refresh() call. + // + // SEC-1 fixed-stdin variant lives here too so the obscure-secret path + // (Pixelpipe.SecretConfig) stops reaching into TrayContext's helpers. + internal sealed class RcloneClient + { + private readonly System.Func _exePathProvider; + + public RcloneClient(System.Func exePathProvider) + { + _exePathProvider = exePathProvider; + } + + public string ResolvedPath { get { return _exePathProvider == null ? "" : (_exePathProvider() ?? ""); } } + + public TrayContext.ProcessResult Run(string arguments, int timeoutMs) + { + return Run(arguments, timeoutMs, null); + } + + public TrayContext.ProcessResult Run(string arguments, int timeoutMs, Dictionary envOverrides) + { + ProcessStartInfo psi = new ProcessStartInfo(); + psi.FileName = ResolvedPath; + psi.Arguments = arguments ?? ""; + if (envOverrides != null) + { + foreach (KeyValuePair kv in envOverrides) + { + if (string.IsNullOrEmpty(kv.Key)) continue; + psi.EnvironmentVariables[kv.Key] = kv.Value ?? ""; + } + } + return TrayContext.RunCaptureCore(psi, timeoutMs); + } + + public TrayContext.ProcessResult RunWithStdin(string arguments, int timeoutMs, string stdinInput) + { + ProcessStartInfo psi = new ProcessStartInfo(); + psi.FileName = ResolvedPath; + psi.Arguments = arguments ?? ""; + return TrayContext.RunCaptureCore(psi, timeoutMs, stdinInput); + } + } +}