Skip to content
Draft
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
208 changes: 208 additions & 0 deletions GVFS/GVFS.Common/Tracing/JsonTracer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ public class JsonTracer : ITracer, IEventListenerEventSink
private readonly EventLevel startStopLevel;
private readonly Keywords startStopKeywords;

// Deferred telemetry pipe: when the daemon listener can't be created
// at construction time (e.g. SYSTEM account can't read user's global
// git config, or telemetry collector not yet installed), buffer
// telemetry messages and replay them when TryAttachTelemetryPipe()
// succeeds. A retry timer periodically re-checks the config.
private string deferredProviderName;
private string deferredEnlistmentId;
private string deferredMountId;
private string deferredGitBinRoot;
private ConcurrentQueue<TraceEventMessage> deferredMessages;
private readonly Lock deferredLock = new Lock();
private Timer deferredRetryTimer;
private int deferredRetryCount;
private const int MaxDeferredMessages = 1000;

private bool isDisposed = false;
private bool stopped = false;

Expand Down Expand Up @@ -59,6 +74,60 @@ public JsonTracer(string providerName, Guid providerActivityId, string activityN
{
this.listeners.Add(daemonListener);
}
else
{
// Pipe config not available (e.g. running as SYSTEM without
// access to user's global git config, or telemetry collector
// not yet installed). Defer listener creation — buffer
// telemetry messages and periodically retry until the config
// becomes available.
this.deferredProviderName = providerName;
this.deferredEnlistmentId = enlistmentId;
this.deferredMountId = mountId;
this.deferredGitBinRoot = gitBinRoot;
this.deferredMessages = new ConcurrentQueue<TraceEventMessage>();
this.StartDeferredRetryTimer();
}
}
}

/// <summary>
/// Creates a JsonTracer in deferred telemetry mode for testing.
/// No timer is started — call TryAttachTelemetryPipe manually.
/// </summary>
internal static JsonTracer CreateDeferredForTesting(string providerName, string activityName)
{
JsonTracer tracer = new JsonTracer(null, Guid.Empty, activityName, EventLevel.Informational, Keywords.Telemetry);
tracer.deferredProviderName = providerName;
tracer.deferredMessages = new ConcurrentQueue<TraceEventMessage>();
return tracer;
}

/// <summary>
/// Attaches the given listener as if TelemetryDaemonEventListener
/// had been created, replaying buffered messages. For testing only.
/// </summary>
internal bool TryAttachTestListener(EventListener listener)
{
lock (this.deferredLock)
{
if (this.deferredMessages == null)
{
return false;
}

this.listeners.Add(listener);

ConcurrentQueue<TraceEventMessage> queue = this.deferredMessages;
this.deferredMessages = null;
this.StopDeferredRetryTimer();

while (queue.TryDequeue(out TraceEventMessage message))
{
listener.RecordMessage(message);
}

return true;
}
}

Expand Down Expand Up @@ -132,6 +201,11 @@ public void Dispose()

this.Stop(null);

lock (this.deferredLock)
{
this.StopDeferredRetryTimer();
}

// If we have no parent, then we are the root tracer and should dispose our eventsource.
if (this.parentActivityId == Guid.Empty)
{
Expand Down Expand Up @@ -394,6 +468,18 @@ private void WriteEvent(string eventName, EventLevel level, Keywords keywords, E
{
listener.RecordMessage(message);
}

// Buffer telemetry messages for deferred pipe attachment.
// Non-telemetry messages (log file, etc.) are not buffered
// since they only matter for the daemon listener.
if (keywords == Keywords.Telemetry)
{
ConcurrentQueue<TraceEventMessage> queue = this.deferredMessages;
if (queue != null && queue.Count < MaxDeferredMessages)
{
queue.Enqueue(message);
}
}
}

private void LogMessageToNonFailedListeners(TraceEventMessage message)
Expand All @@ -404,5 +490,127 @@ private void LogMessageToNonFailedListeners(TraceEventMessage message)
listener.RecordMessage(message);
}
}

/// <summary>
/// Attempts to create and attach a TelemetryDaemonEventListener using
/// the specified git binary path to read config. Call this when a user
/// session becomes available (e.g. on SessionLogon) so the user's global
/// git config can be read. Replays any buffered pre-logon messages.
/// Safe to call multiple times — no-ops after first successful attach.
/// </summary>
/// <returns>true if the listener was attached (now or previously).</returns>
public bool TryAttachTelemetryPipe(string gitBinRoot)
{
lock (this.deferredLock)
{
// Already attached or not in deferred mode
if (this.deferredMessages == null)
{
return this.listeners.Any(l => l is TelemetryDaemonEventListener);
}

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

TelemetryDaemonEventListener daemonListener;
try
{
daemonListener = TelemetryDaemonEventListener.CreateIfEnabled(
gitBinRoot,
this.deferredProviderName,
this.deferredEnlistmentId,
this.deferredMountId,
this);
}
catch (Exception)
{
return false;
}

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

this.listeners.Add(daemonListener);

// Replay buffered messages then stop buffering
ConcurrentQueue<TraceEventMessage> queue = this.deferredMessages;
this.deferredMessages = null;
this.StopDeferredRetryTimer();

while (queue.TryDequeue(out TraceEventMessage message))
{
daemonListener.RecordMessage(message);
}

return true;
}
}

