From eff89858226d81f3efb72eff5c41fb691252e040 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Fri, 30 Jun 2023 14:48:28 -0700 Subject: [PATCH 1/3] Implement remote control session recording. --- Server/API/LoginController.cs | 8 +-- Server/API/RemoteControlController.cs | 10 +-- .../Identity/Pages/Account/Logout.cshtml | 4 +- Server/Hubs/CircuitConnection.cs | 10 +-- Server/Pages/ServerConfig.razor | 7 ++ Server/Pages/ServerConfig.razor.cs | 2 + Server/Program.cs | 2 + Server/Services/ApplicationConfig.cs | 65 ++++++++++--------- .../RcImplementations/SessionRecordingSink.cs | 60 +++++++++++++++++ .../ViewerOptionsProvider.cs | 26 ++++++++ Server/appsettings.json | 1 + Tests/Server.Tests/CircuitConnectionTests.cs | 6 +- submodules/Immense.RemoteControl | 2 +- 13 files changed, 152 insertions(+), 51 deletions(-) create mode 100644 Server/Services/RcImplementations/SessionRecordingSink.cs create mode 100644 Server/Services/RcImplementations/ViewerOptionsProvider.cs diff --git a/Server/API/LoginController.cs b/Server/API/LoginController.cs index da4af862..a9a1443f 100644 --- a/Server/API/LoginController.cs +++ b/Server/API/LoginController.cs @@ -23,7 +23,7 @@ namespace Remotely.Server.API private readonly IApplicationConfig _appConfig; private readonly IDataService _dataService; private readonly IHubContext _desktopHub; - private readonly IDesktopHubSessionCache _desktopSessionCache; + private readonly IRemoteControlSessionCache _remoteControlSessionCache; private readonly SignInManager _signInManager; private readonly IHubContext _viewerHub; private readonly ILogger _logger; @@ -33,7 +33,7 @@ namespace Remotely.Server.API IDataService dataService, IApplicationConfig appConfig, IHubContext casterHubContext, - IDesktopHubSessionCache desktopSessionCache, + IRemoteControlSessionCache remoteControlSessionCache, IHubContext viewerHubContext, ILogger logger) { @@ -41,7 +41,7 @@ namespace Remotely.Server.API _dataService = dataService; _appConfig = appConfig; _desktopHub = casterHubContext; - _desktopSessionCache = desktopSessionCache; + _remoteControlSessionCache = remoteControlSessionCache; _viewerHub = viewerHubContext; _logger = logger; } @@ -54,7 +54,7 @@ namespace Remotely.Server.API if (HttpContext?.User?.Identity?.IsAuthenticated == true) { orgId = _dataService.GetUserByNameWithOrg(HttpContext.User.Identity.Name)?.OrganizationID; - var activeSessions = _desktopSessionCache + var activeSessions = _remoteControlSessionCache .Sessions .Where(x => x.RequesterUserName == HttpContext.User.Identity.Name); diff --git a/Server/API/RemoteControlController.cs b/Server/API/RemoteControlController.cs index b33fbde9..92ec8024 100644 --- a/Server/API/RemoteControlController.cs +++ b/Server/API/RemoteControlController.cs @@ -27,7 +27,7 @@ namespace Remotely.Server.API public class RemoteControlController : ControllerBase { private readonly IHubContext _serviceHub; - private readonly IDesktopHubSessionCache _desktopSessionCache; + private readonly IRemoteControlSessionCache _remoteControlSessionCache; private readonly IAgentHubSessionCache _serviceSessionCache; private readonly IApplicationConfig _appConfig; private readonly IOtpProvider _otpProvider; @@ -39,7 +39,7 @@ namespace Remotely.Server.API public RemoteControlController( SignInManager signInManager, IDataService dataService, - IDesktopHubSessionCache desktopSessionCache, + IRemoteControlSessionCache remoteControlSessionCache, IHubContext serviceHub, IAgentHubSessionCache serviceSessionCache, IOtpProvider otpProvider, @@ -49,7 +49,7 @@ namespace Remotely.Server.API { _dataService = dataService; _serviceHub = serviceHub; - _desktopSessionCache = desktopSessionCache; + _remoteControlSessionCache = remoteControlSessionCache; _serviceSessionCache = serviceSessionCache; _appConfig = appConfig; _otpProvider = otpProvider; @@ -117,7 +117,7 @@ namespace Remotely.Server.API } - var sessionCount = _desktopSessionCache.Sessions + var sessionCount = _remoteControlSessionCache.Sessions .OfType() .Count(x => x.OrganizationId == orgID); @@ -138,7 +138,7 @@ namespace Remotely.Server.API OrganizationId = orgID }; - _desktopSessionCache.AddOrUpdate($"{sessionId}", session, (k, v) => + _remoteControlSessionCache.AddOrUpdate($"{sessionId}", session, (k, v) => { if (v is RemoteControlSessionEx ex) { diff --git a/Server/Areas/Identity/Pages/Account/Logout.cshtml b/Server/Areas/Identity/Pages/Account/Logout.cshtml index bb02698a..e84347e5 100644 --- a/Server/Areas/Identity/Pages/Account/Logout.cshtml +++ b/Server/Areas/Identity/Pages/Account/Logout.cshtml @@ -9,14 +9,14 @@ @inject SignInManager SignInManager @inject IHubContext DesktopHubContext @inject IHubContext ViewerHubContext -@inject IDesktopHubSessionCache DesktopSessionCache +@inject IRemoteControlSessionCache RemoteControlSessionCache @functions { public async Task OnPost() { if (SignInManager.IsSignedIn(User)) { - var activeSessions = DesktopSessionCache.Sessions.Where(x => x.RequesterUserName == HttpContext.User.Identity.Name); + var activeSessions = RemoteControlSessionCache.Sessions.Where(x => x.RequesterUserName == HttpContext.User.Identity.Name); foreach (var session in activeSessions) { await DesktopHubContext.Clients.Client(session.DesktopConnectionId).SendAsync("Disconnect", "User logged out."); diff --git a/Server/Hubs/CircuitConnection.cs b/Server/Hubs/CircuitConnection.cs index d8fc3183..f1a3ea64 100644 --- a/Server/Hubs/CircuitConnection.cs +++ b/Server/Hubs/CircuitConnection.cs @@ -79,7 +79,7 @@ namespace Remotely.Server.Hubs private readonly IAuthService _authService; private readonly ICircuitManager _circuitManager; private readonly IDataService _dataService; - private readonly IDesktopHubSessionCache _desktopSessionCache; + private readonly IRemoteControlSessionCache _remoteControlSessionCache; private readonly ConcurrentQueue _eventQueue = new(); private readonly IExpiringTokenService _expiringTokenService; private readonly ILogger _logger; @@ -94,7 +94,7 @@ namespace Remotely.Server.Hubs ICircuitManager circuitManager, IToastService toastService, IExpiringTokenService expiringTokenService, - IDesktopHubSessionCache desktopSessionCache, + IRemoteControlSessionCache remoteControlSessionCache, IAgentHubSessionCache agentSessionCache, ILogger logger) { @@ -106,7 +106,7 @@ namespace Remotely.Server.Hubs _circuitManager = circuitManager; _toastService = toastService; _expiringTokenService = expiringTokenService; - _desktopSessionCache = desktopSessionCache; + _remoteControlSessionCache = remoteControlSessionCache; _agentSessionCache = agentSessionCache; _logger = logger; } @@ -244,7 +244,7 @@ namespace Remotely.Server.Hubs } - var sessionCount = _desktopSessionCache.Sessions + var sessionCount = _remoteControlSessionCache.Sessions .OfType() .Count(x => x.OrganizationId == User.OrganizationID); @@ -281,7 +281,7 @@ namespace Remotely.Server.Hubs NotifyUserOnStart = _appConfig.RemoteControlNotifyUser }; - _desktopSessionCache.AddOrUpdate($"{sessionId}", session); + _remoteControlSessionCache.AddOrUpdate($"{sessionId}", session); var organization = _dataService.GetOrganizationNameByUserName(User.UserName); await _agentHubContext.Clients.Client(serviceConnectionId).SendAsync("RemoteControl", diff --git a/Server/Pages/ServerConfig.razor b/Server/Pages/ServerConfig.razor index 417dadd6..89c575f2 100644 --- a/Server/Pages/ServerConfig.razor +++ b/Server/Pages/ServerConfig.razor @@ -134,6 +134,13 @@
+
+ +
+ +
+ +

diff --git a/Server/Pages/ServerConfig.razor.cs b/Server/Pages/ServerConfig.razor.cs index a78ce72e..ec026af3 100644 --- a/Server/Pages/ServerConfig.razor.cs +++ b/Server/Pages/ServerConfig.razor.cs @@ -37,6 +37,8 @@ namespace Remotely.Server.Pages [JsonConverter(typeof(JsonStringEnumConverter))] public DbProvider DBProvider { get; set; } + [Display(Name = "Enable Remote Control Recording")] + public bool EnableRemoteControlRecording { get; set; } [Display(Name = "Enable Windows Event Log")] public bool EnableWindowsEventLog { get; set; } diff --git a/Server/Program.cs b/Server/Program.cs index 7e68bb5f..7b32e902 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -226,6 +226,8 @@ services.AddRemoteControlServer(config => config.AddHubEventHandler(); config.AddViewerAuthorizer(); config.AddViewerPageDataProvider(); + config.AddViewerOptionsProvider(); + config.AddSessionRecordingSink(); }); services.AddSingleton(); diff --git a/Server/Services/ApplicationConfig.cs b/Server/Services/ApplicationConfig.cs index f39f8d41..2b67d7b7 100644 --- a/Server/Services/ApplicationConfig.cs +++ b/Server/Services/ApplicationConfig.cs @@ -11,6 +11,7 @@ namespace Remotely.Server.Services string[] BannedDevices { get; } double DataRetentionInDays { get; } string DBProvider { get; } + bool EnableRemoteControlRecording { get; } bool EnableWindowsEventLog { get; } bool EnforceAttendedAccess { get; } bool ForceClientHttps { get; } @@ -40,40 +41,42 @@ namespace Remotely.Server.Services public class ApplicationConfig : IApplicationConfig { + private readonly IConfiguration _config; + public ApplicationConfig(IConfiguration config) { - Config = config; + _config = config; } - public bool AllowApiLogin => bool.Parse(Config["ApplicationOptions:AllowApiLogin"] ?? "false"); - public string[] BannedDevices => Config.GetSection("ApplicationOptions:BannedDevices").Get() ?? System.Array.Empty(); - public double DataRetentionInDays => double.Parse(Config["ApplicationOptions:DataRetentionInDays"] ?? "30"); - public string DBProvider => Config["ApplicationOptions:DBProvider"] ?? "SQLite"; - public bool EnableWindowsEventLog => bool.Parse(Config["ApplicationOptions:EnableWindowsEventLog"]); - public bool EnforceAttendedAccess => bool.Parse(Config["ApplicationOptions:EnforceAttendedAccess"] ?? "false"); - public bool ForceClientHttps => bool.Parse(Config["ApplicationOptions:ForceClientHttps"] ?? "false"); - public string[] KnownProxies => Config.GetSection("ApplicationOptions:KnownProxies").Get() ?? System.Array.Empty(); - public int MaxConcurrentUpdates => int.Parse(Config["ApplicationOptions:MaxConcurrentUpdates"] ?? "10"); - public int MaxOrganizationCount => int.Parse(Config["ApplicationOptions:MaxOrganizationCount"] ?? "1"); - public string MessageOfTheDay => Config["ApplicationOptions:MessageOfTheDay"]; - public bool RedirectToHttps => bool.Parse(Config["ApplicationOptions:RedirectToHttps"] ?? "true"); - public bool RemoteControlNotifyUser => bool.Parse(Config["ApplicationOptions:RemoteControlNotifyUser"] ?? "true"); - public bool RemoteControlRequiresAuthentication => bool.Parse(Config["ApplicationOptions:RemoteControlRequiresAuthentication"] ?? "true"); - public int RemoteControlSessionLimit => int.Parse(Config["ApplicationOptions:RemoteControlSessionLimit"] ?? "3"); - public bool Require2FA => bool.Parse(Config["ApplicationOptions:Require2FA"] ?? "false"); - public string ServerUrl => Config["ApplicationOptions:ServerUrl"]; - public bool SmtpCheckCertificateRevocation => bool.Parse(Config["ApplicationOptions:SmtpCheckCertificateRevocation"] ?? "true"); - public string SmtpDisplayName => Config["ApplicationOptions:SmtpDisplayName"]; - public string SmtpEmail => Config["ApplicationOptions:SmtpEmail"]; - public string SmtpHost => Config["ApplicationOptions:SmtpHost"]; - public string SmtpLocalDomain => Config["ApplicationOptions:SmtpLocalDomain"]; - public string SmtpPassword => Config["ApplicationOptions:SmtpPassword"]; - public int SmtpPort => int.Parse(Config["ApplicationOptions:SmtpPort"] ?? "25"); - public string SmtpUserName => Config["ApplicationOptions:SmtpUserName"]; - public Theme Theme => Enum.Parse(Config["ApplicationOptions:Theme"] ?? "Dark", true); - public string[] TrustedCorsOrigins => Config.GetSection("ApplicationOptions:TrustedCorsOrigins").Get() ?? System.Array.Empty(); - public bool UseHsts => bool.Parse(Config["ApplicationOptions:UseHsts"] ?? "false"); - public bool UseHttpLogging => bool.Parse(Config["ApplicationOptions:UseHttpLogging"] ?? "false"); - private IConfiguration Config { get; set; } + public bool AllowApiLogin => bool.TryParse(_config["ApplicationOptions:AllowApiLogin"], out var result) && result; + public string[] BannedDevices => _config.GetSection("ApplicationOptions:BannedDevices").Get() ?? System.Array.Empty(); + public double DataRetentionInDays => double.TryParse(_config["ApplicationOptions:DataRetentionInDays"], out var result) ? result : 30; + public string DBProvider => _config["ApplicationOptions:DBProvider"] ?? "SQLite"; + public bool EnableRemoteControlRecording => bool.TryParse(_config["ApplicationOptions:EnableRemoteControlRecording"], out var result) && result; + public bool EnableWindowsEventLog => bool.TryParse(_config["ApplicationOptions:EnableWindowsEventLog"], out var result) && result; + public bool EnforceAttendedAccess => bool.TryParse(_config["ApplicationOptions:EnforceAttendedAccess"], out var result) && result; + public bool ForceClientHttps => bool.TryParse(_config["ApplicationOptions:ForceClientHttps"], out var result) && result; + public string[] KnownProxies => _config.GetSection("ApplicationOptions:KnownProxies").Get() ?? System.Array.Empty(); + public int MaxConcurrentUpdates => int.TryParse(_config["ApplicationOptions:MaxConcurrentUpdates"], out var result) ? result : 10; + public int MaxOrganizationCount => int.TryParse(_config["ApplicationOptions:MaxOrganizationCount"], out var result) ? result : 1; + public string MessageOfTheDay => _config["ApplicationOptions:MessageOfTheDay"]; + public bool RedirectToHttps => bool.TryParse(_config["ApplicationOptions:RedirectToHttps"], out var result) && result; + public bool RemoteControlNotifyUser => bool.TryParse(_config["ApplicationOptions:RemoteControlNotifyUser"], out var result) && result; + public bool RemoteControlRequiresAuthentication => bool.TryParse(_config["ApplicationOptions:RemoteControlRequiresAuthentication"], out var result) && result; + public int RemoteControlSessionLimit => int.TryParse(_config["ApplicationOptions:RemoteControlSessionLimit"], out var result) ? result : 3; + public bool Require2FA => bool.TryParse(_config["ApplicationOptions:Require2FA"], out var result) && result; + public string ServerUrl => _config["ApplicationOptions:ServerUrl"]; + public bool SmtpCheckCertificateRevocation => !bool.TryParse(_config["ApplicationOptions:SmtpCheckCertificateRevocation"], out var result) || result; + public string SmtpDisplayName => _config["ApplicationOptions:SmtpDisplayName"]; + public string SmtpEmail => _config["ApplicationOptions:SmtpEmail"]; + public string SmtpHost => _config["ApplicationOptions:SmtpHost"]; + public string SmtpLocalDomain => _config["ApplicationOptions:SmtpLocalDomain"]; + public string SmtpPassword => _config["ApplicationOptions:SmtpPassword"]; + public int SmtpPort => int.TryParse(_config["ApplicationOptions:SmtpPort"], out var result) ? result : 25; + public string SmtpUserName => _config["ApplicationOptions:SmtpUserName"]; + public Theme Theme => Enum.TryParse(_config["ApplicationOptions:Theme"], out var result) ? result : Theme.Dark; + public string[] TrustedCorsOrigins => _config.GetSection("ApplicationOptions:TrustedCorsOrigins").Get() ?? System.Array.Empty(); + public bool UseHsts => bool.TryParse(_config["ApplicationOptions:UseHsts"], out var result) && result; + public bool UseHttpLogging => bool.TryParse(_config["ApplicationOptions:UseHttpLogging"], out var result) && result; } } diff --git a/Server/Services/RcImplementations/SessionRecordingSink.cs b/Server/Services/RcImplementations/SessionRecordingSink.cs new file mode 100644 index 00000000..3ba1b106 --- /dev/null +++ b/Server/Services/RcImplementations/SessionRecordingSink.cs @@ -0,0 +1,60 @@ +using Immense.RemoteControl.Server.Abstractions; +using Immense.RemoteControl.Server.Models; +using Microsoft.Build.Framework; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Remotely.Server.Services.RcImplementations +{ + public class SessionRecordingSink : ISessionRecordingSink + { + private readonly ILogger _logger; + + public SessionRecordingSink(ILogger logger) + { + _logger = logger; + } + + private static string RecordingsDirectory + { + get + { + var logsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "recordings"); + if (Directory.Exists("/remotely-data")) + { + logsDir = "/remotely-data/recordings"; + } + return logsDir; + } + } + + public async Task SinkWebmStream(IAsyncEnumerable webmStream, RemoteControlSession session) + { + try + { + var targetDir = Path.Combine(RecordingsDirectory, $"{DateTime.Now:yyyy-MM-dd}"); + _ = Directory.CreateDirectory(targetDir); + + var viewerName = !string.IsNullOrWhiteSpace(session.RequesterName) ? + session.RequesterName : + "AnonymousUser"; + + var fileName = $"{viewerName}_{DateTime.Now:yyyyMMdd_HHmmssfff}.webm"; + + using var fs = new FileStream(Path.Combine(targetDir, fileName), FileMode.Create); + + await foreach (var chunk in webmStream) + { + await fs.WriteAsync(chunk); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while sinking webm stream."); + } + } + } +} diff --git a/Server/Services/RcImplementations/ViewerOptionsProvider.cs b/Server/Services/RcImplementations/ViewerOptionsProvider.cs new file mode 100644 index 00000000..a014f980 --- /dev/null +++ b/Server/Services/RcImplementations/ViewerOptionsProvider.cs @@ -0,0 +1,26 @@ +using Immense.RemoteControl.Server.Abstractions; +using Immense.RemoteControl.Server.Models; +using Immense.RemoteControl.Shared.Models; +using Microsoft.AspNetCore.SignalR; +using System.Threading.Tasks; + +namespace Remotely.Server.Services.RcImplementations +{ + public class ViewerOptionsProvider : IViewerOptionsProvider + { + private readonly IApplicationConfig _appConfig; + + public ViewerOptionsProvider(IApplicationConfig appConfig) + { + _appConfig = appConfig; + } + public Task GetViewerOptions() + { + var options = new RemoteControlViewerOptions() + { + ShouldRecordSession = _appConfig.EnableRemoteControlRecording + }; + return Task.FromResult(options); + } + } +} diff --git a/Server/appsettings.json b/Server/appsettings.json index 97b59038..da6d238e 100644 --- a/Server/appsettings.json +++ b/Server/appsettings.json @@ -24,6 +24,7 @@ "BannedDevices": [], "DataRetentionInDays": 90, "DBProvider": "SQLite", + "EnableRemoteControlRecording": false, "EnableWindowsEventLog": false, "EnforceAttendedAccess": false, "ForceClientHttps": false, diff --git a/Tests/Server.Tests/CircuitConnectionTests.cs b/Tests/Server.Tests/CircuitConnectionTests.cs index 0b64025b..0064abbd 100644 --- a/Tests/Server.Tests/CircuitConnectionTests.cs +++ b/Tests/Server.Tests/CircuitConnectionTests.cs @@ -35,7 +35,7 @@ namespace Remotely.Server.Tests private Mock _circuitManager; private Mock _toastService; private Mock _expiringTokenService; - private Mock _desktopSessionCache; + private Mock _remoteControlSessionCache; private Mock _agentSessionCache; private Mock> _logger; private CircuitConnection _circuitConnection; @@ -55,7 +55,7 @@ namespace Remotely.Server.Tests _circuitManager = new Mock(); _toastService = new Mock(); _expiringTokenService = new Mock(); - _desktopSessionCache = new Mock(); + _remoteControlSessionCache = new Mock(); _agentSessionCache = new Mock(); _logger = new Mock>(); @@ -68,7 +68,7 @@ namespace Remotely.Server.Tests _circuitManager.Object, _toastService.Object, _expiringTokenService.Object, - _desktopSessionCache.Object, + _remoteControlSessionCache.Object, _agentSessionCache.Object, _logger.Object); } diff --git a/submodules/Immense.RemoteControl b/submodules/Immense.RemoteControl index 2c722cec..85292122 160000 --- a/submodules/Immense.RemoteControl +++ b/submodules/Immense.RemoteControl @@ -1 +1 @@ -Subproject commit 2c722cecdd6502b2524526e5e15564a8db9f4bad +Subproject commit 85292122ef84efe5811e59cdcc208941c2a93037 From 066a1bdd4db7aa15443e1f8174be8f1d48628f52 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 5 Jul 2023 14:51:15 -0700 Subject: [PATCH 2/3] Update DataCleanupService to also remove expired recordings. --- Server/Services/DataCleanupService.cs | 113 +++++++++++++----- Server/Services/DataService.cs | 11 +- .../RcImplementations/SessionRecordingSink.cs | 2 +- 3 files changed, 89 insertions(+), 37 deletions(-) diff --git a/Server/Services/DataCleanupService.cs b/Server/Services/DataCleanupService.cs index 2cad7999..63449d3d 100644 --- a/Server/Services/DataCleanupService.cs +++ b/Server/Services/DataCleanupService.cs @@ -1,62 +1,113 @@ -using Microsoft.Build.Framework; +using Immense.RemoteControl.Shared.Services; +using Microsoft.Build.Framework; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Identity.Client; +using Remotely.Server.Services.RcImplementations; using System; +using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Timers; namespace Remotely.Server.Services { - public class DataCleanupService : IHostedService, IDisposable + public class DataCleanupService : BackgroundService, IDisposable { private readonly ILogger _logger; - - private readonly IServiceProvider _services; - - private System.Timers.Timer _cleanupTimer = new(TimeSpan.FromDays(1)); - + private readonly IServiceScopeFactory _scopeFactory; + private readonly ISystemTime _systemTime; + private readonly IApplicationConfig _appConfig; public DataCleanupService( - IServiceProvider serviceProvider, + IServiceScopeFactory scopeFactory, + ISystemTime systemTime, + IApplicationConfig appConfig, ILogger logger) { - _services = serviceProvider; + _scopeFactory = scopeFactory; + _systemTime = systemTime; + _appConfig = appConfig; _logger = logger; - - _cleanupTimer.Elapsed += CleanupTimer_Elapsed; } - public void Dispose() + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - _cleanupTimer?.Dispose(); - GC.SuppressFinalize(this); + await Task.Yield(); + + await PerformCleanup(); + + using var timer = new PeriodicTimer(TimeSpan.FromDays(1)); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + _ = await timer.WaitForNextTickAsync(stoppingToken); + await PerformCleanup(); + } + catch (OperationCanceledException) + { + _logger.LogInformation("Application is shutting down. Stopping data cleanup service."); + } + + } } - public Task StartAsync(CancellationToken cancellationToken) - { - _cleanupTimer.Start(); - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _cleanupTimer.Stop(); - _cleanupTimer.Dispose(); - return Task.CompletedTask; - } - - private void CleanupTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + private async Task PerformCleanup() { try { - using var scope = _services.CreateScope(); - var dataService = scope.ServiceProvider.GetRequiredService(); - dataService.CleanupOldRecords(); + await RemoveExpiredDbRecords(); + await RemoveExpiredRecordings(); } catch (Exception ex) { _logger.LogError(ex, "Error during data cleanup."); } } + + private async Task RemoveExpiredDbRecords() + { + using var scope = _scopeFactory.CreateScope(); + var dataService = scope.ServiceProvider.GetRequiredService(); + await dataService.CleanupOldRecords(); + } + + private Task RemoveExpiredRecordings() + { + if (!Directory.Exists(SessionRecordingSink.RecordingsDirectory)) + { + return Task.CompletedTask; + } + + var expirationDate = _systemTime.Now.UtcDateTime - TimeSpan.FromDays(_appConfig.DataRetentionInDays); + + var files = Directory + .GetFiles( + SessionRecordingSink.RecordingsDirectory, + "*.webm", + SearchOption.AllDirectories) + .Select(x => new FileInfo(x)) + .Where(x => x.CreationTimeUtc < expirationDate) + .ToList(); + + foreach (var file in files) + { + try + { + file.Delete(); + _logger.LogInformation("Expired recording deleted: {file}", file.FullName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while deleting expired recording: {file}", file); + } + } + + return Task.CompletedTask; + } } } diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 851e0aae..26103c4e 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -51,7 +51,7 @@ namespace Remotely.Server.Services void ChangeUserIsAdmin(string organizationID, string targetUserID, bool isAdmin); - void CleanupOldRecords(); + Task CleanupOldRecords(); Task CreateApiToken(string userName, string tokenName, string secretHash); @@ -623,7 +623,7 @@ namespace Remotely.Server.Services } } - public void CleanupOldRecords() + public async Task CleanupOldRecords() { using var dbContext = _appDbFactory.GetContext(); @@ -631,11 +631,12 @@ namespace Remotely.Server.Services { var expirationDate = DateTimeOffset.Now - TimeSpan.FromDays(_appConfig.DataRetentionInDays); - var scriptRuns = dbContext.ScriptRuns + var scriptRuns = await dbContext.ScriptRuns .Include(x => x.Results) .Include(x => x.Devices) .Include(x => x.DevicesCompleted) - .Where(x => x.RunAt < expirationDate); + .Where(x => x.RunAt < expirationDate) + .ToArrayAsync(); foreach (var run in scriptRuns) { @@ -656,7 +657,7 @@ namespace Remotely.Server.Services dbContext.RemoveRange(sharedFiles); - dbContext.SaveChanges(); + await dbContext.SaveChangesAsync(); } } diff --git a/Server/Services/RcImplementations/SessionRecordingSink.cs b/Server/Services/RcImplementations/SessionRecordingSink.cs index 3ba1b106..0b0488ad 100644 --- a/Server/Services/RcImplementations/SessionRecordingSink.cs +++ b/Server/Services/RcImplementations/SessionRecordingSink.cs @@ -18,7 +18,7 @@ namespace Remotely.Server.Services.RcImplementations _logger = logger; } - private static string RecordingsDirectory + public static string RecordingsDirectory { get { From 4a7bf7c5ae18a88f93112448a2eb3f142837488f Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Tue, 18 Jul 2023 08:50:43 -0700 Subject: [PATCH 3/3] Added missing await. --- Server/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/Program.cs b/Server/Program.cs index 7b32e902..ae20299e 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -294,7 +294,7 @@ using (var scope = app.Services.CreateScope()) } await dataService.SetAllDevicesNotOnline(); - dataService.CleanupOldRecords(); + await dataService.CleanupOldRecords(); } await app.RunAsync();