Skip to content

Commit 3660ff3

Browse files
committed
Serviceless infrastructure: LocalRepoRegistry, logon task, ProjFS boot task
Add the infrastructure that enables GVFS to operate without GVFS.Service: LocalRepoRegistry: file-backed repo registry that replaces the service's named-pipe registry. Wire-compatible on-disk format. SYSTEM account uses ProgramData path for CI agents. Seed-on-first-use copies accessible entries from system registry on first run (with TOCTOU merge safety). FileShare.ReadWrite|Delete for multi-process concurrent access. LogonTaskRegistration: per-user scheduled task (\GVFS\AutoMount) that runs 'conhost --headless gvfs service --mount-all' at logon. SHA-256 hash-based drift detection to avoid unnecessary re-registration. CLI verb fallbacks: MountVerb, UnmountVerb, ServiceVerb fall back to LocalRepoRegistry when the GVFS.Service named pipe is unavailable. GVFSVerb: silent-success fallback for PrjFlt FilterAttach when the service is not available to do it. ProjFS boot task: enable-projfs-on-all-drives.ps1 enables ProjFS and attaches PrjFlt on all volumes at boot and on hot-plug. Embedded in task XML via build-task-xml.ps1 with SHA-256 hash marker for drift detection. InProcessMount: restore exception safety net in HandleRequest that was accidentally removed during refactoring. 39 new unit tests (23 LocalRepoRegistry + 21 LogonTaskRegistration, minus 5 shared helpers). All 927 unit tests pass. Assisted-by: Claude Sonnet 4.5 Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
1 parent 7f1dab2 commit 3660ff3

50 files changed

Lines changed: 2403 additions & 541 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ jobs:
261261

262262
strategy:
263263
matrix:
264-
configuration: [ Release ]
264+
configuration: [ Debug, Release ]
265265
fail-fast: false
266266

267267
steps:

.github/workflows/functional-tests.yaml

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ jobs:
6161

6262
strategy:
6363
matrix:
64-
configuration: [ Release ]
64+
configuration: [ Debug, Release ]
6565
architecture: [ x86_64, arm64 ]
66-
nr: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] # 12 parallel jobs to speed up the tests
66+
nr: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 10 parallel jobs to speed up the tests
6767
fail-fast: false # most failures are flaky tests, no need to stop the other jobs from succeeding
6868

6969
steps:
@@ -142,17 +142,6 @@ jobs:
142142
run-id: ${{ inputs.vfs_run_id || github.run_id }}
143143
github-token: ${{ secrets.vfs_token || github.token }}
144144

145-
- name: Download FastFetch drop
146-
if: steps.skip.outputs.result != 'true'
147-
continue-on-error: true
148-
uses: actions/download-artifact@v8
149-
with:
150-
name: FastFetch_${{ matrix.configuration }}
151-
path: ft
152-
repository: ${{ inputs.vfs_repository || github.repository }}
153-
run-id: ${{ inputs.vfs_run_id || github.run_id }}
154-
github-token: ${{ secrets.vfs_token || github.token }}
155-
156145
- name: ProjFS details (pre-install)
157146
if: steps.skip.outputs.result != 'true'
158147
shell: cmd
@@ -204,7 +193,7 @@ jobs:
204193
run: |
205194
SET PATH=C:\Program Files\VFS for Git;%PATH%
206195
SET GIT_TRACE2_PERF=C:\temp\git-trace2.log
207-
ft\GVFS.FunctionalTests.exe /result:TestResult.xml --ci --slice=${{ matrix.nr }},12
196+
ft\GVFS.FunctionalTests.exe /result:TestResult.xml --ci --slice=${{ matrix.nr }},10
208197
209198
- name: Upload functional test results
210199
if: always() && steps.skip.outputs.result != 'true'

.github/workflows/upgrade-tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626