/// <summary>
/// Starts a timer that periodically retries TryAttachTelemetryPipe
/// using the stashed gitBinRoot from construction time. Uses
/// exponential backoff: 10s, 30s, 1m, 5m, then 5m steady state.
/// The timer is stopped when attach succeeds or the tracer is disposed.
/// </summary>
private void StartDeferredRetryTimer()
{
lock (this.deferredLock)
{
if (this.deferredRetryTimer != null)
{
return;
}

this.deferredRetryCount = 0;
this.deferredRetryTimer = new Timer(
this.OnDeferredRetryTimer,
null,
GetRetryInterval(0),
Timeout.Infinite);
}
}

private void StopDeferredRetryTimer()
{
// Must be called while holding deferredLock
if (this.deferredRetryTimer != null)
{
this.deferredRetryTimer.Dispose();
this.deferredRetryTimer = null;
}
}

private void OnDeferredRetryTimer(object state)
{
bool attached = this.TryAttachTelemetryPipe(this.deferredGitBinRoot);
if (!attached)
{
lock (this.deferredLock)
{
if (this.deferredRetryTimer != null && !this.isDisposed)
{
this.deferredRetryCount++;
this.deferredRetryTimer.Change(
GetRetryInterval(this.deferredRetryCount),
Timeout.Infinite);
}
}
}
}

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
};
}
}
}
68 changes: 68 additions & 0 deletions GVFS/GVFS.Service/GVFSService.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.IO;
using System.Linq;
using System.Security.AccessControl;
using System.Security.Principal;
using System.ServiceProcess;
using System.Threading;

Expand Down Expand Up @@ -75,6 +76,17 @@ public void Run()
{
this.CheckEnableGitStatusCacheTokenFile();

// If a user is already logged in when the service starts
// (e.g. service restart, installer upgrade during active
// session), attach the telemetry pipe now. At boot,
// no session exists yet and this is a harmless no-op;
// OnSessionChange will attach later.
int activeSession = NativeMethods.GetActiveConsoleSessionId();
if (activeSession > 0)
{
this.TryAttachTelemetryPipeForSession(activeSession);
}

using (ITracer activity = this.tracer.StartActivity("EnsurePrjFltHealthy", EventLevel.Informational))
{
// Make a best-effort to enable PrjFlt. Continue even if it fails.
Expand Down Expand Up @@ -147,6 +159,11 @@ protected override void OnSessionChange(SessionChangeDescription changeDescripti
{
this.tracer.RelatedInfo("SessionLogon detected, sessionId: {0}", changeDescription.SessionId);

// Attempt to attach the telemetry pipe now that a user
// session is available. Buffered pre-logon events are
// replayed. No-ops if already attached.
this.TryAttachTelemetryPipeForSession(changeDescription.SessionId);

using (ITracer activity = this.tracer.StartActivity("LogonAutomount", EventLevel.Informational))
{
this.repoRegistry.AutoMountRepos(
Expand Down Expand Up @@ -391,6 +408,57 @@ private void CreateAndConfigureLogDirectory(string path)
}
}

/// <summary>
/// Impersonates the logged-on user to read their global git config
/// (which contains gvfs.telemetry-pipe) and attach the telemetry
/// daemon listener. Any events buffered before logon are replayed.
/// RunImpersonated only changes the thread token — it does not load
/// the user profile. We set HOME explicitly so git can find .gitconfig.
/// </summary>
private void TryAttachTelemetryPipeForSession(int sessionId)
{
try
{
using (CurrentUser user = new CurrentUser(this.tracer, sessionId))
{
if (user.Identity == null)
{
this.tracer.RelatedWarning("TryAttachTelemetryPipe: Could not get user identity for session {0}", sessionId);
return;
}

string gitBinRoot = GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath();

string userProfile = null;
WindowsIdentity.RunImpersonated(user.Identity.AccessToken, () =>
{
userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
});

if (string.IsNullOrEmpty(userProfile))
{
this.tracer.RelatedWarning("TryAttachTelemetryPipe: Could not resolve user profile for session {0}", sessionId);
return;
}

string oldHome = Environment.GetEnvironmentVariable("HOME");
try
{
Environment.SetEnvironmentVariable("HOME", userProfile);
this.tracer.TryAttachTelemetryPipe(gitBinRoot);
}
finally
{
Environment.SetEnvironmentVariable("HOME", oldHome);
}
}
}
catch (Exception e)
{
this.tracer.RelatedWarning("TryAttachTelemetryPipe failed: {0}", e.Message);
}
}

private DirectorySecurity GetServiceDirectorySecurity(string serviceDataRootPath)
{
DirectorySecurity serviceDataRootSecurity;
Expand Down
Loading
Loading