-
Notifications
You must be signed in to change notification settings - Fork 469
Expand file tree
/
Copy pathGVFSLock.cs
More file actions
494 lines (424 loc) · 20.5 KB
/
Copy pathGVFSLock.cs
File metadata and controls
494 lines (424 loc) · 20.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using System;
using System.Diagnostics;
using System.Threading;
namespace GVFS.Common
{
public partial class GVFSLock
{
private readonly Lock acquisitionLock = new Lock();
private readonly ITracer tracer;
private readonly LockHolder currentLockHolder = new LockHolder();
public GVFSLock(ITracer tracer)
{
this.tracer = tracer;
this.Stats = new ActiveGitCommandStats();
}
public ActiveGitCommandStats Stats
{
get;
private set;
}
/// <summary>
/// Allows external callers (non-GVFS) to acquire the lock.
/// </summary>
/// <param name="requestor">The data for the external acquisition request.</param>
/// <param name="existingExternalHolder">The current holder of the lock if the acquisition fails.</param>
/// <returns>True if the lock was acquired, false otherwise.</returns>
public bool TryAcquireLockForExternalRequestor(
NamedPipeMessages.LockData requestor,
out NamedPipeMessages.LockData existingExternalHolder)
{
EventMetadata metadata = new EventMetadata();
EventLevel eventLevel = EventLevel.Verbose;
metadata.Add("LockRequest", requestor.ToString());
metadata.Add("IsElevated", requestor.IsElevated);
existingExternalHolder = null;
// Capture the requestor's process start time so we can later distinguish the
// genuine holder from an unrelated process that happens to be reusing the same
// PID after the holder exits. If we cannot read the start time (e.g. permission
// failure on OpenProcess for a different-integrity caller) we still accept the
// lock and fall back to the legacy PID-only orphan check; record the fallback in
// telemetry so we can spot if it becomes common.
long? requestorStartTime = GVFSPlatform.Instance.TryGetActiveProcessStartTime(requestor.PID, out long startTime)
? startTime
: (long?)null;
if (requestorStartTime == null)
{
metadata.Add("StartTimeUnavailable", true);
}
try
{
lock (this.acquisitionLock)
{
if (this.currentLockHolder.IsGVFS)
{
metadata.Add("CurrentLockHolder", "GVFS");
metadata.Add("Result", "Denied");
return false;
}
existingExternalHolder = this.GetExternalHolder();
if (existingExternalHolder != null)
{
metadata.Add("CurrentLockHolder", existingExternalHolder.ToString());
metadata.Add("Result", "Denied");
return false;
}
metadata.Add("Result", "Accepted");
eventLevel = EventLevel.Informational;
this.currentLockHolder.AcquireForExternalRequestor(requestor, requestorStartTime);
this.Stats = new ActiveGitCommandStats();
return true;
}
}
finally
{
this.tracer.RelatedEvent(eventLevel, "TryAcquireLockExternal", metadata);
}
}
/// <summary>
/// Allow GVFS to acquire the lock.
/// </summary>
/// <returns>True if GVFS was able to acquire the lock or if it already held it. False othwerwise.</returns>
public bool TryAcquireLockForGVFS()
{
EventMetadata metadata = new EventMetadata();
try
{
lock (this.acquisitionLock)
{
if (this.currentLockHolder.IsGVFS)
{
return true;
}
NamedPipeMessages.LockData existingExternalHolder = this.GetExternalHolder();
if (existingExternalHolder != null)
{
metadata.Add("CurrentLockHolder", existingExternalHolder.ToString());
metadata.Add("Result", "Denied");
return false;
}
this.currentLockHolder.AcquireForGVFS();
metadata.Add("Result", "Accepted");
return true;
}
}
finally
{
this.tracer.RelatedEvent(EventLevel.Verbose, "TryAcquireLockInternal", metadata);
}
}
public void ReleaseLockHeldByGVFS()
{
lock (this.acquisitionLock)
{
if (!this.currentLockHolder.IsGVFS)
{
throw new InvalidOperationException("Cannot release lock that is not held by GVFS");
}
this.tracer.RelatedEvent(EventLevel.Verbose, nameof(this.ReleaseLockHeldByGVFS), new EventMetadata());
this.currentLockHolder.Release();
}
}
public bool ReleaseLockHeldByExternalProcess(int pid)
{
return this.ReleaseExternalLock(pid, nameof(this.ReleaseLockHeldByExternalProcess));
}
public NamedPipeMessages.LockData GetExternalHolder()
{
NamedPipeMessages.LockData externalHolder;
this.IsLockAvailable(checkExternalHolderOnly: true, existingExternalHolder: out externalHolder);
return externalHolder;
}
public bool IsLockAvailableForExternalRequestor(out NamedPipeMessages.LockData existingExternalHolder)
{
return this.IsLockAvailable(checkExternalHolderOnly: false, existingExternalHolder: out existingExternalHolder);
}
public string GetLockedGitCommand()
{
// In this code path, we don't care if the process terminated without releasing the lock. The calling code
// is asking us about this lock so that it can determine if git was the cause of certain IO events. Even
// if the git process has terminated, the answer to that question does not change.
NamedPipeMessages.LockData currentHolder = this.currentLockHolder.GetExternalHolder();
if (currentHolder != null)
{
return currentHolder.ParsedCommand;
}
return null;
}
public string GetStatus()
{
lock (this.acquisitionLock)
{
if (this.currentLockHolder.IsGVFS)
{
return "Held by GVFS.";
}
NamedPipeMessages.LockData externalHolder = this.GetExternalHolder();
if (externalHolder != null)
{
return string.Format("Held by {0} (PID:{1})", externalHolder.ParsedCommand, externalHolder.PID);
}
}
return "Free";
}
private bool IsLockAvailable(bool checkExternalHolderOnly, out NamedPipeMessages.LockData existingExternalHolder)
{
lock (this.acquisitionLock)
{
if (!checkExternalHolderOnly &&
this.currentLockHolder.IsGVFS)
{
existingExternalHolder = null;
return false;
}
bool externalHolderTerminatedWithoutReleasingLock;
string terminationReason;
existingExternalHolder = this.currentLockHolder.GetExternalHolder(
out externalHolderTerminatedWithoutReleasingLock,
out terminationReason);
if (externalHolderTerminatedWithoutReleasingLock)
{
this.ReleaseLockForTerminatedProcess(existingExternalHolder.PID, terminationReason);
this.tracer.SetGitCommandSessionId(string.Empty);
existingExternalHolder = null;
}
return existingExternalHolder == null;
}
}
private bool ReleaseExternalLock(int pid, string eventName, EventMetadata extraMetadata = null)
{
lock (this.acquisitionLock)
{
EventMetadata metadata = extraMetadata ?? new EventMetadata();
try
{
if (this.currentLockHolder.IsGVFS)
{
metadata.Add("IsLockedByGVFS", "true");
return false;
}
// We don't care if the process has already terminated. We're just trying to record the info for the last holder.
NamedPipeMessages.LockData previousExternalHolder = this.currentLockHolder.GetExternalHolder();
if (previousExternalHolder == null)
{
metadata.Add("Result", "Failed (no current holder, requested PID=" + pid + ")");
return false;
}
metadata.Add("CurrentLockHolder", previousExternalHolder.ToString());
metadata.Add("IsElevated", previousExternalHolder.IsElevated);
metadata.Add(nameof(RepoMetadata.Instance.EnlistmentId), RepoMetadata.Instance.EnlistmentId);
if (previousExternalHolder.PID != pid)
{
metadata.Add("pid", pid);
metadata.Add("Result", "Failed (wrong PID)");
return false;
}
this.currentLockHolder.Release();
metadata.Add("Result", "Released");
this.Stats.AddStatsToTelemetry(metadata);
return true;
}
finally
{
this.tracer.RelatedEvent(EventLevel.Informational, eventName, metadata, Keywords.Telemetry);
}
}
}
private void ReleaseLockForTerminatedProcess(int pid, string terminationReason)
{
EventMetadata metadata = new EventMetadata();
metadata.Add("ExternalHolderTerminationReason", terminationReason ?? "Unknown");
this.ReleaseExternalLock(pid, "ExternalLockHolderExited", metadata);
}
// The lock release event is a convenient place to record stats about things that happened while a git command was running,
// such as duration/count of object downloads during a git command, cache hits during a git command, etc.
public class ActiveGitCommandStats
{
private Stopwatch lockAcquiredTime;
private long lockHeldExternallyTimeMs;
private long placeholderTotalUpdateTimeMs;
private long placeholderUpdateFilesTimeMs;
private long placeholderUpdateFoldersTimeMs;
private long placeholderWriteAndFlushTimeMs;
private int deleteFolderPlacehoderAttempted;
private int folderPlaceholdersDeleted;
private int folderPlaceholdersPathNotFound;
private int folderPlaceholdersShaUpdate;
private long parseGitIndexTimeMs;
private long projectionWriteLockHeldMs;
private int numBlobs;
private long blobDownloadTimeMs;
private int numCommitsAndTrees;
private long commitAndTreeDownloadTimeMs;
private int numSizeQueries;
private long sizeQueryTimeMs;
public ActiveGitCommandStats()
{
this.lockAcquiredTime = Stopwatch.StartNew();
}
public void RecordReleaseExternalLockRequested()
{
this.lockHeldExternallyTimeMs = this.lockAcquiredTime.ElapsedMilliseconds;
}
public void RecordUpdatePlaceholders(
long durationMs,
long updateFilesMs,
long updateFoldersMs,
long writeAndFlushMs,
int deleteFolderPlacehoderAttempted,
int folderPlaceholdersDeleted,
int folderPlaceholdersPathNotFound,
int folderPlaceholdersShaUpdate)
{
this.placeholderTotalUpdateTimeMs = durationMs;
this.placeholderUpdateFilesTimeMs = updateFilesMs;
this.placeholderUpdateFoldersTimeMs = updateFoldersMs;
this.placeholderWriteAndFlushTimeMs = writeAndFlushMs;
this.deleteFolderPlacehoderAttempted = deleteFolderPlacehoderAttempted;
this.folderPlaceholdersDeleted = folderPlaceholdersDeleted;
this.folderPlaceholdersPathNotFound = folderPlaceholdersPathNotFound;
this.folderPlaceholdersShaUpdate = folderPlaceholdersShaUpdate;
}
public void RecordProjectionWriteLockHeld(long durationMs)
{
this.projectionWriteLockHeldMs = durationMs;
}
public void RecordParseGitIndex(long durationMs)
{
this.parseGitIndexTimeMs = durationMs;
}
public void RecordObjectDownload(bool isBlob, long downloadTimeMs)
{
if (isBlob)
{
Interlocked.Increment(ref this.numBlobs);
Interlocked.Add(ref this.blobDownloadTimeMs, downloadTimeMs);
}
else
{
Interlocked.Increment(ref this.numCommitsAndTrees);
Interlocked.Add(ref this.commitAndTreeDownloadTimeMs, downloadTimeMs);
}
}
public void RecordSizeQuery(long queryTimeMs)
{
Interlocked.Increment(ref this.numSizeQueries);
Interlocked.Add(ref this.sizeQueryTimeMs, queryTimeMs);
}
public void AddStatsToTelemetry(EventMetadata metadata)
{
metadata.Add("DurationMS", this.lockAcquiredTime.ElapsedMilliseconds);
metadata.Add("LockHeldExternallyMS", this.lockHeldExternallyTimeMs);
metadata.Add("ParseGitIndexMS", this.parseGitIndexTimeMs);
metadata.Add("UpdatePlaceholdersMS", this.placeholderTotalUpdateTimeMs);
metadata.Add("UpdateFilePlaceholdersMS", this.placeholderUpdateFilesTimeMs);
metadata.Add("UpdateFolderPlaceholdersMS", this.placeholderUpdateFoldersTimeMs);
metadata.Add("DeleteFolderPlacehoderAttempted", this.deleteFolderPlacehoderAttempted);
metadata.Add("FolderPlaceholdersDeleted", this.folderPlaceholdersDeleted);
metadata.Add("FolderPlaceholdersShaUpdate", this.folderPlaceholdersShaUpdate);
metadata.Add("FolderPlaceholdersPathNotFound", this.folderPlaceholdersPathNotFound);
metadata.Add("PlaceholdersWriteAndFlushMS", this.placeholderWriteAndFlushTimeMs);
metadata.Add("ProjectionWriteLockHeldMs", this.projectionWriteLockHeldMs);
metadata.Add("BlobsDownloaded", this.numBlobs);
metadata.Add("BlobDownloadTimeMS", this.blobDownloadTimeMs);
metadata.Add("CommitsAndTreesDownloaded", this.numCommitsAndTrees);
metadata.Add("CommitsAndTreesDownloadTimeMS", this.commitAndTreeDownloadTimeMs);
metadata.Add("SizeQueries", this.numSizeQueries);
metadata.Add("SizeQueryTimeMS", this.sizeQueryTimeMs);
}
}
/// <summary>
/// This class manages the state of which process currently owns the GVFS lock. This code is complicated because
/// the lock can be held by us or by an external process, and because the external process that holds the lock
/// can terminate without releasing the lock. If that happens, we implicitly release the lock the next time we
/// check to see who is holding it.
///
/// The goal of this class is to make it impossible for the rest of GVFSLock to read the external holder without being
/// aware of the fact that it could have terminated.
///
/// This class assumes that the caller is handling all synchronization.
/// </summary>
private class LockHolder
{
private NamedPipeMessages.LockData externalLockHolder;
private long? externalLockHolderStartTime;
public bool IsFree
{
get { return !this.IsGVFS && this.externalLockHolder == null; }
}
public bool IsGVFS
{
get; private set;
}
public void AcquireForGVFS()
{
if (this.externalLockHolder != null)
{
throw new InvalidOperationException("Cannot acquire for GVFS because there is an external holder");
}
this.IsGVFS = true;
}
public void AcquireForExternalRequestor(NamedPipeMessages.LockData externalLockHolder, long? startTime)
{
if (this.IsGVFS ||
this.externalLockHolder != null)
{
throw new InvalidOperationException("Cannot acquire a lock that is already held");
}
this.externalLockHolder = externalLockHolder;
this.externalLockHolderStartTime = startTime;
}
public void Release()
{
this.IsGVFS = false;
this.externalLockHolder = null;
this.externalLockHolderStartTime = null;
}
public NamedPipeMessages.LockData GetExternalHolder()
{
return this.externalLockHolder;
}
public NamedPipeMessages.LockData GetExternalHolder(out bool externalHolderTerminatedWithoutReleasingLock, out string terminationReason)
{
externalHolderTerminatedWithoutReleasingLock = false;
terminationReason = null;
if (this.externalLockHolder != null)
{
int pid = this.externalLockHolder.PID;
if (this.externalLockHolderStartTime is long capturedStartTime)
{
// Identity check: confirm the same process still owns this PID by comparing
// the OS-supplied process start time we captured at acquisition with the
// current one. A mismatch means the original holder exited and Windows
// recycled the PID to a different process (the bug this code fixes).
if (!GVFSPlatform.Instance.TryGetActiveProcessStartTime(pid, out long currentStartTime))
{
externalHolderTerminatedWithoutReleasingLock = true;
terminationReason = "ProcessNotActive";
}
else if (currentStartTime != capturedStartTime)
{
externalHolderTerminatedWithoutReleasingLock = true;
terminationReason = "PidRecycled";
}
}
else
{
// Fallback for the rare case where we could not capture a start time at
// acquisition time (e.g. cross-integrity OpenProcess denial). Use the
// legacy PID-only liveness check, which is vulnerable to PID recycling
// but matches pre-fix behavior.
if (!GVFSPlatform.Instance.IsProcessActive(pid))
{
externalHolderTerminatedWithoutReleasingLock = true;
terminationReason = "ProcessNotActive";
}
}
}
return this.externalLockHolder;
}
}
}
}