2727
strategy:
2828
matrix:
29-
configuration: [ Release ]
29+
configuration: [ Debug ]
3030
scenario:
3131
- staging-upgrade
3232
- clean-upgrade

AuthoringTests.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ The functional tests are built on NUnit 3, which is available as a set of NuGet
4040

4141
#### Selecting Which Tests are Run
4242

43-
By default, the functional tests run all tests. There are two mutually exclusive arguments that can be passed to the functional tests to change this behavior:
43+
By default, the functional tests run a subset of tests as a quick smoke test for developers. There are three mutually exclusive arguments that can be passed to the functional tests to change this behavior:
4444

45-
- `--full-suite`: Run all configurations of all functional tests (tests all `ValidateWorkingTreeMode` values and all `FileSystemRunner` types)
45+
- `--full-suite`: Run all configurations of all functional tests
46+
- `--extra-only`: Run only those tests marked as "ExtraCoverage" (i.e. the tests that are not run by default)
4647
- `--windows-only`: Run only the tests marked as being Windows specific
4748

4849
**NOTE** `Scripts\RunFunctionalTests.bat` already uses some of these arguments. If you run the tests using `RunFunctionalTests.bat` consider locally modifying the script rather than passing these flags as arguments to the script.

GVFS/GVFS.Common/ConsoleHelper.cs

Lines changed: 3 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,6 @@ public static bool ShowStatusWhileRunning(
2121
bool showSpinner,
2222
string gvfsLogEnlistmentRoot,
2323
int initialDelayMs = 0)
24-
{
25-
return ShowStatusWhileRunning(
26-
action,
27-
getMessage: null,
28-
message: message,
29-
output,
30-
showSpinner,
31-
gvfsLogEnlistmentRoot,
32-
initialDelayMs);
33-
}
34-
35-
/// <summary>
36-
/// Runs an action while displaying a dynamic status message with a spinner.
37-
/// The <paramref name="getMessage"/> delegate is called on each spinner tick
38-
/// and may return a sub-status string (e.g. "Authenticating") that is appended
39-
/// to <paramref name="message"/> in parentheses. When null or returning null,
40-
/// only the base message is shown.
41-
/// </summary>
42-
public static bool ShowStatusWhileRunning(
43-
Func<bool> action,
44-
Func<string> getMessage,
45-
string message,
46-
TextWriter output,
47-
bool showSpinner,
48-
string gvfsLogEnlistmentRoot,
49-
int initialDelayMs = 0)
5024
{
5125
Func<ActionResult> actionResultAction =
5226
() =>
@@ -56,7 +30,6 @@ public static bool ShowStatusWhileRunning(
5630

5731
ActionResult result = ShowStatusWhileRunning(
5832
actionResultAction,
59-
getMessage,
6033
message,
6134
output,
6235
showSpinner,
@@ -73,18 +46,6 @@ public static ActionResult ShowStatusWhileRunning(
7346
bool showSpinner,
7447
string gvfsLogEnlistmentRoot,
7548
int initialDelayMs)
76-
{
77-
return ShowStatusWhileRunning(action, getMessage: null, message, output, showSpinner, gvfsLogEnlistmentRoot, initialDelayMs);
78-
}
79-
80-
public static ActionResult ShowStatusWhileRunning(
81-
Func<ActionResult> action,
82-
Func<string> getMessage,
83-
string message,
84-
TextWriter output,
85-
bool showSpinner,
86-
string gvfsLogEnlistmentRoot,
87-
int initialDelayMs)
8849
{
8950
ActionResult result = ActionResult.Failure;
9051
bool initialMessageWritten = false;
@@ -106,7 +67,6 @@ public static ActionResult ShowStatusWhileRunning(
10667
{
10768
int retries = 0;
10869
char[] waiting = { '\u2014', '\\', '|', '/' };
109-
string lastProgress = null;
11070

11171
while (!isComplete)
11272
{
@@ -116,23 +76,7 @@ public static ActionResult ShowStatusWhileRunning(
11676
}
11777
else
11878
{
119-
string progress = getMessage?.Invoke();
120-
string displayMessage = !string.IsNullOrEmpty(progress)
121-
? $"{message} ({progress})"
122-
: message;
123-
124-
// Clear previous line content when message shrinks
125-
string line = $"\r{displayMessage}...{waiting[(retries / 2) % waiting.Length]}";
126-
if (lastProgress != null && lastProgress.Length > line.Length)
127-
{
128-
output.Write(line + new string(' ', lastProgress.Length - line.Length));
129-
}
130-
else
131-
{
132-
output.Write(line);
133-
}
134-
135-
lastProgress = line;
79+
output.Write("\r{0}...{1}", message, waiting[(retries / 2) % waiting.Length]);
13680
initialMessageWritten = true;
13781
actionIsDone.WaitOne(100);
13882
}
@@ -142,16 +86,8 @@ public static ActionResult ShowStatusWhileRunning(
14286

14387
if (initialMessageWritten)
14488
{
145-
// Clear out any trailing waiting character and sub-status
146-
string finalLine = $"\r{message}...";
147-
if (lastProgress != null && lastProgress.Length > finalLine.Length)
148-
{
149-
output.Write(finalLine + new string(' ', lastProgress.Length - finalLine.Length) + $"\r{message}...");
150-
}
151-
else
152-
{
153-
output.Write(finalLine);
154-
}
89+
// Clear out any trailing waiting character
90+
output.Write("\r{0}...", message);
15591
}
15692
});
15793
spinnerThread.Start();

GVFS/GVFS.Common/Database/SqliteDatabase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public static bool HasIssue(string databasePath, PhysicalFileSystem filesystem,
2121

2222
try
2323
{
24-
string sqliteConnectionString = $"data source={databasePath};Pooling=False";
24+
string sqliteConnectionString = CreateConnectionString(databasePath);
2525
using (SqliteConnection integrityConnection = new SqliteConnection(sqliteConnectionString))
2626
{
2727
integrityConnection.Open();

GVFS/GVFS.Common/Database/SqliteErrorCodes.cs

Lines changed: 0 additions & 15 deletions
This file was deleted.

GVFS/GVFS.Common/GVFSEnlistment.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -212,15 +212,15 @@ public static string GetNewGVFSLogFileName(
212212
fileSystem: fileSystem);
213213
}
214214

215-
public static bool WaitUntilMounted(ITracer tracer, string enlistmentRoot, bool unattended, out string errorMessage, Action<string> onProgress = null)
215+
public static bool WaitUntilMounted(ITracer tracer, string enlistmentRoot, bool unattended, out string errorMessage)
216216
{
217217
string pipeName = GVFSPlatform.Instance.GetNamedPipeName(enlistmentRoot);
218-
return WaitUntilMounted(tracer, pipeName, enlistmentRoot, unattended, out errorMessage, onProgress);
218+
return WaitUntilMounted(tracer, pipeName, enlistmentRoot, unattended, out errorMessage);
219219
}
220220

221-
public static bool WaitUntilMounted(ITracer tracer, string pipeName, string enlistmentRoot, bool unattended, out string errorMessage, Action<string> onProgress = null)
221+
public static bool WaitUntilMounted(ITracer tracer, string pipeName, string enlistmentRoot, bool unattended, out string errorMessage)
222222
{
223-
return WaitUntilMounted(tracer, pipeName, enlistmentRoot, unattended, mountProcessStatus: null, out errorMessage, onProgress);
223+
return WaitUntilMounted(tracer, pipeName, enlistmentRoot, unattended, mountProcessStatus: null, out errorMessage);
224224
}
225225

226226
/// <summary>
@@ -241,8 +241,7 @@ public static bool WaitUntilMounted(
241241
string enlistmentRoot,
242242
bool unattended,
243243
Func<MountProcessSnapshot> mountProcessStatus,
244-
out string errorMessage,
245-
Action<string> onProgress = null)
244+
out string errorMessage)
246245
{
247246
tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Creating NamedPipeClient for pipe '{pipeName}'");
248247
tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Connecting to '{pipeName}'");
@@ -287,11 +286,6 @@ public static bool WaitUntilMounted(
287286
}
288287
else
289288
{
290-
if (onProgress != null && !string.IsNullOrEmpty(getStatusResponse.MountProgress))
291-
{
292-
onProgress(getStatusResponse.MountProgress);
293-
}
294-
295289
tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Waiting 500ms for mount process to be ready");
296290
Thread.Sleep(100);
297291
}

GVFS/GVFS.Common/GitStatusCache.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using GVFS.Common.Tracing;
44
using System;
55
using System.ComponentModel;
6-
6+
using System.Diagnostics;
77
using System.IO;
88
using System.Threading;
99
using System.Threading.Tasks;
@@ -597,10 +597,7 @@ private bool TryRebuildStatusCache()
597597

598598
private bool TryDeleteStatusCacheFile()
599599
{
600-
if (!this.cacheFileLock.IsHeldByCurrentThread)
601-
{
602-
throw new InvalidOperationException("Attempting to delete the git status cache file without the cacheFileLock");
603-
}
600+
Debug.Assert(this.cacheFileLock.IsHeldByCurrentThread, "Attempting to delete the git status cache file without the cacheFileLock");
604601

605602
try
606603
{
@@ -638,10 +635,7 @@ private bool TryDeleteStatusCacheFile()
638635
/// <returns>True on success, False on failure</returns>
639636
private bool MoveCacheFileToFinalLocation(string tmpStatusFilePath)
640637
{
641-
if (!this.cacheFileLock.IsHeldByCurrentThread)
642-
{
643-
throw new InvalidOperationException("Attempting to update the git status cache file without the cacheFileLock");
644-
}
638+
Debug.Assert(this.cacheFileLock.IsHeldByCurrentThread, "Attempting to update the git status cache file without the cacheFileLock");
645639

646640
try
647641
{
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Collections.Generic;
2+
3+
namespace GVFS.Common
4+
{
5+
/// <summary>
6+
/// Abstracts the Windows Task Scheduler operations needed by
7+
/// <see cref="LogonTaskRegistration"/>. Production callers use
8+
/// <see cref="SchTasksScheduledTaskInvoker"/>; tests pass a mock so
9+
/// they can exercise <see cref="LogonTaskRegistration"/>'s logic
10+
/// without actually touching the Task Scheduler on the test machine.
11+
/// </summary>
12+
public interface IScheduledTaskInvoker
13+
{
14+
/// <summary>
15+
/// Register the task at <paramref name="taskPath"/> from the given
16+
/// XML, overwriting any existing task at that path. Returns
17+
/// <c>true</c> on success.
18+
/// </summary>
19+
bool TryRegisterFromXml(string taskPath, string xml, out string errorMessage);
20+
21+
/// <summary>
22+
/// Read back the registered XML for the task at
23+
/// <paramref name="taskPath"/>. Returns <c>true</c> with the XML
24+
/// when the task exists; returns <c>false</c> with a populated
25+
/// <paramref name="errorMessage"/> when it does not.
26+
/// </summary>
27+
bool TryQueryXml(string taskPath, out string xml, out string errorMessage);
28+
29+
/// <summary>
30+
/// Unregister the task at <paramref name="taskPath"/>. Returns
31+
/// <c>true</c> if the task was unregistered OR was not registered
32+
/// to begin with (idempotent). Returns <c>false</c> only on a hard
33+
/// failure (e.g., permission denied).
34+
/// </summary>
35+
bool TryUnregister(string taskPath, out string errorMessage);
36+
}
37+
}

0 commit comments

Comments
 (0)