Skip to content

Clone button's continuation runs RefreshPage on a thread-pool thread, racing the render thread over Options.Repos/ClonedRepos and the browser collection #439

Description

@matt-edmondson

What's wrong

The Clone button (ProjectDirector/ProjectDirector.cs, ~lines 707–712) does this:

Task.Run(() => QueueGitLog(..., GitCli.Run("clone", ...)))
    .ContinueWith((t) => RefreshPage(), new CancellationToken(),
        TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously,
        TaskScheduler.Current);

The app installs no SynchronizationContext, so TaskScheduler.Current at that point is the default thread-pool scheduler. With ExecuteSynchronously, RefreshPage() then runs on the worker thread that did the clone. From that thread it:

  • mutates Options.ClonedRepos (UpdateClonedStatus);
  • enumerates Options.Repos (PairSiblings);
  • clears and replaces BrowserContentsBase (SwitchRepoBrowserPath);
  • writes Options.BaseRepo and related state.

Meanwhile the render thread is enumerating BrowserContentsBase and calling ClonedRepos.ContainsKey/ContainsValue every frame, and the debounced save serialises Options.

Failure scenario

The user clicks Clone, and the clone finishes while a frame is rendering the repo browser. The render thread can then throw InvalidOperationException: Collection was modified, or the non-thread-safe Dictionary can be corrupted by concurrent read and write. Nothing guards against a clone already being in flight either, so clicking Clone again while the first is running starts a duplicate git clone into the same folder.

(Traced from the code; it's a race, so it can't be reproduced deterministically.)

Suggested fix

Do all UI and state mutation on the render thread. In the continuation, set a flag or enqueue a request, then call RefreshPage() from Tick, the same way other background results such as OwnerPendingTokenPopup are handed over. Also track the in-flight clone per repo and disable the button while it runs.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    readyFully specified; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions