Skip to content
Open
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
13 changes: 13 additions & 0 deletions GVFS/GVFS.Common/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,19 @@ private static extern bool DeviceIoControl(
[DllImport("kernel32.dll")]
private static extern ulong GetTickCount64();

[DllImport("kernel32.dll")]
private static extern int WTSGetActiveConsoleSessionId();

/// <summary>
/// Returns the session ID of the physical console session, or -1 if
/// no interactive session is active (e.g. at boot before logon).
/// </summary>
public static int GetActiveConsoleSessionId()
{
int sessionId = WTSGetActiveConsoleSessionId();
return sessionId == unchecked((int)0xFFFFFFFF) ? -1 : sessionId;
}

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetFileTime(
SafeFileHandle hFile,
Expand Down
83 changes: 83 additions & 0 deletions GVFS/GVFS.Common/Tracing/BufferingTelemetryListener.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Collections.Concurrent;

namespace GVFS.Common.Tracing
{
/// <summary>
/// An EventListener that buffers telemetry messages in memory. After
/// a real listener is attached via <see cref="ReplayAndStop"/>, buffered
/// messages are replayed and this listener becomes a no-op.
/// </summary>
public class BufferingTelemetryListener : EventListener
{
public const int DefaultMaxBufferedMessages = 1000;

private ConcurrentQueue<TraceEventMessage> buffer = new ConcurrentQueue<TraceEventMessage>();
private readonly int maxBufferedMessages;
private volatile bool stopped;

public BufferingTelemetryListener(int maxBufferedMessages = DefaultMaxBufferedMessages)
: base(EventLevel.Verbose, Keywords.Telemetry, eventSink: null)
{
this.maxBufferedMessages = maxBufferedMessages;
}

/// <summary>
/// Number of messages currently buffered.
/// </summary>
public int BufferedCount => this.buffer?.Count ?? 0;

/// <summary>
/// Whether this listener has been stopped (replay completed).
/// </summary>
public bool IsStopped => this.stopped;

/// <summary>
/// Replays all buffered messages to <paramref name="target"/> and
/// stops further buffering. This listener remains in the tracer's
/// listener list but becomes a no-op. Safe to call multiple times;
/// only the first call replays.
/// </summary>
/// <returns>Number of messages replayed.</returns>
public int ReplayAndStop(EventListener target)
{
if (this.stopped)
{
return 0;
}

this.stopped = true;
ConcurrentQueue<TraceEventMessage> queue = this.buffer;
this.buffer = null;

int count = 0;
if (queue != null)
{
while (queue.TryDequeue(out TraceEventMessage message))
{
target.RecordMessage(message);
count++;
}
}

return count;
}

protected override void RecordMessageInternal(TraceEventMessage message)
{
if (this.stopped)
{
return;
}

// Soft cap: under high concurrency, a few messages may exceed
// maxBufferedMessages because Count and Enqueue are not atomic.
// This is acceptable — the cap prevents unbounded growth, and
// a small overshoot is harmless.
ConcurrentQueue<TraceEventMessage> queue = this.buffer;
if (queue != null && queue.Count < this.maxBufferedMessages)
{
queue.Enqueue(message);
}
}
}
}
193 changes: 193 additions & 0 deletions GVFS/GVFS.Common/Tracing/DeferredTelemetryAttacher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using System;
using System.Threading;

namespace GVFS.Common.Tracing
{
/// <summary>
/// Manages deferred telemetry pipe attachment for processes that cannot
/// read the pipe config at startup (e.g. GVFS.Service running as SYSTEM,
/// or any process started before the telemetry collector is installed).
///
/// Adds a <see cref="BufferingTelemetryListener"/> to the tracer at
/// construction time, then periodically retries creating a real
/// <see cref="TelemetryDaemonEventListener"/>. On success, buffered
/// messages are replayed and the retry timer stops.
///
/// Callers can also trigger an explicit attach attempt via
/// <see cref="TryAttach(string)"/> — e.g. on session logon when the
/// user's HOME is available.
///
/// Designed for reuse by both GVFS.Service and GVFS.Mount.
/// </summary>
public class DeferredTelemetryAttacher : IDisposable
{
private readonly JsonTracer tracer;
private readonly BufferingTelemetryListener buffer;
private readonly string providerName;
private readonly string enlistmentId;
private readonly string mountId;
private readonly Lock attachLock = new Lock();

private Timer retryTimer;
private string retryGitBinRoot;
private int retryCount;
private bool attached;
private bool disposed;

public DeferredTelemetryAttacher(
JsonTracer tracer,
string providerName,
string enlistmentId,
string mountId)
{
this.tracer = tracer;
this.providerName = providerName;
this.enlistmentId = enlistmentId;
this.mountId = mountId;
this.buffer = new BufferingTelemetryListener();
tracer.AddEventListener(this.buffer);
}

public bool IsAttached
{
get
{
lock (this.attachLock)
{
return this.attached;
}
}
}

/// <summary>
/// Starts a background retry timer that periodically calls
/// <see cref="TryAttach"/> with the given gitBinRoot. Uses
/// exponential backoff: 10s, 30s, 1m, then 5m steady state.
/// </summary>
public void StartRetryTimer(string gitBinRoot)
{
lock (this.attachLock)
{
if (this.attached || this.disposed || this.retryTimer != null)
{
return;
}

this.retryGitBinRoot = gitBinRoot;
this.retryCount = 0;
this.retryTimer = new Timer(
this.OnRetryTimer,
null,
GetRetryInterval(0),
Timeout.Infinite);
}
}

/// <summary>
/// Attempts to create and attach a TelemetryDaemonEventListener.
/// Call this when environment conditions change (e.g. user session
/// becomes available). Replays buffered messages on success.
/// Safe to call multiple times — no-ops after first successful attach.
/// </summary>
/// <returns>true if attached (now or previously).</returns>
public bool TryAttach(string gitBinRoot)
{
lock (this.attachLock)
{
if (this.attached)
{
return true;
}

if (string.IsNullOrEmpty(gitBinRoot))
{
return false;
}

TelemetryDaemonEventListener daemonListener;
try
{
daemonListener = TelemetryDaemonEventListener.CreateIfEnabled(
gitBinRoot,
this.providerName,
this.enlistmentId,
this.mountId,
this.tracer);
}
catch (Exception)
{
return false;
}

if (daemonListener == null)
{
return false;
}

this.tracer.AddEventListener(daemonListener);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could you double-check the interaction between this AddEventListener call and the CreateIfEnabled call that JsonTracer's own constructor already makes (JsonTracer.cs ~line 57)?

The doc comment says this class is designed for reuse by GVFS.Mount, where the constructor-time attach normally succeeds - that path seems even more likely to hit this.

int replayed = this.buffer.ReplayAndStop(daemonListener);
this.StopRetryTimer();
this.attached = true;

this.tracer.RelatedInfo(
"DeferredTelemetryAttacher: Attached, replayed {0} buffered messages",
replayed);

return true;
}
}

public void Dispose()
{
lock (this.attachLock)
{
if (this.disposed)
{
return;
}

this.disposed = true;
this.StopRetryTimer();
}
}

internal static int GetRetryInterval(int retryCount)
{
return retryCount switch
{
0 => 10_000, // 10 seconds
1 => 30_000, // 30 seconds
2 => 60_000, // 1 minute
_ => 300_000, // 5 minutes
};
}

private void StopRetryTimer()
{
// Must be called while holding attachLock
if (this.retryTimer != null)
{
this.retryTimer.Dispose();
this.retryTimer = null;
}
}

private void OnRetryTimer(object state)
{
bool success = this.TryAttach(this.retryGitBinRoot);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

OnRetryTimer doesn't wrap TryAttach in try/catch, is this expected?

if (!success)
{
lock (this.attachLock)
{
if (this.retryTimer != null && !this.disposed)
{
this.retryCount++;
this.retryTimer.Change(
GetRetryInterval(this.retryCount),
Timeout.Infinite);
}
}
}
}
}
}
Loading
Loading