Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<string>` 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.
Expand Down
31 changes: 17 additions & 14 deletions src/Pixelpipe.Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> 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<string, string> 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<string, string> 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;
}
Expand Down
8 changes: 4 additions & 4 deletions src/Pixelpipe.SecretConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
60 changes: 60 additions & 0 deletions src/RcloneClient.cs
Original file line number Diff line number Diff line change
@@ -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<string> _exePathProvider;

public RcloneClient(System.Func<string> 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<string, string> envOverrides)
{
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = ResolvedPath;
psi.Arguments = arguments ?? "";
if (envOverrides != null)
{
foreach (KeyValuePair<string, string> 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);
}
}
}
Loading