Skip to content

Commit 1ea8abe

Browse files
authored
Merge pull request #1992 from tyrielv/tyrielv/fix-rdp-telemetry-attach
Fix deferred telemetry pipe attach for RDP sessions
2 parents cfbecff + 291a567 commit 1ea8abe

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)