Skip to content

Commit 720397b

Browse files
authored
Privacy screen v2. (#60)
* Add CreateWindowInBand. * Disable Aero Peek during remote control session.
1 parent 727a273 commit 720397b

26 files changed

Lines changed: 959 additions & 568 deletions

File tree

‎ControlR.Agent.Common/Services/Windows/DesktopClientWatcherWin.cs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
4747
_logger.LogInformation("Skipping DesktopClientWatcher in Debug mode.");
4848
return;
4949
}
50-
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5), _timeProvider);
5150

51+
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5), _timeProvider);
52+
5253
while (await timer.WaitForNextTick(false, stoppingToken))
5354
{
5455
try
@@ -87,12 +88,18 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
8788
continue;
8889
}
8990

91+
_launchFailCount = 0;
92+
9093
_logger.LogWarning(
9194
"Failed to launch desktop client in one or more sessions. " +
9295
"Deleting existing desktop client installation to force a reinstall.");
9396

9497
await DeleteDesktopClient();
9598
}
99+
catch (OperationCanceledException)
100+
{
101+
_logger.LogInformation("Stopping DesktopClientWatcher. Application is shutting down.");
102+
}
96103
catch (Exception ex)
97104
{
98105
_logger.LogError(ex, "Error while checking for desktop client processes.");

‎ControlR.DesktopClient.Windows/HostAppBuilderExtensions.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ public static IHostApplicationBuilder AddWindowsDesktopServices(
2424
.AddSingleton(services => services.GetRequiredService<IScreenGrabberFactory>().GetOrCreateDefault())
2525
.AddSingleton<IDxOutputDuplicator, DxOutputDuplicator>()
2626
.AddSingleton<IWindowsMessagePump, WindowsMessagePump>()
27+
.AddSingleton<IAeroPeekProvider, AeroPeekProvider>()
2728
.AddHostedService(x => x.GetRequiredService<IWindowsMessagePump>())
2829
.AddHostedService<SystemEventHandler>()
30+
.AddHostedService<RemoteControlEnvironmentService>()
2931
.AddHostedService<InputDesktopReporter>()
3032
.AddHostedService(x => x.GetRequiredService<InputSimulatorWindows>())
3133
.AddHostedService<CursorWatcherWindows>();
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System.Diagnostics;
2+
using System.Security.Principal;
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.Win32;
5+
using Microsoft.Win32.SafeHandles;
6+
using Windows.Win32;
7+
using Windows.Win32.Foundation;
8+
using Windows.Win32.Security;
9+
10+
namespace ControlR.DesktopClient.Windows.Services;
11+
12+
internal interface IAeroPeekProvider
13+
{
14+
bool SetAeroPeekEnabled(bool isEnabled);
15+
}
16+
17+
internal unsafe class AeroPeekProvider(ILogger<AeroPeekProvider> logger) : IAeroPeekProvider
18+
{
19+
private const string AeroPeekValueName = "EnableAeroPeek";
20+
private const string DwmKeyPath = @"Software\Microsoft\Windows\DWM";
21+
22+
private readonly ILogger<AeroPeekProvider> _logger = logger;
23+
24+
public bool SetAeroPeekEnabled(bool isEnabled)
25+
{
26+
try
27+
{
28+
_logger.LogInformation("Setting Aero Peek enabled state to {IsEnabled}", isEnabled);
29+
30+
var identity = WindowsIdentity.GetCurrent();
31+
var isSystem = identity.User?.IsWellKnown(WellKnownSidType.LocalSystemSid) == true;
32+
33+
if (isSystem)
34+
{
35+
_logger.LogDebug("Running as SYSTEM. Using WTSQueryUserToken to target logged-in user's registry.");
36+
return SetAeroPeekEnabledAsSystem(isEnabled);
37+
}
38+
else
39+
{
40+
_logger.LogDebug("Running as regular user. Using Registry.CurrentUser.");
41+
return SetAeroPeekEnabledAsUser(isEnabled);
42+
}
43+
}
44+
catch (Exception ex)
45+
{
46+
_logger.LogError(ex, "Error setting Aero Peek state in registry.");
47+
return false;
48+
}
49+
}
50+
51+
private bool SetAeroPeekEnabledAsSystem(bool isEnabled)
52+
{
53+
try
54+
{
55+
var sessionId = (uint)Process.GetCurrentProcess().SessionId;
56+
57+
HANDLE userTokenHandle = default;
58+
59+
if (!PInvoke.WTSQueryUserToken(sessionId, ref userTokenHandle))
60+
{
61+
_logger.LogError("Failed to get user token for session {SessionId}", sessionId);
62+
return false;
63+
}
64+
65+
using var userToken = new SafeFileHandle(userTokenHandle, ownsHandle: true);
66+
67+
// First call to get buffer size
68+
PInvoke.GetTokenInformation(
69+
userToken,
70+
TOKEN_INFORMATION_CLASS.TokenUser,
71+
null,
72+
out var tokenInfoLength);
73+
74+
var tokenInfoBuffer = new byte[tokenInfoLength];
75+
76+
if (!PInvoke.GetTokenInformation(
77+
userToken,
78+
TOKEN_INFORMATION_CLASS.TokenUser,
79+
tokenInfoBuffer,
80+
out _))
81+
{
82+
_logger.LogError("Failed to get token user information");
83+
return false;
84+
}
85+
86+
fixed (byte* pTokenInfo = tokenInfoBuffer)
87+
{
88+
var tokenUser = (TOKEN_USER*)pTokenInfo;
89+
if (!PInvoke.ConvertSidToStringSid(tokenUser->User.Sid, out var sidString))
90+
{
91+
_logger.LogError("Failed to convert SID to string");
92+
return false;
93+
}
94+
95+
var userKeyPath = $"{sidString}\\{DwmKeyPath}";
96+
_logger.LogDebug("Targeting user registry path: HKEY_USERS\\{UserKeyPath}", userKeyPath);
97+
98+
if (isEnabled)
99+
{
100+
using var key = Registry.Users.OpenSubKey(userKeyPath, writable: true);
101+
// Explicitly set to 1 to enable peek
102+
key?.SetValue(AeroPeekValueName, 1, RegistryValueKind.DWord);
103+
}
104+
else
105+
{
106+
using var key = Registry.Users.CreateSubKey(userKeyPath, writable: true);
107+
key.SetValue(AeroPeekValueName, 0, RegistryValueKind.DWord);
108+
}
109+
}
110+
111+
_logger.LogInformation("Successfully set Aero Peek enabled state to {IsEnabled} via SYSTEM", isEnabled);
112+
return true;
113+
}
114+
catch (Exception ex)
115+
{
116+
_logger.LogError(ex, "Error setting Aero Peek state for SYSTEM user.");
117+
return false;
118+
}
119+
}
120+
121+
private bool SetAeroPeekEnabledAsUser(bool isEnabled)
122+
{
123+
if (isEnabled)
124+
{
125+
using var key = Registry.CurrentUser.OpenSubKey(DwmKeyPath, writable: true);
126+
key?.SetValue(AeroPeekValueName, 1, RegistryValueKind.DWord);
127+
}
128+
else
129+
{
130+
using var key = Registry.CurrentUser.CreateSubKey(DwmKeyPath, writable: true);
131+
key.SetValue(AeroPeekValueName, 0, RegistryValueKind.DWord);
132+
}
133+
134+
_logger.LogInformation("Successfully set Aero Peek enabled state to {IsEnabled} via CurrentUser", isEnabled);
135+
return true;
136+
}
137+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Microsoft.Extensions.Hosting;
2+
3+
namespace ControlR.DesktopClient.Windows.Services;
4+
5+
/// <summary>
6+
/// Responsible for any environment setup or teardown needed for remote control sessions.
7+
/// </summary>
8+
internal class RemoteControlEnvironmentService(IAeroPeekProvider aeroProvider) : IHostedService
9+
{
10+
public Task StartAsync(CancellationToken cancellationToken)
11+
{
12+
_ = aeroProvider.SetAeroPeekEnabled(false);
13+
return Task.CompletedTask;
14+
}
15+
16+
public Task StopAsync(CancellationToken cancellationToken)
17+
{
18+
_ = aeroProvider.SetAeroPeekEnabled(true);
19+
return Task.CompletedTask;
20+
}
21+
}

‎ControlR.DesktopClient.Windows/Services/WindowsMessagePump.cs‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Concurrent;
22
using System.Runtime.Versioning;
3+
using ControlR.Libraries.NativeInterop.Windows;
34
using Microsoft.Extensions.Hosting;
45
using Microsoft.Extensions.Logging;
56
using Windows.Win32;
@@ -26,10 +27,13 @@ public interface IWindowsMessagePump : IHostedService
2627

2728

2829
[SupportedOSPlatform("windows6.1")]
29-
internal class WindowsMessagePump(ILogger<WindowsMessagePump> logger)
30+
internal class WindowsMessagePump(
31+
IWin32Interop win32Interop,
32+
ILogger<WindowsMessagePump> logger)
3033
: BackgroundService, IWindowsMessagePump
3134
{
3235
private readonly ILogger<WindowsMessagePump> _logger = logger;
36+
private readonly IWin32Interop _win32Interop = win32Interop;
3337
private readonly BlockingCollection<WorkItem> _workQueue = [];
3438

3539
private Thread? _messageThread;
@@ -111,7 +115,10 @@ private void RunMessageLoop(CancellationToken stoppingToken)
111115
{
112116
try
113117
{
114-
_logger.LogInformation("Windows message pump thread started");
118+
_logger.LogInformation("Windows message pump thread started.");
119+
120+
var isUiAccess = _win32Interop.IsCurrentProcessUiAccess();
121+
_logger.LogInformation("Current process UIAccess status: {UiAccess}", isUiAccess);
115122

116123
while (!_workQueue.IsCompleted)
117124
{

‎ControlR.DesktopClient/Models/RemoteControlSession.cs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ public async ValueTask DisposeAsync()
1616
{
1717
try
1818
{
19-
await CancellationTokenSource.CancelAsync();
2019
Disposer.DisposeAll(CancellationTokenSource, Host);
2120
}
2221
catch

‎ControlR.DesktopClient/Services/RemoteControlHostManager.cs‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using ControlR.DesktopClient.Common;
44
using ControlR.DesktopClient.Common.Options;
55
using ControlR.DesktopClient.Models;
6-
using ControlR.DesktopClient.ViewModels;
76
using ControlR.Libraries.Shared.Dtos.IpcDtos;
87
using ControlR.Libraries.Shared.Extensions;
98
using ControlR.Libraries.Shared.Primitives;
@@ -48,12 +47,8 @@ public async Task<Result> StartHost(RemoteControlRequestIpcDto requestDto)
4847

4948
var builder = CreateRemoteControlHostBuilder(requestDto);
5049
var app = builder.Build();
51-
var appLifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
52-
appLifetime.ApplicationStopping.Register(async () =>
53-
{
54-
await HandleApplicationStopping(requestDto);
55-
});
5650
var session = CreateRemoteControlSession(requestDto, app);
51+
RegisterHostLifetimeHandlers(app, requestDto, session);
5752
await app.StartAsync(session.CancellationTokenSource.Token);
5853
return Result.Ok();
5954
}
@@ -155,7 +150,7 @@ private RemoteControlSession CreateRemoteControlSession(
155150
});
156151
}
157152

158-
private async Task HandleApplicationStopping(RemoteControlRequestIpcDto requestDto)
153+
private async Task HandleApplicationStopped(RemoteControlRequestIpcDto requestDto)
159154
{
160155
try
161156
{
@@ -181,4 +176,19 @@ private async Task HandleApplicationStopping(RemoteControlRequestIpcDto requestD
181176
_logger.LogError(ex, "Error during remote control session shutdown.");
182177
}
183178
}
179+
180+
private void RegisterHostLifetimeHandlers(IHost app, RemoteControlRequestIpcDto requestDto, RemoteControlSession session)
181+
{
182+
var appLifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
183+
appLifetime.ApplicationStopping.Register(async () =>
184+
{
185+
await session.CancellationTokenSource.CancelAsync();
186+
await app.StopAsync();
187+
});
188+
189+
appLifetime.ApplicationStopped.Register(async () =>
190+
{
191+
await HandleApplicationStopped(requestDto);
192+
});
193+
}
184194
}

‎ControlR.Web.Client/Components/Pages/DeviceAccess/RemoteControl.razor.cs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,9 @@ private async Task<bool> StartRemoteControl(DesktopSession desktopSession, bool
291291
_loadingMessage = "Starting remote control session";
292292
}
293293

294+
RemoteControlState.IsBlockUserInputEnabled = false;
295+
RemoteControlState.IsPrivacyScreenEnabled = false;
296+
294297
_systemSessions = null;
295298

296299
var session = new RemoteControlSession(

0 commit comments

Comments
 (0)