mirror of
https://github.com/immense/Remotely.git
synced 2025-10-26 11:27:15 +00:00
Bring in submodule files.
This commit is contained in:
parent
5c2085fadf
commit
1afcc8c8b5
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "submodules/Immense.RemoteControl"]
|
||||
path = submodules/Immense.RemoteControl
|
||||
url = git@github.com:immense/RemoteControl.git
|
||||
@ -39,8 +39,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Desktop.Native\Desktop.Native.csproj" />
|
||||
<ProjectReference Include="..\Shared\Shared.csproj" />
|
||||
<ProjectReference Include="..\submodules\Immense.RemoteControl\Immense.RemoteControl.Desktop.Shared\Immense.RemoteControl.Desktop.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remotely.Agent.Interfaces;
|
||||
using Remotely.Agent.Models;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
using Immense.RemoteControl.Shared;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remotely.Agent.Interfaces;
|
||||
using Remotely.Shared.Extensions;
|
||||
using Remotely.Shared.Models;
|
||||
using Remotely.Shared.Primitives;
|
||||
using Remotely.Shared.Services;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
|
||||
@ -56,8 +56,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Desktop.Shared\Desktop.Shared.csproj" />
|
||||
<ProjectReference Include="..\submodules\Immense.RemoteControl\Immense.RemoteControl.Desktop.Linux\Immense.RemoteControl.Desktop.Linux.csproj" />
|
||||
<ProjectReference Include="..\submodules\Immense.RemoteControl\Immense.RemoteControl.Desktop.Shared\Immense.RemoteControl.Desktop.Shared.csproj" />
|
||||
<ProjectReference Include="..\submodules\Immense.RemoteControl\Immense.RemoteControl.Desktop.UI\Immense.RemoteControl.Desktop.UI.csproj" />
|
||||
<ProjectReference Include="..\Desktop.UI\Desktop.UI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
150
Desktop.Linux/Services/AppStartup.cs
Normal file
150
Desktop.Linux/Services/AppStartup.cs
Normal file
@ -0,0 +1,150 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Enums;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Immense.RemoteControl.Desktop.UI.Services;
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Linux.Services;
|
||||
|
||||
internal class AppStartup : IAppStartup
|
||||
{
|
||||
private readonly IAppState _appState;
|
||||
private readonly IKeyboardMouseInput _inputService;
|
||||
private readonly IDesktopHubConnection _desktopHub;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IChatHostService _chatHostService;
|
||||
private readonly ICursorIconWatcher _cursorIconWatcher;
|
||||
private readonly IUiDispatcher _dispatcher;
|
||||
private readonly IIdleTimer _idleTimer;
|
||||
private readonly IShutdownService _shutdownService;
|
||||
private readonly IBrandingProvider _brandingProvider;
|
||||
private readonly ILogger<AppStartup> _logger;
|
||||
|
||||
public AppStartup(
|
||||
IAppState appState,
|
||||
IKeyboardMouseInput inputService,
|
||||
IDesktopHubConnection desktopHub,
|
||||
IClipboardService clipboardService,
|
||||
IChatHostService chatHostService,
|
||||
ICursorIconWatcher iconWatcher,
|
||||
IUiDispatcher dispatcher,
|
||||
IIdleTimer idleTimer,
|
||||
IShutdownService shutdownService,
|
||||
IBrandingProvider brandingProvider,
|
||||
ILogger<AppStartup> logger)
|
||||
{
|
||||
_appState = appState;
|
||||
_inputService = inputService;
|
||||
_desktopHub = desktopHub;
|
||||
_clipboardService = clipboardService;
|
||||
_chatHostService = chatHostService;
|
||||
_cursorIconWatcher = iconWatcher;
|
||||
_dispatcher = dispatcher;
|
||||
_idleTimer = idleTimer;
|
||||
_shutdownService = shutdownService;
|
||||
_brandingProvider = brandingProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Run()
|
||||
{
|
||||
await _brandingProvider.Initialize();
|
||||
|
||||
if (_appState.Mode is AppMode.Unattended or AppMode.Attended)
|
||||
{
|
||||
_clipboardService.BeginWatching();
|
||||
_inputService.Init();
|
||||
_cursorIconWatcher.OnChange += CursorIconWatcher_OnChange;
|
||||
}
|
||||
|
||||
switch (_appState.Mode)
|
||||
{
|
||||
case AppMode.Unattended:
|
||||
{
|
||||
var result = await _dispatcher.StartHeadless();
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await StartScreenCasting().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
case AppMode.Attended:
|
||||
{
|
||||
_dispatcher.StartClassicDesktop();
|
||||
break;
|
||||
}
|
||||
case AppMode.Chat:
|
||||
{
|
||||
var result = await _dispatcher.StartHeadless();
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _chatHostService
|
||||
.StartChat(_appState.PipeName, _appState.OrganizationName)
|
||||
.ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task StartScreenCasting()
|
||||
{
|
||||
if (!await _desktopHub.Connect(TimeSpan.FromSeconds(30), _dispatcher.ApplicationExitingToken))
|
||||
{
|
||||
await _shutdownService.Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _desktopHub.SendUnattendedSessionInfo(
|
||||
_appState.SessionId,
|
||||
_appState.AccessKey,
|
||||
Environment.MachineName,
|
||||
_appState.RequesterName,
|
||||
_appState.OrganizationName);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
_logger.LogError(result.Exception, "An error occurred while trying to establish a session with the server.");
|
||||
await _shutdownService.Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_appState.ArgDict.ContainsKey("relaunch"))
|
||||
{
|
||||
_logger.LogInformation("Resuming after relaunch.");
|
||||
var viewerIDs = _appState.RelaunchViewers;
|
||||
await _desktopHub.NotifyViewersRelaunchedScreenCasterReady(viewerIDs);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _desktopHub.NotifyRequesterUnattendedReady();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_idleTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async void CursorIconWatcher_OnChange(object? sender, CursorInfo cursor)
|
||||
{
|
||||
if (_appState.Viewers.Any() == true &&
|
||||
_desktopHub.IsConnected)
|
||||
{
|
||||
foreach (var viewer in _appState.Viewers.Values)
|
||||
{
|
||||
await viewer.SendCursorChange(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Desktop.Linux/Services/AudioCapturerLinux.cs
Normal file
15
Desktop.Linux/Services/AudioCapturerLinux.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Linux.Services;
|
||||
|
||||
public class AudioCapturerLinux : IAudioCapturer
|
||||
{
|
||||
#pragma warning disable CS0067
|
||||
public event EventHandler<byte[]>? AudioSampleReady;
|
||||
#pragma warning restore
|
||||
|
||||
public void ToggleAudio(bool toggleOn)
|
||||
{
|
||||
// Not implemented.
|
||||
}
|
||||
}
|
||||
15
Desktop.Linux/Services/CursorIconWatcherLinux.cs
Normal file
15
Desktop.Linux/Services/CursorIconWatcherLinux.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using System.Drawing;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Linux.Services;
|
||||
|
||||
public class CursorIconWatcherLinux : ICursorIconWatcher
|
||||
{
|
||||
#pragma warning disable CS0067
|
||||
public event EventHandler<CursorInfo>? OnChange;
|
||||
#pragma warning restore
|
||||
|
||||
|
||||
public CursorInfo GetCurrentCursor() => new(Array.Empty<byte>(), Point.Empty, "default");
|
||||
}
|
||||
160
Desktop.Linux/Services/FileTransferServiceLinux.cs
Normal file
160
Desktop.Linux/Services/FileTransferServiceLinux.cs
Normal file
@ -0,0 +1,160 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Immense.RemoteControl.Desktop.Shared.ViewModels;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using Immense.RemoteControl.Desktop.UI.Controls.Dialogs;
|
||||
using Immense.RemoteControl.Desktop.UI.Views;
|
||||
using Immense.RemoteControl.Desktop.UI.Services;
|
||||
using System.Threading;
|
||||
using System.IO;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Linux.Services;
|
||||
|
||||
public class FileTransferServiceLinux : IFileTransferService
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, FileTransferWindow> _fileTransferWindows = new();
|
||||
private static readonly ConcurrentDictionary<string, FileStream> _partialTransfers = new();
|
||||
private static readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
private static volatile bool _messageBoxPending;
|
||||
private readonly IViewModelFactory _viewModelFactory;
|
||||
private readonly IUiDispatcher _dispatcher;
|
||||
private readonly IDialogProvider _dialogProvider;
|
||||
private readonly ILogger<FileTransferServiceLinux> _logger;
|
||||
|
||||
public FileTransferServiceLinux(
|
||||
IViewModelFactory viewModelFactory,
|
||||
IUiDispatcher dispatcher,
|
||||
IDialogProvider dialogProvider,
|
||||
ILogger<FileTransferServiceLinux> logger)
|
||||
{
|
||||
_viewModelFactory = viewModelFactory;
|
||||
_dispatcher = dispatcher;
|
||||
_dialogProvider = dialogProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string GetBaseDirectory()
|
||||
{
|
||||
var desktopDir = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
|
||||
if (Directory.Exists(desktopDir))
|
||||
{
|
||||
return desktopDir;
|
||||
}
|
||||
|
||||
return Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "RemoteControl")).FullName;
|
||||
}
|
||||
|
||||
public void OpenFileTransferWindow(IViewer viewer)
|
||||
{
|
||||
_dispatcher.Post(() =>
|
||||
{
|
||||
if (_fileTransferWindows.TryGetValue(viewer.ViewerConnectionId, out var window))
|
||||
{
|
||||
window.Activate();
|
||||
}
|
||||
else
|
||||
{
|
||||
window = new FileTransferWindow
|
||||
{
|
||||
DataContext = _viewModelFactory.CreateFileTransferWindowViewModel(viewer)
|
||||
};
|
||||
window.Closed += (sender, arg) =>
|
||||
{
|
||||
_fileTransferWindows.Remove(viewer.ViewerConnectionId, out _);
|
||||
};
|
||||
_fileTransferWindows.AddOrUpdate(viewer.ViewerConnectionId, window, (k, v) => window);
|
||||
window.Show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ReceiveFile(byte[] buffer, string fileName, string messageId, bool endOfFile, bool startOfFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _writeLock.WaitAsync();
|
||||
|
||||
var baseDir = GetBaseDirectory();
|
||||
|
||||
if (startOfFile)
|
||||
{
|
||||
var filePath = Path.Combine(baseDir, fileName);
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var count = 0;
|
||||
var ext = Path.GetExtension(fileName);
|
||||
var fileWithoutExt = Path.GetFileNameWithoutExtension(fileName);
|
||||
while (File.Exists(filePath))
|
||||
{
|
||||
filePath = Path.Combine(baseDir, $"{fileWithoutExt}-{count}{ext}");
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
File.Create(filePath).Close();
|
||||
|
||||
var fs = new FileStream(filePath, FileMode.OpenOrCreate);
|
||||
_partialTransfers.AddOrUpdate(messageId, fs, (k, v) => fs);
|
||||
}
|
||||
|
||||
var fileStream = _partialTransfers[messageId];
|
||||
|
||||
if (buffer?.Length > 0)
|
||||
{
|
||||
await fileStream.WriteAsync(buffer);
|
||||
|
||||
}
|
||||
|
||||
if (endOfFile)
|
||||
{
|
||||
fileStream.Close();
|
||||
_partialTransfers.Remove(messageId, out _);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while receiving file.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeLock.Release();
|
||||
if (endOfFile)
|
||||
{
|
||||
await Task.Run(ShowTransferComplete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadFile(
|
||||
FileUpload fileUpload,
|
||||
IViewer viewer,
|
||||
Action<double> progressUpdateCallback,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await viewer.SendFile(fileUpload, progressUpdateCallback, cancelToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while uploading file.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowTransferComplete()
|
||||
{
|
||||
// Prevent multiple dialogs from popping up.
|
||||
if (!_messageBoxPending)
|
||||
{
|
||||
_messageBoxPending = true;
|
||||
|
||||
await _dialogProvider.Show($"File tranfer complete. Files saved to directory:\n\n{GetBaseDirectory()}",
|
||||
"Tranfer Complete",
|
||||
MessageBoxType.OK);
|
||||
|
||||
_messageBoxPending = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
259
Desktop.Linux/Services/KeyboardMouseInputLinux.cs
Normal file
259
Desktop.Linux/Services/KeyboardMouseInputLinux.cs
Normal file
@ -0,0 +1,259 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Enums;
|
||||
using Immense.RemoteControl.Desktop.Shared.Native.Linux;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Linux.Services;
|
||||
|
||||
public class KeyboardMouseInputLinux : IKeyboardMouseInput
|
||||
{
|
||||
private readonly ILogger<KeyboardMouseInputLinux> _logger;
|
||||
|
||||
private nint Display { get; set; }
|
||||
|
||||
public KeyboardMouseInputLinux(ILogger<KeyboardMouseInputLinux> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
public void Init()
|
||||
{
|
||||
// Nothing to do here. The Windows implementation needs to start
|
||||
// a processing queue to keep all input simulation on the same
|
||||
// thread. Linux doesn't.
|
||||
}
|
||||
|
||||
public void SendKeyDown(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitDisplay();
|
||||
key = ConvertJavaScriptKeyToX11Key(key);
|
||||
var keySim = LibX11.XStringToKeysym(key);
|
||||
if (keySim == nint.Zero)
|
||||
{
|
||||
_logger.LogError("Key not mapped: {key}", key);
|
||||
return;
|
||||
}
|
||||
|
||||
var keyCode = LibX11.XKeysymToKeycode(Display, keySim);
|
||||
LibXtst.XTestFakeKeyEvent(Display, keyCode, true, 0);
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while sending key down.");
|
||||
}
|
||||
}
|
||||
|
||||
public void SendKeyUp(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitDisplay();
|
||||
key = ConvertJavaScriptKeyToX11Key(key);
|
||||
var keySim = LibX11.XStringToKeysym(key);
|
||||
if (keySim == nint.Zero)
|
||||
{
|
||||
_logger.LogError("Key not mapped: {key}", key);
|
||||
return;
|
||||
}
|
||||
|
||||
var keyCode = LibX11.XKeysymToKeycode(Display, keySim);
|
||||
LibXtst.XTestFakeKeyEvent(Display, keyCode, false, 0);
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while sending key up.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void SendMouseButtonAction(int button, ButtonAction buttonAction, double percentX, double percentY, IViewer viewer)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isPressed = buttonAction == ButtonAction.Down;
|
||||
// Browser buttons start at 0. XTest starts at 1.
|
||||
var mouseButton = (uint)(button + 1);
|
||||
|
||||
InitDisplay();
|
||||
SendMouseMove(percentX, percentY, viewer);
|
||||
LibXtst.XTestFakeButtonEvent(Display, mouseButton, isPressed, 0);
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while sending mouse button action.");
|
||||
}
|
||||
}
|
||||
|
||||
public void SendMouseMove(double percentX, double percentY, IViewer viewer)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitDisplay();
|
||||
|
||||
var screenBounds = viewer.Capturer.CurrentScreenBounds;
|
||||
LibXtst.XTestFakeMotionEvent(Display,
|
||||
LibX11.XDefaultScreen(Display),
|
||||
screenBounds.X + (int)(screenBounds.Width * percentX),
|
||||
screenBounds.Y + (int)(screenBounds.Height * percentY),
|
||||
0);
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while sending mouse move.");
|
||||
}
|
||||
}
|
||||
|
||||
public void SendMouseWheel(int deltaY)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitDisplay();
|
||||
if (deltaY > 0)
|
||||
{
|
||||
LibXtst.XTestFakeButtonEvent(Display, 4, true, 0);
|
||||
LibXtst.XTestFakeButtonEvent(Display, 4, false, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
LibXtst.XTestFakeButtonEvent(Display, 5, true, 0);
|
||||
LibXtst.XTestFakeButtonEvent(Display, 5, false, 0);
|
||||
}
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while sending mouse wheel.");
|
||||
}
|
||||
}
|
||||
|
||||
public void SendRightMouseDown(double percentX, double percentY, IViewer viewer)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitDisplay();
|
||||
SendMouseMove(percentX, percentY, viewer);
|
||||
LibXtst.XTestFakeButtonEvent(Display, 3, true, 0);
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while sending mouse right down.");
|
||||
}
|
||||
}
|
||||
|
||||
public void SendRightMouseUp(double percentX, double percentY, IViewer viewer)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitDisplay();
|
||||
SendMouseMove(percentX, percentY, viewer);
|
||||
LibXtst.XTestFakeButtonEvent(Display, 3, false, 0);
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while sending mouse right up.");
|
||||
}
|
||||
}
|
||||
public void SendText(string transferText)
|
||||
{
|
||||
foreach (var key in transferText)
|
||||
{
|
||||
SendKeyDown(key.ToString());
|
||||
SendKeyUp(key.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public void SetKeyStatesUp()
|
||||
{
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
public void ToggleBlockInput(bool toggleOn)
|
||||
{
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
private string ConvertJavaScriptKeyToX11Key(string key)
|
||||
{
|
||||
var keySym = key switch
|
||||
{
|
||||
"ArrowDown" => "Down",
|
||||
"ArrowUp" => "Up",
|
||||
"ArrowLeft" => "Left",
|
||||
"ArrowRight" => "Right",
|
||||
"Enter" => "Return",
|
||||
"Esc" => "Escape",
|
||||
"Alt" => "Alt_L",
|
||||
"Control" => "Control_L",
|
||||
"Shift" => "Shift_L",
|
||||
"PAUSE" => "Pause",
|
||||
"BREAK" => "Break",
|
||||
"Backspace" => "BackSpace",
|
||||
"Tab" => "Tab",
|
||||
"CapsLock" => "Caps_Lock",
|
||||
"Delete" => "Delete",
|
||||
"PageUp" => "Page_Up",
|
||||
"PageDown" => "Page_Down",
|
||||
"NumLock" => "Num_Lock",
|
||||
"ScrollLock" => "Scroll_Lock",
|
||||
"ContextMenu" => "Menu",
|
||||
" " => "space",
|
||||
"!" => "exclam",
|
||||
"\"" => "quotedbl",
|
||||
"#" => "numbersign",
|
||||
"$" => "dollar",
|
||||
"%" => "percent",
|
||||
"&" => "ampersand",
|
||||
"'" => "apostrophe",
|
||||
"(" => "parenleft",
|
||||
")" => "parenright",
|
||||
"*" => "asterisk",
|
||||
"+" => "plus",
|
||||
"," => "comma",
|
||||
"-" => "minus",
|
||||
"." => "period",
|
||||
"/" => "slash",
|
||||
":" => "colon",
|
||||
";" => "semicolon",
|
||||
"<" => "less",
|
||||
"=" => "equal",
|
||||
">" => "greater",
|
||||
"?" => "question",
|
||||
"@" => "at",
|
||||
"[" => "bracketleft",
|
||||
"\\" => "backslash",
|
||||
"]" => "bracketright",
|
||||
"_" => "underscore",
|
||||
"`" => "grave",
|
||||
"{" => "braceleft",
|
||||
"|" => "bar",
|
||||
"}" => "braceright",
|
||||
"~" => "asciitilde",
|
||||
_ => key,
|
||||
};
|
||||
return keySym;
|
||||
}
|
||||
private void InitDisplay()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Display == nint.Zero)
|
||||
{
|
||||
Display = LibX11.XOpenDisplay(string.Empty);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while initializing display.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
239
Desktop.Linux/Services/ScreenCapturerLinux.cs
Normal file
239
Desktop.Linux/Services/ScreenCapturerLinux.cs
Normal file
@ -0,0 +1,239 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Native.Linux;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remotely.Shared.Primitives;
|
||||
using SkiaSharp;
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Linux.Services;
|
||||
|
||||
public class ScreenCapturerLinux : IScreenCapturer
|
||||
{
|
||||
private readonly IImageHelper _imageHelper;
|
||||
private readonly ILogger<ScreenCapturerLinux> _logger;
|
||||
private readonly object _screenBoundsLock = new();
|
||||
private readonly Dictionary<string, LibXrandr.XRRMonitorInfo> _x11Screens = new();
|
||||
private SKBitmap? _currentFrame;
|
||||
private SKBitmap? _previousFrame;
|
||||
|
||||
public ScreenCapturerLinux(
|
||||
IImageHelper imageHelper,
|
||||
ILogger<ScreenCapturerLinux> logger)
|
||||
{
|
||||
_imageHelper = imageHelper;
|
||||
_logger = logger;
|
||||
Display = LibX11.XOpenDisplay(string.Empty);
|
||||
Init();
|
||||
}
|
||||
|
||||
public event EventHandler<Rectangle>? ScreenChanged;
|
||||
|
||||
public bool CaptureFullscreen { get; set; } = true;
|
||||
public Rectangle CurrentScreenBounds { get; private set; }
|
||||
public nint Display { get; private set; }
|
||||
public bool IsGpuAccelerated => false;
|
||||
public string SelectedScreen { get; private set; } = string.Empty;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
LibX11.XCloseDisplay(Display);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
public IEnumerable<string> GetDisplayNames()
|
||||
{
|
||||
return _x11Screens.Keys.Select(x => x.ToString());
|
||||
}
|
||||
|
||||
public SKRect GetFrameDiffArea()
|
||||
{
|
||||
if (_currentFrame is null)
|
||||
{
|
||||
return SKRect.Empty;
|
||||
}
|
||||
|
||||
return _imageHelper.GetDiffArea(_currentFrame, _previousFrame, CaptureFullscreen);
|
||||
}
|
||||
|
||||
public Result<SKBitmap> GetImageDiff()
|
||||
{
|
||||
if (_currentFrame is null)
|
||||
{
|
||||
return Result.Fail<SKBitmap>("Current frame is null.");
|
||||
}
|
||||
|
||||
return _imageHelper.GetImageDiff(_currentFrame, _previousFrame);
|
||||
}
|
||||
|
||||
public Result<SKBitmap> GetNextFrame()
|
||||
{
|
||||
lock (_screenBoundsLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_currentFrame != null)
|
||||
{
|
||||
_previousFrame?.Dispose();
|
||||
_previousFrame = _currentFrame;
|
||||
}
|
||||
|
||||
_currentFrame = GetX11Capture();
|
||||
return Result.Ok(_currentFrame);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while getting next frame.");
|
||||
Init();
|
||||
return Result.Fail<SKBitmap>(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
public int GetScreenCount()
|
||||
{
|
||||
return _x11Screens.Count;
|
||||
}
|
||||
|
||||
public Rectangle GetVirtualScreenBounds()
|
||||
{
|
||||
var lowestX = 0;
|
||||
var highestX = 0;
|
||||
var lowestY = 0;
|
||||
var highestY = 0;
|
||||
|
||||
foreach (var screen in _x11Screens)
|
||||
{
|
||||
lowestX = Math.Min(lowestX, screen.Value.x);
|
||||
highestX = Math.Max(highestX, screen.Value.x + screen.Value.width);
|
||||
lowestY = Math.Min(lowestY, screen.Value.y);
|
||||
highestY = Math.Max(highestY, screen.Value.y + screen.Value.height);
|
||||
}
|
||||
|
||||
return new Rectangle(lowestX, lowestY, highestX - lowestX, highestY - lowestY);
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
try
|
||||
{
|
||||
CaptureFullscreen = true;
|
||||
_x11Screens.Clear();
|
||||
|
||||
var monitorsPtr = LibXrandr.XRRGetMonitors(Display, LibX11.XDefaultRootWindow(Display), true, out var monitorCount);
|
||||
|
||||
var monitorInfoSize = Marshal.SizeOf<LibXrandr.XRRMonitorInfo>();
|
||||
|
||||
for (var i = 0; i < monitorCount; i++)
|
||||
{
|
||||
var monitorPtr = new nint(monitorsPtr.ToInt64() + i * monitorInfoSize);
|
||||
var monitorInfo = Marshal.PtrToStructure<LibXrandr.XRRMonitorInfo>(monitorPtr);
|
||||
|
||||
_logger.LogInformation($"Found monitor: " +
|
||||
$"{monitorInfo.width}," +
|
||||
$"{monitorInfo.height}," +
|
||||
$"{monitorInfo.x}, " +
|
||||
$"{monitorInfo.y}");
|
||||
|
||||
_x11Screens.Add(i.ToString(), monitorInfo);
|
||||
}
|
||||
|
||||
LibXrandr.XRRFreeMonitors(monitorsPtr);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SelectedScreen) ||
|
||||
!_x11Screens.ContainsKey(SelectedScreen))
|
||||
{
|
||||
SelectedScreen = _x11Screens.Keys.First();
|
||||
RefreshCurrentScreenBounds();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while initializing.");
|
||||
}
|
||||
}
|
||||
public void SetSelectedScreen(string displayName)
|
||||
{
|
||||
lock (_screenBoundsLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Setting display to {displayName}.", displayName);
|
||||
if (displayName == SelectedScreen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_x11Screens.ContainsKey(displayName))
|
||||
{
|
||||
SelectedScreen = displayName;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedScreen = _x11Screens.Keys.First();
|
||||
}
|
||||
|
||||
RefreshCurrentScreenBounds();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while setting selected display.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SKBitmap GetX11Capture()
|
||||
{
|
||||
var currentFrame = new SKBitmap(CurrentScreenBounds.Width, CurrentScreenBounds.Height);
|
||||
|
||||
var window = LibX11.XDefaultRootWindow(Display);
|
||||
|
||||
var imagePointer = LibX11.XGetImage(Display,
|
||||
window,
|
||||
CurrentScreenBounds.X,
|
||||
CurrentScreenBounds.Y,
|
||||
CurrentScreenBounds.Width,
|
||||
CurrentScreenBounds.Height,
|
||||
~0,
|
||||
2);
|
||||
|
||||
if (imagePointer == nint.Zero)
|
||||
{
|
||||
return currentFrame;
|
||||
}
|
||||
|
||||
var image = Marshal.PtrToStructure<LibX11.XImage>(imagePointer);
|
||||
|
||||
var pixels = currentFrame.GetPixels();
|
||||
unsafe
|
||||
{
|
||||
var scan1 = (byte*)pixels.ToPointer();
|
||||
var scan2 = (byte*)image.data.ToPointer();
|
||||
var bytesPerPixel = currentFrame.BytesPerPixel;
|
||||
var totalSize = currentFrame.Height * currentFrame.Width * bytesPerPixel;
|
||||
for (var counter = 0; counter < totalSize - bytesPerPixel; counter++)
|
||||
{
|
||||
scan1[counter] = scan2[counter];
|
||||
}
|
||||
}
|
||||
|
||||
Marshal.DestroyStructure<LibX11.XImage>(imagePointer);
|
||||
LibX11.XDestroyImage(imagePointer);
|
||||
|
||||
return currentFrame;
|
||||
}
|
||||
|
||||
private void RefreshCurrentScreenBounds()
|
||||
{
|
||||
var screen = _x11Screens[SelectedScreen];
|
||||
|
||||
_logger.LogInformation($"Setting new screen bounds: " +
|
||||
$"{screen.width}," +
|
||||
$"{screen.height}," +
|
||||
$"{screen.x}, " +
|
||||
$"{screen.y}");
|
||||
|
||||
CurrentScreenBounds = new Rectangle(screen.x, screen.y, screen.width, screen.height);
|
||||
CaptureFullscreen = true;
|
||||
ScreenChanged?.Invoke(this, CurrentScreenBounds);
|
||||
}
|
||||
}
|
||||
49
Desktop.Linux/Services/ShutdownServiceLinux.cs
Normal file
49
Desktop.Linux/Services/ShutdownServiceLinux.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Immense.RemoteControl.Desktop.UI.Services;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Linux.Services;
|
||||
|
||||
public class ShutdownServiceLinux : IShutdownService
|
||||
{
|
||||
private readonly IDesktopHubConnection _hubConnection;
|
||||
private readonly IUiDispatcher _dispatcher;
|
||||
private readonly IAppState _appState;
|
||||
private readonly ILogger<ShutdownServiceLinux> _logger;
|
||||
|
||||
public ShutdownServiceLinux(
|
||||
IDesktopHubConnection hubConnection,
|
||||
IUiDispatcher dispatcher,
|
||||
IAppState appState,
|
||||
ILogger<ShutdownServiceLinux> logger)
|
||||
{
|
||||
_hubConnection = hubConnection;
|
||||
_dispatcher = dispatcher;
|
||||
_appState = appState;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Shutdown()
|
||||
{
|
||||
_logger.LogDebug("Exiting process ID {processId}.", Environment.ProcessId);
|
||||
await TryDisconnectViewers();
|
||||
_dispatcher.Shutdown();
|
||||
}
|
||||
|
||||
private async Task TryDisconnectViewers()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_hubConnection.IsConnected && _appState.Viewers.Any())
|
||||
{
|
||||
await _hubConnection.DisconnectAllViewers();
|
||||
await _hubConnection.Disconnect();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while sending shutdown notice to viewers.");
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Desktop.Linux/Startup/IServiceCollectionExtensions.cs
Normal file
37
Desktop.Linux/Startup/IServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Startup;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Immense.RemoteControl.Desktop.Linux.Services;
|
||||
using Immense.RemoteControl.Desktop.UI.ViewModels;
|
||||
using Immense.RemoteControl.Desktop.UI.Services;
|
||||
using Immense.RemoteControl.Desktop.UI.Startup;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Linux.Startup;
|
||||
|
||||
public static class IServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Linux and cross-platform remote control services to the service collection.
|
||||
/// All methods on <see cref="IRemoteControlClientBuilder"/> must be called to register
|
||||
/// required services.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="clientConfig"></param>
|
||||
public static void AddRemoteControlLinux(
|
||||
this IServiceCollection services,
|
||||
Action<IRemoteControlClientBuilder> clientConfig)
|
||||
{
|
||||
services.AddRemoteControlXplat(clientConfig);
|
||||
services.AddRemoteControlUi();
|
||||
|
||||
services.AddSingleton<IAppStartup, AppStartup>();
|
||||
services.AddSingleton<ICursorIconWatcher, CursorIconWatcherLinux>();
|
||||
services.AddSingleton<IKeyboardMouseInput, KeyboardMouseInputLinux>();
|
||||
services.AddSingleton<IClipboardService, ClipboardService>();
|
||||
services.AddSingleton<IAudioCapturer, AudioCapturerLinux>();
|
||||
services.AddTransient<IScreenCapturer, ScreenCapturerLinux>();
|
||||
services.AddScoped<IFileTransferService, FileTransferServiceLinux>();
|
||||
services.AddSingleton<ISessionIndicator, SessionIndicator>();
|
||||
services.AddSingleton<IShutdownService, ShutdownServiceLinux>();
|
||||
}
|
||||
}
|
||||
5
Desktop.Linux/Usings.cs
Normal file
5
Desktop.Linux/Usings.cs
Normal file
@ -0,0 +1,5 @@
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
global using System.Text;
|
||||
global using System.Threading.Tasks;
|
||||
14
Desktop.Native/Desktop.Native.csproj
Normal file
14
Desktop.Native/Desktop.Native.csproj
Normal file
@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Shared\Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
140
Desktop.Native/Linux/LibX11.cs
Normal file
140
Desktop.Native/Linux/LibX11.cs
Normal file
@ -0,0 +1,140 @@
|
||||
/*
|
||||
|
||||
Copyright 1985, 1986, 1987, 1991, 1998 The Open Group
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this software and its
|
||||
documentation for any purpose is hereby granted without fee, provided that
|
||||
the above copyright notice appear in all copies and that both that
|
||||
copyright notice and this permission notice appear in supporting
|
||||
documentation.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of The Open Group shall not be
|
||||
used in advertising or otherwise to promote the sale, use or other dealings
|
||||
in this Software without prior written authorization from The Open Group.
|
||||
|
||||
*/
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Native.Linux;
|
||||
|
||||
public static unsafe class LibX11
|
||||
{
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern nint XGetImage(nint display, nint drawable, int x, int y, int width, int height, long plane_mask, int format);
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern nint XDefaultVisual(nint display, int screen_number);
|
||||
[DllImport("libX11")]
|
||||
public static extern int XScreenCount(nint display);
|
||||
[DllImport("libX11")]
|
||||
public static extern int XDefaultScreen(nint display);
|
||||
[DllImport("libX11")]
|
||||
public static extern nint XOpenDisplay(string display_name);
|
||||
[DllImport("libX11")]
|
||||
public static extern void XCloseDisplay(nint display);
|
||||
[DllImport("libX11")]
|
||||
public static extern nint XRootWindow(nint display, int screen_number);
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern nint XGetSubImage(nint display, nint drawable, int x, int y, uint width, uint height, ulong plane_mask, int format, nint dest_image, int dest_x, int dest_y);
|
||||
[DllImport("libX11")]
|
||||
public static extern nint XScreenOfDisplay(nint display, int screen_number);
|
||||
[DllImport("libX11")]
|
||||
public static extern int XDisplayWidth(nint display, int screen_number);
|
||||
[DllImport("libX11")]
|
||||
public static extern int XDisplayHeight(nint display, int screen_number);
|
||||
[DllImport("libX11")]
|
||||
public static extern int XWidthOfScreen(nint screen);
|
||||
[DllImport("libX11")]
|
||||
public static extern int XHeightOfScreen(nint screen);
|
||||
[DllImport("libX11")]
|
||||
public static extern nint XDefaultGC(nint display, int screen_number);
|
||||
[DllImport("libX11")]
|
||||
public static extern nint XDefaultRootWindow(nint display);
|
||||
[DllImport("libX11")]
|
||||
public static extern void XGetInputFocus(nint display, out nint focus_return, out int revert_to_return);
|
||||
[DllImport("libX11")]
|
||||
public static extern nint XStringToKeysym(string key);
|
||||
[DllImport("libX11")]
|
||||
public static extern uint XKeysymToKeycode(nint display, nint keysym);
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern nint XRootWindowOfScreen(nint screen);
|
||||
[DllImport("libX11")]
|
||||
public static extern ulong XNextRequest(nint display);
|
||||
[DllImport("libX11")]
|
||||
public static extern void XForceScreenSaver(nint display, int mode);
|
||||
[DllImport("libX11")]
|
||||
public static extern void XSync(nint display, bool discard);
|
||||
[DllImport("libX11")]
|
||||
public static extern void XDestroyImage(nint ximage);
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern void XNoOp(nint display);
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern void XFree(nint data);
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern int XGetWindowAttributes(nint display, nint window, out XWindowAttributes windowAttributes);
|
||||
|
||||
public struct XImage
|
||||
{
|
||||
public int width;
|
||||
public int height; /* size of image */
|
||||
public int xoffset; /* number of pixels offset in X direction */
|
||||
public int format; /* XYBitmap, XYPixmap, ZPixmap */
|
||||
//public char* data; /* pointer to image data */
|
||||
public nint data; /* pointer to image data */
|
||||
public int byte_order; /* data byte order, LSBFirst, MSBFirst */
|
||||
public int bitmap_unit; /* quant. of scanline 8, 16, 32 */
|
||||
public int bitmap_bit_order; /* LSBFirst, MSBFirst */
|
||||
public int bitmap_pad; /* 8, 16, 32 either XY or ZPixmap */
|
||||
public int depth; /* depth of image */
|
||||
public int bytes_per_line; /* accelerator to next scanline */
|
||||
public int bits_per_pixel; /* bits per pixel (ZPixmap) */
|
||||
public ulong red_mask; /* bits in z arrangement */
|
||||
public ulong green_mask;
|
||||
public ulong blue_mask;
|
||||
public nint obdata; /* hook for the object routines to hang on */
|
||||
}
|
||||
|
||||
public struct XWindowAttributes
|
||||
{
|
||||
public int x;
|
||||
public int y;
|
||||
public int width;
|
||||
public int height;
|
||||
public int border_width;
|
||||
public int depth;
|
||||
public nint visual;
|
||||
public nint root;
|
||||
public int @class;
|
||||
public int bit_gravity;
|
||||
public int win_gravity;
|
||||
public int backing_store;
|
||||
public ulong backing_planes;
|
||||
public ulong backing_pixel;
|
||||
public bool save_under;
|
||||
public nint colormap;
|
||||
public bool map_installed;
|
||||
public int map_state;
|
||||
public long all_event_masks;
|
||||
public long your_event_mask;
|
||||
public long do_not_propagate_mask;
|
||||
public bool override_redirect;
|
||||
public nint screen;
|
||||
}
|
||||
}
|
||||
15
Desktop.Native/Linux/LibXtst.cs
Normal file
15
Desktop.Native/Linux/LibXtst.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Native.Linux;
|
||||
|
||||
public class LibXtst
|
||||
{
|
||||
[DllImport("libXtst")]
|
||||
public static extern bool XTestQueryExtension(nint display, out int event_base, out int error_base, out int major_version, out int minor_version);
|
||||
[DllImport("libXtst")]
|
||||
public static extern void XTestFakeKeyEvent(nint display, uint keycode, bool is_press, ulong delay);
|
||||
[DllImport("libXtst")]
|
||||
public static extern void XTestFakeButtonEvent(nint display, uint button, bool is_press, ulong delay);
|
||||
[DllImport("libXtst")]
|
||||
public static extern void XTestFakeMotionEvent(nint display, int screen_number, int x, int y, ulong delay);
|
||||
}
|
||||
9
Desktop.Native/Linux/Libc.cs
Normal file
9
Desktop.Native/Linux/Libc.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Native.Linux;
|
||||
|
||||
public class Libc
|
||||
{
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
public static extern uint geteuid();
|
||||
}
|
||||
62
Desktop.Native/Linux/libXrandr.cs
Normal file
62
Desktop.Native/Linux/libXrandr.cs
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright © 2000 Compaq Computer Corporation, Inc.
|
||||
* Copyright © 2002 Hewlett-Packard Company, Inc.
|
||||
* Copyright © 2006 Intel Corporation
|
||||
* Copyright © 2008 Red Hat, Inc.
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that copyright
|
||||
* notice and this permission notice appear in supporting documentation, and
|
||||
* that the name of the copyright holders not be used in advertising or
|
||||
* publicity pertaining to distribution of the software without specific,
|
||||
* written prior permission. The copyright holders make no representations
|
||||
* about the suitability of this software for any purpose. It is provided "as
|
||||
* is" without express or implied warranty.
|
||||
*
|
||||
* THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
|
||||
* INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
|
||||
* EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
|
||||
* CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
|
||||
* DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
||||
* OF THIS SOFTWARE.
|
||||
*
|
||||
* Author: Jim Gettys, HP Labs, Hewlett-Packard, Inc.
|
||||
* Keith Packard, Intel Corporation
|
||||
*/
|
||||
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Native.Linux;
|
||||
|
||||
public static class LibXrandr
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct XRRMonitorInfo
|
||||
{
|
||||
// Atom
|
||||
public nint name;
|
||||
public bool primary;
|
||||
public bool automatic;
|
||||
public int noutput;
|
||||
public int x;
|
||||
public int y;
|
||||
public int width;
|
||||
public int height;
|
||||
public int mwidth;
|
||||
public int mheight;
|
||||
// RROutput*
|
||||
public nint outputs;
|
||||
}
|
||||
|
||||
[DllImport("libXrandr")]
|
||||
public static extern nint XRRGetMonitors(nint display, nint window, bool get_active, out int monitors);
|
||||
|
||||
[DllImport("libXrandr")]
|
||||
public static extern void XRRFreeMonitors(nint monitors);
|
||||
|
||||
[DllImport("libXrandr")]
|
||||
public static extern nint XRRAllocateMonitor(nint display, int output);
|
||||
}
|
||||
365
Desktop.Native/Windows/ADVAPI32.cs
Normal file
365
Desktop.Native/Windows/ADVAPI32.cs
Normal file
@ -0,0 +1,365 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Native.Windows;
|
||||
|
||||
public static class ADVAPI32
|
||||
{
|
||||
#region Structs
|
||||
public struct TOKEN_PRIVILEGES
|
||||
{
|
||||
public struct LUID
|
||||
{
|
||||
public uint LowPart;
|
||||
public int HighPart;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
public struct LUID_AND_ATTRIBUTES
|
||||
{
|
||||
public LUID Luid;
|
||||
public uint Attributes;
|
||||
}
|
||||
public int PrivilegeCount;
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = ANYSIZE_ARRAY)]
|
||||
public LUID_AND_ATTRIBUTES[] Privileges;
|
||||
}
|
||||
public class USEROBJECTFLAGS
|
||||
{
|
||||
public int fInherit = 0;
|
||||
public int fReserved = 0;
|
||||
public int dwFlags = 0;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct SECURITY_ATTRIBUTES
|
||||
{
|
||||
public int Length;
|
||||
public nint lpSecurityDescriptor;
|
||||
public bool bInheritHandle;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct PROCESS_INFORMATION
|
||||
{
|
||||
public nint hProcess;
|
||||
public nint hThread;
|
||||
public int dwProcessId;
|
||||
public int dwThreadId;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public struct STARTUPINFO
|
||||
{
|
||||
public int cb;
|
||||
public string lpReserved;
|
||||
public string lpDesktop;
|
||||
public string lpTitle;
|
||||
public int dwX;
|
||||
public int dwY;
|
||||
public int dwXSize;
|
||||
public int dwYSize;
|
||||
public int dwXCountChars;
|
||||
public int dwYCountChars;
|
||||
public int dwFillAttribute;
|
||||
public int dwFlags;
|
||||
public short wShowWindow;
|
||||
public short cbReserved2;
|
||||
public nint lpReserved2;
|
||||
public nint hStdInput;
|
||||
public nint hStdOutput;
|
||||
public nint hStdError;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Enums
|
||||
public enum TOKEN_INFORMATION_CLASS
|
||||
{
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_USER structure that contains the user account of the token.
|
||||
/// </summary>
|
||||
TokenUser = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_GROUPS structure that contains the group accounts associated with the token.
|
||||
/// </summary>
|
||||
TokenGroups,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_PRIVILEGES structure that contains the privileges of the token.
|
||||
/// </summary>
|
||||
TokenPrivileges,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_OWNER structure that contains the default owner security identifier (SID) for newly created objects.
|
||||
/// </summary>
|
||||
TokenOwner,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_PRIMARY_GROUP structure that contains the default primary group SID for newly created objects.
|
||||
/// </summary>
|
||||
TokenPrimaryGroup,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_DEFAULT_DACL structure that contains the default DACL for newly created objects.
|
||||
/// </summary>
|
||||
TokenDefaultDacl,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_SOURCE structure that contains the source of the token. TOKEN_QUERY_SOURCE access is needed to retrieve this information.
|
||||
/// </summary>
|
||||
TokenSource,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_TYPE value that indicates whether the token is a primary or impersonation token.
|
||||
/// </summary>
|
||||
TokenType,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a SECURITY_IMPERSONATION_LEVEL value that indicates the impersonation level of the token. If the access token is not an impersonation token, the function fails.
|
||||
/// </summary>
|
||||
TokenImpersonationLevel,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_STATISTICS structure that contains various token statistics.
|
||||
/// </summary>
|
||||
TokenStatistics,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_GROUPS structure that contains the list of restricting SIDs in a restricted token.
|
||||
/// </summary>
|
||||
TokenRestrictedSids,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a DWORD value that indicates the Terminal Services session identifier that is associated with the token.
|
||||
/// </summary>
|
||||
TokenSessionId,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_GROUPS_AND_PRIVILEGES structure that contains the user SID, the group accounts, the restricted SIDs, and the authentication ID associated with the token.
|
||||
/// </summary>
|
||||
TokenGroupsAndPrivileges,
|
||||
|
||||
/// <summary>
|
||||
/// Reserved.
|
||||
/// </summary>
|
||||
TokenSessionReference,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a DWORD value that is nonzero if the token includes the SANDBOX_INERT flag.
|
||||
/// </summary>
|
||||
TokenSandBoxInert,
|
||||
|
||||
/// <summary>
|
||||
/// Reserved.
|
||||
/// </summary>
|
||||
TokenAuditPolicy,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_ORIGIN value.
|
||||
/// </summary>
|
||||
TokenOrigin,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_ELEVATION_TYPE value that specifies the elevation level of the token.
|
||||
/// </summary>
|
||||
TokenElevationType,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_LINKED_TOKEN structure that contains a handle to another token that is linked to this token.
|
||||
/// </summary>
|
||||
TokenLinkedToken,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_ELEVATION structure that specifies whether the token is elevated.
|
||||
/// </summary>
|
||||
TokenElevation,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a DWORD value that is nonzero if the token has ever been filtered.
|
||||
/// </summary>
|
||||
TokenHasRestrictions,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_ACCESS_INFORMATION structure that specifies security information contained in the token.
|
||||
/// </summary>
|
||||
TokenAccessInformation,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a DWORD value that is nonzero if virtualization is allowed for the token.
|
||||
/// </summary>
|
||||
TokenVirtualizationAllowed,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a DWORD value that is nonzero if virtualization is enabled for the token.
|
||||
/// </summary>
|
||||
TokenVirtualizationEnabled,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_MANDATORY_LABEL structure that specifies the token's integrity level.
|
||||
/// </summary>
|
||||
TokenIntegrityLevel,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a DWORD value that is nonzero if the token has the UIAccess flag set.
|
||||
/// </summary>
|
||||
TokenUIAccess,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_MANDATORY_POLICY structure that specifies the token's mandatory integrity policy.
|
||||
/// </summary>
|
||||
TokenMandatoryPolicy,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives the token's logon security identifier (SID).
|
||||
/// </summary>
|
||||
TokenLogonSid,
|
||||
|
||||
/// <summary>
|
||||
/// The maximum value for this enumeration
|
||||
/// </summary>
|
||||
MaxTokenInfoClass
|
||||
}
|
||||
public enum LOGON_TYPE
|
||||
{
|
||||
LOGON32_LOGON_INTERACTIVE = 2,
|
||||
LOGON32_LOGON_NETWORK,
|
||||
LOGON32_LOGON_BATCH,
|
||||
LOGON32_LOGON_SERVICE,
|
||||
LOGON32_LOGON_UNLOCK = 7,
|
||||
LOGON32_LOGON_NETWORK_CLEARTEXT,
|
||||
LOGON32_LOGON_NEW_CREDENTIALS
|
||||
}
|
||||
public enum LOGON_PROVIDER
|
||||
{
|
||||
LOGON32_PROVIDER_DEFAULT,
|
||||
LOGON32_PROVIDER_WINNT35,
|
||||
LOGON32_PROVIDER_WINNT40,
|
||||
LOGON32_PROVIDER_WINNT50
|
||||
}
|
||||
[Flags]
|
||||
public enum CreateProcessFlags
|
||||
{
|
||||
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
|
||||
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
|
||||
CREATE_NEW_CONSOLE = 0x00000010,
|
||||
CREATE_NEW_PROCESS_GROUP = 0x00000200,
|
||||
CREATE_NO_WINDOW = 0x08000000,
|
||||
CREATE_PROTECTED_PROCESS = 0x00040000,
|
||||
CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
|
||||
CREATE_SEPARATE_WOW_VDM = 0x00000800,
|
||||
CREATE_SHARED_WOW_VDM = 0x00001000,
|
||||
CREATE_SUSPENDED = 0x00000004,
|
||||
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
|
||||
DEBUG_ONLY_THIS_PROCESS = 0x00000002,
|
||||
DEBUG_PROCESS = 0x00000001,
|
||||
DETACHED_PROCESS = 0x00000008,
|
||||
EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
|
||||
INHERIT_PARENT_AFFINITY = 0x00010000
|
||||
}
|
||||
public enum TOKEN_TYPE : int
|
||||
{
|
||||
TokenPrimary = 1,
|
||||
TokenImpersonation = 2
|
||||
}
|
||||
|
||||
public enum SECURITY_IMPERSONATION_LEVEL : int
|
||||
{
|
||||
SecurityAnonymous = 0,
|
||||
SecurityIdentification = 1,
|
||||
SecurityImpersonation = 2,
|
||||
SecurityDelegation = 3,
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constants
|
||||
public const int TOKEN_DUPLICATE = 0x0002;
|
||||
public const uint MAXIMUM_ALLOWED = 0x2000000;
|
||||
public const int CREATE_NEW_CONSOLE = 0x00000010;
|
||||
public const int CREATE_NO_WINDOW = 0x08000000;
|
||||
public const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
|
||||
public const int STARTF_USESHOWWINDOW = 0x00000001;
|
||||
public const int DETACHED_PROCESS = 0x00000008;
|
||||
public const int TOKEN_ALL_ACCESS = 0x000f01ff;
|
||||
public const int PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFF;
|
||||
public const int STANDARD_RIGHTS_REQUIRED = 0x000F0000;
|
||||
public const int SYNCHRONIZE = 0x00100000;
|
||||
|
||||
public const int IDLE_PRIORITY_CLASS = 0x40;
|
||||
public const int NORMAL_PRIORITY_CLASS = 0x20;
|
||||
public const int HIGH_PRIORITY_CLASS = 0x80;
|
||||
public const int REALTIME_PRIORITY_CLASS = 0x100;
|
||||
public const uint SE_PRIVILEGE_ENABLED_BY_DEFAULT = 0x00000001;
|
||||
public const uint SE_PRIVILEGE_ENABLED = 0x00000002;
|
||||
public const uint SE_PRIVILEGE_REMOVED = 0x00000004;
|
||||
public const uint SE_PRIVILEGE_USED_FOR_ACCESS = 0x80000000;
|
||||
public const int ANYSIZE_ARRAY = 1;
|
||||
|
||||
public const int UOI_FLAGS = 1;
|
||||
public const int UOI_NAME = 2;
|
||||
public const int UOI_TYPE = 3;
|
||||
public const int UOI_USER_SID = 4;
|
||||
public const int UOI_HEAPSIZE = 5;
|
||||
public const int UOI_IO = 6;
|
||||
#endregion
|
||||
|
||||
#region DLL Imports
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool AdjustTokenPrivileges(nint tokenHandle,
|
||||
[MarshalAs(UnmanagedType.Bool)] bool disableAllPrivileges,
|
||||
ref TOKEN_PRIVILEGES newState,
|
||||
uint bufferLengthInBytes,
|
||||
ref TOKEN_PRIVILEGES previousState,
|
||||
out uint returnLengthInBytes);
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
public static extern bool CreateProcessAsUser(
|
||||
nint hToken,
|
||||
string? lpApplicationName,
|
||||
string lpCommandLine,
|
||||
ref SECURITY_ATTRIBUTES lpProcessAttributes,
|
||||
ref SECURITY_ATTRIBUTES lpThreadAttributes,
|
||||
bool bInheritHandles,
|
||||
uint dwCreationFlags,
|
||||
nint lpEnvironment,
|
||||
string? lpCurrentDirectory,
|
||||
ref STARTUPINFO lpStartupInfo,
|
||||
out PROCESS_INFORMATION lpProcessInformation);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
public static extern bool AllocateLocallyUniqueId(out nint pLuid);
|
||||
|
||||
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = false)]
|
||||
public static extern SECUR32.WinErrors LsaNtStatusToWinError(SECUR32.WinStatusCodes status);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
public static extern bool GetTokenInformation(
|
||||
nint TokenHandle,
|
||||
SECUR32.TOKEN_INFORMATION_CLASS TokenInformationClass,
|
||||
nint TokenInformation,
|
||||
uint TokenInformationLength,
|
||||
out uint ReturnLength);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool LogonUser(
|
||||
[MarshalAs(UnmanagedType.LPStr)] string pszUserName,
|
||||
[MarshalAs(UnmanagedType.LPStr)] string pszDomain,
|
||||
[MarshalAs(UnmanagedType.LPStr)] string pszPassword,
|
||||
int dwLogonType,
|
||||
int dwLogonProvider,
|
||||
out nint phToken);
|
||||
|
||||
[DllImport("advapi32", SetLastError = true), SuppressUnmanagedCodeSecurity]
|
||||
public static extern bool OpenProcessToken(nint ProcessHandle, int DesiredAccess, ref nint TokenHandle);
|
||||
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
public static extern bool DuplicateTokenEx(
|
||||
nint hExistingToken,
|
||||
uint dwDesiredAccess,
|
||||
ref SECURITY_ATTRIBUTES lpTokenAttributes,
|
||||
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
|
||||
TOKEN_TYPE TokenType,
|
||||
out nint phNewToken);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = false)]
|
||||
public static extern uint LsaNtStatusToWinError(uint status);
|
||||
#endregion
|
||||
}
|
||||
80
Desktop.Native/Windows/GDI32.cs
Normal file
80
Desktop.Native/Windows/GDI32.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Native.Windows;
|
||||
|
||||
public static class GDI32
|
||||
{
|
||||
#region Enums
|
||||
/// <summary>
|
||||
/// Specifies a raster-operation code. These codes define how the color data for the
|
||||
/// source rectangle is to be combined with the color data for the destination
|
||||
/// rectangle to achieve the final color.
|
||||
/// </summary>
|
||||
public enum TernaryRasterOperations : uint
|
||||
{
|
||||
/// <summary>dest = source</summary>
|
||||
SRCCOPY = 0x00CC0020,
|
||||
/// <summary>dest = source OR dest</summary>
|
||||
SRCPAINT = 0x00EE0086,
|
||||
/// <summary>dest = source AND dest</summary>
|
||||
SRCAND = 0x008800C6,
|
||||
/// <summary>dest = source XOR dest</summary>
|
||||
SRCINVERT = 0x00660046,
|
||||
/// <summary>dest = source AND (NOT dest)</summary>
|
||||
SRCERASE = 0x00440328,
|
||||
/// <summary>dest = (NOT source)</summary>
|
||||
NOTSRCCOPY = 0x00330008,
|
||||
/// <summary>dest = (NOT src) AND (NOT dest)</summary>
|
||||
NOTSRCERASE = 0x001100A6,
|
||||
/// <summary>dest = (source AND pattern)</summary>
|
||||
MERGECOPY = 0x00C000CA,
|
||||
/// <summary>dest = (NOT source) OR dest</summary>
|
||||
MERGEPAINT = 0x00BB0226,
|
||||
/// <summary>dest = pattern</summary>
|
||||
PATCOPY = 0x00F00021,
|
||||
/// <summary>dest = DPSnoo</summary>
|
||||
PATPAINT = 0x00FB0A09,
|
||||
/// <summary>dest = pattern XOR dest</summary>
|
||||
PATINVERT = 0x005A0049,
|
||||
/// <summary>dest = (NOT dest)</summary>
|
||||
DSTINVERT = 0x00550009,
|
||||
/// <summary>dest = BLACK</summary>
|
||||
BLACKNESS = 0x00000042,
|
||||
/// <summary>dest = WHITE</summary>
|
||||
WHITENESS = 0x00FF0062,
|
||||
/// <summary>
|
||||
/// Capture window as seen on screen. This includes layered windows
|
||||
/// such as WPF windows with AllowsTransparency="true"
|
||||
/// </summary>
|
||||
CAPTUREBLT = 0x40000000
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region DLL Imports
|
||||
|
||||
[DllImport("gdi32.dll", EntryPoint = "BitBlt", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool BitBlt([In] nint hdc, int nXDest, int nYDest, int nWidth, int nHeight, [In] nint hdcSrc, int nXSrc, int nYSrc, TernaryRasterOperations dwRop);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
public static extern nint CreateDC(string lpszDriver, string lpszDevice, string lpszOutput, nint lpInitData);
|
||||
|
||||
[DllImport("GDI32.dll")]
|
||||
public static extern nint CreateCompatibleBitmap(nint hdc, int nWidth, int nHeight); [DllImport("GDI32.dll")]
|
||||
public static extern nint CreateCompatibleDC(nint hdc);
|
||||
|
||||
[DllImport("GDI32.dll")]
|
||||
public static extern bool DeleteDC(nint hdc);
|
||||
|
||||
[DllImport("GDI32.dll")]
|
||||
public static extern bool DeleteObject(nint hObject);
|
||||
|
||||
[DllImport("GDI32.dll")]
|
||||
public static extern nint GetDeviceCaps(nint hdc, int nIndex);
|
||||
|
||||
[DllImport("GDI32.dll")]
|
||||
public static extern nint SelectObject(nint hdc, nint hgdiobj);
|
||||
|
||||
#endregion
|
||||
}
|
||||
89
Desktop.Native/Windows/Kernel32.cs
Normal file
89
Desktop.Native/Windows/Kernel32.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Native.Windows;
|
||||
|
||||
public static class Kernel32
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool CloseHandle(nint hSnapshot);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
|
||||
public static extern nint GetCommandLine();
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern nint GetConsoleWindow();
|
||||
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
public static extern bool GlobalMemoryStatusEx([In, Out] MEMORYSTATUSEX lpBuffer);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern nint OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern bool ProcessIdToSessionId(uint dwProcessId, ref uint pSessionId);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern uint WTSGetActiveConsoleSessionId();
|
||||
|
||||
/// <summary>
|
||||
/// contains information about the current state of both physical and virtual memory, including extended memory
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
|
||||
public class MEMORYSTATUSEX
|
||||
{
|
||||
/// <summary>
|
||||
/// Size of the structure, in bytes. You must set this member before calling GlobalMemoryStatusEx.
|
||||
/// </summary>
|
||||
public uint dwLength;
|
||||
|
||||
/// <summary>
|
||||
/// Number between 0 and 100 that specifies the approximate percentage of physical memory that is in use (0 indicates no memory use and 100 indicates full memory use).
|
||||
/// </summary>
|
||||
public uint dwMemoryLoad;
|
||||
|
||||
/// <summary>
|
||||
/// Total size of physical memory, in bytes.
|
||||
/// </summary>
|
||||
public ulong ullTotalPhys;
|
||||
|
||||
/// <summary>
|
||||
/// Size of physical memory available, in bytes.
|
||||
/// </summary>
|
||||
public ulong ullAvailPhys;
|
||||
|
||||
/// <summary>
|
||||
/// Size of the committed memory limit, in bytes. This is physical memory plus the size of the page file, minus a small overhead.
|
||||
/// </summary>
|
||||
public ulong ullTotalPageFile;
|
||||
|
||||
/// <summary>
|
||||
/// Size of available memory to commit, in bytes. The limit is ullTotalPageFile.
|
||||
/// </summary>
|
||||
public ulong ullAvailPageFile;
|
||||
|
||||
/// <summary>
|
||||
/// Total size of the user mode portion of the virtual address space of the calling process, in bytes.
|
||||
/// </summary>
|
||||
public ulong ullTotalVirtual;
|
||||
|
||||
/// <summary>
|
||||
/// Size of unreserved and uncommitted memory in the user mode portion of the virtual address space of the calling process, in bytes.
|
||||
/// </summary>
|
||||
public ulong ullAvailVirtual;
|
||||
|
||||
/// <summary>
|
||||
/// Size of unreserved and uncommitted memory in the extended portion of the virtual address space of the calling process, in bytes.
|
||||
/// </summary>
|
||||
public ulong ullAvailExtendedVirtual;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="T:MEMORYSTATUSEX"/> class.
|
||||
/// </summary>
|
||||
public MEMORYSTATUSEX()
|
||||
{
|
||||
dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
|
||||
}
|
||||
}
|
||||
}
|
||||
376
Desktop.Native/Windows/SECUR32.cs
Normal file
376
Desktop.Native/Windows/SECUR32.cs
Normal file
@ -0,0 +1,376 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Native.Windows;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
public static class SECUR32
|
||||
{
|
||||
public enum WinStatusCodes : uint
|
||||
{
|
||||
STATUS_SUCCESS = 0
|
||||
}
|
||||
|
||||
public enum WinErrors : uint
|
||||
{
|
||||
NO_ERROR = 0,
|
||||
}
|
||||
public enum WinLogonType
|
||||
{
|
||||
LOGON32_LOGON_INTERACTIVE = 2,
|
||||
LOGON32_LOGON_NETWORK = 3,
|
||||
LOGON32_LOGON_BATCH = 4,
|
||||
LOGON32_LOGON_SERVICE = 5,
|
||||
LOGON32_LOGON_UNLOCK = 7,
|
||||
LOGON32_LOGON_NETWORK_CLEARTEXT = 8,
|
||||
LOGON32_LOGON_NEW_CREDENTIALS = 9
|
||||
}
|
||||
|
||||
// SECURITY_LOGON_TYPE
|
||||
public enum SecurityLogonType
|
||||
{
|
||||
Interactive = 2, // Interactively logged on (locally or remotely)
|
||||
Network, // Accessing system via network
|
||||
Batch, // Started via a batch queue
|
||||
Service, // Service started by service controller
|
||||
Proxy, // Proxy logon
|
||||
Unlock, // Unlock workstation
|
||||
NetworkCleartext, // Network logon with cleartext credentials
|
||||
NewCredentials, // Clone caller, new default credentials
|
||||
RemoteInteractive, // Remote, yet interactive. Terminal server
|
||||
CachedInteractive, // Try cached credentials without hitting the net.
|
||||
CachedRemoteInteractive, // Same as RemoteInteractive, this is used internally for auditing purpose
|
||||
CachedUnlock // Cached Unlock workstation
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct LSA_UNICODE_STRING
|
||||
{
|
||||
public UInt16 Length;
|
||||
public UInt16 MaximumLength;
|
||||
public IntPtr Buffer;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct TOKEN_SOURCE
|
||||
{
|
||||
public TOKEN_SOURCE(string name)
|
||||
{
|
||||
SourceName = new byte[8];
|
||||
System.Text.Encoding.GetEncoding(1252).GetBytes(name, 0, name.Length, SourceName, 0);
|
||||
if (!ADVAPI32.AllocateLocallyUniqueId(out SourceIdentifier))
|
||||
throw new System.ComponentModel.Win32Exception();
|
||||
}
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] public byte[] SourceName;
|
||||
public IntPtr SourceIdentifier;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct KERB_INTERACTIVE_LOGON
|
||||
{
|
||||
public KERB_LOGON_SUBMIT_TYPE MessageType;
|
||||
public string LogonDomainName;
|
||||
public string UserName;
|
||||
public string Password;
|
||||
}
|
||||
public enum KERB_LOGON_SUBMIT_TYPE
|
||||
{
|
||||
KerbInteractiveLogon = 2,
|
||||
KerbSmartCardLogon = 6,
|
||||
KerbWorkstationUnlockLogon = 7,
|
||||
KerbSmartCardUnlockLogon = 8,
|
||||
KerbProxyLogon = 9,
|
||||
KerbTicketLogon = 10,
|
||||
KerbTicketUnlockLogon = 11,
|
||||
KerbS4ULogon = 12,
|
||||
KerbCertificateLogon = 13,
|
||||
KerbCertificateS4ULogon = 14,
|
||||
KerbCertificateUnlockLogon = 15
|
||||
}
|
||||
public enum TOKEN_INFORMATION_CLASS
|
||||
{
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_USER structure that contains the user account of the token.
|
||||
/// </summary>
|
||||
TokenUser = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_GROUPS structure that contains the group accounts associated with the token.
|
||||
/// </summary>
|
||||
TokenGroups,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_PRIVILEGES structure that contains the privileges of the token.
|
||||
/// </summary>
|
||||
TokenPrivileges,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_OWNER structure that contains the default owner security identifier (SID) for newly created objects.
|
||||
/// </summary>
|
||||
TokenOwner,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_PRIMARY_GROUP structure that contains the default primary group SID for newly created objects.
|
||||
/// </summary>
|
||||
TokenPrimaryGroup,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_DEFAULT_DACL structure that contains the default DACL for newly created objects.
|
||||
/// </summary>
|
||||
TokenDefaultDacl,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_SOURCE structure that contains the source of the token. TOKEN_QUERY_SOURCE access is needed to retrieve this information.
|
||||
/// </summary>
|
||||
TokenSource,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_TYPE value that indicates whether the token is a primary or impersonation token.
|
||||
/// </summary>
|
||||
TokenType,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a SECURITY_IMPERSONATION_LEVEL value that indicates the impersonation level of the token. If the access token is not an impersonation token, the function fails.
|
||||
/// </summary>
|
||||
TokenImpersonationLevel,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_STATISTICS structure that contains various token statistics.
|
||||
/// </summary>
|
||||
TokenStatistics,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_GROUPS structure that contains the list of restricting SIDs in a restricted token.
|
||||
/// </summary>
|
||||
TokenRestrictedSids,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a DWORD value that indicates the Terminal Services session identifier that is associated with the token.
|
||||
/// </summary>
|
||||
TokenSessionId,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_GROUPS_AND_PRIVILEGES structure that contains the user SID, the group accounts, the restricted SIDs, and the authentication ID associated with the token.
|
||||
/// </summary>
|
||||
TokenGroupsAndPrivileges,
|
||||
|
||||
/// <summary>
|
||||
/// Reserved.
|
||||
/// </summary>
|
||||
TokenSessionReference,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a DWORD value that is nonzero if the token includes the SANDBOX_INERT flag.
|
||||
/// </summary>
|
||||
TokenSandBoxInert,
|
||||
|
||||
/// <summary>
|
||||
/// Reserved.
|
||||
/// </summary>
|
||||
TokenAuditPolicy,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_ORIGIN value.
|
||||
/// </summary>
|
||||
TokenOrigin,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_ELEVATION_TYPE value that specifies the elevation level of the token.
|
||||
/// </summary>
|
||||
TokenElevationType,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_LINKED_TOKEN structure that contains a handle to another token that is linked to this token.
|
||||
/// </summary>
|
||||
TokenLinkedToken,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_ELEVATION structure that specifies whether the token is elevated.
|
||||
/// </summary>
|
||||
TokenElevation,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a DWORD value that is nonzero if the token has ever been filtered.
|
||||
/// </summary>
|
||||
TokenHasRestrictions,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_ACCESS_INFORMATION structure that specifies security information contained in the token.
|
||||
/// </summary>
|
||||
TokenAccessInformation,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a DWORD value that is nonzero if virtualization is allowed for the token.
|
||||
/// </summary>
|
||||
TokenVirtualizationAllowed,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a DWORD value that is nonzero if virtualization is enabled for the token.
|
||||
/// </summary>
|
||||
TokenVirtualizationEnabled,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_MANDATORY_LABEL structure that specifies the token's integrity level.
|
||||
/// </summary>
|
||||
TokenIntegrityLevel,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a DWORD value that is nonzero if the token has the UIAccess flag set.
|
||||
/// </summary>
|
||||
TokenUIAccess,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives a TOKEN_MANDATORY_POLICY structure that specifies the token's mandatory integrity policy.
|
||||
/// </summary>
|
||||
TokenMandatoryPolicy,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer receives the token's logon security identifier (SID).
|
||||
/// </summary>
|
||||
TokenLogonSid,
|
||||
|
||||
/// <summary>
|
||||
/// The maximum value for this enumeration
|
||||
/// </summary>
|
||||
MaxTokenInfoClass
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct QUOTA_LIMITS
|
||||
{
|
||||
readonly UInt32 PagedPoolLimit;
|
||||
readonly UInt32 NonPagedPoolLimit;
|
||||
readonly UInt32 MinimumWorkingSetSize;
|
||||
readonly UInt32 MaximumWorkingSetSize;
|
||||
readonly UInt32 PagefileLimit;
|
||||
readonly Int64 TimeLimit;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct LSA_STRING
|
||||
{
|
||||
public UInt16 Length;
|
||||
public UInt16 MaximumLength;
|
||||
public /*PCHAR*/ IntPtr Buffer;
|
||||
}
|
||||
|
||||
|
||||
[DllImport("secur32.dll", SetLastError = true)]
|
||||
public static extern WinStatusCodes LsaLogonUser(
|
||||
[In] IntPtr LsaHandle,
|
||||
[In] ref LSA_STRING OriginName,
|
||||
[In] SecurityLogonType LogonType,
|
||||
[In] UInt32 AuthenticationPackage,
|
||||
[In] IntPtr AuthenticationInformation,
|
||||
[In] UInt32 AuthenticationInformationLength,
|
||||
[In] /*PTOKEN_GROUPS*/ IntPtr LocalGroups,
|
||||
[In] ref TOKEN_SOURCE SourceContext,
|
||||
[Out] /*PVOID*/ out IntPtr ProfileBuffer,
|
||||
[Out] out UInt32 ProfileBufferLength,
|
||||
[Out] out Int64 LogonId,
|
||||
[Out] out IntPtr Token,
|
||||
[Out] out QUOTA_LIMITS Quotas,
|
||||
[Out] out WinStatusCodes SubStatus
|
||||
);
|
||||
|
||||
[DllImport("secur32.dll", SetLastError = true)]
|
||||
public static extern WinStatusCodes LsaRegisterLogonProcess(
|
||||
IntPtr LogonProcessName,
|
||||
out IntPtr LsaHandle,
|
||||
out ulong SecurityMode
|
||||
);
|
||||
|
||||
[DllImport("secur32.dll", SetLastError = false)]
|
||||
public static extern WinStatusCodes LsaLookupAuthenticationPackage([In] IntPtr LsaHandle, [In] ref LSA_STRING PackageName, [Out] out UInt32 AuthenticationPackage);
|
||||
|
||||
[DllImport("secur32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
[ResourceExposure(ResourceScope.None)]
|
||||
internal static extern int LsaConnectUntrusted(
|
||||
[In, Out] ref SafeLsaLogonProcessHandle LsaHandle);
|
||||
|
||||
[DllImport("secur32.dll", SetLastError = false)]
|
||||
public static extern WinStatusCodes LsaConnectUntrusted([Out] out IntPtr LsaHandle);
|
||||
|
||||
[System.Security.SecurityCritical] // auto-generated
|
||||
internal sealed class SafeLsaLogonProcessHandle : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
private SafeLsaLogonProcessHandle() : base(true) { }
|
||||
|
||||
// 0 is an Invalid Handle
|
||||
internal SafeLsaLogonProcessHandle(IntPtr handle) : base(true)
|
||||
{
|
||||
SetHandle(handle);
|
||||
}
|
||||
|
||||
internal static SafeLsaLogonProcessHandle InvalidHandle
|
||||
{
|
||||
get { return new SafeLsaLogonProcessHandle(IntPtr.Zero); }
|
||||
}
|
||||
|
||||
[System.Security.SecurityCritical]
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
// LsaDeregisterLogonProcess returns an NTSTATUS
|
||||
return LsaDeregisterLogonProcess(handle) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("secur32.dll", SetLastError = true)]
|
||||
[ResourceExposure(ResourceScope.None)]
|
||||
internal static extern int LsaDeregisterLogonProcess(IntPtr handle);
|
||||
|
||||
|
||||
public static void CreateNewSession()
|
||||
{
|
||||
var kli = new SECUR32.KERB_INTERACTIVE_LOGON()
|
||||
{
|
||||
MessageType = SECUR32.KERB_LOGON_SUBMIT_TYPE.KerbInteractiveLogon,
|
||||
UserName = "",
|
||||
Password = ""
|
||||
};
|
||||
IntPtr kerbLogInfo;
|
||||
SECUR32.LSA_STRING logonProc = new()
|
||||
{
|
||||
Buffer = Marshal.StringToHGlobalAuto("InstaLogon"),
|
||||
Length = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("InstaLogon")),
|
||||
MaximumLength = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("InstaLogon"))
|
||||
};
|
||||
SECUR32.LSA_STRING originName = new()
|
||||
{
|
||||
Buffer = Marshal.StringToHGlobalAuto("InstaLogon"),
|
||||
Length = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("InstaLogon")),
|
||||
MaximumLength = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("InstaLogon"))
|
||||
};
|
||||
SECUR32.LSA_STRING authPackage = new()
|
||||
{
|
||||
Buffer = Marshal.StringToHGlobalAuto("MICROSOFT_KERBEROS_NAME_A"),
|
||||
Length = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("MICROSOFT_KERBEROS_NAME_A")),
|
||||
MaximumLength = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("MICROSOFT_KERBEROS_NAME_A"))
|
||||
};
|
||||
IntPtr hLogonProc = Marshal.AllocHGlobal(Marshal.SizeOf(logonProc));
|
||||
Marshal.StructureToPtr(logonProc, hLogonProc, false);
|
||||
ADVAPI32.AllocateLocallyUniqueId(out IntPtr pluid);
|
||||
LsaConnectUntrusted(out IntPtr lsaHan);
|
||||
//SECUR32.LsaRegisterLogonProcess(hLogonProc, out lsaHan, out secMode);
|
||||
SECUR32.LsaLookupAuthenticationPackage(lsaHan, ref authPackage, out uint authPackID);
|
||||
|
||||
kerbLogInfo = Marshal.AllocHGlobal(Marshal.SizeOf(kli));
|
||||
Marshal.StructureToPtr(kli, kerbLogInfo, false);
|
||||
|
||||
var ts = new SECUR32.TOKEN_SOURCE("Insta");
|
||||
SECUR32.LsaLogonUser(
|
||||
lsaHan,
|
||||
ref originName,
|
||||
SECUR32.SecurityLogonType.Interactive,
|
||||
authPackID,
|
||||
kerbLogInfo,
|
||||
(uint)Marshal.SizeOf(kerbLogInfo),
|
||||
IntPtr.Zero,
|
||||
ref ts,
|
||||
out IntPtr profBuf,
|
||||
out uint profBufLen,
|
||||
out long logonID,
|
||||
out IntPtr logonToken,
|
||||
out QUOTA_LIMITS quotas,
|
||||
out WinStatusCodes subStatus);
|
||||
}
|
||||
}
|
||||
51
Desktop.Native/Windows/Shlwapi.cs
Normal file
51
Desktop.Native/Windows/Shlwapi.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Native.Windows;
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-isos
|
||||
public class Shlwapi
|
||||
{
|
||||
[DllImport("shlwapi.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool IsOS(OsType osType);
|
||||
}
|
||||
|
||||
public enum OsType
|
||||
{
|
||||
OS_WINDOWS = 0,
|
||||
OS_NT = 1,
|
||||
OS_WIN95ORGREATER = 2,
|
||||
OS_NT4ORGREATER = 3,
|
||||
OS_WIN98ORGREATER = 5,
|
||||
OS_WIN98_GOLD = 6,
|
||||
OS_WIN2000ORGREATER = 7,
|
||||
OS_WIN2000PRO = 8,
|
||||
OS_WIN2000SERVER = 9,
|
||||
OS_WIN2000ADVSERVER = 10,
|
||||
OS_WIN2000DATACENTER = 11,
|
||||
OS_WIN2000TERMINAL = 12,
|
||||
OS_EMBEDDED = 13,
|
||||
OS_TERMINALCLIENT = 14,
|
||||
OS_TERMINALREMOTEADMIN = 15,
|
||||
OS_WIN95_GOLD = 16,
|
||||
OS_MEORGREATER = 17,
|
||||
OS_XPORGREATER = 18,
|
||||
OS_HOME = 19,
|
||||
OS_PROFESSIONAL = 20,
|
||||
OS_DATACENTER = 21,
|
||||
OS_ADVSERVER = 22,
|
||||
OS_SERVER = 23,
|
||||
OS_TERMINALSERVER = 24,
|
||||
OS_PERSONALTERMINALSERVER = 25,
|
||||
OS_FASTUSERSWITCHING = 26,
|
||||
OS_WELCOMELOGONUI = 27,
|
||||
OS_DOMAINMEMBER = 28,
|
||||
OS_ANYSERVER = 29,
|
||||
OS_WOW6432 = 30,
|
||||
OS_WEBSERVER = 31,
|
||||
OS_SMALLBUSINESSSERVER = 32,
|
||||
OS_TABLETPC = 33,
|
||||
OS_SERVERADMINUI = 34,
|
||||
OS_MEDIACENTER = 35,
|
||||
OS_APPLIANCE = 36,
|
||||
}
|
||||
1348
Desktop.Native/Windows/User32.cs
Normal file
1348
Desktop.Native/Windows/User32.cs
Normal file
File diff suppressed because it is too large
Load Diff
79
Desktop.Native/Windows/WTSAPI32.cs
Normal file
79
Desktop.Native/Windows/WTSAPI32.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Native.Windows;
|
||||
|
||||
public static class WTSAPI32
|
||||
{
|
||||
public static nint WTS_CURRENT_SERVER_HANDLE = nint.Zero;
|
||||
|
||||
public enum WTS_CONNECTSTATE_CLASS
|
||||
{
|
||||
WTSActive,
|
||||
WTSConnected,
|
||||
WTSConnectQuery,
|
||||
WTSShadow,
|
||||
WTSDisconnected,
|
||||
WTSIdle,
|
||||
WTSListen,
|
||||
WTSReset,
|
||||
WTSDown,
|
||||
WTSInit
|
||||
}
|
||||
|
||||
public enum WTS_INFO_CLASS
|
||||
{
|
||||
WTSInitialProgram,
|
||||
WTSApplicationName,
|
||||
WTSWorkingDirectory,
|
||||
WTSOEMId,
|
||||
WTSSessionId,
|
||||
WTSUserName,
|
||||
WTSWinStationName,
|
||||
WTSDomainName,
|
||||
WTSConnectState,
|
||||
WTSClientBuildNumber,
|
||||
WTSClientName,
|
||||
WTSClientDirectory,
|
||||
WTSClientProductId,
|
||||
WTSClientHardwareId,
|
||||
WTSClientAddress,
|
||||
WTSClientDisplay,
|
||||
WTSClientProtocolType,
|
||||
WTSIdleTime,
|
||||
WTSLogonTime,
|
||||
WTSIncomingBytes,
|
||||
WTSOutgoingBytes,
|
||||
WTSIncomingFrames,
|
||||
WTSOutgoingFrames,
|
||||
WTSClientInfo,
|
||||
WTSSessionInfo
|
||||
}
|
||||
|
||||
|
||||
[DllImport("wtsapi32.dll", SetLastError = true)]
|
||||
public static extern int WTSEnumerateSessions(
|
||||
nint hServer,
|
||||
int Reserved,
|
||||
int Version,
|
||||
ref nint ppSessionInfo,
|
||||
ref int pCount);
|
||||
|
||||
[DllImport("wtsapi32.dll", ExactSpelling = true, SetLastError = false)]
|
||||
public static extern void WTSFreeMemory(nint memory);
|
||||
|
||||
[DllImport("Wtsapi32.dll")]
|
||||
public static extern bool WTSQuerySessionInformation(nint hServer, uint sessionId, WTS_INFO_CLASS wtsInfoClass, out nint ppBuffer, out uint pBytesReturned);
|
||||
|
||||
[DllImport("wtsapi32.dll", SetLastError = true)]
|
||||
static extern nint WTSOpenServer(string pServerName);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct WTS_SESSION_INFO
|
||||
{
|
||||
public uint SessionID;
|
||||
[MarshalAs(UnmanagedType.LPStr)]
|
||||
public string pWinStationName;
|
||||
public WTS_CONNECTSTATE_CLASS State;
|
||||
}
|
||||
}
|
||||
281
Desktop.Native/Windows/Win32Interop.cs
Normal file
281
Desktop.Native/Windows/Win32Interop.cs
Normal file
@ -0,0 +1,281 @@
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using static Immense.RemoteControl.Desktop.Shared.Native.Windows.ADVAPI32;
|
||||
using static Immense.RemoteControl.Desktop.Shared.Native.Windows.User32;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Native.Windows;
|
||||
|
||||
// TODO: Use https://github.com/microsoft/CsWin32 for all p/invokes.
|
||||
public class Win32Interop
|
||||
{
|
||||
public static List<WindowsSession> GetActiveSessions()
|
||||
{
|
||||
var sessions = new List<WindowsSession>();
|
||||
var consoleSessionId = Kernel32.WTSGetActiveConsoleSessionId();
|
||||
sessions.Add(new WindowsSession()
|
||||
{
|
||||
Id = consoleSessionId,
|
||||
Type = WindowsSessionType.Console,
|
||||
Name = "Console",
|
||||
Username = GetUsernameFromSessionId(consoleSessionId)
|
||||
});
|
||||
|
||||
nint ppSessionInfo = nint.Zero;
|
||||
var count = 0;
|
||||
var enumSessionResult = WTSAPI32.WTSEnumerateSessions(WTSAPI32.WTS_CURRENT_SERVER_HANDLE, 0, 1, ref ppSessionInfo, ref count);
|
||||
var dataSize = Marshal.SizeOf(typeof(WTSAPI32.WTS_SESSION_INFO));
|
||||
var current = ppSessionInfo;
|
||||
|
||||
if (enumSessionResult != 0)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var wtsInfo = Marshal.PtrToStructure(current, typeof(WTSAPI32.WTS_SESSION_INFO));
|
||||
if (wtsInfo is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var sessionInfo = (WTSAPI32.WTS_SESSION_INFO)wtsInfo;
|
||||
current += dataSize;
|
||||
if (sessionInfo.State == WTSAPI32.WTS_CONNECTSTATE_CLASS.WTSActive && sessionInfo.SessionID != consoleSessionId)
|
||||
{
|
||||
|
||||
sessions.Add(new WindowsSession()
|
||||
{
|
||||
Id = sessionInfo.SessionID,
|
||||
Name = sessionInfo.pWinStationName,
|
||||
Type = WindowsSessionType.RDP,
|
||||
Username = GetUsernameFromSessionId(sessionInfo.SessionID)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sessions;
|
||||
}
|
||||
|
||||
public static string GetCommandLine()
|
||||
{
|
||||
var commandLinePtr = Kernel32.GetCommandLine();
|
||||
return Marshal.PtrToStringAuto(commandLinePtr) ?? string.Empty;
|
||||
}
|
||||
|
||||
public static bool GetCurrentDesktop([NotNullWhen(true)] out string? desktopName)
|
||||
{
|
||||
desktopName = null;
|
||||
var inputDesktop = OpenInputDesktop();
|
||||
try
|
||||
{
|
||||
if (TryGetDesktopName(inputDesktop, out desktopName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseDesktop(inputDesktop);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static string GetUsernameFromSessionId(uint sessionId)
|
||||
{
|
||||
var username = string.Empty;
|
||||
|
||||
if (WTSAPI32.WTSQuerySessionInformation(nint.Zero, sessionId, WTSAPI32.WTS_INFO_CLASS.WTSUserName, out var buffer, out var strLen) && strLen > 1)
|
||||
{
|
||||
username = Marshal.PtrToStringAnsi(buffer);
|
||||
WTSAPI32.WTSFreeMemory(buffer);
|
||||
}
|
||||
|
||||
return username ?? string.Empty;
|
||||
}
|
||||
|
||||
public static nint OpenInputDesktop()
|
||||
{
|
||||
return User32.OpenInputDesktop(0, true, ACCESS_MASK.GENERIC_ALL);
|
||||
}
|
||||
|
||||
public static bool CreateInteractiveSystemProcess(
|
||||
string commandLine,
|
||||
int targetSessionId,
|
||||
bool forceConsoleSession,
|
||||
string desktopName,
|
||||
bool hiddenWindow,
|
||||
out PROCESS_INFORMATION procInfo)
|
||||
{
|
||||
uint winlogonPid = 0;
|
||||
var hUserTokenDup = nint.Zero;
|
||||
var hPToken = nint.Zero;
|
||||
var hProcess = nint.Zero;
|
||||
|
||||
procInfo = new PROCESS_INFORMATION();
|
||||
|
||||
// If not force console, find target session. If not present,
|
||||
// use last active session.
|
||||
var dwSessionId = Kernel32.WTSGetActiveConsoleSessionId();
|
||||
if (!forceConsoleSession)
|
||||
{
|
||||
var activeSessions = GetActiveSessions();
|
||||
if (activeSessions.Any(x => x.Id == targetSessionId))
|
||||
{
|
||||
dwSessionId = (uint)targetSessionId;
|
||||
}
|
||||
else
|
||||
{
|
||||
dwSessionId = activeSessions.Last().Id;
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain the process ID of the winlogon process that is running within the currently active session.
|
||||
var processes = Process.GetProcessesByName("winlogon");
|
||||
foreach (Process p in processes)
|
||||
{
|
||||
if ((uint)p.SessionId == dwSessionId)
|
||||
{
|
||||
winlogonPid = (uint)p.Id;
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain a handle to the winlogon process.
|
||||
hProcess = Kernel32.OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);
|
||||
|
||||
// Obtain a handle to the access token of the winlogon process.
|
||||
if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken))
|
||||
{
|
||||
Kernel32.CloseHandle(hProcess);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Security attibute structure used in DuplicateTokenEx and CreateProcessAsUser.
|
||||
var sa = new SECURITY_ATTRIBUTES();
|
||||
sa.Length = Marshal.SizeOf(sa);
|
||||
|
||||
// Copy the access token of the winlogon process; the newly created token will be a primary token.
|
||||
if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa, SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, TOKEN_TYPE.TokenPrimary, out hUserTokenDup))
|
||||
{
|
||||
Kernel32.CloseHandle(hProcess);
|
||||
Kernel32.CloseHandle(hPToken);
|
||||
return false;
|
||||
}
|
||||
|
||||
// By default, CreateProcessAsUser creates a process on a non-interactive window station, meaning
|
||||
// the window station has a desktop that is invisible and the process is incapable of receiving
|
||||
// user input. To remedy this we set the lpDesktop parameter to indicate we want to enable user
|
||||
// interaction with the new process.
|
||||
var si = new STARTUPINFO();
|
||||
si.cb = Marshal.SizeOf(si);
|
||||
si.lpDesktop = @"winsta0\" + desktopName;
|
||||
|
||||
// Flags that specify the priority and creation method of the process.
|
||||
uint dwCreationFlags;
|
||||
if (hiddenWindow)
|
||||
{
|
||||
dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW;
|
||||
si.dwFlags = STARTF_USESHOWWINDOW;
|
||||
si.wShowWindow = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE;
|
||||
}
|
||||
|
||||
// Create a new process in the current user's logon session.
|
||||
var result = CreateProcessAsUser(
|
||||
hUserTokenDup,
|
||||
null,
|
||||
commandLine,
|
||||
ref sa,
|
||||
ref sa,
|
||||
false,
|
||||
dwCreationFlags,
|
||||
nint.Zero,
|
||||
null,
|
||||
ref si,
|
||||
out procInfo);
|
||||
|
||||
// Invalidate the handles.
|
||||
Kernel32.CloseHandle(hProcess);
|
||||
Kernel32.CloseHandle(hPToken);
|
||||
Kernel32.CloseHandle(hUserTokenDup);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void SetMonitorState(MonitorState state)
|
||||
{
|
||||
SendMessage(0xFFFF, 0x112, 0xF170, (int)state);
|
||||
}
|
||||
|
||||
public static MessageBoxResult ShowMessageBox(nint owner,
|
||||
string message,
|
||||
string caption,
|
||||
MessageBoxType messageBoxType)
|
||||
{
|
||||
return (MessageBoxResult)MessageBox(owner, message, caption, (long)messageBoxType);
|
||||
}
|
||||
|
||||
public static bool SwitchToInputDesktop()
|
||||
{
|
||||
try
|
||||
{
|
||||
var inputDesktop = OpenInputDesktop();
|
||||
|
||||
try
|
||||
{
|
||||
if (inputDesktop == nint.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return SetThreadDesktop(inputDesktop);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseDesktop(inputDesktop);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetConsoleWindowVisibility(bool isVisible)
|
||||
{
|
||||
var handle = Kernel32.GetConsoleWindow();
|
||||
|
||||
if (isVisible)
|
||||
{
|
||||
ShowWindow(handle, (int)SW.SW_SHOW);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowWindow(handle, (int)SW.SW_HIDE);
|
||||
}
|
||||
|
||||
Kernel32.CloseHandle(handle);
|
||||
}
|
||||
|
||||
public static bool TryGetDesktopName(nint desktopHandle, [NotNullWhen(true)] out string? desktopName)
|
||||
{
|
||||
var deskBytes = new byte[256];
|
||||
if (!GetUserObjectInformationW(desktopHandle, UOI_NAME, deskBytes, 256, out uint lenNeeded))
|
||||
{
|
||||
desktopName = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
desktopName = Encoding.Unicode
|
||||
.GetString(deskBytes.Take((int)lenNeeded).ToArray())
|
||||
.Replace("\0", "");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
6
Desktop.Shared/Abstractions/IAppStartup.cs
Normal file
6
Desktop.Shared/Abstractions/IAppStartup.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
|
||||
public interface IAppStartup
|
||||
{
|
||||
Task Run();
|
||||
}
|
||||
7
Desktop.Shared/Abstractions/IAudioCapturer.cs
Normal file
7
Desktop.Shared/Abstractions/IAudioCapturer.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
|
||||
public interface IAudioCapturer
|
||||
{
|
||||
event EventHandler<byte[]> AudioSampleReady;
|
||||
void ToggleAudio(bool toggleOn);
|
||||
}
|
||||
11
Desktop.Shared/Abstractions/IBrandingProvider.cs
Normal file
11
Desktop.Shared/Abstractions/IBrandingProvider.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using Remotely.Shared.Entities;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
|
||||
public interface IBrandingProvider
|
||||
{
|
||||
BrandingInfo CurrentBranding { get; }
|
||||
Task Initialize();
|
||||
void SetBrandingInfo(BrandingInfo brandingInfo);
|
||||
}
|
||||
11
Desktop.Shared/Abstractions/IChatUiService.cs
Normal file
11
Desktop.Shared/Abstractions/IChatUiService.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Remotely.Shared.Models;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
|
||||
public interface IChatUiService
|
||||
{
|
||||
event EventHandler ChatWindowClosed;
|
||||
|
||||
void ShowChatWindow(string organizationName, StreamWriter writer);
|
||||
Task ReceiveChat(ChatMessage chatMessage);
|
||||
}
|
||||
10
Desktop.Shared/Abstractions/IClipboardService.cs
Normal file
10
Desktop.Shared/Abstractions/IClipboardService.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
|
||||
public interface IClipboardService
|
||||
{
|
||||
event EventHandler<string> ClipboardTextChanged;
|
||||
|
||||
void BeginWatching();
|
||||
|
||||
Task SetText(string clipboardText);
|
||||
}
|
||||
11
Desktop.Shared/Abstractions/ICursorIconWatcher.cs
Normal file
11
Desktop.Shared/Abstractions/ICursorIconWatcher.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
|
||||
public interface ICursorIconWatcher
|
||||
{
|
||||
[Obsolete("This should be replaced with a message published by IMessenger.")]
|
||||
event EventHandler<CursorInfo> OnChange;
|
||||
|
||||
CursorInfo GetCurrentCursor();
|
||||
}
|
||||
13
Desktop.Shared/Abstractions/IFileTransferService.cs
Normal file
13
Desktop.Shared/Abstractions/IFileTransferService.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Immense.RemoteControl.Desktop.Shared.ViewModels;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
|
||||
public interface IFileTransferService
|
||||
{
|
||||
string GetBaseDirectory();
|
||||
|
||||
Task ReceiveFile(byte[] buffer, string fileName, string messageId, bool endOfFile, bool startOfFile);
|
||||
void OpenFileTransferWindow(IViewer viewer);
|
||||
Task UploadFile(FileUpload file, IViewer viewer, Action<double> progressUpdateCallback, CancellationToken cancelToken);
|
||||
}
|
||||
17
Desktop.Shared/Abstractions/IKeyboardMouseInput.cs
Normal file
17
Desktop.Shared/Abstractions/IKeyboardMouseInput.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Enums;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
|
||||
public interface IKeyboardMouseInput
|
||||
{
|
||||
void Init();
|
||||
void SendKeyDown(string key);
|
||||
void SendKeyUp(string key);
|
||||
void SendMouseMove(double percentX, double percentY, IViewer viewer);
|
||||
void SendMouseWheel(int deltaY);
|
||||
void SendText(string transferText);
|
||||
void ToggleBlockInput(bool toggleOn);
|
||||
void SetKeyStatesUp();
|
||||
void SendMouseButtonAction(int button, ButtonAction buttonAction, double percentX, double percentY, IViewer viewer);
|
||||
}
|
||||
10
Desktop.Shared/Abstractions/IRemoteControlAccessService.cs
Normal file
10
Desktop.Shared/Abstractions/IRemoteControlAccessService.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using Immense.RemoteControl.Shared.Enums;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
|
||||
public interface IRemoteControlAccessService
|
||||
{
|
||||
bool IsPromptOpen { get; }
|
||||
|
||||
Task<PromptForAccessResult> PromptForAccess(string requesterName, string organizationName);
|
||||
}
|
||||
29
Desktop.Shared/Abstractions/IScreenCapturer.cs
Normal file
29
Desktop.Shared/Abstractions/IScreenCapturer.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using Remotely.Shared.Primitives;
|
||||
using SkiaSharp;
|
||||
using System.Drawing;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
|
||||
public interface IScreenCapturer : IDisposable
|
||||
{
|
||||
event EventHandler<Rectangle> ScreenChanged;
|
||||
|
||||
bool CaptureFullscreen { get; set; }
|
||||
Rectangle CurrentScreenBounds { get; }
|
||||
bool IsGpuAccelerated { get; }
|
||||
string SelectedScreen { get; }
|
||||
IEnumerable<string> GetDisplayNames();
|
||||
SKRect GetFrameDiffArea();
|
||||
|
||||
Result<SKBitmap> GetImageDiff();
|
||||
|
||||
Result<SKBitmap> GetNextFrame();
|
||||
|
||||
int GetScreenCount();
|
||||
|
||||
Rectangle GetVirtualScreenBounds();
|
||||
|
||||
void Init();
|
||||
|
||||
void SetSelectedScreen(string displayName);
|
||||
}
|
||||
6
Desktop.Shared/Abstractions/ISessionIndicator.cs
Normal file
6
Desktop.Shared/Abstractions/ISessionIndicator.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
|
||||
public interface ISessionIndicator
|
||||
{
|
||||
void Show();
|
||||
}
|
||||
6
Desktop.Shared/Abstractions/IShutdownService.cs
Normal file
6
Desktop.Shared/Abstractions/IShutdownService.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
|
||||
public interface IShutdownService
|
||||
{
|
||||
Task Shutdown();
|
||||
}
|
||||
BIN
Desktop.Shared/Assets/DefaultIcon.ico
Normal file
BIN
Desktop.Shared/Assets/DefaultIcon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
BIN
Desktop.Shared/Assets/DefaultIcon.png
Normal file
BIN
Desktop.Shared/Assets/DefaultIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
@ -4,24 +4,39 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\favicon.ico" />
|
||||
<None Remove="Assets\Remotely_Icon.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Assets\Remotely_Icon.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bitbound.SimpleMessenger" Version="2.2.1" />
|
||||
<PackageReference Include="CommunityToolkit.Diagnostics" Version="8.2.2" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.8" />
|
||||
<PackageReference Include="SkiaSharp.Views.Desktop.Common" Version="2.88.8" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageReference Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta4.22272.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Desktop.Native\Desktop.Native.csproj" />
|
||||
<ProjectReference Include="..\Shared\Shared.csproj" />
|
||||
<ProjectReference Include="..\submodules\Immense.RemoteControl\Immense.RemoteControl.Desktop.Shared\Immense.RemoteControl.Desktop.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="Assets\favicon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Native\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
8
Desktop.Shared/Enums/AppMode.cs
Normal file
8
Desktop.Shared/Enums/AppMode.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Enums;
|
||||
|
||||
public enum AppMode
|
||||
{
|
||||
Unattended,
|
||||
Attended,
|
||||
Chat
|
||||
}
|
||||
7
Desktop.Shared/Enums/ButtonAction.cs
Normal file
7
Desktop.Shared/Enums/ButtonAction.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Enums;
|
||||
|
||||
public enum ButtonAction
|
||||
{
|
||||
Down,
|
||||
Up
|
||||
}
|
||||
11
Desktop.Shared/Extensions/SKBitmapExtensions.cs
Normal file
11
Desktop.Shared/Extensions/SKBitmapExtensions.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Extensions;
|
||||
|
||||
public static class SKBitmapExtensions
|
||||
{
|
||||
public static SKRect ToRectangle(this SKBitmap bitmap)
|
||||
{
|
||||
return new SKRect(0, 0, bitmap.Width, bitmap.Height);
|
||||
}
|
||||
}
|
||||
11
Desktop.Shared/Messages/AppStateHostChangedMessage.cs
Normal file
11
Desktop.Shared/Messages/AppStateHostChangedMessage.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Messages;
|
||||
|
||||
public class AppStateHostChangedMessage
|
||||
{
|
||||
public AppStateHostChangedMessage(string newHost)
|
||||
{
|
||||
NewHost = newHost;
|
||||
}
|
||||
|
||||
public string NewHost { get; }
|
||||
}
|
||||
2
Desktop.Shared/Messages/DisplaySettingsChangedMessage.cs
Normal file
2
Desktop.Shared/Messages/DisplaySettingsChangedMessage.cs
Normal file
@ -0,0 +1,2 @@
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Messages;
|
||||
public record DisplaySettingsChangedMessage();
|
||||
13
Desktop.Shared/Messages/WindowsSessionEndingMessage.cs
Normal file
13
Desktop.Shared/Messages/WindowsSessionEndingMessage.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Immense.RemoteControl.Shared.Enums;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Messages;
|
||||
|
||||
public class WindowsSessionEndingMessage
|
||||
{
|
||||
public WindowsSessionEndingMessage(SessionEndReasonsEx reason)
|
||||
{
|
||||
Reason = reason;
|
||||
}
|
||||
|
||||
public SessionEndReasonsEx Reason { get; }
|
||||
}
|
||||
15
Desktop.Shared/Messages/WindowsSessionSwitchedMessage.cs
Normal file
15
Desktop.Shared/Messages/WindowsSessionSwitchedMessage.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Immense.RemoteControl.Shared.Enums;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Messages;
|
||||
|
||||
public class WindowsSessionSwitchedMessage
|
||||
{
|
||||
public WindowsSessionSwitchedMessage(SessionSwitchReasonEx reason, int sessionId)
|
||||
{
|
||||
Reason = reason;
|
||||
SessionId = sessionId;
|
||||
}
|
||||
|
||||
public SessionSwitchReasonEx Reason { get; }
|
||||
public int SessionId { get; }
|
||||
}
|
||||
3
Desktop.Shared/Properties/AssemblyInfo.cs
Normal file
3
Desktop.Shared/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Remotely_Desktop")]
|
||||
101
Desktop.Shared/Reactive/AsyncRelayCommand.cs
Normal file
101
Desktop.Shared/Reactive/AsyncRelayCommand.cs
Normal file
@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Reactive;
|
||||
|
||||
public class AsyncRelayCommand : ICommand
|
||||
{
|
||||
private readonly Func<bool> _canExecute;
|
||||
private readonly Func<Task> _execute;
|
||||
public AsyncRelayCommand(Func<Task> execute)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = () => true;
|
||||
}
|
||||
|
||||
public AsyncRelayCommand(Func<Task> execute, Func<bool> canExecute)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return _canExecute.Invoke();
|
||||
}
|
||||
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
_execute.Invoke();
|
||||
}
|
||||
|
||||
public void NotifyCanExecuteChanged()
|
||||
{
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public class AsyncRelayCommand<T> : ICommand
|
||||
{
|
||||
private readonly Func<T?, bool> _canExecute;
|
||||
private readonly Func<T?, Task> _execute;
|
||||
|
||||
public AsyncRelayCommand(Func<T?, Task> execute)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = (parameter) => true;
|
||||
}
|
||||
|
||||
public AsyncRelayCommand(Func<T?, Task> execute, Func<T?, bool> canExecute)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
if (parameter is null)
|
||||
{
|
||||
return _canExecute.Invoke(default);
|
||||
}
|
||||
|
||||
if (parameter is not T typedParam)
|
||||
{
|
||||
throw new InvalidOperationException("Paramter is not of the correct type.");
|
||||
}
|
||||
|
||||
return _canExecute.Invoke(typedParam);
|
||||
}
|
||||
|
||||
// Async void is una*void*able here (heh, heh) due to ICommand's interface.
|
||||
// Though we shouldn't need to in modern .NET, we're handling UnobservedTaskException
|
||||
// in IServiceProviderExtensions.UseRemoteControl. In older versions of .NET, this
|
||||
// would have been required to prevent the app from terminating.
|
||||
public async void Execute(object? parameter)
|
||||
{
|
||||
if (parameter is null)
|
||||
{
|
||||
await _execute.Invoke(default);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parameter is not T typedParam)
|
||||
{
|
||||
throw new InvalidOperationException("Paramter is not of the correct type.");
|
||||
}
|
||||
|
||||
await _execute.Invoke(typedParam);
|
||||
}
|
||||
|
||||
public void NotifyCanExecuteChanged()
|
||||
{
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
45
Desktop.Shared/Reactive/ObservableObject.cs
Normal file
45
Desktop.Shared/Reactive/ObservableObject.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Reactive;
|
||||
|
||||
public class ObservableObject : INotifyPropertyChanged
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, object?> _backingFields = new();
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
protected T? Get<T>([CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (_backingFields.TryGetValue(propertyName, out var value) &&
|
||||
value is T typedValue)
|
||||
{
|
||||
return typedValue;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
protected T Get<T>(T defaultValue, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (_backingFields.TryGetValue(propertyName, out var value) &&
|
||||
value is T typedValue)
|
||||
{
|
||||
return typedValue;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
protected void Set<T>(T newValue, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
_backingFields.AddOrUpdate(propertyName, newValue, (k, v) => newValue);
|
||||
NotifyPropertyChanged(propertyName);
|
||||
}
|
||||
}
|
||||
104
Desktop.Shared/Reactive/RelayCommand.cs
Normal file
104
Desktop.Shared/Reactive/RelayCommand.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Reactive;
|
||||
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Func<bool> _canExecute;
|
||||
private readonly Action _execute;
|
||||
public RelayCommand(Action execute)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = () => true;
|
||||
}
|
||||
|
||||
public RelayCommand(Action execute, Func<bool> canExecute)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return _canExecute.Invoke();
|
||||
}
|
||||
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
_execute.Invoke();
|
||||
}
|
||||
|
||||
public void NotifyCanExecuteChanged()
|
||||
{
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public class RelayCommand<T> : ICommand
|
||||
{
|
||||
private readonly Func<T?, bool> _canExecute;
|
||||
private readonly Action<T?> _execute;
|
||||
|
||||
public RelayCommand(Action<T?> execute)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = (parameter) => true;
|
||||
}
|
||||
|
||||
public RelayCommand(Action<T?> execute, Func<T?, bool> canExecute)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
if (parameter is null)
|
||||
{
|
||||
return _canExecute.Invoke(default);
|
||||
}
|
||||
|
||||
if (parameter is not T typedParam)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Parameter is not of the correct type. " +
|
||||
$"Expected type {typeof(T)}. " +
|
||||
$"Received type {parameter.GetType()}.");
|
||||
}
|
||||
|
||||
return _canExecute.Invoke(typedParam);
|
||||
}
|
||||
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
if (parameter is null)
|
||||
{
|
||||
_execute.Invoke(default);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parameter is not T typedParam)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Parameter is not of the correct type. " +
|
||||
$"Expected type {typeof(T)}. " +
|
||||
$"Received type {parameter.GetType()}.");
|
||||
}
|
||||
|
||||
_execute.Invoke(typedParam);
|
||||
}
|
||||
|
||||
public void NotifyCanExecuteChanged()
|
||||
{
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
190
Desktop.Shared/Services/AppState.cs
Normal file
190
Desktop.Shared/Services/AppState.cs
Normal file
@ -0,0 +1,190 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Enums;
|
||||
using Immense.RemoteControl.Desktop.Shared.Messages;
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Bitbound.SimpleMessenger;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Services;
|
||||
|
||||
public interface IAppState
|
||||
{
|
||||
event EventHandler<ScreenCastRequest> ScreenCastRequested;
|
||||
|
||||
event EventHandler<IViewer> ViewerAdded;
|
||||
|
||||
event EventHandler<string> ViewerRemoved;
|
||||
|
||||
string AccessKey { get; }
|
||||
Dictionary<string, string> ArgDict { get; }
|
||||
string Host { get; set; }
|
||||
bool IsElevate { get; }
|
||||
bool IsRelaunch { get; }
|
||||
AppMode Mode { get; set; }
|
||||
string OrganizationName { get; }
|
||||
string PipeName { get; }
|
||||
string[] RelaunchViewers { get; }
|
||||
string RequesterName { get; }
|
||||
string SessionId { get; }
|
||||
ConcurrentDictionary<string, IViewer> Viewers { get; }
|
||||
|
||||
void Configure(
|
||||
string host,
|
||||
AppMode mode,
|
||||
string sessionId,
|
||||
string accessKey,
|
||||
string requesterName,
|
||||
string organizationName,
|
||||
string pipeName,
|
||||
bool relaunch,
|
||||
string viewers,
|
||||
bool elevate);
|
||||
|
||||
void InvokeScreenCastRequested(ScreenCastRequest viewerIdAndRequesterName);
|
||||
void InvokeViewerAdded(IViewer viewer);
|
||||
void InvokeViewerRemoved(string viewerID);
|
||||
void UpdateHost(string host);
|
||||
}
|
||||
|
||||
public class AppState : IAppState
|
||||
{
|
||||
private readonly Dictionary<string, string> _argDict = new();
|
||||
private readonly ILogger<AppState> _logger;
|
||||
private readonly IMessenger _messenger;
|
||||
private string _host = string.Empty;
|
||||
|
||||
private bool _isConfigured;
|
||||
|
||||
public AppState(IMessenger messenger, ILogger<AppState> logger)
|
||||
{
|
||||
_messenger = messenger;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public event EventHandler<ScreenCastRequest>? ScreenCastRequested;
|
||||
|
||||
public event EventHandler<IViewer>? ViewerAdded;
|
||||
|
||||
public event EventHandler<string>? ViewerRemoved;
|
||||
|
||||
public string AccessKey { get; private set; } = string.Empty;
|
||||
|
||||
public Dictionary<string, string> ArgDict
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_argDict.Any())
|
||||
{
|
||||
ProcessArgs();
|
||||
}
|
||||
return _argDict;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string Host
|
||||
{
|
||||
get => _host;
|
||||
set
|
||||
{
|
||||
_host = value?.Trim()?.TrimEnd('/') ?? string.Empty;
|
||||
_messenger.Send(new AppStateHostChangedMessage(_host));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsElevate { get; private set; }
|
||||
public bool IsRelaunch { get; private set; }
|
||||
|
||||
public AppMode Mode { get; set; }
|
||||
|
||||
public string OrganizationName { get; private set; } = string.Empty;
|
||||
|
||||
public string PipeName { get; private set; } = string.Empty;
|
||||
public string[] RelaunchViewers { get; private set; } = Array.Empty<string>();
|
||||
public string RequesterName { get; private set; } = string.Empty;
|
||||
public string SessionId { get; private set; } = string.Empty;
|
||||
public ConcurrentDictionary<string, IViewer> Viewers { get; } = new();
|
||||
public void Configure(
|
||||
string host,
|
||||
AppMode mode,
|
||||
string sessionId,
|
||||
string accessKey,
|
||||
string requesterName,
|
||||
string organizationName,
|
||||
string pipeName,
|
||||
bool relaunch,
|
||||
string viewers,
|
||||
bool elevate)
|
||||
{
|
||||
if (_isConfigured)
|
||||
{
|
||||
throw new InvalidOperationException("AppState has already been configured.");
|
||||
}
|
||||
|
||||
_isConfigured = true;
|
||||
Host = host;
|
||||
Mode = mode;
|
||||
SessionId = sessionId;
|
||||
AccessKey = accessKey;
|
||||
RequesterName = requesterName;
|
||||
OrganizationName = organizationName;
|
||||
PipeName = pipeName;
|
||||
IsRelaunch = relaunch;
|
||||
RelaunchViewers = viewers.Split(",");
|
||||
IsElevate = elevate;
|
||||
}
|
||||
|
||||
public void InvokeScreenCastRequested(ScreenCastRequest viewerIdAndRequesterName)
|
||||
{
|
||||
ScreenCastRequested?.Invoke(null, viewerIdAndRequesterName);
|
||||
}
|
||||
|
||||
public void InvokeViewerAdded(IViewer viewer)
|
||||
{
|
||||
ViewerAdded?.Invoke(null, viewer);
|
||||
}
|
||||
|
||||
public void InvokeViewerRemoved(string viewerID)
|
||||
{
|
||||
ViewerRemoved?.Invoke(null, viewerID);
|
||||
}
|
||||
|
||||
public void UpdateHost(string host)
|
||||
{
|
||||
Host = host;
|
||||
}
|
||||
|
||||
private void ProcessArgs()
|
||||
{
|
||||
var cmdArgs = Environment.GetCommandLineArgs();
|
||||
var args = Environment.GetCommandLineArgs()
|
||||
.SkipWhile(x => !x.StartsWith("-"))
|
||||
.ToArray();
|
||||
|
||||
for (var i = 0; i < args.Length; i += 2)
|
||||
{
|
||||
try
|
||||
{
|
||||
var key = args[i];
|
||||
if (key != null)
|
||||
{
|
||||
if (!key.Contains('-'))
|
||||
{
|
||||
_logger.LogWarning("Command line arguments are invalid. Key: {key}", key);
|
||||
i -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
key = key.Trim().TrimStart('-').TrimStart('-').ToLower();
|
||||
|
||||
_argDict.Add(key, args[i + 1].Trim());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while processing args.");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Immense.RemoteControl.Shared;
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remotely.Shared.Entities;
|
||||
using Remotely.Shared.Primitives;
|
||||
using Remotely.Shared.Services;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http.Json;
|
||||
@ -16,7 +16,7 @@ public class BrandingProvider : IBrandingProvider
|
||||
private readonly IEmbeddedServerDataProvider _embeddedDataSearcher;
|
||||
private readonly ILogger<BrandingProvider> _logger;
|
||||
private readonly IOrganizationIdProvider _orgIdProvider;
|
||||
private BrandingInfoBase? _brandingInfo;
|
||||
private BrandingInfo? _brandingInfo;
|
||||
|
||||
|
||||
public BrandingProvider(
|
||||
@ -31,7 +31,7 @@ public class BrandingProvider : IBrandingProvider
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public BrandingInfoBase CurrentBranding => _brandingInfo ??
|
||||
public BrandingInfo CurrentBranding => _brandingInfo ??
|
||||
throw new InvalidOperationException("Branding info has not been set or initialized.");
|
||||
|
||||
public async Task Initialize()
|
||||
@ -66,7 +66,7 @@ public class BrandingProvider : IBrandingProvider
|
||||
}
|
||||
}
|
||||
|
||||
public void SetBrandingInfo(BrandingInfoBase brandingInfo)
|
||||
public void SetBrandingInfo(BrandingInfo brandingInfo)
|
||||
{
|
||||
_brandingInfo = brandingInfo;
|
||||
}
|
||||
|
||||
88
Desktop.Shared/Services/ChatHostService.cs
Normal file
88
Desktop.Shared/Services/ChatHostService.cs
Normal file
@ -0,0 +1,88 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remotely.Shared.Models;
|
||||
using System.IO.Pipes;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Services;
|
||||
|
||||
public interface IChatHostService
|
||||
{
|
||||
Task StartChat(string requesterID, string organizationName);
|
||||
}
|
||||
public class ChatHostService : IChatHostService
|
||||
{
|
||||
private readonly IChatUiService _chatUiService;
|
||||
private readonly ILogger<ChatHostService> _logger;
|
||||
|
||||
private NamedPipeServerStream? _namedPipeStream;
|
||||
private StreamReader? _reader;
|
||||
private StreamWriter? _writer;
|
||||
|
||||
public ChatHostService(IChatUiService chatUiService, ILogger<ChatHostService> logger)
|
||||
{
|
||||
_chatUiService = chatUiService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartChat(string pipeName, string organizationName)
|
||||
{
|
||||
_namedPipeStream = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 10, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
|
||||
_writer = new StreamWriter(_namedPipeStream);
|
||||
_reader = new StreamReader(_namedPipeStream);
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Waiting for chat client to connect via pipe {name}.", pipeName);
|
||||
await _namedPipeStream.WaitForConnectionAsync(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning("A chat session was attempted, but the client failed to connect in time.");
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Chat client connected.");
|
||||
_chatUiService.ChatWindowClosed += OnChatWindowClosed;
|
||||
|
||||
_chatUiService.ShowChatWindow(organizationName, _writer);
|
||||
|
||||
_ = Task.Run(ReadFromStream);
|
||||
}
|
||||
|
||||
private void OnChatWindowClosed(object? sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_namedPipeStream?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task ReadFromStream()
|
||||
{
|
||||
while (_namedPipeStream?.IsConnected == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var messageJson = await _reader!.ReadLineAsync();
|
||||
if (!string.IsNullOrWhiteSpace(messageJson))
|
||||
{
|
||||
var chatMessage = JsonSerializer.Deserialize<ChatMessage>(messageJson);
|
||||
if (chatMessage is null)
|
||||
{
|
||||
_logger.LogWarning("Deserialized message was null. Value: {value}", messageJson);
|
||||
continue;
|
||||
}
|
||||
await _chatUiService.ReceiveChat(chatMessage);
|
||||
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while reading from chat IPC stream.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Desktop.Shared/Services/DesktopEnvironment.cs
Normal file
43
Desktop.Shared/Services/DesktopEnvironment.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Native.Linux;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace Desktop.Shared.Services;
|
||||
|
||||
public interface IDesktopEnvironment
|
||||
{
|
||||
bool IsElevated { get; }
|
||||
bool IsDebug { get; }
|
||||
}
|
||||
|
||||
internal class DesktopEnvironment : IDesktopEnvironment
|
||||
{
|
||||
public bool IsDebug
|
||||
{
|
||||
get
|
||||
{
|
||||
#if DEBUG
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsElevated
|
||||
{
|
||||
get
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
using var identity = WindowsIdentity.GetCurrent();
|
||||
var principal = new WindowsPrincipal(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
return Libc.geteuid() == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
461
Desktop.Shared/Services/DesktopHubConnection.cs
Normal file
461
Desktop.Shared/Services/DesktopHubConnection.cs
Normal file
@ -0,0 +1,461 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Messages;
|
||||
using Immense.RemoteControl.Desktop.Shared.Native.Windows;
|
||||
using Immense.RemoteControl.Shared.Enums;
|
||||
using Immense.RemoteControl.Shared.Interfaces;
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using Bitbound.SimpleMessenger;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remotely.Shared.Primitives;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Services;
|
||||
|
||||
public interface IDesktopHubConnection
|
||||
{
|
||||
HubConnection? Connection { get; }
|
||||
HubConnectionState ConnectionState { get; }
|
||||
bool IsConnected { get; }
|
||||
|
||||
Task<Result<TimeSpan>> CheckRoundtripLatency(string viewerConnectionId);
|
||||
Task<bool> Connect(TimeSpan timeout, CancellationToken cancellationToken);
|
||||
Task Disconnect();
|
||||
Task DisconnectAllViewers();
|
||||
Task DisconnectViewer(IViewer viewer, bool notifyViewer);
|
||||
Task<string> GetSessionID();
|
||||
Task NotifyRequesterUnattendedReady();
|
||||
Task NotifyViewersRelaunchedScreenCasterReady(string[] viewerIDs);
|
||||
Task SendAttendedSessionInfo(string machineName);
|
||||
|
||||
Task SendConnectionFailedToViewers(List<string> viewerIDs);
|
||||
Task SendConnectionRequestDenied(string viewerID);
|
||||
Task SendDtoToViewer<T>(T dto, string viewerId);
|
||||
|
||||
Task SendMessageToViewer(string viewerID, string message);
|
||||
Task<Result> SendUnattendedSessionInfo(string sessionId, string accessKey, string machineName, string requesterName, string organizationName);
|
||||
}
|
||||
|
||||
public class DesktopHubConnection : IDesktopHubConnection, IDesktopHubClient
|
||||
{
|
||||
private readonly IAppState _appState;
|
||||
|
||||
private readonly ILogger<DesktopHubConnection> _logger;
|
||||
private readonly IDtoMessageHandler _messageHandler;
|
||||
private readonly IRemoteControlAccessService _remoteControlAccessService;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public DesktopHubConnection(
|
||||
IDtoMessageHandler messageHandler,
|
||||
IServiceProvider serviceProvider,
|
||||
IAppState appState,
|
||||
IRemoteControlAccessService remoteControlAccessService,
|
||||
IMessenger messenger,
|
||||
ILogger<DesktopHubConnection> logger)
|
||||
{
|
||||
_messageHandler = messageHandler;
|
||||
_remoteControlAccessService = remoteControlAccessService;
|
||||
_serviceProvider = serviceProvider;
|
||||
_appState = appState;
|
||||
_logger = logger;
|
||||
|
||||
messenger.Register<WindowsSessionEndingMessage>(this, HandleWindowsSessionEnding);
|
||||
messenger.Register<WindowsSessionSwitchedMessage>(this, HandleWindowsSessionChanged);
|
||||
}
|
||||
|
||||
public HubConnection? Connection { get; private set; }
|
||||
public HubConnectionState ConnectionState => Connection?.State ?? HubConnectionState.Disconnected;
|
||||
public bool IsConnected => Connection?.State == HubConnectionState.Connected;
|
||||
|
||||
public async Task<Result<TimeSpan>> CheckRoundtripLatency(string viewerConnectionId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return Result.Fail<TimeSpan>("Connection is not yet established.");
|
||||
}
|
||||
var sw = Stopwatch.StartNew();
|
||||
var result = await Connection.InvokeAsync<Result<string>>("PingViewer", viewerConnectionId);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return Result.Ok(sw.Elapsed);
|
||||
}
|
||||
return Result.Fail<TimeSpan>("Latency check failed.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to check latency.");
|
||||
return Result.Fail<TimeSpan>("An error occurred while checking latency.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> Connect(TimeSpan timeout, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Connection is not null)
|
||||
{
|
||||
await Connection.DisposeAsync();
|
||||
}
|
||||
|
||||
var result = BuildConnection();
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Connection = result.Value;
|
||||
|
||||
ApplyConnectionHandlers(Connection);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Connecting to server.");
|
||||
|
||||
await Connection.StartAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Connected to server.");
|
||||
|
||||
break;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogWarning("Failed to connect to server. Status Code: {code}", ex.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in hub connection.");
|
||||
}
|
||||
await Task.Delay(3_000, cancellationToken);
|
||||
|
||||
if (sw.Elapsed > timeout)
|
||||
{
|
||||
_logger.LogWarning("Timed out while trying to connect to desktop hub.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while connecting to hub.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Disconnect()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Connection is not null)
|
||||
{
|
||||
await Connection.StopAsync();
|
||||
await Connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error disconnecting websocket.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Disconnect(string reason)
|
||||
{
|
||||
_logger.LogInformation("Disconnecting caster socket. Reason: {reason}", reason);
|
||||
await DisconnectAllViewers();
|
||||
}
|
||||
|
||||
public async Task DisconnectAllViewers()
|
||||
{
|
||||
foreach (var viewer in _appState.Viewers.Values.ToList())
|
||||
{
|
||||
await DisconnectViewer(viewer, true);
|
||||
}
|
||||
}
|
||||
|
||||
public Task DisconnectViewer(IViewer viewer, bool notifyViewer)
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
viewer.DisconnectRequested = true;
|
||||
viewer.Dispose();
|
||||
return Connection.SendAsync("DisconnectViewer", viewer.ViewerConnectionId, notifyViewer);
|
||||
}
|
||||
|
||||
public Task GetScreenCast(
|
||||
string viewerId,
|
||||
string requesterName,
|
||||
bool notifyUser,
|
||||
Guid streamId)
|
||||
{
|
||||
// We don't want to tie up the invocation from the server, so we'll
|
||||
// start this in a new task.
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var screenCaster = _serviceProvider.GetRequiredService<IScreenCaster>();
|
||||
await screenCaster.BeginScreenCasting(
|
||||
new ScreenCastRequest()
|
||||
{
|
||||
NotifyUser = notifyUser,
|
||||
ViewerId = viewerId,
|
||||
RequesterName = requesterName,
|
||||
StreamId = streamId
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while casting screen.");
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<string> GetSessionID()
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return await Connection.InvokeAsync<string>("GetSessionID");
|
||||
}
|
||||
|
||||
public Task NotifyRequesterUnattendedReady()
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return Connection.SendAsync("NotifyRequesterUnattendedReady");
|
||||
}
|
||||
|
||||
|
||||
public Task NotifyViewersRelaunchedScreenCasterReady(string[] viewerIDs)
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return Connection.SendAsync("NotifyViewersRelaunchedScreenCasterReady", viewerIDs);
|
||||
}
|
||||
|
||||
public async Task<PromptForAccessResult> PromptForAccess(RemoteControlAccessRequest accessRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: Add this to Win32Interop service/interface when it's
|
||||
// extracted from current static class.
|
||||
if (OperatingSystem.IsWindows() &&
|
||||
Shlwapi.IsOS(OsType.OS_ANYSERVER) &&
|
||||
Process.GetCurrentProcess().SessionId == Kernel32.WTSGetActiveConsoleSessionId())
|
||||
{
|
||||
// Bypass "consent prompt" if we're targeting the console session
|
||||
// on a Windows Server OS.
|
||||
return PromptForAccessResult.Accepted;
|
||||
}
|
||||
await SendMessageToViewer(accessRequest.ViewerConnectionId, "Asking user for permission");
|
||||
return await _remoteControlAccessService.PromptForAccess(
|
||||
accessRequest.RequesterDisplayName,
|
||||
accessRequest.OrganizationName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while applying connection handlers.");
|
||||
return PromptForAccessResult.Error;
|
||||
}
|
||||
}
|
||||
|
||||
public Task RequestScreenCast(string viewerId, string requesterName, bool notifyUser, Guid streamId)
|
||||
{
|
||||
_appState.InvokeScreenCastRequested(new ScreenCastRequest()
|
||||
{
|
||||
NotifyUser = notifyUser,
|
||||
ViewerId = viewerId,
|
||||
RequesterName = requesterName,
|
||||
StreamId = streamId
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendAttendedSessionInfo(string machineName)
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return Connection.InvokeAsync("ReceiveAttendedSessionInfo", machineName);
|
||||
}
|
||||
|
||||
public Task SendConnectionFailedToViewers(List<string> viewerIDs)
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return Connection.SendAsync("SendConnectionFailedToViewers", viewerIDs);
|
||||
}
|
||||
|
||||
public Task SendConnectionRequestDenied(string viewerID)
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
return Connection.SendAsync("SendConnectionRequestDenied", viewerID);
|
||||
}
|
||||
|
||||
public async Task SendDtoToClient(byte[] dtoWrapper, string viewerConnectionId)
|
||||
{
|
||||
if (_appState.Viewers.TryGetValue(viewerConnectionId, out var viewer))
|
||||
{
|
||||
await _messageHandler.ParseMessage(viewer, dtoWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
public Task SendDtoToViewer<T>(T dto, string viewerId)
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var serializedDto = MessagePack.MessagePackSerializer.Serialize(dto);
|
||||
return Connection.SendAsync("SendDtoToViewer", serializedDto, viewerId);
|
||||
}
|
||||
|
||||
public Task SendMessageToViewer(string viewerID, string message)
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return Connection.SendAsync("SendMessageToViewer", viewerID, message);
|
||||
}
|
||||
|
||||
public async Task<Result> SendUnattendedSessionInfo(string unattendedSessionId, string accessKey, string machineName, string requesterName, string organizationName)
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return Result.Fail("Connection hasn't been made yet.");
|
||||
}
|
||||
|
||||
return await Connection.InvokeAsync<Result>("ReceiveUnattendedSessionInfo", unattendedSessionId, accessKey, machineName, requesterName, organizationName);
|
||||
}
|
||||
|
||||
public async Task ViewerDisconnected(string viewerId)
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Connection.SendAsync("DisconnectViewer", viewerId, false);
|
||||
if (_appState.Viewers.TryRemove(viewerId, out var viewer))
|
||||
{
|
||||
viewer.DisconnectRequested = true;
|
||||
viewer.Dispose();
|
||||
}
|
||||
_appState.InvokeViewerRemoved(viewerId);
|
||||
}
|
||||
|
||||
private void ApplyConnectionHandlers(HubConnection connection)
|
||||
{
|
||||
connection.Closed += (ex) =>
|
||||
{
|
||||
_logger.LogWarning(ex, "Connection closed.");
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
// TODO: Replace parameters with singular DTOs for both client and server methods.
|
||||
connection.On<string>(nameof(Disconnect), Disconnect);
|
||||
connection.On<string, string, bool, Guid>(nameof(GetScreenCast), GetScreenCast);
|
||||
connection.On<string, string, bool, Guid>(nameof(RequestScreenCast), RequestScreenCast);
|
||||
connection.On<byte[], string>(nameof(SendDtoToClient), SendDtoToClient);
|
||||
connection.On<string>(nameof(ViewerDisconnected), ViewerDisconnected);
|
||||
connection.On<RemoteControlAccessRequest, PromptForAccessResult>(nameof(PromptForAccess), PromptForAccess);
|
||||
}
|
||||
|
||||
private Result<HubConnection> BuildConnection()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Uri.TryCreate(_appState.Host, UriKind.Absolute, out _))
|
||||
{
|
||||
return Result.Fail<HubConnection>("Invalid server URI.");
|
||||
}
|
||||
|
||||
var builder = _serviceProvider.GetRequiredService<IHubConnectionBuilder>();
|
||||
|
||||
var connection = builder
|
||||
.WithUrl($"{_appState.Host.Trim().TrimEnd('/')}/hubs/desktop")
|
||||
.AddMessagePackProtocol()
|
||||
.WithAutomaticReconnect(new RetryPolicy())
|
||||
.Build();
|
||||
return Result.Ok(connection);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<HubConnection>(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleWindowsSessionChanged(object subscriber, WindowsSessionSwitchedMessage message)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Connection.SendAsync("NotifySessionChanged", message.Reason, message.SessionId);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while notifying of session change.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleWindowsSessionEnding(object subscriber, WindowsSessionEndingMessage message)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Connection is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Connection.SendAsync("NotifySessionEnding", message.Reason);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while notifying of session ending.");
|
||||
}
|
||||
}
|
||||
private class RetryPolicy : IRetryPolicy
|
||||
{
|
||||
public TimeSpan? NextRetryDelay(RetryContext retryContext)
|
||||
{
|
||||
return TimeSpan.FromSeconds(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
331
Desktop.Shared/Services/DtoMessageHandler.cs
Normal file
331
Desktop.Shared/Services/DtoMessageHandler.cs
Normal file
@ -0,0 +1,331 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Enums;
|
||||
using Immense.RemoteControl.Desktop.Shared.Native.Windows;
|
||||
using Immense.RemoteControl.Shared.Helpers;
|
||||
using Immense.RemoteControl.Shared.Models.Dtos;
|
||||
using MessagePack;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Services;
|
||||
|
||||
public interface IDtoMessageHandler
|
||||
{
|
||||
Task ParseMessage(IViewer viewer, byte[] message);
|
||||
}
|
||||
public class DtoMessageHandler : IDtoMessageHandler
|
||||
{
|
||||
private readonly IAudioCapturer _audioCapturer;
|
||||
|
||||
private readonly IClipboardService _clipboardService;
|
||||
|
||||
private readonly IFileTransferService _fileTransferService;
|
||||
|
||||
private readonly IKeyboardMouseInput _keyboardMouseInput;
|
||||
|
||||
private readonly ILogger<DtoMessageHandler> _logger;
|
||||
|
||||
|
||||
public DtoMessageHandler(
|
||||
IKeyboardMouseInput keyboardMouseInput,
|
||||
IAudioCapturer audioCapturer,
|
||||
IClipboardService clipboardService,
|
||||
IFileTransferService fileTransferService,
|
||||
ILogger<DtoMessageHandler> logger)
|
||||
{
|
||||
_keyboardMouseInput = keyboardMouseInput;
|
||||
_audioCapturer = audioCapturer;
|
||||
_clipboardService = clipboardService;
|
||||
_fileTransferService = fileTransferService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task ParseMessage(IViewer viewer, byte[] message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var wrapper = MessagePackSerializer.Deserialize<DtoWrapper>(message);
|
||||
|
||||
switch (wrapper.DtoType)
|
||||
{
|
||||
case DtoType.MouseMove:
|
||||
case DtoType.MouseDown:
|
||||
case DtoType.MouseUp:
|
||||
case DtoType.Tap:
|
||||
case DtoType.MouseWheel:
|
||||
case DtoType.KeyDown:
|
||||
case DtoType.KeyUp:
|
||||
case DtoType.CtrlAltDel:
|
||||
case DtoType.ToggleBlockInput:
|
||||
case DtoType.TextTransfer:
|
||||
case DtoType.KeyPress:
|
||||
case DtoType.SetKeyStatesUp:
|
||||
{
|
||||
if (!viewer.HasControl)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (wrapper.DtoType)
|
||||
{
|
||||
case DtoType.SelectScreen:
|
||||
SelectScreen(wrapper, viewer);
|
||||
break;
|
||||
case DtoType.MouseMove:
|
||||
MouseMove(wrapper, viewer);
|
||||
break;
|
||||
case DtoType.MouseDown:
|
||||
MouseDown(wrapper, viewer);
|
||||
break;
|
||||
case DtoType.MouseUp:
|
||||
MouseUp(wrapper, viewer);
|
||||
break;
|
||||
case DtoType.Tap:
|
||||
Tap(wrapper, viewer);
|
||||
break;
|
||||
case DtoType.MouseWheel:
|
||||
MouseWheel(wrapper);
|
||||
break;
|
||||
case DtoType.KeyDown:
|
||||
KeyDown(wrapper);
|
||||
break;
|
||||
case DtoType.KeyUp:
|
||||
KeyUp(wrapper);
|
||||
break;
|
||||
case DtoType.CtrlAltDel:
|
||||
CtrlAltDel();
|
||||
break;
|
||||
case DtoType.ToggleAudio:
|
||||
ToggleAudio(wrapper);
|
||||
break;
|
||||
case DtoType.ToggleBlockInput:
|
||||
ToggleBlockInput(wrapper);
|
||||
break;
|
||||
case DtoType.TextTransfer:
|
||||
await TransferText(wrapper);
|
||||
break;
|
||||
case DtoType.KeyPress:
|
||||
await KeyPress(wrapper);
|
||||
break;
|
||||
case DtoType.File:
|
||||
await DownloadFile(wrapper);
|
||||
break;
|
||||
case DtoType.WindowsSessions:
|
||||
await GetWindowsSessions(viewer);
|
||||
break;
|
||||
case DtoType.SetKeyStatesUp:
|
||||
SetKeyStatesUp();
|
||||
break;
|
||||
case DtoType.FrameReceived:
|
||||
HandleFrameReceived(wrapper, viewer);
|
||||
break;
|
||||
case DtoType.OpenFileTransferWindow:
|
||||
OpenFileTransferWindow(viewer);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while parsing message.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TransferText(DtoWrapper wrapper)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<TextTransferDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (dto!.TypeText)
|
||||
{
|
||||
_keyboardMouseInput.SendText(dto.Text);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _clipboardService.SetText(dto.Text);
|
||||
}
|
||||
}
|
||||
|
||||
private void CtrlAltDel()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// Might as well try both.
|
||||
User32.SendSAS(AsUser: false);
|
||||
User32.SendSAS(true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadFile(DtoWrapper wrapper)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<FileDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _fileTransferService.ReceiveFile(dto!.Buffer,
|
||||
dto.FileName,
|
||||
dto.MessageId,
|
||||
dto.EndOfFile,
|
||||
dto.StartOfFile);
|
||||
}
|
||||
|
||||
private async Task GetWindowsSessions(IViewer viewer)
|
||||
{
|
||||
await viewer.SendWindowsSessions();
|
||||
}
|
||||
|
||||
private void HandleFrameReceived(DtoWrapper wrapper, IViewer viewer)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<FrameReceivedDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(dto.Timestamp);
|
||||
viewer.SetLastFrameReceived(timestamp.ToLocalTime());
|
||||
}
|
||||
private void KeyDown(DtoWrapper wrapper)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<KeyDownDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (dto?.Key is null)
|
||||
{
|
||||
_logger.LogWarning("Key input is empty.");
|
||||
return;
|
||||
}
|
||||
_keyboardMouseInput.SendKeyDown(dto.Key);
|
||||
}
|
||||
|
||||
private async Task KeyPress(DtoWrapper wrapper)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<KeyPressDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (dto?.Key is null)
|
||||
{
|
||||
_logger.LogWarning("Key input is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
_keyboardMouseInput.SendKeyDown(dto.Key);
|
||||
await Task.Delay(1);
|
||||
_keyboardMouseInput.SendKeyUp(dto.Key);
|
||||
}
|
||||
|
||||
private void KeyUp(DtoWrapper wrapper)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<KeyUpDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (dto?.Key is null)
|
||||
{
|
||||
_logger.LogWarning("Key input is empty.");
|
||||
return;
|
||||
}
|
||||
_keyboardMouseInput.SendKeyUp(dto.Key);
|
||||
}
|
||||
|
||||
private void MouseDown(DtoWrapper wrapper, IViewer viewer)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<MouseDownDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_keyboardMouseInput.SendMouseButtonAction(dto!.Button, ButtonAction.Down, dto.PercentX, dto.PercentY, viewer);
|
||||
}
|
||||
|
||||
private void MouseMove(DtoWrapper wrapper, IViewer viewer)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<MouseMoveDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_keyboardMouseInput.SendMouseMove(dto!.PercentX, dto.PercentY, viewer);
|
||||
}
|
||||
|
||||
private void MouseUp(DtoWrapper wrapper, IViewer viewer)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<MouseUpDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_keyboardMouseInput.SendMouseButtonAction(dto!.Button, ButtonAction.Up, dto.PercentX, dto.PercentY, viewer);
|
||||
}
|
||||
|
||||
private void MouseWheel(DtoWrapper wrapper)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<MouseWheelDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_keyboardMouseInput.SendMouseWheel(-(int)dto!.DeltaY);
|
||||
}
|
||||
|
||||
private void OpenFileTransferWindow(IViewer viewer)
|
||||
{
|
||||
_fileTransferService.OpenFileTransferWindow(viewer);
|
||||
}
|
||||
|
||||
private void SelectScreen(DtoWrapper wrapper, IViewer viewer)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<SelectScreenDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
viewer.Capturer.SetSelectedScreen(dto!.DisplayName);
|
||||
}
|
||||
|
||||
private void SetKeyStatesUp()
|
||||
{
|
||||
_keyboardMouseInput.SetKeyStatesUp();
|
||||
}
|
||||
|
||||
private void Tap(DtoWrapper wrapper, IViewer viewer)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<TapDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_keyboardMouseInput.SendMouseButtonAction(0, ButtonAction.Down, dto!.PercentX, dto.PercentY, viewer);
|
||||
_keyboardMouseInput.SendMouseButtonAction(0, ButtonAction.Up, dto.PercentX, dto.PercentY, viewer);
|
||||
}
|
||||
|
||||
private void ToggleAudio(DtoWrapper wrapper)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<ToggleAudioDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_audioCapturer.ToggleAudio(dto!.ToggleOn);
|
||||
}
|
||||
|
||||
private void ToggleBlockInput(DtoWrapper wrapper)
|
||||
{
|
||||
if (!DtoChunker.TryComplete<ToggleBlockInputDto>(wrapper, out var dto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_keyboardMouseInput.ToggleBlockInput(dto!.ToggleOn);
|
||||
}
|
||||
}
|
||||
96
Desktop.Shared/Services/IdleTimer.cs
Normal file
96
Desktop.Shared/Services/IdleTimer.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Timers;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Services;
|
||||
|
||||
public interface IIdleTimer
|
||||
{
|
||||
DateTimeOffset ViewersLastSeen { get; }
|
||||
|
||||
void Start();
|
||||
void Stop();
|
||||
}
|
||||
|
||||
public class IdleTimer : IIdleTimer
|
||||
{
|
||||
private readonly IAppState _appState;
|
||||
private readonly IRemoteControlAccessService _accessService;
|
||||
private readonly IDesktopHubConnection _desktopHubConnection;
|
||||
private readonly IShutdownService _shutdownService;
|
||||
private readonly ILogger<IdleTimer> _logger;
|
||||
private readonly SemaphoreSlim _elapseLock = new(1, 1);
|
||||
private System.Timers.Timer? _timer;
|
||||
|
||||
public IdleTimer(
|
||||
IAppState appState,
|
||||
IRemoteControlAccessService accessService,
|
||||
IDesktopHubConnection desktopHubConnection,
|
||||
IShutdownService shutdownService,
|
||||
ILogger<IdleTimer> logger)
|
||||
{
|
||||
_appState = appState;
|
||||
_accessService = accessService;
|
||||
_desktopHubConnection = desktopHubConnection;
|
||||
_shutdownService = shutdownService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public DateTimeOffset ViewersLastSeen { get; private set; } = DateTimeOffset.Now;
|
||||
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_logger.LogInformation("Starting idle timer.");
|
||||
_timer?.Dispose();
|
||||
_timer = new System.Timers.Timer(100);
|
||||
_timer.Elapsed += Timer_Elapsed;
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_timer?.Stop();
|
||||
_timer?.Dispose();
|
||||
}
|
||||
|
||||
private async void Timer_Elapsed(object? sender, ElapsedEventArgs e)
|
||||
{
|
||||
if (!await _elapseLock.WaitAsync(0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_appState.Mode == Enums.AppMode.Unattended &&
|
||||
!_desktopHubConnection.IsConnected)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"App is in unattended mode and is disconnected " +
|
||||
"from the server. Shutting down.");
|
||||
await _shutdownService.Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_appState.Viewers.IsEmpty ||
|
||||
_accessService.IsPromptOpen)
|
||||
{
|
||||
ViewersLastSeen = DateTimeOffset.Now;
|
||||
return;
|
||||
}
|
||||
|
||||
if (DateTimeOffset.Now - ViewersLastSeen > TimeSpan.FromSeconds(30))
|
||||
{
|
||||
_logger.LogWarning("No viewers connected for 30 seconds. Shutting down.");
|
||||
await _shutdownService.Shutdown();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_elapseLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
212
Desktop.Shared/Services/ImageHelper.cs
Normal file
212
Desktop.Shared/Services/ImageHelper.cs
Normal file
@ -0,0 +1,212 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IO;
|
||||
using Remotely.Shared.Primitives;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Services;
|
||||
|
||||
public interface IImageHelper
|
||||
{
|
||||
SKBitmap CropBitmap(SKBitmap bitmap, SKRect cropArea);
|
||||
byte[] EncodeBitmap(SKBitmap bitmap, SKEncodedImageFormat format, int quality);
|
||||
SKRect GetDiffArea(SKBitmap currentFrame, SKBitmap? previousFrame, bool forceFullscreen = false);
|
||||
Result<SKBitmap> GetImageDiff(SKBitmap currentFrame, SKBitmap? previousFrame, bool forceFullscreen = false);
|
||||
}
|
||||
|
||||
public class ImageHelper : IImageHelper
|
||||
{
|
||||
private static readonly RecyclableMemoryStreamManager _recycleManager = new();
|
||||
private readonly ILogger<ImageHelper> _logger;
|
||||
|
||||
public ImageHelper(ILogger<ImageHelper> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public byte[] EncodeBitmap(SKBitmap bitmap, SKEncodedImageFormat format, int quality)
|
||||
{
|
||||
using var ms = _recycleManager.GetStream();
|
||||
bitmap.Encode(ms, format, quality);
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
public SKBitmap CropBitmap(SKBitmap bitmap, SKRect cropArea)
|
||||
{
|
||||
var cropped = new SKBitmap((int)cropArea.Width, (int)cropArea.Height);
|
||||
using var canvas = new SKCanvas(cropped);
|
||||
canvas.DrawBitmap(
|
||||
bitmap,
|
||||
cropArea,
|
||||
new SKRect(0, 0, cropArea.Width, cropArea.Height));
|
||||
return cropped;
|
||||
}
|
||||
|
||||
public Result<SKBitmap> GetImageDiff(SKBitmap currentFrame, SKBitmap? previousFrame, bool forceFullscreen = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (currentFrame is null)
|
||||
{
|
||||
return Result.Fail<SKBitmap>("Current frame cannot be null.");
|
||||
}
|
||||
|
||||
if (previousFrame is null || forceFullscreen)
|
||||
{
|
||||
return Result.Ok(currentFrame.Copy());
|
||||
}
|
||||
|
||||
|
||||
if (currentFrame.Height != previousFrame.Height ||
|
||||
currentFrame.Width != previousFrame.Width ||
|
||||
currentFrame.BytesPerPixel != previousFrame.BytesPerPixel)
|
||||
{
|
||||
return Result.Fail<SKBitmap>("Frames are not of equal size.");
|
||||
}
|
||||
|
||||
var width = currentFrame.Width;
|
||||
var height = currentFrame.Height;
|
||||
var anyChanges = false;
|
||||
var diffFrame = new SKBitmap(width, height);
|
||||
|
||||
var bytesPerPixel = currentFrame.BytesPerPixel;
|
||||
var totalSize = currentFrame.ByteCount;
|
||||
|
||||
unsafe
|
||||
{
|
||||
byte* scan1 = (byte*)currentFrame.GetPixels().ToPointer();
|
||||
byte* scan2 = (byte*)previousFrame.GetPixels().ToPointer();
|
||||
byte* scan3 = (byte*)diffFrame.GetPixels().ToPointer();
|
||||
|
||||
for (var row = 0; row < height; row++)
|
||||
{
|
||||
for (var column = 0; column < width; column++)
|
||||
{
|
||||
var index = (row * width * bytesPerPixel) + (column * bytesPerPixel);
|
||||
|
||||
byte* data1 = scan1 + index;
|
||||
byte* data2 = scan2 + index;
|
||||
byte* data3 = scan3 + index;
|
||||
|
||||
if (data1[0] != data2[0] ||
|
||||
data1[1] != data2[1] ||
|
||||
data1[2] != data2[2] ||
|
||||
data1[3] != data2[3])
|
||||
{
|
||||
anyChanges = true;
|
||||
data3[0] = data2[0];
|
||||
data3[1] = data2[1];
|
||||
data3[2] = data2[2];
|
||||
data3[3] = data2[3];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (anyChanges)
|
||||
{
|
||||
return Result.Ok(diffFrame);
|
||||
}
|
||||
|
||||
diffFrame.Dispose();
|
||||
return Result.Fail<SKBitmap>("No difference found.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while getting image diff.");
|
||||
return Result.Fail<SKBitmap>(ex);
|
||||
}
|
||||
}
|
||||
public SKRect GetDiffArea(SKBitmap currentFrame, SKBitmap? previousFrame, bool forceFullscreen = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (currentFrame is null)
|
||||
{
|
||||
return SKRect.Empty;
|
||||
}
|
||||
|
||||
if (previousFrame is null || forceFullscreen)
|
||||
{
|
||||
return currentFrame.ToRectangle();
|
||||
}
|
||||
|
||||
|
||||
if (currentFrame.Height != previousFrame.Height ||
|
||||
currentFrame.Width != previousFrame.Width ||
|
||||
currentFrame.BytesPerPixel != previousFrame.BytesPerPixel)
|
||||
{
|
||||
return SKRect.Empty;
|
||||
}
|
||||
|
||||
var width = currentFrame.Width;
|
||||
var height = currentFrame.Height;
|
||||
int left = int.MaxValue;
|
||||
int top = int.MaxValue;
|
||||
int right = int.MinValue;
|
||||
int bottom = int.MinValue;
|
||||
|
||||
var bytesPerPixel = currentFrame.BytesPerPixel;
|
||||
var totalSize = currentFrame.ByteCount;
|
||||
|
||||
unsafe
|
||||
{
|
||||
byte* scan1 = (byte*)currentFrame.GetPixels().ToPointer();
|
||||
byte* scan2 = (byte*)previousFrame.GetPixels().ToPointer();
|
||||
|
||||
for (var row = 0; row < height; row++)
|
||||
{
|
||||
for (var column = 0; column < width; column++)
|
||||
{
|
||||
var index = (row * width * bytesPerPixel) + (column * bytesPerPixel);
|
||||
|
||||
byte* data1 = scan1 + index;
|
||||
byte* data2 = scan2 + index;
|
||||
|
||||
if (data1[0] != data2[0] ||
|
||||
data1[1] != data2[1] ||
|
||||
data1[2] != data2[2])
|
||||
{
|
||||
|
||||
if (row < top)
|
||||
{
|
||||
top = row;
|
||||
}
|
||||
if (row > bottom)
|
||||
{
|
||||
bottom = row;
|
||||
}
|
||||
if (column < left)
|
||||
{
|
||||
left = column;
|
||||
}
|
||||
if (column > right)
|
||||
{
|
||||
right = column;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Check for valid bounding box.
|
||||
if (left <= right && top <= bottom)
|
||||
{
|
||||
left = Math.Max(left - 2, 0);
|
||||
top = Math.Max(top - 2, 0);
|
||||
right = Math.Min(right + 2, width);
|
||||
bottom = Math.Min(bottom + 2, height);
|
||||
return new SKRect(left, top, right, bottom);
|
||||
}
|
||||
|
||||
return SKRect.Empty;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while getting area diff.");
|
||||
return SKRect.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
283
Desktop.Shared/Services/ScreenCaster.cs
Normal file
283
Desktop.Shared/Services/ScreenCaster.cs
Normal file
@ -0,0 +1,283 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SkiaSharp;
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Enums;
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Immense.RemoteControl.Shared.Helpers;
|
||||
using Immense.RemoteControl.Shared.Models.Dtos;
|
||||
using MessagePack;
|
||||
using Immense.RemoteControl.Shared.Services;
|
||||
using Microsoft.IO;
|
||||
using System.Diagnostics;
|
||||
using Bitbound.SimpleMessenger;
|
||||
using Immense.RemoteControl.Desktop.Shared.Messages;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Services;
|
||||
|
||||
public interface IScreenCaster : IDisposable
|
||||
{
|
||||
Task BeginScreenCasting(ScreenCastRequest screenCastRequest);
|
||||
}
|
||||
|
||||
internal class ScreenCaster : IScreenCaster
|
||||
{
|
||||
private readonly IAppState _appState;
|
||||
private readonly ICursorIconWatcher _cursorIconWatcher;
|
||||
private readonly IImageHelper _imageHelper;
|
||||
private readonly ILogger<ScreenCaster> _logger;
|
||||
private readonly CancellationTokenSource _metricsCts = new();
|
||||
private readonly RecyclableMemoryStreamManager _recycleStreams = new();
|
||||
private readonly ISessionIndicator _sessionIndicator;
|
||||
private readonly IShutdownService _shutdownService;
|
||||
private readonly ISystemTime _systemTime;
|
||||
private readonly IViewerFactory _viewerFactory;
|
||||
private readonly IDisposable[] _messengerRegistrations;
|
||||
private bool _isWindowsSessionEnding;
|
||||
|
||||
public ScreenCaster(
|
||||
IAppState appState,
|
||||
IViewerFactory viewerFactory,
|
||||
ICursorIconWatcher cursorIconWatcher,
|
||||
ISessionIndicator sessionIndicator,
|
||||
IShutdownService shutdownService,
|
||||
IImageHelper imageHelper,
|
||||
ISystemTime systemTime,
|
||||
IMessenger messenger,
|
||||
ILogger<ScreenCaster> logger)
|
||||
{
|
||||
_appState = appState;
|
||||
_cursorIconWatcher = cursorIconWatcher;
|
||||
_sessionIndicator = sessionIndicator;
|
||||
_shutdownService = shutdownService;
|
||||
_imageHelper = imageHelper;
|
||||
_systemTime = systemTime;
|
||||
_viewerFactory = viewerFactory;
|
||||
_logger = logger;
|
||||
|
||||
_messengerRegistrations =
|
||||
[
|
||||
messenger.Register<WindowsSessionSwitchedMessage>(this, HandleWindowsSessionSwitchedMessage),
|
||||
messenger.Register<WindowsSessionEndingMessage>(this, HandleWindowsSessionEndingMessage)
|
||||
];
|
||||
}
|
||||
|
||||
public async Task BeginScreenCasting(ScreenCastRequest screenCastRequest)
|
||||
{
|
||||
await BeginScreenCastingImpl(screenCastRequest).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var registration in _messengerRegistrations)
|
||||
{
|
||||
try
|
||||
{
|
||||
registration.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
_metricsCts.Cancel();
|
||||
_metricsCts.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private async Task BeginScreenCastingImpl(ScreenCastRequest screenCastRequest)
|
||||
{
|
||||
using var viewer = _viewerFactory.CreateViewer(screenCastRequest.RequesterName, screenCastRequest.ViewerId);
|
||||
|
||||
try
|
||||
{
|
||||
viewer.Name = screenCastRequest.RequesterName;
|
||||
viewer.ViewerConnectionId = screenCastRequest.ViewerId;
|
||||
|
||||
var screenBounds = viewer.Capturer.CurrentScreenBounds;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Starting screen cast. Requester: {viewerName}. " +
|
||||
"Viewer ID: {viewerViewerConnectionID}. App Mode: {mode}",
|
||||
viewer.Name,
|
||||
viewer.ViewerConnectionId,
|
||||
_appState.Mode);
|
||||
|
||||
_appState.Viewers.AddOrUpdate(viewer.ViewerConnectionId, viewer, (id, v) => viewer);
|
||||
|
||||
if (_appState.Mode == AppMode.Attended)
|
||||
{
|
||||
_appState.InvokeViewerAdded(viewer);
|
||||
}
|
||||
|
||||
if (_appState.Mode == AppMode.Unattended && screenCastRequest.NotifyUser)
|
||||
{
|
||||
_sessionIndicator.Show();
|
||||
}
|
||||
|
||||
await viewer.SendScreenData(
|
||||
viewer.Capturer.SelectedScreen,
|
||||
viewer.Capturer.GetDisplayNames(),
|
||||
screenBounds.Width,
|
||||
screenBounds.Height);
|
||||
|
||||
await viewer.SendCursorChange(_cursorIconWatcher.GetCurrentCursor());
|
||||
|
||||
await viewer.SendWindowsSessions();
|
||||
|
||||
viewer.Capturer.ScreenChanged += async (sender, bounds) =>
|
||||
{
|
||||
await viewer.SendScreenSize(bounds.Width, bounds.Height);
|
||||
};
|
||||
|
||||
_ = Task.Run(() => LogMetrics(viewer, _metricsCts.Token));
|
||||
using var sessionEndSignal = new SemaphoreSlim(0, 1);
|
||||
await viewer.SendDesktopStream(GetDesktopStream(viewer, sessionEndSignal), screenCastRequest.StreamId);
|
||||
if (!await sessionEndSignal.WaitAsync(TimeSpan.FromHours(8)))
|
||||
{
|
||||
_logger.LogWarning("Timed out while waiting for session to end.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while starting screen casting.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Ended desktop stream. " +
|
||||
"Requester: {viewerName}. " +
|
||||
"Viewer ID: {viewerConnectionID}. " +
|
||||
"Viewer Responsive: {isResponsive}. " +
|
||||
"Viewer Disconnected Requested: {viewerDisconnectRequested}. " +
|
||||
"Windows Session Ending: {windowsSessionEnding}",
|
||||
viewer.Name,
|
||||
viewer.ViewerConnectionId,
|
||||
viewer.IsResponsive,
|
||||
viewer.DisconnectRequested,
|
||||
_isWindowsSessionEnding);
|
||||
|
||||
_appState.Viewers.TryRemove(viewer.ViewerConnectionId, out _);
|
||||
Disposer.TryDisposeAll(viewer);
|
||||
|
||||
// Close if no one is viewing.
|
||||
if (_appState.Viewers.IsEmpty && _appState.Mode == AppMode.Unattended)
|
||||
{
|
||||
_logger.LogInformation("No more viewers. Calling shutdown service.");
|
||||
await _shutdownService.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<byte[]> GetDesktopStream(IViewer viewer, SemaphoreSlim sessionEndedSignal)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
try
|
||||
{
|
||||
while (!viewer.DisconnectRequested && viewer.IsResponsive && !_isWindowsSessionEnding)
|
||||
{
|
||||
viewer.IncrementFpsCount();
|
||||
|
||||
await viewer.ApplyAutoQuality();
|
||||
|
||||
if (!await viewer.WaitForViewer())
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Viewer is behind on frames and did not catch up in time.");
|
||||
}
|
||||
|
||||
var result = viewer.Capturer.GetNextFrame();
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
await Task.Yield();
|
||||
continue;
|
||||
}
|
||||
|
||||
var diffArea = viewer.Capturer.GetFrameDiffArea();
|
||||
|
||||
if (diffArea.IsEmpty)
|
||||
{
|
||||
await Task.Yield();
|
||||
continue;
|
||||
}
|
||||
|
||||
viewer.Capturer.CaptureFullscreen = false;
|
||||
|
||||
using var croppedFrame = _imageHelper.CropBitmap(result.Value, diffArea);
|
||||
|
||||
var encodedImageBytes = _imageHelper.EncodeBitmap(croppedFrame, SKEncodedImageFormat.Jpeg, viewer.ImageQuality);
|
||||
|
||||
if (encodedImageBytes.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
viewer.AppendSentFrame(new SentFrame(encodedImageBytes.Length, _systemTime.Now));
|
||||
|
||||
using var frameStream = _recycleStreams.GetStream();
|
||||
using var writer = new BinaryWriter(frameStream);
|
||||
writer.Write(encodedImageBytes.Length);
|
||||
writer.Write(diffArea.Left);
|
||||
writer.Write(diffArea.Top);
|
||||
writer.Write(diffArea.Width);
|
||||
writer.Write(diffArea.Height);
|
||||
writer.Write(DateTimeOffset.Now.ToUnixTimeMilliseconds());
|
||||
writer.Write(encodedImageBytes);
|
||||
|
||||
frameStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
foreach (var chunk in frameStream.ToArray().Chunk(50_000))
|
||||
{
|
||||
yield return chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
sessionEndedSignal.Release();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Task HandleWindowsSessionEndingMessage(object subscriber, WindowsSessionEndingMessage arg)
|
||||
{
|
||||
_logger.LogInformation("Windows session ending. Stopping screen cast.");
|
||||
_isWindowsSessionEnding = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task HandleWindowsSessionSwitchedMessage(object subscriber, WindowsSessionSwitchedMessage arg)
|
||||
{
|
||||
_logger.LogInformation("Windows session switched. Stopping screen cast.");
|
||||
_isWindowsSessionEnding = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task LogMetrics(IViewer viewer, CancellationToken cancellationToken)
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3));
|
||||
while (await timer.WaitForNextTickAsync(cancellationToken))
|
||||
{
|
||||
await viewer.CalculateMetrics();
|
||||
|
||||
var metrics = new SessionMetricsDto(
|
||||
Math.Round(viewer.CurrentMbps, 2),
|
||||
viewer.CurrentFps,
|
||||
viewer.RoundTripLatency.TotalMilliseconds,
|
||||
viewer.Capturer.IsGpuAccelerated);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Current Mbps: {currentMbps}. " +
|
||||
"Current FPS: {currentFps}. " +
|
||||
"Roundtrip Latency: {roundTripLatency}ms. " +
|
||||
"Image Quality: {imageQuality}",
|
||||
metrics.Mbps,
|
||||
metrics.Fps,
|
||||
metrics.RoundTripLatency,
|
||||
viewer.ImageQuality);
|
||||
|
||||
|
||||
await viewer.SendSessionMetrics(metrics);
|
||||
}
|
||||
}
|
||||
}
|
||||
374
Desktop.Shared/Services/Viewer.cs
Normal file
374
Desktop.Shared/Services/Viewer.cs
Normal file
@ -0,0 +1,374 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Immense.RemoteControl.Shared.Helpers;
|
||||
using Immense.RemoteControl.Shared.Models.Dtos;
|
||||
using Immense.RemoteControl.Desktop.Shared.ViewModels;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Immense.RemoteControl.Shared.Services;
|
||||
using Immense.RemoteControl.Desktop.Shared.Native.Windows;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Services;
|
||||
|
||||
public interface IViewer : IDisposable
|
||||
{
|
||||
IScreenCapturer Capturer { get; }
|
||||
double CurrentFps { get; }
|
||||
double CurrentMbps { get; }
|
||||
bool DisconnectRequested { get; set; }
|
||||
bool HasControl { get; set; }
|
||||
int ImageQuality { get; }
|
||||
bool IsResponsive { get; }
|
||||
string Name { get; set; }
|
||||
TimeSpan RoundTripLatency { get; }
|
||||
string ViewerConnectionId { get; set; }
|
||||
|
||||
void AppendSentFrame(SentFrame sentFrame);
|
||||
Task ApplyAutoQuality();
|
||||
Task CalculateMetrics();
|
||||
void IncrementFpsCount();
|
||||
Task SendAudioSample(byte[] audioSample);
|
||||
Task SendClipboardText(string clipboardText);
|
||||
Task SendCursorChange(CursorInfo cursorInfo);
|
||||
Task SendDesktopStream(IAsyncEnumerable<byte[]> asyncEnumerable, Guid streamId);
|
||||
Task SendFile(FileUpload fileUpload, Action<double> progressUpdateCallback, CancellationToken cancelToken);
|
||||
Task SendScreenData(string selectedDisplay, IEnumerable<string> displayNames, int screenWidth, int screenHeight);
|
||||
Task SendScreenSize(int width, int height);
|
||||
Task SendSessionMetrics(SessionMetricsDto metrics);
|
||||
Task SendWindowsSessions();
|
||||
void SetLastFrameReceived(DateTimeOffset timestamp);
|
||||
Task<bool> WaitForViewer();
|
||||
}
|
||||
|
||||
public class Viewer : IViewer
|
||||
{
|
||||
public const int DefaultQuality = 80;
|
||||
|
||||
private readonly IAudioCapturer _audioCapturer;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IDesktopHubConnection _desktopHubConnection;
|
||||
private readonly ConcurrentQueue<DateTimeOffset> _fpsQueue = new();
|
||||
private readonly ILogger<Viewer> _logger;
|
||||
private readonly ConcurrentQueue<SentFrame> _sentFrames = new();
|
||||
private readonly ISystemTime _systemTime;
|
||||
private bool _disconnectRequested;
|
||||
private volatile int _framesSentSinceLastReceipt;
|
||||
private DateTimeOffset _lastFrameReceived = DateTimeOffset.Now;
|
||||
private DateTimeOffset _lastFrameSent = DateTimeOffset.Now;
|
||||
private int _pingFailures;
|
||||
|
||||
public Viewer(
|
||||
string requesterName,
|
||||
string viewerHubConnectionId,
|
||||
IDesktopHubConnection desktopHubConnection,
|
||||
IScreenCapturer screenCapturer,
|
||||
IClipboardService clipboardService,
|
||||
IAudioCapturer audioCapturer,
|
||||
ISystemTime systemTime,
|
||||
ILogger<Viewer> logger)
|
||||
{
|
||||
Name = requesterName;
|
||||
ViewerConnectionId = viewerHubConnectionId;
|
||||
Capturer = screenCapturer;
|
||||
_desktopHubConnection = desktopHubConnection;
|
||||
_clipboardService = clipboardService;
|
||||
_audioCapturer = audioCapturer;
|
||||
_systemTime = systemTime;
|
||||
_logger = logger;
|
||||
|
||||
_clipboardService.ClipboardTextChanged += ClipboardService_ClipboardTextChanged;
|
||||
_audioCapturer.AudioSampleReady += AudioCapturer_AudioSampleReady;
|
||||
}
|
||||
|
||||
public IScreenCapturer Capturer { get; }
|
||||
public double CurrentFps { get; private set; }
|
||||
public double CurrentMbps { get; private set; }
|
||||
public bool DisconnectRequested
|
||||
{
|
||||
get => _disconnectRequested;
|
||||
set
|
||||
{
|
||||
_disconnectRequested = value;
|
||||
}
|
||||
}
|
||||
public bool HasControl { get; set; } = true;
|
||||
public int ImageQuality { get; private set; } = DefaultQuality;
|
||||
public bool IsResponsive { get; private set; } = true;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public TimeSpan RoundTripLatency { get; private set; }
|
||||
|
||||
public string ViewerConnectionId { get; set; } = string.Empty;
|
||||
public void AppendSentFrame(SentFrame sentFrame)
|
||||
{
|
||||
Interlocked.Increment(ref _framesSentSinceLastReceipt);
|
||||
_lastFrameSent = sentFrame.Timestamp;
|
||||
_sentFrames.Enqueue(sentFrame);
|
||||
}
|
||||
|
||||
public Task ApplyAutoQuality()
|
||||
{
|
||||
if (ImageQuality < DefaultQuality)
|
||||
{
|
||||
ImageQuality = Math.Min(DefaultQuality, ImageQuality + 2);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task CalculateMetrics()
|
||||
{
|
||||
if (_desktopHubConnection.Connection is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CalculateMbps();
|
||||
CalculateFps();
|
||||
await CalculateLatency();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisconnectRequested = true;
|
||||
Disposer.TryDisposeAll(Capturer);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void IncrementFpsCount()
|
||||
{
|
||||
_fpsQueue.Enqueue(_systemTime.Now);
|
||||
}
|
||||
|
||||
public async Task SendAudioSample(byte[] audioSample)
|
||||
{
|
||||
var dto = new AudioSampleDto(audioSample);
|
||||
await TrySendToViewer(dto, DtoType.AudioSample, ViewerConnectionId);
|
||||
}
|
||||
|
||||
public async Task SendClipboardText(string clipboardText)
|
||||
{
|
||||
var dto = new ClipboardTextDto(clipboardText);
|
||||
await TrySendToViewer(dto, DtoType.ClipboardText, ViewerConnectionId);
|
||||
}
|
||||
|
||||
public async Task SendCursorChange(CursorInfo cursorInfo)
|
||||
{
|
||||
if (cursorInfo is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dto = new CursorChangeDto(cursorInfo.ImageBytes, cursorInfo.HotSpot.X, cursorInfo.HotSpot.Y, cursorInfo.CssOverride);
|
||||
await TrySendToViewer(dto, DtoType.CursorChange, ViewerConnectionId);
|
||||
}
|
||||
|
||||
public async Task SendDesktopStream(IAsyncEnumerable<byte[]> stream, Guid streamId)
|
||||
{
|
||||
if (_desktopHubConnection.Connection is not null)
|
||||
{
|
||||
await _desktopHubConnection.Connection.SendAsync("SendDesktopStream", stream, streamId);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendFile(
|
||||
FileUpload fileUpload,
|
||||
Action<double> progressUpdateCallback,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var messageId = Guid.NewGuid().ToString();
|
||||
var fileDto = new FileDto()
|
||||
{
|
||||
EndOfFile = false,
|
||||
FileName = fileUpload.DisplayName,
|
||||
MessageId = messageId,
|
||||
StartOfFile = true
|
||||
};
|
||||
|
||||
await TrySendToViewer(fileDto, DtoType.File, ViewerConnectionId);
|
||||
|
||||
using var fs = File.OpenRead(fileUpload.FilePath);
|
||||
using var br = new BinaryReader(fs);
|
||||
while (fs.Position < fs.Length)
|
||||
{
|
||||
if (cancelToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
fileDto = new FileDto()
|
||||
{
|
||||
Buffer = br.ReadBytes(40_000),
|
||||
FileName = fileUpload.DisplayName,
|
||||
MessageId = messageId
|
||||
};
|
||||
|
||||
await TrySendToViewer(fileDto, DtoType.File, ViewerConnectionId);
|
||||
|
||||
progressUpdateCallback((double)fs.Position / fs.Length);
|
||||
}
|
||||
|
||||
fileDto = new FileDto()
|
||||
{
|
||||
EndOfFile = true,
|
||||
FileName = fileUpload.DisplayName,
|
||||
MessageId = messageId,
|
||||
StartOfFile = false
|
||||
};
|
||||
|
||||
await TrySendToViewer(fileDto, DtoType.File, ViewerConnectionId);
|
||||
|
||||
progressUpdateCallback(1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while sending file.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendScreenData(
|
||||
string selectedDisplay,
|
||||
IEnumerable<string> displayNames,
|
||||
int screenWidth,
|
||||
int screenHeight)
|
||||
{
|
||||
var dto = new ScreenDataDto()
|
||||
{
|
||||
MachineName = Environment.MachineName,
|
||||
DisplayNames = displayNames,
|
||||
SelectedDisplay = selectedDisplay,
|
||||
ScreenWidth = screenWidth,
|
||||
ScreenHeight = screenHeight
|
||||
};
|
||||
await TrySendToViewer(dto, DtoType.ScreenData, ViewerConnectionId);
|
||||
}
|
||||
|
||||
public async Task SendScreenSize(int width, int height)
|
||||
{
|
||||
var dto = new ScreenSizeDto(width, height);
|
||||
await TrySendToViewer(dto, DtoType.ScreenSize, ViewerConnectionId);
|
||||
}
|
||||
|
||||
public async Task SendSessionMetrics(SessionMetricsDto metrics)
|
||||
{
|
||||
await TrySendToViewer(metrics, DtoType.SessionMetrics, ViewerConnectionId);
|
||||
}
|
||||
|
||||
public async Task SendWindowsSessions()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var dto = new WindowsSessionsDto(Win32Interop.GetActiveSessions());
|
||||
await TrySendToViewer(dto, DtoType.WindowsSessions, ViewerConnectionId);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLastFrameReceived(DateTimeOffset timestamp)
|
||||
{
|
||||
_lastFrameReceived = timestamp;
|
||||
_framesSentSinceLastReceipt = 0;
|
||||
}
|
||||
|
||||
public async Task<bool> WaitForViewer()
|
||||
{
|
||||
// Prevent publisher from overwhelming consumer bewteen receipts.
|
||||
var result = await WaitHelper.WaitForAsync(
|
||||
() => _framesSentSinceLastReceipt < 10,
|
||||
TimeSpan.FromSeconds(5));
|
||||
|
||||
// Prevent viewer from getting too far behind.
|
||||
result &= await WaitHelper.WaitForAsync(
|
||||
() => _lastFrameSent - _lastFrameReceived < TimeSpan.FromSeconds(1),
|
||||
TimeSpan.FromSeconds(5));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async void AudioCapturer_AudioSampleReady(object? sender, byte[] sample)
|
||||
{
|
||||
await SendAudioSample(sample);
|
||||
}
|
||||
|
||||
private void CalculateFps()
|
||||
{
|
||||
if (_fpsQueue.Count >= 2)
|
||||
{
|
||||
var sendTime = _fpsQueue.Last() - _fpsQueue.First();
|
||||
CurrentFps = _fpsQueue.Count / sendTime.TotalSeconds;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
CurrentFps = _fpsQueue.Count;
|
||||
}
|
||||
_fpsQueue.Clear();
|
||||
}
|
||||
|
||||
private async Task CalculateLatency()
|
||||
{
|
||||
var latencyResult = await _desktopHubConnection.CheckRoundtripLatency(ViewerConnectionId);
|
||||
if (latencyResult.IsSuccess)
|
||||
{
|
||||
_pingFailures = 0;
|
||||
IsResponsive = true;
|
||||
RoundTripLatency = latencyResult.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_pingFailures++;
|
||||
if (_pingFailures > 3)
|
||||
{
|
||||
IsResponsive = false;
|
||||
_logger.LogWarning("Failed to check roundtrip latency: {reason}", latencyResult.Reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
private void CalculateMbps()
|
||||
{
|
||||
if (_sentFrames.Count >= 2)
|
||||
{
|
||||
var sendTime = _sentFrames.Last().Timestamp - _sentFrames.First().Timestamp;
|
||||
var sentBits = (double)_sentFrames.Sum(x => x.FrameSize) / 1024 / 1024 * 8;
|
||||
CurrentMbps = sentBits / sendTime.TotalSeconds;
|
||||
}
|
||||
else if (_sentFrames.Count == 1)
|
||||
{
|
||||
CurrentMbps = _sentFrames.First().FrameSize / 1024 / 1024 * 8;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentMbps = 0;
|
||||
}
|
||||
_sentFrames.Clear();
|
||||
}
|
||||
private async void ClipboardService_ClipboardTextChanged(object? sender, string clipboardText)
|
||||
{
|
||||
await SendClipboardText(clipboardText);
|
||||
}
|
||||
|
||||
private async Task TrySendToViewer<T>(T dto, DtoType type, string viewerConnectionId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_desktopHubConnection.IsConnected)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to send DTO type {type} because the app is disconnected from the server.",
|
||||
type);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var chunk in DtoChunker.ChunkDto(dto, type))
|
||||
{
|
||||
await _desktopHubConnection.SendDtoToViewer(chunk, viewerConnectionId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while sending DTO type {type} to viewer connection ID {viewerId}.",
|
||||
type,
|
||||
viewerConnectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Desktop.Shared/Services/ViewerFactory.cs
Normal file
46
Desktop.Shared/Services/ViewerFactory.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Shared.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Services;
|
||||
|
||||
internal interface IViewerFactory
|
||||
{
|
||||
IViewer CreateViewer(string viewerName, string viewerConnectionId);
|
||||
}
|
||||
|
||||
internal class ViewerFactory : IViewerFactory
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public ViewerFactory(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public IViewer CreateViewer(string viewerName, string viewerConnectionId)
|
||||
{
|
||||
var desktopHubConnection = _serviceProvider.GetRequiredService<IDesktopHubConnection>();
|
||||
var screenCapturer = _serviceProvider.GetRequiredService<IScreenCapturer>();
|
||||
var clipboardService = _serviceProvider.GetRequiredService<IClipboardService>();
|
||||
var audioCapturer = _serviceProvider.GetRequiredService<IAudioCapturer>();
|
||||
var systemTime = _serviceProvider.GetRequiredService<ISystemTime>();
|
||||
var logger = _serviceProvider.GetRequiredService<ILogger<Viewer>>();
|
||||
|
||||
return new Viewer(
|
||||
viewerName,
|
||||
viewerConnectionId,
|
||||
desktopHubConnection,
|
||||
screenCapturer,
|
||||
clipboardService,
|
||||
audioCapturer,
|
||||
systemTime,
|
||||
logger);
|
||||
}
|
||||
}
|
||||
99
Desktop.Shared/Startup/CommandProvider.cs
Normal file
99
Desktop.Shared/Startup/CommandProvider.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using System.CommandLine;
|
||||
using CommunityToolkit.Diagnostics;
|
||||
using Immense.RemoteControl.Desktop.Shared.Enums;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Startup;
|
||||
public static class CommandProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Command"/> for starting the remote control client.
|
||||
/// </summary>
|
||||
/// <param name="isRootCommand">Whether to create a <see cref="RootCommand"/> or <see cref="Command"/>.</param>
|
||||
/// <param name="commandLineDescription">The description for the command.</param>
|
||||
/// <param name="commandName">The name used to invoke the command. Required if not a root command.</param>
|
||||
/// <returns></returns>
|
||||
public static Command CreateRemoteControlCommand(
|
||||
bool isRootCommand,
|
||||
string commandLineDescription,
|
||||
string commandName = "")
|
||||
{
|
||||
Command? rootCommand;
|
||||
|
||||
if (isRootCommand)
|
||||
{
|
||||
rootCommand = new RootCommand(commandLineDescription);
|
||||
}
|
||||
else
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(commandName);
|
||||
rootCommand = new Command(commandName, commandLineDescription);
|
||||
}
|
||||
|
||||
var hostOption = new Option<string>(
|
||||
new[] { "-h", "--host" },
|
||||
"The hostname of the server to which to connect (e.g. https://example.com).");
|
||||
rootCommand.AddOption(hostOption);
|
||||
|
||||
var modeOption = new Option<AppMode>(
|
||||
new[] { "-m", "--mode" },
|
||||
() => AppMode.Attended,
|
||||
"The remote control mode to use. Either Attended, Unattended, or Chat.");
|
||||
rootCommand.AddOption(modeOption);
|
||||
|
||||
|
||||
var pipeNameOption = new Option<string>(
|
||||
new[] { "-p", "--pipe-name" },
|
||||
"When AppMode is Chat, this is the pipe name used by the named pipes server.");
|
||||
pipeNameOption.AddValidator((context) =>
|
||||
{
|
||||
if (context.GetValueForOption(modeOption) == AppMode.Chat &&
|
||||
string.IsNullOrWhiteSpace(context.GetValueOrDefault<string>()))
|
||||
{
|
||||
context.ErrorMessage = "A pipe name must be specified when AppMode is Chat.";
|
||||
}
|
||||
});
|
||||
rootCommand.AddOption(pipeNameOption);
|
||||
|
||||
var sessionIdOption = new Option<string>(
|
||||
new[] { "-s", "--session-id" },
|
||||
"In Unattended mode, this unique session ID will be assigned to this connection and " +
|
||||
"shared with the server. The connection can then be found in the RemoteControlSessionCache " +
|
||||
"using this ID.");
|
||||
rootCommand.AddOption(sessionIdOption);
|
||||
|
||||
var accessKeyOption = new Option<string>(
|
||||
new[] { "-a", "--access-key" },
|
||||
"In Unattended mode, secures access to the connection using the provided key.");
|
||||
rootCommand.AddOption(accessKeyOption);
|
||||
|
||||
var requesterNameOption = new Option<string>(
|
||||
new[] { "-r", "--requester-name" },
|
||||
"The name of the technician requesting to connect.");
|
||||
rootCommand.AddOption(requesterNameOption);
|
||||
|
||||
var organizationNameOption = new Option<string>(
|
||||
new[] { "-o", "--org-name" },
|
||||
"The organization name of the technician requesting to connect.");
|
||||
rootCommand.AddOption(organizationNameOption);
|
||||
|
||||
var relaunchOption = new Option<bool>(
|
||||
"--relaunch",
|
||||
"Used to indicate that process is being relaunched from a previous session " +
|
||||
"and should notify viewers when it's ready.");
|
||||
rootCommand.AddOption(relaunchOption);
|
||||
|
||||
var viewersOption = new Option<string>(
|
||||
"--viewers",
|
||||
"Used with --relaunch. Should be a comma-separated list of viewers' " +
|
||||
"SignalR connection IDs.");
|
||||
rootCommand.AddOption(viewersOption);
|
||||
|
||||
var elevateOption = new Option<bool>(
|
||||
"--elevate",
|
||||
"Must be called from a Windows service. The process will relaunch " +
|
||||
"itself in the console session with elevated rights.");
|
||||
rootCommand.AddOption(elevateOption);
|
||||
|
||||
return rootCommand;
|
||||
}
|
||||
}
|
||||
39
Desktop.Shared/Startup/IServiceCollectionExtensions.cs
Normal file
39
Desktop.Shared/Startup/IServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Immense.RemoteControl.Shared.Services;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Bitbound.SimpleMessenger;
|
||||
using Desktop.Shared.Services;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Startup;
|
||||
|
||||
public static class IServiceCollectionExtensions
|
||||
{
|
||||
internal static void AddRemoteControlXplat(
|
||||
this IServiceCollection services,
|
||||
Action<IRemoteControlClientBuilder> clientConfig)
|
||||
{
|
||||
var builder = new RemoteControlClientBuilder(services);
|
||||
clientConfig.Invoke(builder);
|
||||
builder.Validate();
|
||||
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
builder.AddConsole().AddDebug();
|
||||
});
|
||||
|
||||
services.AddSingleton<ISystemTime, SystemTime>();
|
||||
services.AddSingleton<IDesktopHubConnection, DesktopHubConnection>();
|
||||
services.AddSingleton<IIdleTimer, IdleTimer>();
|
||||
services.AddSingleton<IImageHelper, ImageHelper>();
|
||||
services.AddSingleton<IChatHostService, ChatHostService>();
|
||||
services.AddSingleton(s => WeakReferenceMessenger.Default);
|
||||
services.AddSingleton<IDesktopEnvironment, DesktopEnvironment>();
|
||||
services.AddSingleton<IDtoMessageHandler, DtoMessageHandler>();
|
||||
services.AddSingleton<IAppState, AppState>();
|
||||
services.AddSingleton<IViewerFactory, ViewerFactory>();
|
||||
services.AddTransient<IScreenCaster, ScreenCaster>();
|
||||
services.AddTransient<IHubConnectionBuilder>(s => new HubConnectionBuilder());
|
||||
}
|
||||
}
|
||||
163
Desktop.Shared/Startup/IServiceProviderExtensions.cs
Normal file
163
Desktop.Shared/Startup/IServiceProviderExtensions.cs
Normal file
@ -0,0 +1,163 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Enums;
|
||||
using Immense.RemoteControl.Desktop.Shared.Native.Windows;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remotely.Shared.Primitives;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.NamingConventionBinder;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Startup;
|
||||
|
||||
public static class IServiceProviderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the remote control startup with the specified arguments.
|
||||
/// </summary>
|
||||
public static async Task<Result> UseRemoteControlClient(
|
||||
this IServiceProvider services,
|
||||
string host,
|
||||
AppMode mode,
|
||||
string pipeName,
|
||||
string sessionId,
|
||||
string accessKey,
|
||||
string requesterName,
|
||||
string organizationName,
|
||||
bool relaunch,
|
||||
string viewers,
|
||||
bool elevate)
|
||||
{
|
||||
try
|
||||
{
|
||||
var logger = services.GetRequiredService<ILogger<IServiceProvider>>();
|
||||
TaskScheduler.UnobservedTaskException += (object? sender, UnobservedTaskExceptionEventArgs e) =>
|
||||
{
|
||||
HandleUnobservedTask(e, logger);
|
||||
};
|
||||
|
||||
if (OperatingSystem.IsWindows() && elevate)
|
||||
{
|
||||
RelaunchElevated();
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
var appState = services.GetRequiredService<IAppState>();
|
||||
appState.Configure(
|
||||
host ?? string.Empty,
|
||||
mode,
|
||||
sessionId ?? string.Empty,
|
||||
accessKey ?? string.Empty,
|
||||
requesterName ?? string.Empty,
|
||||
organizationName ?? string.Empty,
|
||||
pipeName ?? string.Empty,
|
||||
relaunch,
|
||||
viewers ?? string.Empty,
|
||||
elevate);
|
||||
|
||||
StaticServiceProvider.Instance = services;
|
||||
|
||||
var appStartup = services.GetRequiredService<IAppStartup>();
|
||||
await appStartup.Run();
|
||||
return Result.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the remote control startup as a root command. This uses the System.CommandLine package.
|
||||
/// </summary>
|
||||
/// <param name="services">The service provider fo rthe app using this library.</param>
|
||||
/// <param name="args">The original command line arguments passed into the app.</param>
|
||||
/// <param name="commandLineDescription">The description to use for the remote control command.</param>
|
||||
/// <param name="serverUri">If provided, will be used as a fallback if --host option is missing.</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<Result> UseRemoteControlClient(
|
||||
this IServiceProvider services,
|
||||
string[] args,
|
||||
string commandLineDescription,
|
||||
string serverUri = "",
|
||||
bool treatUnmatchedArgsAsErrors = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rootCommand = CommandProvider.CreateRemoteControlCommand(true, commandLineDescription);
|
||||
|
||||
rootCommand.Handler = CommandHandler.Create(async (
|
||||
string host,
|
||||
AppMode mode,
|
||||
string pipeName,
|
||||
string sessionId,
|
||||
string accessKey,
|
||||
string requesterName,
|
||||
string organizationName,
|
||||
bool relaunch,
|
||||
string viewers,
|
||||
bool elevate) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host) && !string.IsNullOrWhiteSpace(serverUri))
|
||||
{
|
||||
host = serverUri;
|
||||
}
|
||||
|
||||
return await services.UseRemoteControlClient(
|
||||
host,
|
||||
mode,
|
||||
pipeName,
|
||||
sessionId,
|
||||
accessKey,
|
||||
requesterName,
|
||||
organizationName,
|
||||
relaunch,
|
||||
viewers,
|
||||
elevate);
|
||||
});
|
||||
|
||||
rootCommand.TreatUnmatchedTokensAsErrors = treatUnmatchedArgsAsErrors;
|
||||
|
||||
var result = await rootCommand.InvokeAsync(args);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
return Result.Ok();
|
||||
}
|
||||
return Result.Fail($"Remote control command returned code {result}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// This shouldn't be required in modern .NET to prevent the app from crashing,
|
||||
// but it could be useful to log it.
|
||||
private static void HandleUnobservedTask(
|
||||
UnobservedTaskExceptionEventArgs e,
|
||||
ILogger<IServiceProvider> logger)
|
||||
{
|
||||
e.SetObserved();
|
||||
logger.LogError(e.Exception, "An unobserved task exception occurred.");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static void RelaunchElevated()
|
||||
{
|
||||
var commandLine = Win32Interop.GetCommandLine().Replace(" --elevate", "");
|
||||
|
||||
Console.WriteLine($"Elevating process {commandLine}.");
|
||||
var result = Win32Interop.CreateInteractiveSystemProcess(
|
||||
commandLine,
|
||||
-1,
|
||||
false,
|
||||
"default",
|
||||
true,
|
||||
out var procInfo);
|
||||
Console.WriteLine($"Elevate result: {result}. Process ID: {procInfo.dwProcessId}.");
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
42
Desktop.Shared/Startup/RemoteControlClientBuilder.cs
Normal file
42
Desktop.Shared/Startup/RemoteControlClientBuilder.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.Startup;
|
||||
|
||||
public interface IRemoteControlClientBuilder
|
||||
{
|
||||
void AddBrandingProvider<T>()
|
||||
where T : class, IBrandingProvider;
|
||||
}
|
||||
|
||||
internal class RemoteControlClientBuilder : IRemoteControlClientBuilder
|
||||
{
|
||||
private readonly IServiceCollection _services;
|
||||
|
||||
public RemoteControlClientBuilder(IServiceCollection services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public void AddBrandingProvider<T>()
|
||||
where T : class, IBrandingProvider
|
||||
{
|
||||
_services.AddSingleton<IBrandingProvider, T>();
|
||||
}
|
||||
|
||||
internal void Validate()
|
||||
{
|
||||
var serviceTypes = new[]
|
||||
{
|
||||
typeof(IBrandingProvider)
|
||||
};
|
||||
|
||||
foreach (var type in serviceTypes)
|
||||
{
|
||||
if (!_services.Any(x => x.ServiceType == type))
|
||||
{
|
||||
throw new Exception($"Missing service registration for type {type.Name}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
Desktop.Shared/StaticServiceProvider.cs
Normal file
6
Desktop.Shared/StaticServiceProvider.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Immense.RemoteControl.Desktop.Shared;
|
||||
|
||||
public static class StaticServiceProvider
|
||||
{
|
||||
public static IServiceProvider? Instance { get; set; }
|
||||
}
|
||||
23
Desktop.Shared/ViewModels/FileUpload.cs
Normal file
23
Desktop.Shared/ViewModels/FileUpload.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Reactive;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.Shared.ViewModels;
|
||||
|
||||
public partial class FileUpload : ObservableObject
|
||||
{
|
||||
public string FilePath
|
||||
{
|
||||
get => Get(defaultValue: string.Empty);
|
||||
set => Set(value);
|
||||
}
|
||||
|
||||
|
||||
public double PercentProgress
|
||||
{
|
||||
get => Get<double>();
|
||||
set => Set(value);
|
||||
}
|
||||
|
||||
public CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource();
|
||||
|
||||
public string DisplayName => Path.GetFileName(FilePath);
|
||||
}
|
||||
30
Desktop.UI/App.axaml
Normal file
30
Desktop.UI/App.axaml
Normal file
@ -0,0 +1,30 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Immense.RemoteControl.Desktop.UI.App"
|
||||
RequestedThemeVariant="Light">
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
|
||||
<Style Selector="TextBlock.SectionHeader">
|
||||
<Setter Property="FontWeight" Value="Bold"></Setter>
|
||||
<Setter Property="FontSize" Value="18"></Setter>
|
||||
</Style>
|
||||
<Style Selector="Button.NormalButton">
|
||||
<Setter Property="Background" Value="White"></Setter>
|
||||
<Setter Property="BorderThickness" Value="1"></Setter>
|
||||
<Setter Property="BorderBrush" Value="Black"></Setter>
|
||||
<Setter Property="Padding" Value="6,4"></Setter>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style Selector="Button.HyperlinkButton">
|
||||
<Setter Property="Foreground" Value="DodgerBlue"></Setter>
|
||||
<Setter Property="BorderThickness" Value="0"></Setter>
|
||||
<Setter Property="BorderBrush" Value="Transparent"></Setter>
|
||||
<Setter Property="Cursor" Value="Hand"></Setter>
|
||||
<Setter Property="Background" Value="Transparent"></Setter>
|
||||
<Setter Property="Padding" Value="0"></Setter>
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
|
||||
</Application>
|
||||
32
Desktop.UI/App.axaml.cs
Normal file
32
Desktop.UI/App.axaml.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Data.Core.Plugins;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
// Line below is needed to remove Avalonia data validation.
|
||||
// Without this line you will get duplicate validations from both Avalonia and CT
|
||||
BindingPlugins.DataValidators.RemoveAt(0);
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow();
|
||||
}
|
||||
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
|
||||
{
|
||||
singleViewPlatform.MainView = new MainView();
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
BIN
Desktop.UI/Assets/DefaultIcon.ico
Normal file
BIN
Desktop.UI/Assets/DefaultIcon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
BIN
Desktop.UI/Assets/DefaultIcon.png
Normal file
BIN
Desktop.UI/Assets/DefaultIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
Desktop.UI/Assets/Gear.png
Normal file
BIN
Desktop.UI/Assets/Gear.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
BIN
Desktop.UI/Assets/avalonia-logo.ico
Normal file
BIN
Desktop.UI/Assets/avalonia-logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
47
Desktop.UI/Controls/Dialogs/MessageBox.axaml
Normal file
47
Desktop.UI/Controls/Dialogs/MessageBox.axaml
Normal file
@ -0,0 +1,47 @@
|
||||
<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"
|
||||
xmlns:vm="clr-namespace:Immense.RemoteControl.Desktop.UI.ViewModels"
|
||||
xmlns:fakes="clr-namespace:Immense.RemoteControl.Desktop.UI.ViewModels.Fakes"
|
||||
x:Class="Immense.RemoteControl.Desktop.UI.Controls.Dialogs.MessageBox"
|
||||
Icon="{Binding WindowIcon}"
|
||||
Title="{Binding Caption}"
|
||||
Topmost="True"
|
||||
ShowActivated="True"
|
||||
SizeToContent="WidthAndHeight" MinWidth="300" MinHeight="100"
|
||||
x:DataType="vm:MessageBoxViewModel"
|
||||
x:Name="MessageBoxWindow"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
|
||||
<Design.DataContext>
|
||||
<fakes:FakeMessageBoxViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<StackPanel Margin="10, 20">
|
||||
<TextBlock Text="{Binding Message}"></TextBlock>
|
||||
|
||||
<Grid Margin="0,20,0,0">
|
||||
<Button Classes="NormalButton" HorizontalAlignment="Right" IsVisible="{Binding IsOkButtonVisible}"
|
||||
Command="{Binding OKCommand}"
|
||||
CommandParameter="{Binding #MessageBoxWindow}"
|
||||
Content="OK">
|
||||
</Button>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Classes="NormalButton" HorizontalAlignment="Right" IsVisible="{Binding AreYesNoButtonsVisible}" Margin="5,0,5,0"
|
||||
Command="{Binding YesCommand}"
|
||||
CommandParameter="{Binding #MessageBoxWindow}"
|
||||
Content="Yes">
|
||||
</Button>
|
||||
<Button Classes="NormalButton" HorizontalAlignment="Right" IsVisible="{Binding AreYesNoButtonsVisible}" Margin="5,0,5,0"
|
||||
Command="{Binding NoCommand}"
|
||||
CommandParameter="{Binding #MessageBoxWindow}"
|
||||
Content="No">
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</Window>
|
||||
51
Desktop.UI/Controls/Dialogs/MessageBox.axaml.cs
Normal file
51
Desktop.UI/Controls/Dialogs/MessageBox.axaml.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using CommunityToolkit.Diagnostics;
|
||||
using Immense.RemoteControl.Desktop.Shared;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Threading;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.Controls.Dialogs;
|
||||
|
||||
public partial class MessageBox : Window
|
||||
{
|
||||
public MessageBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public static async Task<MessageBoxResult> Show(string message, string caption, MessageBoxType type)
|
||||
{
|
||||
Guard.IsNotNull(StaticServiceProvider.Instance, nameof(StaticServiceProvider.Instance));
|
||||
|
||||
var dispatcher = StaticServiceProvider.Instance.GetRequiredService<IUiDispatcher>();
|
||||
|
||||
return await dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
var viewModel = StaticServiceProvider.Instance.GetRequiredService<IMessageBoxViewModel>();
|
||||
var messageBox = new MessageBox()
|
||||
{
|
||||
DataContext = viewModel
|
||||
};
|
||||
viewModel.Caption = caption;
|
||||
viewModel.Message = message;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case MessageBoxType.OK:
|
||||
viewModel.IsOkButtonVisible = true;
|
||||
break;
|
||||
case MessageBoxType.YesNo:
|
||||
viewModel.AreYesNoButtonsVisible = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
await dispatcher.ShowDialog(messageBox);
|
||||
|
||||
return viewModel.Result;
|
||||
});
|
||||
}
|
||||
}
|
||||
9
Desktop.UI/Controls/Dialogs/MessageBoxResult.cs
Normal file
9
Desktop.UI/Controls/Dialogs/MessageBoxResult.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Immense.RemoteControl.Desktop.UI.Controls.Dialogs;
|
||||
|
||||
public enum MessageBoxResult
|
||||
{
|
||||
Cancel,
|
||||
OK,
|
||||
Yes,
|
||||
No
|
||||
}
|
||||
7
Desktop.UI/Controls/Dialogs/MessageBoxType.cs
Normal file
7
Desktop.UI/Controls/Dialogs/MessageBoxType.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Immense.RemoteControl.Desktop.UI.Controls.Dialogs;
|
||||
|
||||
public enum MessageBoxType
|
||||
{
|
||||
OK,
|
||||
YesNo
|
||||
}
|
||||
50
Desktop.UI/Desktop.UI.csproj
Normal file
50
Desktop.UI/Desktop.UI.csproj
Normal file
@ -0,0 +1,50 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 -->
|
||||
<TrimMode>copyused</TrimMode>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\avalonia-logo.ico" />
|
||||
<None Remove="Assets\DefaultIcon.ico" />
|
||||
<None Remove="Assets\DefaultIcon.png" />
|
||||
<None Remove="Assets\Gear.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\avalonia-logo.ico" />
|
||||
<AvaloniaResource Include="Assets\DefaultIcon.ico" />
|
||||
<AvaloniaResource Include="Assets\DefaultIcon.png" />
|
||||
<AvaloniaResource Include="Assets\Gear.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!--This helps with theme dll-s trimming.
|
||||
If you will publish your application in self-contained mode with p:PublishTrimmed=true and it will use Fluent theme Default theme will be trimmed from the output and vice versa.
|
||||
https://github.com/AvaloniaUI/Avalonia/issues/5593 -->
|
||||
<TrimmableAssembly Include="Avalonia.Themes.Fluent" />
|
||||
<TrimmableAssembly Include="Avalonia.Themes.Default" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.0.11" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.11" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.11" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.11" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Desktop.Shared\Desktop.Shared.csproj" />
|
||||
<ProjectReference Include="..\Shared\Shared.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
8
Desktop.UI/GlobalUsings.cs
Normal file
8
Desktop.UI/GlobalUsings.cs
Normal file
@ -0,0 +1,8 @@
|
||||
global using System;
|
||||
global using System.IO;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
global using System.Threading.Tasks;
|
||||
global using Immense.RemoteControl.Desktop.UI.Services;
|
||||
global using Immense.RemoteControl.Desktop.UI.ViewModels;
|
||||
global using Immense.RemoteControl.Desktop.UI.Views;
|
||||
67
Desktop.UI/Services/ChatUiService.cs
Normal file
67
Desktop.UI/Services/ChatUiService.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using Avalonia.Controls;
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using System.ComponentModel;
|
||||
using Immense.RemoteControl.Desktop.UI.Controls.Dialogs;
|
||||
using CommunityToolkit.Diagnostics;
|
||||
using Remotely.Shared.Models;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.Services;
|
||||
|
||||
public class ChatUiService : IChatUiService
|
||||
{
|
||||
private readonly IUiDispatcher _dispatcher;
|
||||
private readonly IDialogProvider _dialogProvider;
|
||||
private readonly IViewModelFactory _viewModelFactory;
|
||||
private IChatWindowViewModel? _chatViewModel;
|
||||
|
||||
public ChatUiService(
|
||||
IUiDispatcher dispatcher,
|
||||
IDialogProvider dialogProvider,
|
||||
IViewModelFactory viewModelFactory)
|
||||
{
|
||||
_dispatcher = dispatcher;
|
||||
_dialogProvider = dialogProvider;
|
||||
_viewModelFactory = viewModelFactory;
|
||||
}
|
||||
|
||||
public event EventHandler? ChatWindowClosed;
|
||||
|
||||
public async Task ReceiveChat(ChatMessage chatMessage)
|
||||
{
|
||||
await _dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
if (chatMessage.Disconnected)
|
||||
{
|
||||
await _dialogProvider.Show("Your partner has disconnected from the chat.", "Partner Disconnected", MessageBoxType.OK);
|
||||
Environment.Exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_chatViewModel != null)
|
||||
{
|
||||
_chatViewModel.SenderName = chatMessage.SenderName;
|
||||
_chatViewModel.ChatMessages.Add(chatMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowChatWindow(string organizationName, StreamWriter writer)
|
||||
{
|
||||
_dispatcher.Post(() =>
|
||||
{
|
||||
_chatViewModel = _viewModelFactory.CreateChatWindowViewModel(organizationName, writer);
|
||||
var chatWindow = new ChatWindow()
|
||||
{
|
||||
DataContext = _chatViewModel
|
||||
};
|
||||
|
||||
chatWindow.Closing += ChatWindow_Closing;
|
||||
_dispatcher.ShowMainWindow(chatWindow);
|
||||
});
|
||||
}
|
||||
|
||||
private void ChatWindow_Closing(object? sender, CancelEventArgs e)
|
||||
{
|
||||
ChatWindowClosed?.Invoke(this, e);
|
||||
}
|
||||
}
|
||||
88
Desktop.UI/Services/ClipboardService.cs
Normal file
88
Desktop.UI/Services/ClipboardService.cs
Normal file
@ -0,0 +1,88 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.Services;
|
||||
|
||||
public class ClipboardService : IClipboardService
|
||||
{
|
||||
private readonly IUiDispatcher _dispatcher;
|
||||
private readonly ILogger<ClipboardService> _logger;
|
||||
private Task? _watcherTask;
|
||||
|
||||
public event EventHandler<string>? ClipboardTextChanged;
|
||||
|
||||
public ClipboardService(
|
||||
IUiDispatcher dispatcher,
|
||||
ILogger<ClipboardService> logger)
|
||||
{
|
||||
_dispatcher = dispatcher;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private string ClipboardText { get; set; } = string.Empty;
|
||||
|
||||
public void BeginWatching()
|
||||
{
|
||||
if (_watcherTask?.Status == TaskStatus.Running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_watcherTask = Task.Run(
|
||||
async () => await WatchClipboard(_dispatcher.ApplicationExitingToken),
|
||||
_dispatcher.ApplicationExitingToken);
|
||||
}
|
||||
|
||||
public async Task SetText(string clipboardText)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_dispatcher?.Clipboard is null)
|
||||
{
|
||||
_logger.LogWarning("Clipboard is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(clipboardText))
|
||||
{
|
||||
await _dispatcher.Clipboard.ClearAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await _dispatcher.Clipboard.SetTextAsync(clipboardText);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while setting text.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WatchClipboard(CancellationToken cancelToken)
|
||||
{
|
||||
while (
|
||||
!cancelToken.IsCancellationRequested &&
|
||||
!Environment.HasShutdownStarted)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_dispatcher?.Clipboard is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentText = await _dispatcher.Clipboard.GetTextAsync();
|
||||
if (!string.IsNullOrEmpty(currentText) && currentText != ClipboardText)
|
||||
{
|
||||
ClipboardText = currentText;
|
||||
ClipboardTextChanged?.Invoke(this, ClipboardText);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Desktop.UI/Services/DialogProvider.cs
Normal file
22
Desktop.UI/Services/DialogProvider.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Immense.RemoteControl.Desktop.UI.Controls.Dialogs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.Services;
|
||||
|
||||
public interface IDialogProvider
|
||||
{
|
||||
Task<MessageBoxResult> Show(string message, string caption, MessageBoxType type);
|
||||
}
|
||||
|
||||
internal class DialogProvider : IDialogProvider
|
||||
{
|
||||
public async Task<MessageBoxResult> Show(string message, string caption, MessageBoxType type)
|
||||
{
|
||||
return await MessageBox.Show(message, caption, type);
|
||||
}
|
||||
}
|
||||
62
Desktop.UI/Services/RemoteControlAccessService.cs
Normal file
62
Desktop.UI/Services/RemoteControlAccessService.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Shared.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.Services;
|
||||
|
||||
public class RemoteControlAccessService : IRemoteControlAccessService
|
||||
{
|
||||
private readonly IViewModelFactory _viewModelFactory;
|
||||
private readonly IUiDispatcher _dispatcher;
|
||||
private readonly ILogger<RemoteControlAccessService> _logger;
|
||||
private volatile int _promptCount = 0;
|
||||
|
||||
public RemoteControlAccessService(
|
||||
IViewModelFactory viewModelFactory,
|
||||
IUiDispatcher dispatcher,
|
||||
ILogger<RemoteControlAccessService> logger)
|
||||
{
|
||||
_viewModelFactory = viewModelFactory;
|
||||
_dispatcher = dispatcher;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool IsPromptOpen => _promptCount > 0;
|
||||
|
||||
public async Task<PromptForAccessResult> PromptForAccess(string requesterName, string organizationName)
|
||||
{
|
||||
return await _dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Interlocked.Increment(ref _promptCount);
|
||||
var viewModel = _viewModelFactory.CreatePromptForAccessViewModel(requesterName, organizationName);
|
||||
var promptWindow = new PromptForAccessWindow()
|
||||
{
|
||||
DataContext = viewModel
|
||||
};
|
||||
|
||||
var result = await _dispatcher.Show(promptWindow, TimeSpan.FromMinutes(1));
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return PromptForAccessResult.TimedOut;
|
||||
}
|
||||
|
||||
return viewModel.PromptResult ?
|
||||
PromptForAccessResult.Accepted :
|
||||
PromptForAccessResult.Denied;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while prompting for remote control access.");
|
||||
return PromptForAccessResult.Error;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Decrement(ref _promptCount);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
26
Desktop.UI/Services/SessionIndicator.cs
Normal file
26
Desktop.UI/Services/SessionIndicator.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Immense.RemoteControl.Desktop.Shared;
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.Services;
|
||||
|
||||
public class SessionIndicator : ISessionIndicator
|
||||
{
|
||||
private readonly IUiDispatcher _dispatcher;
|
||||
|
||||
public SessionIndicator(IUiDispatcher dispatcher)
|
||||
{
|
||||
_dispatcher = dispatcher;
|
||||
}
|
||||
public void Show()
|
||||
{
|
||||
_dispatcher.Post(() =>
|
||||
{
|
||||
var indicatorWindow = new SessionIndicatorWindow()
|
||||
{
|
||||
DataContext = StaticServiceProvider.Instance?.GetRequiredService<ISessionIndicatorWindowViewModel>()
|
||||
};
|
||||
_dispatcher.ShowMainWindow(indicatorWindow);
|
||||
});
|
||||
}
|
||||
}
|
||||
255
Desktop.UI/Services/UiDispatcher.cs
Normal file
255
Desktop.UI/Services/UiDispatcher.cs
Normal file
@ -0,0 +1,255 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Threading;
|
||||
using Immense.RemoteControl.Shared.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remotely.Shared.Primitives;
|
||||
using System.Threading;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.Services;
|
||||
|
||||
public interface IUiDispatcher
|
||||
{
|
||||
CancellationToken ApplicationExitingToken { get; }
|
||||
IClipboard? Clipboard { get; }
|
||||
Application? CurrentApp { get; }
|
||||
|
||||
Window? MainWindow { get; }
|
||||
|
||||
void Invoke(Action action);
|
||||
Task InvokeAsync(Action action, DispatcherPriority priority = default);
|
||||
Task InvokeAsync(Func<Task> func, DispatcherPriority priority = default);
|
||||
Task<T> InvokeAsync<T>(Func<Task<T>> func, DispatcherPriority priority = default);
|
||||
void Post(Action action, DispatcherPriority priority = default);
|
||||
Task<bool> Show(Window window, TimeSpan timeout);
|
||||
|
||||
Task ShowDialog(Window window);
|
||||
void ShowMainWindow(Window window);
|
||||
|
||||
void ShowWindow(Window window);
|
||||
void Shutdown();
|
||||
void StartClassicDesktop();
|
||||
Task<Result> StartHeadless();
|
||||
}
|
||||
|
||||
internal class UiDispatcher : IUiDispatcher
|
||||
{
|
||||
private static readonly CancellationTokenSource _appCts = new();
|
||||
private static Application? _currentApp;
|
||||
private readonly ILogger<UiDispatcher> _logger;
|
||||
private AppBuilder? _appBuilder;
|
||||
private Window? _headlessMainWindow;
|
||||
|
||||
public UiDispatcher(ILogger<UiDispatcher> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public CancellationToken ApplicationExitingToken => _appCts.Token;
|
||||
|
||||
public IClipboard? Clipboard
|
||||
{
|
||||
get
|
||||
{
|
||||
if (CurrentApp?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopApp)
|
||||
{
|
||||
return desktopApp.MainWindow?.Clipboard;
|
||||
}
|
||||
|
||||
if (CurrentApp?.ApplicationLifetime is ISingleViewApplicationLifetime svApp)
|
||||
{
|
||||
return TopLevel.GetTopLevel(svApp.MainView)?.Clipboard;
|
||||
}
|
||||
|
||||
if (_headlessMainWindow is not null)
|
||||
{
|
||||
return _headlessMainWindow.Clipboard;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Application? CurrentApp => _currentApp ?? _appBuilder?.Instance;
|
||||
|
||||
public Window? MainWindow
|
||||
{
|
||||
get
|
||||
{
|
||||
if (CurrentApp?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime app)
|
||||
{
|
||||
return app.MainWindow;
|
||||
}
|
||||
|
||||
return _headlessMainWindow;
|
||||
}
|
||||
}
|
||||
public void Invoke(Action action)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(action);
|
||||
}
|
||||
|
||||
public Task InvokeAsync(Func<Task> func, DispatcherPriority priority = default)
|
||||
{
|
||||
|
||||
return Dispatcher.UIThread.InvokeAsync(func, priority);
|
||||
}
|
||||
|
||||
public Task<T> InvokeAsync<T>(Func<Task<T>> func, DispatcherPriority priority = default)
|
||||
{
|
||||
return Dispatcher.UIThread.InvokeAsync(func, priority);
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(Action action, DispatcherPriority priority = default)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(action, priority);
|
||||
}
|
||||
|
||||
public void Post(Action action, DispatcherPriority priority = default)
|
||||
{
|
||||
Dispatcher.UIThread.Post(action, priority);
|
||||
}
|
||||
|
||||
public async Task<bool> Show(Window window, TimeSpan timeout)
|
||||
{
|
||||
return await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
using var closeSignal = new SemaphoreSlim(0, 1);
|
||||
window.Closed += (sender, arg) =>
|
||||
{
|
||||
closeSignal.Release();
|
||||
};
|
||||
|
||||
window.Show();
|
||||
var result = await closeSignal.WaitAsync(timeout);
|
||||
if (!result)
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ShowDialog(Window window)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
if (MainWindow is not null)
|
||||
{
|
||||
await window.ShowDialog(MainWindow);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var closeSignal = new SemaphoreSlim(0, 1);
|
||||
window.Closed += (sender, arg) =>
|
||||
{
|
||||
closeSignal.Release();
|
||||
};
|
||||
window.Show();
|
||||
|
||||
await closeSignal.WaitAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
public void ShowMainWindow(Window window)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
_headlessMainWindow = window;
|
||||
window.Show();
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowWindow(Window window)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
if (MainWindow is not null)
|
||||
{
|
||||
window.Show(MainWindow);
|
||||
}
|
||||
else
|
||||
{
|
||||
window.Show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_appCts.Cancel();
|
||||
if (CurrentApp?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime &&
|
||||
lifetime.TryShutdown())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
public void StartClassicDesktop()
|
||||
{
|
||||
try
|
||||
{
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
_appBuilder = BuildAvaloniaApp();
|
||||
_appBuilder.StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while starting foreground app.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> StartHeadless()
|
||||
{
|
||||
try
|
||||
{
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
var argString = string.Join(" ", args);
|
||||
_logger.LogInformation("Starting dispatcher in unattended mode with args: [{args}].", argString);
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
_appBuilder = BuildAvaloniaApp();
|
||||
_appBuilder.Start(RunHeadless, args);
|
||||
}, _appCts.Token);
|
||||
|
||||
var waitResult = await WaitHelper.WaitForAsync(
|
||||
() => CurrentApp is not null,
|
||||
TimeSpan.FromSeconds(10))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!waitResult)
|
||||
{
|
||||
const string err = "Unattended dispatcher failed to start in time.";
|
||||
_logger.LogError(err);
|
||||
Shutdown();
|
||||
return Result.Fail(err);
|
||||
}
|
||||
|
||||
return Result.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while starting background app.");
|
||||
return Result.Fail(ex);
|
||||
}
|
||||
}
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
private static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
|
||||
private static void RunHeadless(Application app, string[] args)
|
||||
{
|
||||
_currentApp = app;
|
||||
app.Run(_appCts.Token);
|
||||
}
|
||||
}
|
||||
60
Desktop.UI/Services/ViewModelFactory.cs
Normal file
60
Desktop.UI/Services/ViewModelFactory.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.IO;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.Services;
|
||||
|
||||
// Normally, I'd use a view model locator. But enough view models require a factory pattern
|
||||
// that I thought it more consistent to put them all here.
|
||||
public interface IViewModelFactory
|
||||
{
|
||||
IChatWindowViewModel CreateChatWindowViewModel(string organizationName, StreamWriter streamWriter);
|
||||
IFileTransferWindowViewModel CreateFileTransferWindowViewModel(IViewer viewer);
|
||||
IHostNamePromptViewModel CreateHostNamePromptViewModel();
|
||||
IPromptForAccessWindowViewModel CreatePromptForAccessViewModel(string requesterName, string organizationName);
|
||||
}
|
||||
|
||||
internal class ViewModelFactory : IViewModelFactory
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public ViewModelFactory(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public IChatWindowViewModel CreateChatWindowViewModel(string organizationName, StreamWriter streamWriter)
|
||||
{
|
||||
var branding = _serviceProvider.GetRequiredService<IBrandingProvider>();
|
||||
var dispatcher = _serviceProvider.GetRequiredService<IUiDispatcher>();
|
||||
var logger = _serviceProvider.GetRequiredService<ILogger<ChatWindowViewModel>>();
|
||||
return new ChatWindowViewModel(streamWriter, organizationName, branding, dispatcher, logger);
|
||||
}
|
||||
|
||||
public IFileTransferWindowViewModel CreateFileTransferWindowViewModel(IViewer viewer)
|
||||
{
|
||||
var brandingProvider = _serviceProvider.GetRequiredService<IBrandingProvider>();
|
||||
var dispatcher = _serviceProvider.GetRequiredService<IUiDispatcher>();
|
||||
var logger = _serviceProvider.GetRequiredService<ILogger<FileTransferWindowViewModel>>();
|
||||
var fileTransfer = _serviceProvider.GetRequiredService<IFileTransferService>();
|
||||
return new FileTransferWindowViewModel(viewer, brandingProvider, dispatcher, fileTransfer, logger);
|
||||
}
|
||||
|
||||
public IPromptForAccessWindowViewModel CreatePromptForAccessViewModel(string requesterName, string organizationName)
|
||||
{
|
||||
var brandingProvider = _serviceProvider.GetRequiredService<IBrandingProvider>();
|
||||
var dispatcher = _serviceProvider.GetRequiredService<IUiDispatcher>();
|
||||
var logger = _serviceProvider.GetRequiredService<ILogger<PromptForAccessWindowViewModel>>();
|
||||
return new PromptForAccessWindowViewModel(requesterName, organizationName, brandingProvider, dispatcher, logger);
|
||||
}
|
||||
|
||||
public IHostNamePromptViewModel CreateHostNamePromptViewModel()
|
||||
{
|
||||
var brandingProvider = _serviceProvider.GetRequiredService<IBrandingProvider>();
|
||||
var dispatcher = _serviceProvider.GetRequiredService<IUiDispatcher>();
|
||||
var logger = _serviceProvider.GetRequiredService<ILogger<HostNamePromptViewModel>>();
|
||||
return new HostNamePromptViewModel(brandingProvider, dispatcher, logger);
|
||||
}
|
||||
}
|
||||
23
Desktop.UI/Startup/IServiceCollectionExtensions.cs
Normal file
23
Desktop.UI/Startup/IServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.Startup;
|
||||
|
||||
public static class IServiceCollectionExtensions
|
||||
{
|
||||
public static void AddRemoteControlUi(
|
||||
this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IUiDispatcher, UiDispatcher>();
|
||||
services.AddSingleton<IChatUiService, ChatUiService>();
|
||||
services.AddSingleton<IClipboardService, ClipboardService>();
|
||||
services.AddSingleton<ISessionIndicator, SessionIndicator>();
|
||||
services.AddSingleton<IRemoteControlAccessService, RemoteControlAccessService>();
|
||||
services.AddSingleton<IViewModelFactory, ViewModelFactory>();
|
||||
services.AddSingleton<IMainWindowViewModel, MainWindowViewModel>();
|
||||
services.AddSingleton<IMainViewViewModel, MainViewViewModel>();
|
||||
services.AddSingleton<ISessionIndicatorWindowViewModel, SessionIndicatorWindowViewModel>();
|
||||
services.AddTransient<IMessageBoxViewModel, MessageBoxViewModel>();
|
||||
services.AddSingleton<IDialogProvider, DialogProvider>();
|
||||
}
|
||||
}
|
||||
92
Desktop.UI/ViewModels/BrandedViewModelBase.cs
Normal file
92
Desktop.UI/ViewModels/BrandedViewModelBase.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Reactive;
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remotely.Shared.Entities;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.ViewModels;
|
||||
|
||||
public interface IBrandedViewModelBase
|
||||
{
|
||||
Bitmap? Icon { get; set; }
|
||||
string ProductName { get; set; }
|
||||
WindowIcon? WindowIcon { get; set; }
|
||||
}
|
||||
|
||||
public class BrandedViewModelBase : ObservableObject, IBrandedViewModelBase
|
||||
{
|
||||
private static BrandingInfo? _brandingInfo;
|
||||
protected readonly ILogger<BrandedViewModelBase> _logger;
|
||||
protected readonly IUiDispatcher _dispatcher;
|
||||
private readonly IBrandingProvider _brandingProvider;
|
||||
|
||||
|
||||
public BrandedViewModelBase(
|
||||
IBrandingProvider brandingProvider,
|
||||
IUiDispatcher dispatcher,
|
||||
ILogger<BrandedViewModelBase> logger)
|
||||
{
|
||||
_brandingProvider = brandingProvider;
|
||||
_dispatcher = dispatcher;
|
||||
_logger = logger;
|
||||
|
||||
ApplyBrandingImpl();
|
||||
}
|
||||
|
||||
public Bitmap? Icon
|
||||
{
|
||||
get => Get<Bitmap?>();
|
||||
set => Set(value);
|
||||
}
|
||||
|
||||
public string ProductName
|
||||
{
|
||||
get => Get<string?>() ?? "Remote Control";
|
||||
set => Set(value ?? "Remote Control");
|
||||
}
|
||||
|
||||
public WindowIcon? WindowIcon
|
||||
{
|
||||
get => Get<WindowIcon?>();
|
||||
set => Set(value);
|
||||
}
|
||||
|
||||
private void ApplyBrandingImpl()
|
||||
{
|
||||
_dispatcher.Invoke(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_brandingInfo ??= _brandingProvider.CurrentBranding;
|
||||
|
||||
ProductName = _brandingInfo.Product;
|
||||
|
||||
if (_brandingInfo.Icon?.Any() == true)
|
||||
{
|
||||
using var imageStream = new MemoryStream(_brandingInfo.Icon);
|
||||
Icon = new Bitmap(imageStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var imageStream =
|
||||
Assembly
|
||||
.GetExecutingAssembly()
|
||||
.GetManifestResourceStream("Immense.RemoteControl.Desktop.Shared.Assets.DefaultIcon.png") ?? new MemoryStream();
|
||||
|
||||
Icon = new Bitmap(imageStream);
|
||||
}
|
||||
|
||||
WindowIcon = new WindowIcon(Icon);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error applying branding.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
115
Desktop.UI/ViewModels/ChatWindowViewModel.cs
Normal file
115
Desktop.UI/ViewModels/ChatWindowViewModel.cs
Normal file
@ -0,0 +1,115 @@
|
||||
using Avalonia.Controls;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Windows.Input;
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Immense.RemoteControl.Desktop.Shared.Reactive;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Remotely.Shared.Models;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.ViewModels;
|
||||
|
||||
public interface IChatWindowViewModel : IBrandedViewModelBase
|
||||
{
|
||||
ObservableCollection<ChatMessage> ChatMessages { get; }
|
||||
string ChatSessionHeader { get; }
|
||||
ICommand CloseCommand { get; }
|
||||
string InputText { get; set; }
|
||||
ICommand MinimizeCommand { get; }
|
||||
string OrganizationName { get; set; }
|
||||
string SenderName { get; set; }
|
||||
|
||||
Task SendChatMessage();
|
||||
}
|
||||
|
||||
public class ChatWindowViewModel : BrandedViewModelBase, IChatWindowViewModel
|
||||
{
|
||||
private readonly StreamWriter? _streamWriter;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ChatWindowViewModel(
|
||||
StreamWriter streamWriter,
|
||||
string organizationName,
|
||||
IBrandingProvider brandingProvider,
|
||||
IUiDispatcher dispatcher,
|
||||
ILogger<ChatWindowViewModel> logger)
|
||||
: base(brandingProvider, dispatcher, logger)
|
||||
{
|
||||
_streamWriter = streamWriter;
|
||||
if (!string.IsNullOrWhiteSpace(organizationName))
|
||||
{
|
||||
OrganizationName = organizationName;
|
||||
}
|
||||
CloseCommand = new RelayCommand<Window>(CloseWindow);
|
||||
MinimizeCommand = new RelayCommand<Window>(MinimizeWindow);
|
||||
}
|
||||
|
||||
|
||||
public ObservableCollection<ChatMessage> ChatMessages { get; } = new ObservableCollection<ChatMessage>();
|
||||
|
||||
public string ChatSessionHeader => $"Chat session with {OrganizationName}";
|
||||
|
||||
public ICommand CloseCommand { get; }
|
||||
|
||||
public string InputText
|
||||
{
|
||||
get => Get<string>() ?? string.Empty;
|
||||
set => Set(value);
|
||||
}
|
||||
|
||||
public ICommand MinimizeCommand { get; }
|
||||
|
||||
public string OrganizationName
|
||||
{
|
||||
get => Get<string>() ?? "your IT provider";
|
||||
set
|
||||
{
|
||||
Set(value);
|
||||
NotifyPropertyChanged(nameof(ChatSessionHeader));
|
||||
}
|
||||
}
|
||||
|
||||
public string SenderName
|
||||
{
|
||||
get => Get<string>() ?? "a technician";
|
||||
set => Set(value);
|
||||
}
|
||||
|
||||
public async Task SendChatMessage()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(InputText) ||
|
||||
_streamWriter is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var chatMessage = new ChatMessage(string.Empty, InputText);
|
||||
InputText = string.Empty;
|
||||
await _streamWriter.WriteLineAsync(JsonSerializer.Serialize(chatMessage));
|
||||
await _streamWriter.FlushAsync();
|
||||
chatMessage.SenderName = "You";
|
||||
ChatMessages.Add(chatMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error sending chat message");
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseWindow(Window? obj)
|
||||
{
|
||||
obj?.Close();
|
||||
}
|
||||
|
||||
private void MinimizeWindow(Window? obj)
|
||||
{
|
||||
if (obj is not null)
|
||||
{
|
||||
obj.WindowState = WindowState.Minimized;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Desktop.UI/ViewModels/Fakes/FakeBrandedViewModelBase.cs
Normal file
51
Desktop.UI/ViewModels/Fakes/FakeBrandedViewModelBase.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Immense.RemoteControl.Desktop.UI.Controls.Dialogs;
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using Remotely.Shared.Entities;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.ViewModels.Fakes;
|
||||
|
||||
public class FakeBrandedViewModelBase : IBrandedViewModelBase
|
||||
{
|
||||
private readonly BrandingInfo _brandingInfo;
|
||||
private Bitmap? _icon;
|
||||
|
||||
public FakeBrandedViewModelBase()
|
||||
{
|
||||
_brandingInfo = new BrandingInfo();
|
||||
_icon = GetBitmapImageIcon(_brandingInfo);
|
||||
}
|
||||
public Bitmap? Icon
|
||||
{
|
||||
get => _icon;
|
||||
set => _icon = value;
|
||||
}
|
||||
public string ProductName { get; set; } = "Test Product";
|
||||
public WindowIcon? WindowIcon { get; set; }
|
||||
|
||||
public Task ApplyBranding()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Bitmap? GetBitmapImageIcon(BrandingInfo bi)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var imageStream = typeof(Shared.Services.AppState)
|
||||
.Assembly
|
||||
.GetManifestResourceStream("Immense.RemoteControl.Desktop.Shared.Assets.DefaultIcon.png") ?? new MemoryStream();
|
||||
|
||||
return new Bitmap(imageStream);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Desktop.UI/ViewModels/Fakes/FakeChatWindowViewModel.cs
Normal file
41
Desktop.UI/ViewModels/Fakes/FakeChatWindowViewModel.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Reactive;
|
||||
using Remotely.Shared.Models;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.ViewModels.Fakes;
|
||||
|
||||
public class FakeChatWindowViewModel : FakeBrandedViewModelBase, IChatWindowViewModel
|
||||
{
|
||||
public ObservableCollection<ChatMessage> ChatMessages { get; } = new()
|
||||
{
|
||||
new ChatMessage("Designer", "This is a design-time test message.")
|
||||
};
|
||||
|
||||
public string InputText
|
||||
{
|
||||
get => "Some text I'm going to send.";
|
||||
set { }
|
||||
}
|
||||
public string OrganizationName
|
||||
{
|
||||
get => "Design-Time Technicians";
|
||||
set { }
|
||||
}
|
||||
public string SenderName
|
||||
{
|
||||
get => "Test Tech";
|
||||
set { }
|
||||
}
|
||||
|
||||
public string ChatSessionHeader => "Test Chat";
|
||||
|
||||
public ICommand CloseCommand => new RelayCommand(() => { });
|
||||
|
||||
public ICommand MinimizeCommand => new RelayCommand(() => { });
|
||||
|
||||
public Task SendChatMessage()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
33
Desktop.UI/ViewModels/Fakes/FakeFileTransferViewModel.cs
Normal file
33
Desktop.UI/ViewModels/Fakes/FakeFileTransferViewModel.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Reactive;
|
||||
using Immense.RemoteControl.Desktop.Shared.ViewModels;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.ViewModels.Fakes;
|
||||
|
||||
public class FakeFileTransferViewModel : FakeBrandedViewModelBase, IFileTransferWindowViewModel
|
||||
{
|
||||
public ObservableCollection<FileUpload> FileUploads { get; } = new();
|
||||
|
||||
public string ViewerConnectionId { get; set; } = string.Empty;
|
||||
public string ViewerName { get; set; } = string.Empty;
|
||||
|
||||
public ICommand OpenFileUploadDialogCommand { get; } = new RelayCommand(() => { });
|
||||
|
||||
public ICommand RemoveFileUploadCommand { get; } = new RelayCommand(() => { });
|
||||
|
||||
public Task OpenFileUploadDialog()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void RemoveFileUpload(FileUpload? fileUpload)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Task UploadFile(string filePath)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
11
Desktop.UI/ViewModels/Fakes/FakeHostNamePromptViewModel.cs
Normal file
11
Desktop.UI/ViewModels/Fakes/FakeHostNamePromptViewModel.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Reactive;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.ViewModels.Fakes;
|
||||
|
||||
public class FakeHostNamePromptViewModel : FakeBrandedViewModelBase, IHostNamePromptViewModel
|
||||
{
|
||||
public string Host { get; set; } = "https://localhost:7024";
|
||||
|
||||
public ICommand OKCommand => new RelayCommand(() => { });
|
||||
}
|
||||
169
Desktop.UI/ViewModels/Fakes/FakeMainViewViewModel.cs
Normal file
169
Desktop.UI/ViewModels/Fakes/FakeMainViewViewModel.cs
Normal file
@ -0,0 +1,169 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Reactive;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Immense.RemoteControl.Desktop.Shared.ViewModels;
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using Immense.RemoteControl.Shared.Models.Dtos;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.ViewModels.Fakes;
|
||||
public class FakeMainViewViewModel : FakeBrandedViewModelBase, IMainViewViewModel
|
||||
{
|
||||
|
||||
public AsyncRelayCommand ChangeServerCommand => new(() => Task.CompletedTask);
|
||||
|
||||
public ICommand CloseCommand => new RelayCommand(() => { });
|
||||
public AsyncRelayCommand CopyLinkCommand => new(() => Task.CompletedTask);
|
||||
public double CopyMessageOpacity { get; set; }
|
||||
|
||||
public string Host { get; set; } = string.Empty;
|
||||
|
||||
public bool IsAdministrator => true;
|
||||
|
||||
public bool IsCopyMessageVisible { get; set; }
|
||||
public ICommand MinimizeCommand => new RelayCommand(() => { });
|
||||
public ICommand OpenOptionsMenu => new RelayCommand(() => { });
|
||||
public AsyncRelayCommand RemoveViewersCommand => new(() => Task.CompletedTask);
|
||||
|
||||
public string StatusMessage { get; set; } = "392 527 094";
|
||||
|
||||
public ObservableCollection<IViewer> Viewers { get; } = new() { new FakeViewer() };
|
||||
|
||||
public IList<IViewer> SelectedViewers { get; } = new List<IViewer>();
|
||||
|
||||
public bool CanRemoveViewers()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task ChangeServer()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task CopyLink()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task GetSessionID()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Init()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task PromptForHostName()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task RemoveViewers()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private class FakeViewer : IViewer
|
||||
{
|
||||
public IScreenCapturer Capturer => null!;
|
||||
|
||||
public double CurrentFps => default;
|
||||
|
||||
public double CurrentMbps => default;
|
||||
|
||||
public bool DisconnectRequested { get; set; } = false;
|
||||
public bool HasControl { get; set; } = true;
|
||||
|
||||
public int ImageQuality => 80;
|
||||
|
||||
public bool IsResponsive => true;
|
||||
|
||||
public string Name { get; set; } = "Rick James";
|
||||
|
||||
public TimeSpan RoundTripLatency => default;
|
||||
|
||||
public string ViewerConnectionId { get; set; } = string.Empty;
|
||||
|
||||
public void AppendSentFrame(SentFrame sentFrame)
|
||||
{
|
||||
}
|
||||
|
||||
public Task ApplyAutoQuality()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task CalculateMetrics()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public void IncrementFpsCount()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Task SendAudioSample(byte[] audioSample)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendClipboardText(string clipboardText)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendCursorChange(CursorInfo cursorInfo)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendDesktopStream(IAsyncEnumerable<byte[]> asyncEnumerable, Guid streamId)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendFile(FileUpload fileUpload, Action<double> progressUpdateCallback, CancellationToken cancelToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendScreenData(string selectedDisplay, IEnumerable<string> displayNames, int screenWidth, int screenHeight)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendScreenSize(int width, int height)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendSessionMetrics(SessionMetricsDto metrics)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendWindowsSessions()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void SetLastFrameReceived(DateTimeOffset timestamp)
|
||||
{
|
||||
}
|
||||
|
||||
public Task<bool> WaitForViewer()
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
Desktop.UI/ViewModels/Fakes/FakeMainWindowViewModel.cs
Normal file
5
Desktop.UI/ViewModels/Fakes/FakeMainWindowViewModel.cs
Normal file
@ -0,0 +1,5 @@
|
||||
namespace Immense.RemoteControl.Desktop.UI.ViewModels.Fakes;
|
||||
|
||||
public class FakeMainWindowViewModel : FakeBrandedViewModelBase, IMainWindowViewModel
|
||||
{
|
||||
}
|
||||
21
Desktop.UI/ViewModels/Fakes/FakeMessageBoxViewModel.cs
Normal file
21
Desktop.UI/ViewModels/Fakes/FakeMessageBoxViewModel.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Reactive;
|
||||
using System.Windows.Input;
|
||||
using Immense.RemoteControl.Desktop.UI.Controls.Dialogs;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.ViewModels.Fakes;
|
||||
|
||||
public class FakeMessageBoxViewModel : FakeBrandedViewModelBase, IMessageBoxViewModel
|
||||
{
|
||||
public bool AreYesNoButtonsVisible { get; set; } = true;
|
||||
public string Caption { get; set; } = "Test Caption";
|
||||
public bool IsOkButtonVisible { get; set; } = false;
|
||||
public string Message { get; set; } = "This is a test message.";
|
||||
|
||||
public ICommand NoCommand => new RelayCommand(() => { });
|
||||
|
||||
public ICommand OKCommand => new RelayCommand(() => { });
|
||||
|
||||
public MessageBoxResult Result { get; set; } = MessageBoxResult.Yes;
|
||||
|
||||
public ICommand YesCommand => new RelayCommand(() => { });
|
||||
}
|
||||
23
Desktop.UI/ViewModels/Fakes/FakePromptForAccessViewModel.cs
Normal file
23
Desktop.UI/ViewModels/Fakes/FakePromptForAccessViewModel.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Reactive;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Immense.RemoteControl.Desktop.UI.ViewModels.Fakes;
|
||||
|
||||
public class FakePromptForAccessViewModel : FakeBrandedViewModelBase, IPromptForAccessWindowViewModel
|
||||
{
|
||||
public string OrganizationName { get; set; } = "Test Organization";
|
||||
public bool PromptResult { get; set; }
|
||||
public string RequesterName { get; set; } = "Test Requester";
|
||||
|
||||
|
||||
|
||||
public ICommand CloseCommand => new RelayCommand(() => { });
|
||||
|
||||
public ICommand MinimizeCommand => new RelayCommand(() => { });
|
||||
|
||||
public string RequestMessage => "Test request message";
|
||||
|
||||
public ICommand SetResultNo => new RelayCommand(() => { });
|
||||
|
||||
public ICommand SetResultYes => new RelayCommand(() => { });
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user