Skip to content

Commit 291a567

Browse files
committed
Fix deferred telemetry pipe attach for RDP sessions
At startup, the deferred telemetry attacher only checked the physical console session via WTSGetActiveConsoleSessionId. On Cloud PCs and RDP-only machines, the console session has no logged-in user — the real user is in an RDP session. This meant the startup attach always failed, and the retry timer could never succeed either (SYSTEM has no git global config). A SessionLogon event would fix it, but that only fires for NEW logins — not when the service restarts while a user is already connected. Enumerate all interactive sessions (Active/Connected, session > 0) via WTSEnumerateSessions and try each until the pipe attaches. Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
1 parent b65af3a commit 291a567

2 files changed

Lines changed: 141 additions & 27 deletions

File tree

GVFS/GVFS.Platform.Windows/CurrentUser.cs

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public bool RunAs(string processName, string args)
134134
if (CreateEnvironmentBlock(ref environment, duplicate, false))
135135
{
136136
STARTUP_INFO info = new STARTUP_INFO();
137-
info.Length = Marshal.SizeOf(typeof(STARTUP_INFO));
137+
info.Length = Marshal.SizeOf<STARTUP_INFO>();
138138

139139
PROCESS_INFORMATION procInfo = new PROCESS_INFORMATION();
140140
if (CreateProcessAsUser(
@@ -193,6 +193,55 @@ public bool RunAs(string processName, string args)
193193
return false;
194194
}
195195

196+
/// <summary>
197+
/// Returns session IDs for sessions that have a logged-in user
198+
/// whose token can be queried via <c>WTSQueryUserToken</c>.
199+
/// </summary>
200+
/// <remarks>
201+
/// <para>
202+
/// WTS session states relevant to telemetry pipe attachment:
203+
/// </para>
204+
/// <list type="table">
205+
/// <listheader><term>State</term><description>Meaning</description></listheader>
206+
/// <item><term>Active</term><description>
207+
/// User logged in, session connected (console or RDP).
208+
/// Has a valid user token. This is the primary case.
209+
/// </description></item>
210+
/// <item><term>Connected</term><description>
211+
/// Client attached but no user logged in (e.g. the console
212+
/// session showing the Windows login screen on a Cloud PC).
213+
/// No user token — <c>WTSQueryUserToken</c> will fail.
214+
/// </description></item>
215+
/// <item><term>Disconnected</term><description>
216+
/// User logged in but client disconnected (e.g. closed RDP
217+
/// window without logging off). The user's profile, processes,
218+
/// and token are still available. Included so the service can
219+
/// attach telemetry even when no client is actively connected.
220+
/// </description></item>
221+
/// </list>
222+
/// <para>
223+
/// Session 0 is the services session and never has a user token,
224+
/// even when its state is Disconnected. Excluded by the ID > 0
225+
/// guard.
226+
/// </para>
227+
/// </remarks>
228+
public static List<int> GetInteractiveSessionIds(ITracer tracer)
229+
{
230+
List<int> sessionIds = new List<int>();
231+
List<WTS_SESSION_INFO> sessions = ListSessions(tracer);
232+
foreach (WTS_SESSION_INFO session in sessions)
233+
{
234+
if (session.SessionID > 0 &&
235+
(session.State == ConnectionState.Active ||
236+
session.State == ConnectionState.Disconnected))
237+
{
238+
sessionIds.Add(session.SessionID);
239+
}
240+
}
241+
242+
return sessionIds;
243+
}
244+
196245
public void Dispose()
197246
{
198247
if (this.token != IntPtr.Zero)
@@ -216,7 +265,10 @@ private static IntPtr GetCurrentUserToken(ITracer tracer, int sessionId)
216265
}
217266
else
218267
{
219-
TraceWin32Error(tracer, string.Format("Unable to query user token from session '{0}'.", sessionId));
268+
// Warning, not error: sessions without a logged-in user
269+
// (e.g. the console session on a Cloud PC) are expected.
270+
Win32Exception e = new Win32Exception(Marshal.GetLastWin32Error());
271+
tracer.RelatedWarning("Unable to query user token from session '{0}'. Exception: {1}", sessionId, e.Message);
220272
}
221273

222274
return IntPtr.Zero;
@@ -234,12 +286,12 @@ private static List<WTS_SESSION_INFO> ListSessions(ITracer tracer)
234286
int retval = WTSEnumerateSessions(IntPtr.Zero, 0, 1, ref sessionInfo, ref count);
235287
if (retval != 0)
236288
{
237-
int dataSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
289+
int dataSize = Marshal.SizeOf<WTS_SESSION_INFO>();
238290
long current = sessionInfo.ToInt64();
239291

240292
for (int i = 0; i < count; i++)
241293
{
242-
WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure((IntPtr)current, typeof(WTS_SESSION_INFO));
294+
WTS_SESSION_INFO si = Marshal.PtrToStructure<WTS_SESSION_INFO>((IntPtr)current);
243295
current += dataSize;
244296

245297
output.Add(si);

GVFS/GVFS.Service/GVFSService.Windows.cs

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using GVFS.Platform.Windows;
66
using GVFS.Service.Handlers;
77
using System;
8+
using System.Collections.Generic;
89
using System.IO;
910
using System.Linq;
1011
using System.Security.AccessControl;
@@ -46,6 +47,46 @@ public void Run()
4647
metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion());
4748
this.tracer.RelatedEvent(EventLevel.Informational, $"{nameof(GVFSService)}_{nameof(this.Run)}", metadata);
4849

50+
// Set up deferred telemetry pipe attachment FIRST, before any
51+
// telemetry-emitting work (particularly PendingUpgradeHandler,
52+
// whose Deferred/Complete events we want to capture).
53+
//
54+
// The service runs as SYSTEM and can't read the user's global
55+
// git config (where gvfs.telemetry-pipe is configured) at
56+
// startup. The DeferredTelemetryAttacher adds a
57+
// BufferingTelemetryListener that captures events in memory,
58+
// then replays them once the real pipe listener attaches.
59+
//
60+
// Three attach paths exist:
61+
// 1. TryAttachTelemetryPipeForAnySessions() below — tries
62+
// all Active/Disconnected sessions immediately.
63+
// 2. OnSessionChange (SessionLogon) — fires when a new user
64+
// logs in after the service is already running.
65+
// 3. StartRetryTimer — periodic retry (10s, 30s, 1m, 5m)
66+
// as a fallback, reads system config only (no user
67+
// config available without a session to impersonate).
68+
string gitBinRoot = GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath();
69+
if (!string.IsNullOrEmpty(gitBinRoot))
70+
{
71+
this.telemetryAttacher = new DeferredTelemetryAttacher(
72+
this.tracer,
73+
GVFSConstants.Service.ServiceName,
74+
enlistmentId: null,
75+
mountId: null);
76+
this.telemetryAttacher.StartRetryTimer(gitBinRoot);
77+
78+
// If a user is already logged in (e.g. service restart
79+
// during active session), try attaching immediately by
80+
// enumerating all interactive sessions. This is needed
81+
// because WTSGetActiveConsoleSessionId only returns the
82+
// physical console session, which on Cloud PCs / DevBoxes
83+
// (RDP-only) has no logged-in user. The actual user is
84+
// in an RDP session that the console-only check misses.
85+
// SessionLogon events also won't fire for sessions that
86+
// were already established before the service started.
87+
this.TryAttachTelemetryPipeForAnySessions();
88+
}
89+
4990
// Check for a staged upgrade before doing anything else.
5091
// If no GVFS.Mount processes are running (typical at boot or after
5192
// unmount-all), copy staged files in-place and proceed normally.
@@ -77,29 +118,6 @@ public void Run()
77118
{
78119
this.CheckEnableGitStatusCacheTokenFile();
79120

80-
// Set up deferred telemetry pipe attachment. The service
81-
// runs as SYSTEM and can't read the user's global git
82-
// config at startup, so the daemon listener can't be
83-
// created in the JsonTracer constructor.
84-
string gitBinRoot = GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath();
85-
if (!string.IsNullOrEmpty(gitBinRoot))
86-
{
87-
this.telemetryAttacher = new DeferredTelemetryAttacher(
88-
this.tracer,
89-
GVFSConstants.Service.ServiceName,
90-
enlistmentId: null,
91-
mountId: null);
92-
this.telemetryAttacher.StartRetryTimer(gitBinRoot);
93-
94-
// If a user is already logged in (e.g. service restart
95-
// during active session), try attaching immediately.
96-
int activeSession = NativeMethods.GetActiveConsoleSessionId();
97-
if (activeSession > 0)
98-
{
99-
this.TryAttachTelemetryPipeForSession(activeSession);
100-
}
101-
}
102-
103121
using (ITracer activity = this.tracer.StartActivity("EnsurePrjFltHealthy", EventLevel.Informational))
104122
{
105123
// Make a best-effort to enable PrjFlt. Continue even if it fails.
@@ -475,6 +493,50 @@ private void TryAttachTelemetryPipeForSession(int sessionId)
475493
}
476494
}
477495

496+
/// <summary>
497+
/// Enumerates all interactive sessions (Active and Disconnected)
498+
/// and tries to attach the telemetry pipe using each session's
499+
/// user profile. Stops on the first successful attach.
500+
/// </summary>
501+
/// <remarks>
502+
/// This method exists because the console-only check
503+
/// (<c>WTSGetActiveConsoleSessionId</c>) fails on Cloud PCs and
504+
/// RDP-only machines where the console session is in the Connected
505+
/// state (login screen, no user). Disconnected sessions are also
506+
/// checked because an RDP user who disconnected without logging
507+
/// off still has a valid token and git config.
508+
/// </remarks>
509+
private void TryAttachTelemetryPipeForAnySessions()
510+
{
511+
if (this.telemetryAttacher == null || this.telemetryAttacher.IsAttached)
512+
{
513+
return;
514+
}
515+
516+
List<int> sessionIds = CurrentUser.GetInteractiveSessionIds(this.tracer);
517+
if (sessionIds.Count == 0)
518+
{
519+
this.tracer.RelatedInfo("TryAttachTelemetryPipeForAnySessions: No interactive sessions found");
520+
return;
521+
}
522+
523+
foreach (int sessionId in sessionIds)
524+
{
525+
this.TryAttachTelemetryPipeForSession(sessionId);
526+
if (this.telemetryAttacher.IsAttached)
527+
{
528+
break;
529+
}
530+
}
531+
532+
if (!this.telemetryAttacher.IsAttached)
533+
{
534+
this.tracer.RelatedWarning(
535+
"TryAttachTelemetryPipeForAnySessions: Could not attach from any of {0} interactive session(s)",
536+
sessionIds.Count);
537+
}
538+
}
539+
478540
private DirectorySecurity GetServiceDirectorySecurity(string serviceDataRootPath)
479541
{
480542
DirectorySecurity serviceDataRootSecurity;

0 commit comments

Comments
 (0)