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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
69 changes: 69 additions & 0 deletions src/MountManager.cs
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If p.LogFile is null or empty, passing --log-file "" to rclone can cause it to fail to start on Windows due to an invalid path. It is safer to conditionally append the --log-file argument only when a valid log file path is provided.

                + (!String.IsNullOrEmpty(p.LogFile) ? " --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<string> 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);
Comment on lines +64 to +65
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Defensive programming: Process.Start can theoretically return null (e.g., if an existing process is reused, though less likely with UseShellExecute = false). To prevent a potential NullReferenceException in RcloneJob.TryAssign, we should add a null check for child before assigning it.

            Process child = Process.Start(psi);
            if (child != null)
            {
                RcloneJob.TryAssign(child, logJobWarn);
            }

return child;
}
}
}
35 changes: 6 additions & 29 deletions src/Pixelpipe.Mount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading