Skip to content

Commit 25ce6f4

Browse files
authored
Refactors/fixes. See description. (#58)
* Add DataRedaction library references and apply ProtectedDataClassification attributes * Remove unneeded BusyCounter. * Round MinCanvasScale. * Remove unused using directives from DesktopClientRpcService and StaticServiceProvider. * Fix XDG initialization. * File system path and installer refactors. * Fix member order. * Refactor permission handling for macOS in DesktopClient components * Add prod-release workflow. * Add MacPermissionStaleHint localization and update permission status handling * Refactor permission status handling in ManagedDeviceViewModelFake and update UI bindings in ManagedDeviceView * Swap StackPanel with Grid. * Fix extraction path. * Update .gitignore * Refactor NavigationProvider to activate main window if already visible; update ManagedDeviceView design dimensions and binding layout; adjust MacInterop key mapping for Alt and Meta keys. * Refactor WindowsMessagePump to handle cancellation token correctly and log thread cancellation events.
1 parent a8382fb commit 25ce6f4

41 files changed

Lines changed: 343 additions & 236 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/docker-deploy.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ on:
1717
description: "Build workflow run ID (leave empty to use latest)"
1818
required: false
1919
type: string
20+
workflow_call:
21+
inputs:
22+
docker_tag:
23+
description: "Docker tag to deploy"
24+
required: true
25+
type: string
26+
run_id:
27+
description: "Build workflow run ID (leave empty to use latest)"
28+
required: false
29+
type: string
2030

2131
permissions:
2232
actions: read

‎.github/workflows/github-release.yml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ on:
1313
description: "Build workflow run ID (leave empty to use latest)"
1414
required: false
1515
type: string
16+
workflow_call:
17+
inputs:
18+
prerelease:
19+
description: "Mark as pre-release"
20+
required: true
21+
type: boolean
22+
default: false
23+
run_id:
24+
description: "Build workflow run ID (leave empty to use latest)"
25+
required: false
26+
type: string
1627

1728
permissions:
1829
contents: write

‎.github/workflows/prod-release.yml‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Production Release
2+
run-name: "Production Release (Run ID: ${{ inputs.run_id || 'latest build' }})"
3+
4+
on:
5+
workflow_dispatch:
6+
inputs:
7+
run_id:
8+
description: "Build workflow run ID (leave empty to use latest)"
9+
required: false
10+
type: string
11+
prerelease:
12+
description: "Mark GitHub release as pre-release"
13+
required: true
14+
type: boolean
15+
default: false
16+
17+
permissions:
18+
actions: read
19+
contents: write
20+
21+
jobs:
22+
docker-deploy:
23+
name: Deploy to Docker Hub
24+
uses: ./.github/workflows/docker-deploy.yml
25+
with:
26+
docker_tag: latest
27+
run_id: ${{ inputs.run_id }}
28+
permissions:
29+
actions: read
30+
contents: read
31+
32+
github-release:
33+
name: Create GitHub Release
34+
needs: docker-deploy
35+
uses: ./.github/workflows/github-release.yml
36+
with:
37+
prerelease: ${{ inputs.prerelease }}
38+
run_id: ${{ inputs.run_id }}
39+
permissions:
40+
contents: write
41+
42+
publish-nugets:
43+
name: Publish NuGet Packages
44+
needs: docker-deploy
45+
uses: ./.github/workflows/publish-nugets.yml
46+
with:
47+
run_id: ${{ inputs.run_id }}
48+
permissions:
49+
actions: read
50+
contents: read

‎.github/workflows/publish-nugets.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ on:
88
description: "Build workflow run ID (leave empty to use latest)"
99
required: false
1010
type: string
11+
workflow_call:
12+
inputs:
13+
run_id:
14+
description: "Build workflow run ID (leave empty to use latest)"
15+
required: false
16+
type: string
1117

1218
permissions:
1319
actions: read

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,4 @@ FodyWeavers.xsd
375375
/.build/staging-desktop
376376
ControlR.Agent.Common/Resources/ControlR.DesktopClient.zip
377377
ControlR.ApiClient/Output/.kiota.log
378+
ControlR.Agent.Common/Resources/ControlR.app.zip

‎ControlR.Agent.Common/Services/Base/AgentInstallerBase.cs‎

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
using ControlR.Agent.Common.Interfaces;
2-
using ControlR.Agent.Common.Models;
32
using ControlR.Libraries.DevicesCommon.Services.Processes;
43
using ControlR.Libraries.Shared.Constants;
5-
using ControlR.Libraries.Shared.Dtos.ServerApi;
64
using ControlR.Libraries.Shared.Services.Http;
75
using Microsoft.Extensions.Options;
86

@@ -20,6 +18,7 @@ internal abstract class AgentInstallerBase(
2018
private readonly IControlrApi _controlrApi = controlrApi;
2119
private readonly IDeviceInfoProvider _deviceDataGenerator = deviceDataGenerator;
2220
private readonly ISettingsProvider _settingsProvider = settingsProvider;
21+
2322
protected IOptionsMonitor<AgentAppOptions> AppOptions { get; } = appOptions;
2423
protected IFileSystem FileSystem { get; } = fileSystem;
2524
protected ILogger<AgentInstallerBase> Logger { get; } = logger;
@@ -98,6 +97,46 @@ protected Result StopProcesses()
9897
}
9998
}
10099

