Skip to content
Merged
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);
}
}
}
}
208 changes: 208 additions & 0 deletions GVFS/GVFS.Common/Tracing/DeferredTelemetryAttacher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
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>
/// <param name="gitBinRoot">Path to git binary.</param>
/// <param name="globalConfigPath">
/// If non-null, reads this file with <c>git config --file</c> instead
/// of <c>--global</c>. Use this when the caller needs to read another
/// user's .gitconfig without mutating the process-wide HOME variable.
/// </param>
/// <returns>true if attached (now or previously).</returns>
public bool TryAttach(string gitBinRoot, string globalConfigPath = null)
{
lock (this.attachLock)
{
if (this.attached || this.tracer.HasTelemetryDaemonListener)
{
return true;
}

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

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

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

this.tracer.AddEventListener(daemonListener);
Comment thread
tyrielv marked this conversation as resolved.
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)
{
try
{
bool success = this.TryAttach(this.retryGitBinRoot);
if (!success)
{
lock (this.attachLock)
{
if (this.retryTimer != null && !this.disposed)
{
this.retryCount++;
this.retryTimer.Change(
GetRetryInterval(this.retryCount),
Timeout.Infinite);
}
}
}
}
catch (Exception)
{
// Swallow — timer will not reschedule, but the explicit
// TryAttach path (e.g. on SessionLogon) can still succeed.
}
}
}
}
8 changes: 8 additions & 0 deletions GVFS/GVFS.Common/Tracing/JsonTracer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ public bool HasLogFileEventListener
}
}

public bool HasTelemetryDaemonListener
{
get
{
return this.listeners.Any(listener => listener is TelemetryDaemonEventListener);
}
}

public void SetGitCommandSessionId(string sessionId)
{
TelemetryDaemonEventListener daemonListener = this.listeners.FirstOrDefault(x => x is TelemetryDaemonEventListener) as TelemetryDaemonEventListener;
Expand Down
27 changes: 24 additions & 3 deletions GVFS/GVFS.Common/Tracing/TelemetryDaemonEventListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,22 @@ private TelemetryDaemonEventListener(
public string GitCommandSessionId { get; set; }

public static TelemetryDaemonEventListener CreateIfEnabled(string gitBinRoot, string providerName, string enlistmentId, string mountId, IEventListenerEventSink eventSink)
{
return CreateIfEnabled(gitBinRoot, providerName, enlistmentId, mountId, eventSink, globalConfigPath: null);
}

/// <summary>
/// Creates a TelemetryDaemonEventListener if the telemetry pipe config
/// is set. When <paramref name="globalConfigPath"/> is provided, reads
/// that file directly instead of using <c>git config --global</c>.
/// This avoids mutating the process-wide HOME environment variable
/// when the caller needs to read another user's config (e.g.
/// GVFS.Service reading the logged-on user's .gitconfig).
/// </summary>
public static TelemetryDaemonEventListener CreateIfEnabled(string gitBinRoot, string providerName, string enlistmentId, string mountId, IEventListenerEventSink eventSink, string globalConfigPath)
{
// This listener is disabled unless the user specifies the proper git config setting.
string telemetryPipe = GetConfigValue(gitBinRoot, GVFSConstants.GitConfig.GVFSTelemetryPipe);
string telemetryPipe = GetConfigValue(gitBinRoot, GVFSConstants.GitConfig.GVFSTelemetryPipe, globalConfigPath);
if (!string.IsNullOrEmpty(telemetryPipe))
{
return new TelemetryDaemonEventListener(providerName, enlistmentId, mountId, telemetryPipe, eventSink);
Expand Down Expand Up @@ -90,15 +103,23 @@ protected override void RecordMessageInternal(TraceEventMessage message)
}
}

private static string GetConfigValue(string gitBinRoot, string configKey)
private static string GetConfigValue(string gitBinRoot, string configKey, string globalConfigPath = null)
{
string value = string.Empty;
string error;

GitProcess.ConfigResult result = GitProcess.GetFromSystemConfig(gitBinRoot, configKey);
if (!result.TryParseAsString(out value, out error, defaultValue: string.Empty) || string.IsNullOrWhiteSpace(value))
{
result = GitProcess.GetFromGlobalConfig(gitBinRoot, configKey);
if (!string.IsNullOrEmpty(globalConfigPath))
{
result = GitProcess.GetFromFileConfig(gitBinRoot, globalConfigPath, configKey);
}
else
{
result = GitProcess.GetFromGlobalConfig(gitBinRoot, configKey);
}

result.TryParseAsString(out value, out error, defaultValue: string.Empty);
}

Expand Down
Loading
Loading