Skip to content

v0.15.3: ARCH-1 step 3 — extract DependencyProbe#46

Merged
NathanNeurotic merged 1 commit into
mainfrom
feature/v0.15.3-dependency-probe-r
May 29, 2026
Merged

v0.15.3: ARCH-1 step 3 — extract DependencyProbe#46
NathanNeurotic merged 1 commit into
mainfrom
feature/v0.15.3-dependency-probe-r

Conversation

@NathanNeurotic
Copy link
Copy Markdown
Owner

Summary

Third collaborator extracted from `TrayContext`. No user-visible changes. Rebased onto main after v0.15.2 (#43) merged (supersedes #44, which was stacked on the pre-squash branch).

  • New `src/DependencyProbe.cs` owns the cached `RcloneAvailable` / `WinFspInstalled` booleans, their TTL stamp, and the slow synchronous probes (`File.Exists` + `rclone version` spawn + registry lookups). Constructor takes a `Func` rclone path provider + a log callback.
  • `TrayContext.Setup.cs` delegates the cached-state and probe methods; the async refresh worker stays in `TrayContext` and calls `DependencyProbe.PublishProbeResults` when done.

Test plan

  • `scripts/run-tests.ps1` — 53/53 green (rebased on current main)
  • Real-world: dependency-status chip still updates correctly

🤖 Generated with Claude Code

Third collaborator extracted from TrayContext. No user-visible
changes.

- New src/DependencyProbe.cs owns cached RcloneAvailable /
  WinFspInstalled state, TTL stamp, and the slow synchronous
  probes (File.Exists, spawn rclone version, registry lookups).
  Constructor takes Func<string> rclone path provider + log
  callback.
- TrayContext.Setup.cs delegates RcloneAvailable, WinFspInstalled,
  ProbeRcloneAvailableSync, ProbeWinFspInstalledSync. The async
  refresh worker stays in TrayContext (composes results into the
  setup-status line) and calls Deps.PublishProbeResults at the
  end.

53/53 tests green, FileVersion 0.15.3.0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@NathanNeurotic NathanNeurotic merged commit 0b858c0 into main May 29, 2026
1 check passed
@NathanNeurotic NathanNeurotic deleted the feature/v0.15.3-dependency-probe-r branch May 29, 2026 08:33
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request extracts the cached dependency state and synchronous probe logic from the TrayContext partial into a new, dedicated DependencyProbe class, updating Pixelpipe.Setup.cs to delegate to this new class. The review feedback highlights two critical concurrency issues: first, the lazy initialization of the Deps property in Pixelpipe.Setup.cs lacks thread-safety guards, which can lead to race conditions; second, the volatile keyword cannot be applied to the DateTime struct in DependencyProbe, making the caching mechanism prone to thread-safety issues. Implementing thread-safe lazy initialization and a lock-based synchronization mechanism for the cached fields is highly recommended.

Comment thread src/Pixelpipe.Setup.cs
Comment on lines +50 to 63
private DependencyProbe _depProbe;
private DependencyProbe Deps
{
EnsureDependencyCacheFresh();
return cachedRcloneAvailable;
get
{
if (_depProbe == null)
{
_depProbe = new DependencyProbe(
delegate { rclonePath = FindRclonePath(); return rclonePath; },
LogUiIssue);
}
return _depProbe;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The Deps property lazily initializes _depProbe without any thread-safety guards. Since Deps is accessed concurrently from both the UI thread (e.g., via RcloneAvailable()) and background threads (via ProbeRcloneAvailableSync() on a ThreadPool worker), there is a race condition where multiple instances of DependencyProbe could be created, or a thread could read a partially-constructed object.

To prevent this, we should use a lock or double-check locking to ensure thread-safe lazy initialization.

        private readonly object _depProbeLock = new object();
        private DependencyProbe _depProbe;
        private DependencyProbe Deps
        {
            get
            {
                if (_depProbe == null)
                {
                    lock (_depProbeLock)
                    {
                        if (_depProbe == null)
                        {
                            _depProbe = new DependencyProbe(
                                delegate { rclonePath = FindRclonePath(); return rclonePath; },
                                LogUiIssue);
                        }
                    }
                }
                return _depProbe;
            }
        }

Comment thread src/DependencyProbe.cs
Comment on lines +23 to +44
private volatile bool _cachedRcloneAvailable;
private volatile bool _cachedWinfspInstalled;
private DateTime _cachedStampUtc = DateTime.MinValue;

public DependencyProbe(Func<string> rclonePathProvider, Action<string, Exception> logIssue)
{
_rclonePathProvider = rclonePathProvider;
_logIssue = logIssue;
}

public bool RcloneAvailable { get { return _cachedRcloneAvailable; } }
public bool WinFspInstalled { get { return _cachedWinfspInstalled; } }
public DateTime LastProbeUtc { get { return _cachedStampUtc; } }
public bool IsStale { get { return (DateTime.UtcNow - _cachedStampUtc).TotalSeconds >= CacheTtlSeconds; } }

// Called by the async refresh worker after the slow probes complete.
public void PublishProbeResults(bool rcloneAvailable, bool winfspInstalled)
{
_cachedRcloneAvailable = rcloneAvailable;
_cachedWinfspInstalled = winfspInstalled;
_cachedStampUtc = DateTime.UtcNow;
}
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

The _cachedStampUtc field is a 64-bit DateTime struct, and volatile cannot be applied to it in C#. Since IsStale and PublishProbeResults are accessed concurrently across the UI thread and background threads, there is a risk of memory reordering or stale reads.

To ensure complete thread safety, atomicity, and proper memory barriers without relying on complex volatile ordering, we can use a simple lock to protect the cached fields and properties. Since these are read infrequently (on UI refresh ticks) and written even less frequently (every 30 seconds), lock contention will be virtually zero.

        private readonly object _lock = new object();
        private bool _cachedRcloneAvailable;
        private bool _cachedWinfspInstalled;
        private DateTime _cachedStampUtc = DateTime.MinValue;

        public DependencyProbe(Func<string> rclonePathProvider, Action<string, Exception> logIssue)
        {
            _rclonePathProvider = rclonePathProvider;
            _logIssue = logIssue;
        }

        public bool RcloneAvailable { get { lock (_lock) return _cachedRcloneAvailable; } }
        public bool WinFspInstalled { get { lock (_lock) return _cachedWinfspInstalled; } }
        public DateTime LastProbeUtc { get { lock (_lock) return _cachedStampUtc; } }
        public bool IsStale
        {
            get
            {
                lock (_lock)
                {
                    return (DateTime.UtcNow - _cachedStampUtc).TotalSeconds >= CacheTtlSeconds;
                }
            }
        }

        // Called by the async refresh worker after the slow probes complete.
        public void PublishProbeResults(bool rcloneAvailable, bool winfspInstalled)
        {
            lock (_lock)
            {
                _cachedRcloneAvailable = rcloneAvailable;
                _cachedWinfspInstalled = winfspInstalled;
                _cachedStampUtc = DateTime.UtcNow;
            }
        }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant