Merge pull request #678 from immense/feature/server-side-recording

Server-side remote control session recording.
This commit is contained in:
Jared Goodwin 2023-07-18 09:16:24 -07:00 committed by GitHub
commit 567c6afd7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 241 additions and 88 deletions

View File

@ -23,7 +23,7 @@ namespace Remotely.Server.API
private readonly IApplicationConfig _appConfig;
private readonly IDataService _dataService;
private readonly IHubContext<DesktopHub> _desktopHub;
private readonly IDesktopHubSessionCache _desktopSessionCache;
private readonly IRemoteControlSessionCache _remoteControlSessionCache;
private readonly SignInManager<RemotelyUser> _signInManager;
private readonly IHubContext<ViewerHub> _viewerHub;
private readonly ILogger<LoginController> _logger;
@ -33,7 +33,7 @@ namespace Remotely.Server.API
IDataService dataService,
IApplicationConfig appConfig,
IHubContext<DesktopHub> casterHubContext,
IDesktopHubSessionCache desktopSessionCache,
IRemoteControlSessionCache remoteControlSessionCache,
IHubContext<ViewerHub> viewerHubContext,
ILogger<LoginController> 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);

View File

@ -27,7 +27,7 @@ namespace Remotely.Server.API
public class RemoteControlController : ControllerBase
{
private readonly IHubContext<AgentHub> _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<RemotelyUser> signInManager,
IDataService dataService,
IDesktopHubSessionCache desktopSessionCache,
IRemoteControlSessionCache remoteControlSessionCache,
IHubContext<AgentHub> 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<RemoteControlSessionEx>()
.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)
{

View File

@ -9,14 +9,14 @@
@inject SignInManager<RemotelyUser> SignInManager
@inject IHubContext<DesktopHub> DesktopHubContext
@inject IHubContext<ViewerHub> ViewerHubContext
@inject IDesktopHubSessionCache DesktopSessionCache
@inject IRemoteControlSessionCache RemoteControlSessionCache
@functions {
public async Task<IActionResult> 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.");

View File

@ -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<CircuitEvent> _eventQueue = new();
private readonly IExpiringTokenService _expiringTokenService;
private readonly ILogger<CircuitConnection> _logger;
@ -94,7 +94,7 @@ namespace Remotely.Server.Hubs
ICircuitManager circuitManager,
IToastService toastService,
IExpiringTokenService expiringTokenService,
IDesktopHubSessionCache desktopSessionCache,
IRemoteControlSessionCache remoteControlSessionCache,
IAgentHubSessionCache agentSessionCache,
ILogger<CircuitConnection> 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<RemoteControlSessionEx>()
.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",

View File

@ -134,6 +134,13 @@
<br />
<ValidationMessage For="() => Input.DBProvider" />
</div>
<div class="form-group">
<label>Enable Remote Control Recording</label>
<br />
<InputCheckbox @bind-Value="Input.EnableRemoteControlRecording" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.EnableRemoteControlRecording" />
</div>
<div class="form-group">
<label>Enable Windows Event Log</label>
<br />

View File

@ -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; }

View File

@ -226,6 +226,8 @@ services.AddRemoteControlServer(config =>
config.AddHubEventHandler<HubEventHandler>();
config.AddViewerAuthorizer<ViewerAuthorizer>();
config.AddViewerPageDataProvider<ViewerPageDataProvider>();
config.AddViewerOptionsProvider<ViewerOptionsProvider>();
config.AddSessionRecordingSink<SessionRecordingSink>();
});
services.AddSingleton<IAgentHubSessionCache, AgentHubSessionCache>();
@ -292,7 +294,7 @@ using (var scope = app.Services.CreateScope())
}
await dataService.SetAllDevicesNotOnline();
dataService.CleanupOldRecords();
await dataService.CleanupOldRecords();
}
await app.RunAsync();

View File

@ -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<string[]>() ?? System.Array.Empty<string>();
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<string[]>() ?? System.Array.Empty<string>();
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<Theme>(Config["ApplicationOptions:Theme"] ?? "Dark", true);
public string[] TrustedCorsOrigins => Config.GetSection("ApplicationOptions:TrustedCorsOrigins").Get<string[]>() ?? System.Array.Empty<string>();
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<string[]>() ?? System.Array.Empty<string>();
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<string[]>() ?? System.Array.Empty<string>();
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<Theme>(_config["ApplicationOptions:Theme"], out var result) ? result : Theme.Dark;
public string[] TrustedCorsOrigins => _config.GetSection("ApplicationOptions:TrustedCorsOrigins").Get<string[]>() ?? System.Array.Empty<string>();
public bool UseHsts => bool.TryParse(_config["ApplicationOptions:UseHsts"], out var result) && result;
public bool UseHttpLogging => bool.TryParse(_config["ApplicationOptions:UseHttpLogging"], out var result) && result;
}
}

View File

@ -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<DataCleanupService> _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<DataCleanupService> 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<IDataService>();
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<IDataService>();
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;
}
}
}

View File

@ -51,7 +51,7 @@ namespace Remotely.Server.Services
void ChangeUserIsAdmin(string organizationID, string targetUserID, bool isAdmin);
void CleanupOldRecords();
Task CleanupOldRecords();
Task<ApiToken> 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();
}
}

View File

@ -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<SessionRecordingSink> _logger;
public SessionRecordingSink(ILogger<SessionRecordingSink> logger)
{
_logger = logger;
}
public 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<byte[]> 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.");
}
}
}
}

View File

@ -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<RemoteControlViewerOptions> GetViewerOptions()
{
var options = new RemoteControlViewerOptions()
{
ShouldRecordSession = _appConfig.EnableRemoteControlRecording
};
return Task.FromResult(options);
}
}
}

View File

@ -24,6 +24,7 @@
"BannedDevices": [],
"DataRetentionInDays": 90,
"DBProvider": "SQLite",
"EnableRemoteControlRecording": false,
"EnableWindowsEventLog": false,
"EnforceAttendedAccess": false,
"ForceClientHttps": false,

View File

@ -35,7 +35,7 @@ namespace Remotely.Server.Tests
private Mock<ICircuitManager> _circuitManager;
private Mock<IToastService> _toastService;
private Mock<IExpiringTokenService> _expiringTokenService;
private Mock<IDesktopHubSessionCache> _desktopSessionCache;
private Mock<IRemoteControlSessionCache> _remoteControlSessionCache;
private Mock<IAgentHubSessionCache> _agentSessionCache;
private Mock<ILogger<CircuitConnection>> _logger;
private CircuitConnection _circuitConnection;
@ -55,7 +55,7 @@ namespace Remotely.Server.Tests
_circuitManager = new Mock<ICircuitManager>();
_toastService = new Mock<IToastService>();
_expiringTokenService = new Mock<IExpiringTokenService>();
_desktopSessionCache = new Mock<IDesktopHubSessionCache>();
_remoteControlSessionCache = new Mock<IRemoteControlSessionCache>();
_agentSessionCache = new Mock<IAgentHubSessionCache>();
_logger = new Mock<ILogger<CircuitConnection>>();
@ -68,7 +68,7 @@ namespace Remotely.Server.Tests
_circuitManager.Object,
_toastService.Object,
_expiringTokenService.Object,
_desktopSessionCache.Object,
_remoteControlSessionCache.Object,
_agentSessionCache.Object,
_logger.Object);
}

@ -1 +1 @@
Subproject commit 2c722cecdd6502b2524526e5e15564a8db9f4bad
Subproject commit 85292122ef84efe5811e59cdcc208941c2a93037