100+
/// <summary>
101+
/// Attempts to clear old .NET extraction directories to free up space.
102+
/// </summary>
103+
/// <param name="agentTempDirBase">
104+
/// The base directory where .NET extracts files for the agent (e.g. "C:\Windows\SystemTemp\.net\ControlR.Agent").
105+
/// </param>
106+
protected void TryClearDotnetExtractDir(string agentTempDirBase)
107+
{
108+
try
109+
{
110+
if (!FileSystem.DirectoryExists(agentTempDirBase))
111+
{
112+
return;
113+
}
114+
115+
var subdirs = FileSystem
116+
.GetDirectories(agentTempDirBase)
117+
.Select(x => new DirectoryInfo(x))
118+
.OrderByDescending(x => x.CreationTime)
119+
.Skip(3)
120+
.ToArray();
121+
122+
foreach (var subdir in subdirs)
123+
{
124+
try
125+
{
126+
subdir.Delete(true);
127+
}
128+
catch (Exception ex)
129+
{
130+
Logger.LogError(ex, "Failed to delete directory {SubDir}.", subdir);
131+
}
132+
}
133+
}
134+
catch (Exception ex)
135+
{
136+
Logger.LogError(ex, "Error while cleaning up .net extraction directory.");
137+
}
138+
}
139+
101140
protected async Task UpdateAppSettings(Uri? serverUri, Guid? tenantId, Guid? deviceId)
102141
{
103142
using var _ = Logger.BeginMemberScope();

‎ControlR.Agent.Common/Services/DesktopClientUpdater.cs‎

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,22 @@ internal class DesktopClientUpdater(
1515
IControlrMutationLock mutationLock,
1616
IProcessManager processManager,
1717
IDesktopSessionProvider desktopSessionProvider,
18+
IFileSystemPathProvider fileSystemPathProvider,
1819
IEmbeddedDesktopClientProvider embeddedDesktopClientProvider,
1920
ILogger<DesktopClientUpdater> logger) : BackgroundService, IDesktopClientUpdater
2021
{
2122
private readonly IDesktopSessionProvider _desktopSessionProvider = desktopSessionProvider;
2223
private readonly IEmbeddedDesktopClientProvider _embeddedDesktopClientProvider = embeddedDesktopClientProvider;
2324
private readonly IFileSystem _fileSystem = fileSystem;
25+
private readonly IFileSystemPathProvider _fileSystemPathProvider = fileSystemPathProvider;
2426
private readonly ILogger<DesktopClientUpdater> _logger = logger;
2527
private readonly IControlrMutationLock _mutationLock = mutationLock;
2628
private readonly IProcessManager _processManager = processManager;
2729
private readonly IServiceControl _serviceControl = serviceControl;
2830
private readonly ISettingsProvider _settings = settings;
2931
private readonly ISystemEnvironment _systemEnvironment = systemEnvironment;
3032
private readonly SemaphoreSlim _updateLock = new(1, 1);
31-
33+
3234
public async Task<bool> EnsureLatestVersion(
3335
bool acquireGlobalLock,
3436
CancellationToken cancellationToken)
@@ -49,8 +51,8 @@ public async Task<bool> EnsureLatestVersion(
4951
_logger.LogInformation("Ensuring latest version of desktop client.");
5052

5153
var startupDir = _systemEnvironment.StartupDirectory;
52-
var desktopDir = Path.Combine(startupDir, "DesktopClient");
53-
var binaryPath = AppConstants.GetDesktopExecutablePath(startupDir);
54+
var desktopDir = _fileSystemPathProvider.GetDesktopClientDirectory();
55+
var binaryPath = _fileSystemPathProvider.GetDesktopExecutablePath();
5456
var extractedZipPath = Path.Combine(startupDir, AppConstants.DesktopClientZipFileName);
5557

5658
// 1) Compute embedded zip hash
@@ -174,7 +176,7 @@ private async Task<bool> ExtractDesktopClient(string desktopDir)
174176

175177
_fileSystem.ExtractZipArchive(targetPath, desktopDir, true);
176178

177-
await SetDesktopClientPermissions(desktopDir);
179+
await SetDesktopClientFileAttributes(desktopDir);
178180

179181
if (_systemEnvironment.Platform == SystemPlatform.MacOs || _systemEnvironment.Platform == SystemPlatform.Linux)
180182
{
@@ -205,14 +207,14 @@ private async Task<Result<string>> GetEmbeddedZipHash(CancellationToken cancella
205207

206208
private async Task<string?> GetFileHash(string path, CancellationToken cancellationToken)
207209
{
208-
if (!_fileSystem.FileExists(path))
209-
{
210-
_logger.LogInformation("Extracted desktop client ZIP not found. Update required.");
211-
return null;
212-
}
213-
214210
try
215211
{
212+
if (!_fileSystem.FileExists(path))
213+
{
214+
_logger.LogInformation("Extracted desktop client ZIP not found. Update required.");
215+
return null;
216+
}
217+
216218
await using var fileStream = _fileSystem.OpenFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
217219
var hashBytes = await SHA256.HashDataAsync(fileStream, cancellationToken);
218220
var extractedHash = Convert.ToHexString(hashBytes);
@@ -226,41 +228,47 @@ private async Task<Result<string>> GetEmbeddedZipHash(CancellationToken cancella
226228
}
227229
}
228230

229-
230231
private bool IsUpToDate(string? extractedHash, string embeddedHash, string binaryPath)
231232
{
232233
return !string.IsNullOrWhiteSpace(extractedHash)
233234
&& string.Equals(extractedHash, embeddedHash, StringComparison.OrdinalIgnoreCase)
234235
&& _fileSystem.FileExists(binaryPath);
235236
}
236237

237-
private async Task SetDesktopClientPermissions(string desktopDir)
238+
private async Task SetDesktopClientFileAttributes(string desktopDir)
238239
{
239-
if (_systemEnvironment.Platform == SystemPlatform.MacOs)
240+
try
240241
{
241-
// Ensure the Mac app bundle executable has correct permissions
242-
var appBundleExecutablePath = Path.Combine(desktopDir, "ControlR.app", "Contents", "MacOS", "ControlR.DesktopClient");
243-
if (_fileSystem.FileExists(appBundleExecutablePath))
242+
if (_systemEnvironment.Platform == SystemPlatform.MacOs)
244243
{
245-
var chmodResult = await _processManager.GetProcessOutput("chmod", "+x " + appBundleExecutablePath, 5000);
246-
if (!chmodResult.IsSuccess)
244+
// Ensure the Mac app bundle executable has correct file attributes.
245+
var appBundleExecutablePath = Path.Combine(desktopDir, "ControlR.app", "Contents", "MacOS", "ControlR.DesktopClient");
246+
if (_fileSystem.FileExists(appBundleExecutablePath))
247247
{
248-
_logger.LogWarning("Failed to set executable permissions on Mac app bundle: {Error}", chmodResult.Reason);
248+
var chmodResult = await _processManager.GetProcessOutput("chmod", "+x " + appBundleExecutablePath, 5000);
249+
if (!chmodResult.IsSuccess)
250+
{
251+
_logger.LogWarning("Failed to set executable permissions on Mac app bundle: {Error}", chmodResult.Reason);
252+
}
249253
}
250254
}
251-
}
252-
else if (_systemEnvironment.Platform == SystemPlatform.Linux)
253-
{
254-
// Ensure the Linux executable has correct permissions
255-
var linuxExecutablePath = Path.Combine(desktopDir, AppConstants.DesktopClientFileName);
256-
if (_fileSystem.FileExists(linuxExecutablePath))
255+
else if (_systemEnvironment.Platform == SystemPlatform.Linux)
257256
{
258-
var chmodResult = await _processManager.GetProcessOutput("chmod", "+x " + linuxExecutablePath, 5000);
259-
if (!chmodResult.IsSuccess)
257+
// Ensure the Linux executable has correct file attributes.
258+
var linuxExecutablePath = Path.Combine(desktopDir, AppConstants.DesktopClientFileName);
259+
if (_fileSystem.FileExists(linuxExecutablePath))
260260
{
261-
_logger.LogWarning("Failed to set executable permissions on Linux executable: {Error}", chmodResult.Reason);
261+
var chmodResult = await _processManager.GetProcessOutput("chmod", "+x " + linuxExecutablePath, 5000);
262+
if (!chmodResult.IsSuccess)
263+
{
264+
_logger.LogWarning("Failed to set executable file attributes on Linux executable: {Error}", chmodResult.Reason);
265+
}
262266
}
263267
}
264268
}
269+
catch (Exception ex)
270+
{
271+
_logger.LogError(ex, "Error setting desktop client file attributes.");
272+
}
265273
}
266274
}

‎ControlR.Agent.Common/Services/FileSystemPathProvider.cs‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using ControlR.Agent.Common.Interfaces;
2-
using ControlR.Agent.Common.Startup;
3-
using ControlR.Libraries.DevicesCommon.Services;
2+
using ControlR.Libraries.Shared.Constants;
43
using Microsoft.Extensions.Options;
54

65
namespace ControlR.Agent.Common.Services;
@@ -13,6 +12,10 @@ public interface IFileSystemPathProvider
1312

1413
string GetAgentLogsDirectoryPath();
1514

15+
string GetDesktopClientDirectory();
16+
17+
string GetDesktopExecutablePath();
18+
1619
string GetUnixDesktopClientLogsDirectory(string username);
1720

1821
string GetUnixDesktopClientLogsDirectoryForRoot();
@@ -68,6 +71,23 @@ public string GetAgentLogsDirectoryPath()
6871
throw new PlatformNotSupportedException();
6972
}
7073

74+
public string GetDesktopClientDirectory()
75+
{
76+
var startupDir = _systemEnvironment.StartupDirectory;
77+
return Path.Combine(startupDir, "DesktopClient");
78+
}
79+
80+
public string GetDesktopExecutablePath()
81+
{
82+
var desktopDir = GetDesktopClientDirectory();
83+
84+
return SystemEnvironment.Instance.Platform switch
85+
{
86+
SystemPlatform.MacOs => Path.Combine(desktopDir, "ControlR.app", "Contents", "MacOS", AppConstants.DesktopClientFileName),
87+
_ => Path.Combine(desktopDir, AppConstants.DesktopClientFileName)
88+
};
89+
}
90+
7191
public string GetUnixDesktopClientLogsDirectory(string username)
7292
{
7393
if (!_systemEnvironment.IsLinux() && !_systemEnvironment.IsMacOS())

‎ControlR.Agent.Common/Services/IpcClientAuthenticator.cs‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,20 @@ public class IpcClientAuthenticator(
1818
IClientCredentialsProvider credentialsProvider,
1919
ISystemEnvironment systemEnvironment,
2020
IDesktopClientFileVerifier fileVerifier,
21+
IFileSystemPathProvider pathProvider,
2122
ILogger<IpcClientAuthenticator> logger) : IIpcClientAuthenticator
2223
{
2324
private const int MaxFailuresPerMinute = 5;
2425

25-
2626
private readonly IClientCredentialsProvider _credentialsProvider = credentialsProvider;
2727
private readonly ConcurrentDictionary<string, List<DateTimeOffset>> _failedAttempts = new();
2828
private readonly IDesktopClientFileVerifier _fileVerifier = fileVerifier;
2929
private readonly ILogger<IpcClientAuthenticator> _logger = logger;
30+
private readonly IFileSystemPathProvider _pathProvider = pathProvider;
3031
private readonly SemaphoreSlim _rateLimitLock = new(1, 1);
3132
private readonly ISystemEnvironment _systemEnvironment = systemEnvironment;
3233
private readonly TimeProvider _timeProvider = timeProvider;
33-
34+
3435
public async Task<Result<ClientCredentials>> AuthenticateConnection(IIpcServer server)
3536
{
3637
try
@@ -169,7 +170,6 @@ public async Task RecordFailedAttempt(string executablePath)
169170
}
170171
}
171172

172-
173173
private Result ValidateExecutablePath(string executablePath)
174174
{
175175
if (string.IsNullOrWhiteSpace(executablePath))
@@ -183,6 +183,7 @@ private Result ValidateExecutablePath(string executablePath)
183183
return ValidateExecutablePathDebug(executablePath);
184184
}
185185

186+
// Release mode validation
186187
return ValidateExecutablePathRelease(executablePath);
187188
}
188189

@@ -229,9 +230,7 @@ private Result ValidateExecutablePathDebug(string executablePath)
229230

230231
private Result ValidateExecutablePathRelease(string executablePath)
231232
{
232-
// Production/Release mode: strict validation
233-
var expectedPath = AppConstants.GetDesktopExecutablePath(_systemEnvironment.StartupDirectory);
234-
233+
var expectedPath = _pathProvider.GetDesktopExecutablePath();
235234
if (executablePath.Equals(expectedPath, StringComparison.OrdinalIgnoreCase))
236235
{
237236
return Result.Ok();

0 commit comments

Comments
 (0)