diff --git a/CHANGELOG.md b/CHANGELOG.md index 741623a..5cfe783 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.15.4 + +ARCH-1 step 4 (final). Fourth collaborator extracted from `TrayContext`. No user-visible changes. With this the audit's ARCH-1 incremental work is done. + +Changed: + +- **New `MountManager` class** (`src/MountManager.cs`) owns the rclone mount process lifecycle bits: + - `BuildMountArgs(profile, fullCache, rcCommonFlags, effectiveBandwidth)` — pure helper that assembles the full rclone `mount` argv with all the cache-mode, VFS, network-mode, RC, log, and bandwidth flags Pixelpipe has used since v0.5.x. Unit-testable without touching disk. + - `StartMountProcess(rclonePath, args, logJobWarn)` — `Process.Start` plus the v0.11.4 Job Object binding so the rclone child still dies with Pixelpipe. +- **`TrayContext.Mount.cs` delegates** argv assembly and the spawn. The dialog flow (rclone-missing, WinFsp-missing, remote-not-configured, drive-in-use, post-launch result) stays in `TrayContext` because it's UI-coupled. + ## 0.15.3 ARCH-1 step 3. Third collaborator extracted from `TrayContext`. No user-visible changes. diff --git a/src/MountManager.cs b/src/MountManager.cs new file mode 100644 index 0000000..1d6acf3 --- /dev/null +++ b/src/MountManager.cs @@ -0,0 +1,69 @@ +using System; +using System.Diagnostics; +using System.IO; + +namespace Pixelpipe +{ + // ARCH-1 step 4 (v0.15.4, audit): fourth and final collaborator + // extracted from the TrayContext partial. Owns just the rclone mount + // process-lifecycle bits: argument assembly + Process.Start + Job Object + // binding. The dialog flow (rclone-missing, WinFsp-missing, remote-not- + // configured, drive-in-use, post-launch result) stays in TrayContext + // because it's heavily UI-coupled. + // + // This is intentionally a thin extraction. The audit calls ARCH-1 + // incremental; later releases can pull more orchestration in once the + // shape of "what doesn't need a TrayContext" stabilises. + internal sealed class MountManager + { + // Pure helper, tests cover it. Builds the rclone mount argv that + // Pixelpipe has used since v0.5.x with all the cache-mode, VFS, + // network-mode, RC, and bandwidth flags in one place. Caller passes + // RcCommonFlags + effective bandwidth so this class doesn't need to + // know about the auth token / per-profile bandwidth resolution. + internal static string BuildMountArgs(RemoteProfile p, bool fullCache, string rcCommonFlags, string effectiveBandwidth) + { + if (p == null) return ""; + string cacheMode = fullCache ? "full" : "writes"; + string args = "mount " + TrayContext.QuoteArg(TrayContext.NormalizeRemoteName(p.Remote)) + + " " + TrayContext.QuoteArg(TrayContext.NormalizeDriveLetter(p.DriveLetter)) + + " --links" + + (String.Equals(p.MountMode, "network", StringComparison.OrdinalIgnoreCase) ? " --network-mode" : "") + + " --vfs-cache-mode " + cacheMode + + " --dir-cache-time 10m" + + " --poll-interval 1m" + + " --vfs-write-back 10s" + + " --vfs-cache-max-age 6h" + + " --vfs-cache-max-size 5G" + + " --volname " + TrayContext.QuoteArg(p.Label) + + " --rc " + (rcCommonFlags ?? "") + + " --log-level INFO" + + " --log-file " + TrayContext.QuoteArg(p.LogFile); + if (!String.IsNullOrEmpty(effectiveBandwidth) + && !String.Equals(effectiveBandwidth, "off", StringComparison.OrdinalIgnoreCase)) + { + args += " --bwlimit " + effectiveBandwidth; + } + return args; + } + + // Spawns rclone mount and binds the child to the kill-on-job-close + // Job Object so it dies with Pixelpipe even on Task Manager kill / + // crash / sign-out. Returns the started Process; caller is + // responsible for the post-launch monitor (1900 ms wait + exit + // check + UI feedback). + internal static Process StartMountProcess(string rclonePath, string args, Action logJobWarn) + { + ProcessStartInfo psi = new ProcessStartInfo(); + psi.FileName = rclonePath; + psi.Arguments = args; + psi.UseShellExecute = false; + psi.CreateNoWindow = true; + psi.WindowStyle = ProcessWindowStyle.Hidden; + psi.WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + Process child = Process.Start(psi); + RcloneJob.TryAssign(child, logJobWarn); + return child; + } + } +} diff --git a/src/Pixelpipe.Mount.cs b/src/Pixelpipe.Mount.cs index 3c1a147..c4938db 100644 --- a/src/Pixelpipe.Mount.cs +++ b/src/Pixelpipe.Mount.cs @@ -168,39 +168,16 @@ private void ContinueMountOnUiThread(RemoteProfile p, bool fullCache, bool rclon p.FullCache = fullCache; p.DesiredMounted = true; p.LastError = ""; - string cacheMode = fullCache ? "full" : "writes"; - string args = "mount " + QuoteArg(NormalizeRemoteName(p.Remote)) + " " + QuoteArg(NormalizeDriveLetter(p.DriveLetter)) + - " --links" + - (String.Equals(p.MountMode, "network", StringComparison.OrdinalIgnoreCase) ? " --network-mode" : "") + - " --vfs-cache-mode " + cacheMode + - " --dir-cache-time 10m" + - " --poll-interval 1m" + - " --vfs-write-back 10s" + - " --vfs-cache-max-age 6h" + - " --vfs-cache-max-size 5G" + - " --volname " + QuoteArg(p.Label) + - " --rc " + RcCommonFlags(p.RcPort) + - " --log-level INFO" + - " --log-file " + QuoteArg(p.LogFile); - + // ARCH-1 step 4 (v0.15.4): argv assembly and process spawn + // delegated to MountManager. The post-launch monitor stays here + // because it does UI work (BeginUi, MessageBox, ShowBalloon). string effectiveBandwidth = EffectiveBandwidthFor(p); - if (!String.Equals(effectiveBandwidth, "off", StringComparison.OrdinalIgnoreCase)) args += " --bwlimit " + effectiveBandwidth; + string args = MountManager.BuildMountArgs(p, fullCache, RcCommonFlags(p.RcPort), effectiveBandwidth); try { - ProcessStartInfo psi = new ProcessStartInfo(); - psi.FileName = rclonePath; - psi.Arguments = args; - psi.UseShellExecute = false; - psi.CreateNoWindow = true; - psi.WindowStyle = ProcessWindowStyle.Hidden; - psi.WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - p.MountProcess = Process.Start(psi); - // Bind the new rclone to our kill-on-job-close Job Object so - // it dies with Pixelpipe even if Pixelpipe is killed via Task - // Manager / crashes / etc. Best-effort; the orphan-scan path - // catches anything that slips through. - RcloneJob.TryAssign(p.MountProcess, delegate(string warn) { LogUiWarn("rclone job assign " + p.Label, warn); }); + p.MountProcess = MountManager.StartMountProcess(rclonePath, args, + delegate(string warn) { LogUiWarn("rclone job assign " + p.Label, warn); }); p.StatusText = "mounting " + GetDriveRoot(p); SaveProfiles(); RebuildMenu();