Add Linux session indicator. Add NotifyUser to app settings. Add ShutdownService.

This commit is contained in:
Jared Goodwin 2020-08-19 17:20:31 -07:00
parent b94435d55e
commit 75db74c85e
28 changed files with 271 additions and 57 deletions

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Remotely.Desktop.Core.Interfaces
{
public interface IShutdownService
{
Task Shutdown();
}
}

View File

@ -103,8 +103,11 @@ namespace Remotely.Desktop.Core.Models
public void Dispose()
{
DisconnectRequested = true;
RtcSession?.Dispose();
Capturer?.Dispose();
Disposer.TryDisposeAll(new IDisposable[]
{
RtcSession,
Capturer
});
}
public async Task InitializeWebRtc()

View File

@ -272,11 +272,16 @@ namespace Remotely.Desktop.Core.Services
await DisconnectAllViewers();
});
Connection.On("GetScreenCast", (string viewerID, string requesterName) =>
Connection.On("GetScreenCast", (string viewerID, string requesterName, bool notifyUser) =>
{
try
{
ScreenCaster.BeginScreenCasting(new ScreenCastRequest() { ViewerID = viewerID, RequesterName = requesterName });
ScreenCaster.BeginScreenCasting(new ScreenCastRequest()
{
NotifyUser = notifyUser,
ViewerID = viewerID,
RequesterName = requesterName
});
}
catch (Exception ex)
{
@ -292,9 +297,14 @@ namespace Remotely.Desktop.Core.Services
}
});
Connection.On("RequestScreenCast", (string viewerID, string requesterName) =>
Connection.On("RequestScreenCast", (string viewerID, string requesterName, bool notifyUser) =>
{
conductor.InvokeScreenCastRequested(new ScreenCastRequest() { ViewerID = viewerID, RequesterName = requesterName });
conductor.InvokeScreenCastRequested(new ScreenCastRequest()
{
NotifyUser = notifyUser,
ViewerID = viewerID,
RequesterName = requesterName
});
});
Connection.On("KeyDown", (string key, string viewerID) =>

View File

@ -22,16 +22,19 @@ namespace Remotely.Desktop.Core.Services
{
public ScreenCaster(Conductor conductor,
ICursorIconWatcher cursorIconWatcher,
ISessionIndicator sessionIndicator)
ISessionIndicator sessionIndicator,
IShutdownService shutdownService)
{
Conductor = conductor;
CursorIconWatcher = cursorIconWatcher;
SessionIndicator = sessionIndicator;
ShutdownService = shutdownService;
}
private Conductor Conductor { get; }
private ICursorIconWatcher CursorIconWatcher { get; }
private ISessionIndicator SessionIndicator { get; }
private IShutdownService ShutdownService { get; }
public async Task BeginScreenCasting(ScreenCastRequest screenCastRequest)
{
@ -56,7 +59,8 @@ namespace Remotely.Desktop.Core.Services
{
Conductor.InvokeViewerAdded(viewer);
}
else
if (mode == AppMode.Unattended && screenCastRequest.NotifyUser)
{
SessionIndicator.Show();
}
@ -174,8 +178,7 @@ namespace Remotely.Desktop.Core.Services
// Close if no one is viewing.
if (Conductor.Viewers.Count == 0 && mode == AppMode.Unattended)
{
Logger.Debug($"Exiting process ID {Process.GetCurrentProcess().Id}.");
Environment.Exit(0);
await ShutdownService.Shutdown();
}
}
}

View File

@ -1,6 +1,7 @@
using MessagePack;
using Microsoft.MixedReality.WebRTC;
using Remotely.Desktop.Core.Models;
using Remotely.Shared.Helpers;
using Remotely.Shared.Models;
using Remotely.Shared.Models.RtcDtos;
using Remotely.Shared.Utilities;
@ -57,18 +58,17 @@ namespace Remotely.Desktop.Core.Services
{
try
{
Transceiver?.LocalVideoTrack?.Dispose();
VideoSource?.Dispose();
try
{
// Unable to exit process until DataChannel is removed/disposed,
// and this throws internally (at least in 2.0 version).
PeerSession?.RemoveDataChannel(CaptureChannel);
}
catch { }
PeerSession?.Dispose();
// Unable to exit process until DataChannel is removed/disposed,
// and this throws internally (at least in 2.0 version).
PeerSession?.RemoveDataChannel(CaptureChannel);
}
catch { }
Disposer.TryDisposeAll(new IDisposable[]
{
PeerSession,
Transceiver?.LocalVideoTrack,
VideoSource
});
}
public async Task Init(IceServerModel[] iceServers)

View File

@ -9,7 +9,7 @@ namespace Remotely.Desktop.Linux.Controls
{
public HostNamePrompt()
{
this.InitializeComponent();
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif

View File

@ -34,7 +34,7 @@ namespace Remotely.Desktop.Linux.Controls
}
public MessageBox()
{
this.InitializeComponent();
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif

View File

@ -109,6 +109,7 @@ namespace Remotely.Desktop.Linux
serviceCollection.AddScoped<IWebRtcSessionFactory, WebRtcSessionFactory>();
serviceCollection.AddSingleton<ICursorIconWatcher, CursorIconWatcherLinux>();
serviceCollection.AddSingleton<ISessionIndicator, SessionIndicatorLinux>();
serviceCollection.AddSingleton<IShutdownService, ShutdownServiceLinux>();
ServiceContainer.Instance = serviceCollection.BuildServiceProvider();
}

View File

@ -1,4 +1,6 @@
using Remotely.Desktop.Core.Interfaces;
using Avalonia.Threading;
using Remotely.Desktop.Core.Interfaces;
using Remotely.Desktop.Linux.Views;
using System;
using System.Collections.Generic;
using System.Text;
@ -9,7 +11,11 @@ namespace Remotely.Desktop.Linux.Services
{
public void Show()
{
// TODO.
Dispatcher.UIThread.Post(() =>
{
var indicatorWindow = new SessionIndicatorWindow();
indicatorWindow.Show();
});
}
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.Extensions.DependencyInjection;
using Remotely.Desktop.Core;
using Remotely.Desktop.Core.Interfaces;
using Remotely.Desktop.Core.Services;
using Remotely.Shared.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
namespace Remotely.Desktop.Linux.Services
{
public class ShutdownServiceLinux : IShutdownService
{
public async Task Shutdown()
{
Logger.Debug($"Exiting process ID {Process.GetCurrentProcess().Id}.");
var casterSocket = ServiceContainer.Instance.GetRequiredService<CasterSocket>();
await casterSocket.DisconnectAllViewers();
Environment.Exit(0);
}
}
}

View File

@ -15,7 +15,7 @@ namespace Remotely.Desktop.Linux.Views
{
public ChatWindow()
{
this.InitializeComponent();
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
@ -40,7 +40,7 @@ namespace Remotely.Desktop.Linux.Views
{
AvaloniaXamlLoader.Load(this);
this.Closed += ChatWindow_Closed;
Closed += ChatWindow_Closed;
this.FindControl<Border>("TitleBanner").PointerPressed += TitleBanner_PointerPressed;
@ -63,7 +63,7 @@ namespace Remotely.Desktop.Linux.Views
{
if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == Avalonia.Input.PointerUpdateKind.LeftButtonPressed)
{
this.BeginMoveDrag(e);
BeginMoveDrag(e);
}
}
}

View File

@ -7,11 +7,10 @@ namespace Remotely.Desktop.Linux.Views
{
public class MainWindow : Window
{
public static MainWindow Current { get; set; }
public MainWindow()
{
Current = this;
InitializeComponent();
#if DEBUG
@ -19,26 +18,27 @@ namespace Remotely.Desktop.Linux.Views
#endif
}
private void TitleBanner_PointerPressed(object sender, Avalonia.Input.PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == Avalonia.Input.PointerUpdateKind.LeftButtonPressed)
{
this.BeginMoveDrag(e);
}
}
public static MainWindow Current { get; set; }
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
this.FindControl<Border>("TitleBanner").PointerPressed += TitleBanner_PointerPressed;
this.Opened += MainWindow_Opened;
Opened += MainWindow_Opened;
}
private async void MainWindow_Opened(object sender, System.EventArgs e)
{
await (this.DataContext as MainWindowViewModel).Init();
await (DataContext as MainWindowViewModel).Init();
}
private void TitleBanner_PointerPressed(object sender, Avalonia.Input.PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == Avalonia.Input.PointerUpdateKind.LeftButtonPressed)
{
BeginMoveDrag(e);
}
}
}
}

View File

@ -0,0 +1,23 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
Width="300"
Height="100"
x:Class="Remotely.Desktop.Linux.Views.SessionIndicatorWindow"
Background="#2b2726"
Title="Remotely Desktop"
Icon="/Assets/favicon.ico"
WindowStartupLocation="Manual"
SizeToContent="WidthAndHeight">
<StackPanel Margin="15 10 40 15">
<TextBlock Classes="SectionHeader" Foreground="White">
Remote Control Started
</TextBlock>
<TextBlock Foreground="LightGray">
A remote control session has started.
</TextBlock>
</StackPanel>
</Window>

View File

@ -0,0 +1,58 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Remotely.Desktop.Core;
using Remotely.Desktop.Core.Interfaces;
using Remotely.Desktop.Linux.Controls;
using System;
namespace Remotely.Desktop.Linux.Views
{
public class SessionIndicatorWindow : Window
{
public SessionIndicatorWindow()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
Closing += SessionIndicatorWindow_Closing;
PointerPressed += SessionIndicatorWindow_PointerPressed;
Opened += SessionIndicatorWindow_Opened;
}
private void SessionIndicatorWindow_Opened(object sender, EventArgs e)
{
var left = Screens.Primary.WorkingArea.Width - Width;
var top = Screens.Primary.WorkingArea.Width - Width;
Position = new PixelPoint((int)left, (int)top);
}
private void SessionIndicatorWindow_PointerPressed(object sender, Avalonia.Input.PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == Avalonia.Input.PointerUpdateKind.LeftButtonPressed)
{
BeginMoveDrag(e);
}
}
private async void SessionIndicatorWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
e.Cancel = true;
var result = await MessageBox.Show("Stop the remote control session?", "Stop Session", MessageBoxType.YesNo);
if (result == MessageBoxResult.Yes)
{
var shutdownService = ServiceContainer.Instance.GetRequiredService<IShutdownService>();
await shutdownService.Shutdown();
}
}
}
}

View File

@ -1,4 +1,5 @@
using SharpDX.Direct3D11;
using Remotely.Shared.Helpers;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using System;
@ -24,10 +25,13 @@ namespace Remotely.Desktop.Win.Models
public void Dispose()
{
Adapter?.Dispose();
Device?.Dispose();
OutputDuplication?.Dispose();
Texture2D?.Dispose();
Disposer.TryDisposeAll(new IDisposable[]
{
Adapter,
Device,
OutputDuplication,
Texture2D
});
}
}
}

View File

@ -98,6 +98,7 @@ namespace Remotely.Desktop.Win
serviceCollection.AddScoped<IWebRtcSessionFactory, WebRtcSessionFactory>();
serviceCollection.AddScoped<IFileTransferService, FileTransferService>();
serviceCollection.AddSingleton<ISessionIndicator, SessionIndicatorWin>();
serviceCollection.AddSingleton<IShutdownService, ShutdownServiceWin>();
BackgroundForm = new Form()
{

View File

@ -149,7 +149,11 @@ namespace Remotely.Desktop.Win.Services
{
foreach (var screen in directxScreens.Values)
{
screen.Dispose();
try
{
screen.Dispose();
}
catch { }
}
directxScreens.Clear();
}

View File

@ -40,7 +40,7 @@ namespace Remotely.Desktop.Win.Services
return;
}
BackgroundForm.Invoke(new Action(()=>
BackgroundForm.Invoke(new Action(() =>
{
container = new Container();
contextMenuStrip = new ContextMenuStrip(container);

View File

@ -0,0 +1,25 @@
using Microsoft.Extensions.DependencyInjection;
using Remotely.Desktop.Core;
using Remotely.Desktop.Core.Interfaces;
using Remotely.Desktop.Core.Services;
using Remotely.Shared.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
namespace Remotely.Desktop.Win.Services
{
public class ShutdownServiceWin : IShutdownService
{
public async Task Shutdown()
{
Logger.Debug($"Exiting process ID {Process.GetCurrentProcess().Id}.");
var casterSocket = ServiceContainer.Instance.GetRequiredService<CasterSocket>();
await casterSocket.DisconnectAllViewers();
System.Windows.Forms.Application.Exit();
App.Current.Shutdown();
}
}
}

View File

@ -99,21 +99,24 @@ The following settings are available in appsettings.json.
Note: To retain your settings between upgrades, copy your settings to appsettings.Production.json, which will supersede the original.
* DefaultPrompt: The default prompt string you'll see for each line on the console.
* AllowApiLogin: Whether to allow logging in via the API controller. API access tokens are recommended over this approach.
* DataRetentionInDays: How long event logs and remote command logs will be kept.
* DBProvider: Determines which of the three connection strings (at the top) will be used. The appropriate DB provider for the database type is automatically loaded in code.
* DefaultPrompt: The default prompt string you'll see for each line on the console.
* EnableWindowsEventLog: Whether to also add server log entries to the Windows Event Log.
* IceServers: The ICE (STUN/TURN) servers to use for WebRTC.
* KnownProxies: If your Nginx server is on a different machine and is forwarding requests to the Remotely server, you will need to add the IP of the Nginx server to this array.
* MaxOrganizationCount: By default, one organization can exist on the server, which is created automatically when the first account is registered. Afterward, self-registration will be disabled.
* Set this to -1 or increase it to a specific number to allow multi-tenancy.
* RedirectToHttps: Whether ASP.NET Core will redirect all traffic from HTTP to HTTPS. This is independent of Nginx and IIS configurations that do the same.
* UseHsts: Whether ASP.NET Core will use HTTP Strict Transport Security.
* DataRetentionInDays: How long event logs and remote command logs will be kept.
* RemoteControlNotifyUsers: Whether to show a notification to the end user when an unattended remote control session starts.
* RemoteControlSessionLimit: How many concurrent remote control sessions are allowed per organization.
* RemoteControlRequiresAuthentication: Whether the remote control page requires authentication to establish a connection.
* Require2FA: Require users to set up 2FA before they can use the main app.
* AllowApiLogin: Whether to allow logging in via the API controller. API access tokens are recommended over this approach.
* TrustedCorsOrigins: For cross-origin API requests via JavaScript. The websites listed in this array with be allowed to make requests to the API. This does not grant authentication, which is still required on most endpoints.
* KnownProxies: If your Nginx server is on a different machine and is forwarding requests to the Remotely server, you will need to add the IP of the Nginx server to this array.
* Smpt*: SMTP settings for auto-generated system emails (such as registration and password reset).
* Theme: The color theme to use for the site. Values are "Light" or "Dark". This can also be configured per-user in Account - Options.
* TrustedCorsOrigins: For cross-origin API requests via JavaScript. The websites listed in this array with be allowed to make requests to the API. This does not grant authentication, which is still required on most endpoints.
* UseHsts: Whether ASP.NET Core will use HTTP Strict Transport Security.
* UseWebRtc: Attempt to create a peer-to-peer connection via WebRTC for screen sharing.
* Only works on Windows agents.

View File

@ -92,6 +92,13 @@
<br />
<span asp-validation-for="AppSettingsInput.RedirectToHttps" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AppSettingsInput.RemoteControlNotifyUser"></label>
<br />
<input type="checkbox" asp-for="AppSettingsInput.RemoteControlNotifyUser" />
<br />
<span asp-validation-for="AppSettingsInput.RemoteControlNotifyUser" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AppSettingsInput.RemoteControlRequiresAuthentication"></label>
<br />

View File

@ -169,6 +169,9 @@ namespace Remotely.Server.Areas.Identity.Pages.Account.Manage
[Display(Name = "Redirect To HTTPS")]
public bool RedirectToHttps { get; set; }
[Display(Name = "Remote Control Notify User")]
public bool RemoteControlNotifyUser { get; set; }
[Display(Name = "Remote Control Requires Authentication")]
public bool RemoteControlRequiresAuthentication { get; set; }

View File

@ -268,7 +268,7 @@ namespace Remotely.Server.Hubs
(Context.User.Identity.IsAuthenticated &&
DataService.DoesUserHaveAccessToDevice(deviceID, Context.UserIdentifier)))
{
return CasterHubContext.Clients.Client(screenCasterID).SendAsync("GetScreenCast", Context.ConnectionId, requesterName);
return CasterHubContext.Clients.Client(screenCasterID).SendAsync("GetScreenCast", Context.ConnectionId, requesterName, AppConfig.RemoteControlNotifyUser);
}
else
{
@ -279,7 +279,7 @@ namespace Remotely.Server.Hubs
{
SessionInfo.Mode = RemoteControlMode.Normal;
_ = Clients.Caller.SendAsync("RequestingScreenCast");
return CasterHubContext.Clients.Client(screenCasterID).SendAsync("RequestScreenCast", Context.ConnectionId, requesterName);
return CasterHubContext.Clients.Client(screenCasterID).SendAsync("RequestScreenCast", Context.ConnectionId, requesterName, AppConfig.RemoteControlNotifyUser);
}
}
public Task SendSetKeyStatesUp()

View File

@ -26,6 +26,7 @@ namespace Remotely.Server.Services
public int MaxConcurrentUpdates => int.Parse(Config["ApplicationOptions:MaxConcurrentUpdates"] ?? "10");
public int MaxOrganizationCount => int.Parse(Config["ApplicationOptions:MaxOrganizationCount"] ?? "1");
public bool RedirectToHttps => bool.Parse(Config["ApplicationOptions:RedirectToHttps"] ?? "false");
public bool RemoteControlNotifyUser => bool.Parse(Config["ApplicationOptions:RemoteControlNotifyUser"] ?? "true");
public bool RemoteControlRequiresAuthentication => bool.Parse(Config["ApplicationOptions:RemoteControlRequiresAuthentication"] ?? "true");
public double RemoteControlSessionLimit => double.Parse(Config["ApplicationOptions:RemoteControlSessionLimit"] ?? "3");
public bool Require2FA => bool.Parse(Config["ApplicationOptions:Require2FA"] ?? "false");

View File

@ -81,8 +81,6 @@ namespace Remotely.Server
.AddDefaultUI()
.AddDefaultTokenProviders();
var remoteControlAuthentication = Configuration.GetSection("ApplicationOptions:RemoteControlRequiresAuthentication").Get<bool>();
services.ConfigureApplicationCookie(cookieOptions =>
{
cookieOptions.Cookie.SameSite = SameSiteMode.None;

View File

@ -31,6 +31,7 @@
"MaxConcurrentUpdates": 10,
"MaxOrganizationCount": 1,
"RedirectToHttps": false,
"RemoteControlNotifyUser": true,
"RemoteControlSessionLimit": 3,
"RemoteControlRequiresAuthentication": true,
"Require2FA": false,

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Remotely.Shared.Helpers
{
public static class Disposer
{
public static void TryDisposeAll(IDisposable[] disposables)
{
if (disposables is null)
{
return;
}
foreach (var disposable in disposables)
{
try
{
disposable?.Dispose();
}
catch { }
}
}
}
}

View File

@ -2,7 +2,8 @@
{
public class ScreenCastRequest
{
public string ViewerID { get; set; }
public bool NotifyUser { get; set; }
public string RequesterName { get; set; }
public string ViewerID { get; set; }
}
}