diff --git a/.gitmodules b/.gitmodules index 1990546c7..e69de29bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "submodules/Immense.RemoteControl"] - path = submodules/Immense.RemoteControl - url = git@github.com:immense/RemoteControl.git diff --git a/Agent.Installer.Win/Agent.Installer.Win.csproj b/Agent.Installer.Win/Agent.Installer.Win.csproj deleted file mode 100644 index 42b0ce376..000000000 --- a/Agent.Installer.Win/Agent.Installer.Win.csproj +++ /dev/null @@ -1,210 +0,0 @@ - - - - - Debug - AnyCPU - {A3D0368C-0850-4614-B5B5-41B9D5135AA9} - WinExe - Remotely.Agent.Installer.Win - Remotely_Installer - v4.8 - 512 - {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 4 - true - true - - 10 - enable - - - win;win-x64;win10-x64;win-x64;win10-x86; - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - Assets\favicon.ico - - - app.manifest - - - true - bin\x64\Debug\ - DEBUG;TRACE - full - x64 - 7.3 - prompt - MinimumRecommendedRules.ruleset - true - - - bin\x64\Release\ - TRACE - true - pdbonly - x64 - 7.3 - prompt - MinimumRecommendedRules.ruleset - true - - - true - bin\x86\Debug\ - DEBUG;TRACE - full - x86 - 7.3 - prompt - MinimumRecommendedRules.ruleset - true - - - bin\x86\Release\ - TRACE - true - pdbonly - x86 - 7.3 - prompt - MinimumRecommendedRules.ruleset - true - - - - - - - - - - - - - - C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.2\System.Runtime.Serialization.dll - - - - - - - - - - - 4.0 - - - - - - - - MSBuild:Compile - Designer - - - AppConstants.cs - - - Models\ConnectionInfo.cs - - - Models\DeviceSetupOptions.cs - - - - - - - - - - - - - - MSBuild:Compile - Designer - - - App.xaml - Code - - - MainWindow.xaml - Code - - - - - Code - - - True - True - Resources.resx - - - True - Settings.settings - True - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - - - - - - - - - - - - {F935DC20-1CF0-11D0-ADB9-00C04FD58A0B} - 1 - 0 - 0 - tlbimp - False - True - - - - - - - - \ No newline at end of file diff --git a/Agent.Installer.Win/App.config b/Agent.Installer.Win/App.config deleted file mode 100644 index 4bfa00561..000000000 --- a/Agent.Installer.Win/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Agent.Installer.Win/App.xaml b/Agent.Installer.Win/App.xaml deleted file mode 100644 index aa4b0db2f..000000000 --- a/Agent.Installer.Win/App.xaml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - diff --git a/Agent.Installer.Win/App.xaml.cs b/Agent.Installer.Win/App.xaml.cs deleted file mode 100644 index 82a1a9d36..000000000 --- a/Agent.Installer.Win/App.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Windows; - -namespace Remotely.Agent.Installer.Win; - -/// -/// Interaction logic for App.xaml -/// -public partial class App : Application -{ - -} diff --git a/Agent.Installer.Win/MainWindow.xaml b/Agent.Installer.Win/MainWindow.xaml deleted file mode 100644 index 43c3469a5..000000000 --- a/Agent.Installer.Win/MainWindow.xaml +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Server URL: - - - - - - - - - Organization ID - - - - - - - - Support Shortcut - - - - - - - - - - - - - - - - - - - - - - diff --git a/Agent.Installer.Win/MainWindow.xaml.cs b/Agent.Installer.Win/MainWindow.xaml.cs deleted file mode 100644 index 9176cd2c5..000000000 --- a/Agent.Installer.Win/MainWindow.xaml.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Remotely.Agent.Installer.Win.Utilities; -using Remotely.Agent.Installer.Win.ViewModels; -using System; -using System.Windows; -using System.Windows.Documents; -using System.Windows.Input; - -namespace Remotely.Agent.Installer.Win; - -/// -/// Interaction logic for MainWindow.xaml -/// -public partial class MainWindow : Window -{ - public MainWindow() - { - if (CommandLineParser.CommandLineArgs.ContainsKey("quiet")) - { - Hide(); - ShowInTaskbar = false; - _ = new MainWindowViewModel().Init(); - } - InitializeComponent(); - } - - private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) - { - DragMove(); - } - - private async void Window_Loaded(object sender, RoutedEventArgs e) - { - if (DataContext is MainWindowViewModel viewModel) - { - await viewModel.Init(); - } - } - - private void CloseButton_Click(object sender, RoutedEventArgs e) - { - App.Current.Shutdown(); - } - - private void MinimizeButton_Click(object sender, RoutedEventArgs e) - { - this.WindowState = WindowState.Minimized; - } - - private void ShowServerUrlHelp(object sender, RoutedEventArgs e) - { - MessageBox.Show( - "This is the URL of the Remotely server that you're hosting. The device will connect to this URL.", - "Server URL", - MessageBoxButton.OK, - MessageBoxImage.Information); - } - - private void ShowOrganizationIdHelp(object sender, RoutedEventArgs e) - { - MessageBox.Show( - "This is your organization ID on the Remotely server. Since Remotely supports multi-tenancy, " + - "this ID needs to be provided to determine who should have access." - + Environment.NewLine + Environment.NewLine + - "You can find this ID on the Organization tab on the web app.", - "Organization ID", - MessageBoxButton.OK, - MessageBoxImage.Information); - } - private void ShowSupportShortcutHelp(object sender, RoutedEventArgs e) - { - MessageBox.Show("If selected, the installer will create a desktop shortcut to the Get Support page for this device.", - "Support Shortcut", - MessageBoxButton.OK, - MessageBoxImage.Information); - } -} diff --git a/Agent.Installer.Win/Models/BrandingInfo.cs b/Agent.Installer.Win/Models/BrandingInfo.cs deleted file mode 100644 index 3d9d077fc..000000000 --- a/Agent.Installer.Win/Models/BrandingInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable enable - -namespace Remotely.Agent.Installer.Win.Models; - -public class BrandingInfo -{ - public string Product { get; set; } = "Remotely"; - - public string? Icon { get; set; } -} diff --git a/Agent.Installer.Win/Models/EmbeddedServerData.cs b/Agent.Installer.Win/Models/EmbeddedServerData.cs deleted file mode 100644 index e1eaaff46..000000000 --- a/Agent.Installer.Win/Models/EmbeddedServerData.cs +++ /dev/null @@ -1,28 +0,0 @@ -#nullable enable -using System; -using System.Runtime.Serialization; - -namespace Remotely.Agent.Installer.Models; - -[DataContract] -public class EmbeddedServerData -{ - /// - /// Parameterless constructor for JsonSerializer. - /// - public EmbeddedServerData() { } - - public EmbeddedServerData(Uri serverUrl, string organizationId) - { - ServerUrl = serverUrl; - OrganizationId = organizationId; - } - - public static EmbeddedServerData Empty { get; } = new EmbeddedServerData(); - - [DataMember] - public string OrganizationId { get; set; } = string.Empty; - - [DataMember] - public Uri? ServerUrl { get; set; } -} diff --git a/Agent.Installer.Win/Properties/AssemblyInfo.cs b/Agent.Installer.Win/Properties/AssemblyInfo.cs deleted file mode 100644 index 08bc29a9b..000000000 --- a/Agent.Installer.Win/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; -using System.Windows; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Remotely Installer")] -[assembly: AssemblyDescription("An installer for the Remotely service, which provides unattended remote access and remote scripting.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Immense Networks")] -[assembly: AssemblyProduct("Remotely Installer")] -[assembly: AssemblyCopyright("Copyright © 2020 Immense Networks")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -//In order to begin building localizable applications, set -//CultureYouAreCodingWith in your .csproj file -//inside a . For example, if you are using US english -//in your source files, set the to en-US. Then uncomment -//the NeutralResourceLanguage attribute below. Update the "en-US" in -//the line below to match the UICulture setting in the project file. - -//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] - - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] - - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Agent.Installer.Win/Properties/Resources.Designer.cs b/Agent.Installer.Win/Properties/Resources.Designer.cs deleted file mode 100644 index fb5f7c6e5..000000000 --- a/Agent.Installer.Win/Properties/Resources.Designer.cs +++ /dev/null @@ -1,63 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Remotely.Agent.Installer.Win.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Remotely.Agent.Installer.Win.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - } -} diff --git a/Agent.Installer.Win/Properties/Resources.resx b/Agent.Installer.Win/Properties/Resources.resx deleted file mode 100644 index af7dbebba..000000000 --- a/Agent.Installer.Win/Properties/Resources.resx +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/Agent.Installer.Win/Properties/Settings.Designer.cs b/Agent.Installer.Win/Properties/Settings.Designer.cs deleted file mode 100644 index 760c1573f..000000000 --- a/Agent.Installer.Win/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Remotely.Agent.Installer.Win.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/Agent.Installer.Win/Properties/Settings.settings b/Agent.Installer.Win/Properties/Settings.settings deleted file mode 100644 index 033d7a5e9..000000000 --- a/Agent.Installer.Win/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Agent.Installer.Win/Services/EmbeddedServerDataReader.cs b/Agent.Installer.Win/Services/EmbeddedServerDataReader.cs deleted file mode 100644 index 42f55db71..000000000 --- a/Agent.Installer.Win/Services/EmbeddedServerDataReader.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using System.Web.Script.Serialization; -using Remotely.Agent.Installer.Models; -using Remotely.Agent.Installer.Win.Utilities; - -namespace Remotely.Agent.Installer.Win.Services; - -internal class EmbeddedServerDataReader -{ - private readonly JavaScriptSerializer _serializer = new JavaScriptSerializer(); - - public async Task TryGetEmbeddedData(string filePath) - { - try - { - if (!File.Exists(filePath)) - { - throw new Exception($"File path does not exist: {filePath}"); - } - - using var fs = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); - using var br = new BinaryReader(fs); - using var sr = new StreamReader(fs); - - fs.Seek(-4, SeekOrigin.End); - var dataSize = br.ReadInt32(); - fs.Seek(-dataSize - 4, SeekOrigin.End); - - if (dataSize == 0) - { - return EmbeddedServerData.Empty; - } - - var buffer = new byte[dataSize]; - await fs.ReadAsync(buffer, 0, dataSize); - var json = Encoding.UTF8.GetString(buffer); - - Logger.Write($"Extracted embedded data from EXE: {json}"); - - var embeddedData = _serializer.Deserialize(json); - if (embeddedData is not null) - { - return embeddedData; - } - } - catch (Exception ex) - { - Logger.Write(ex); - } - return EmbeddedServerData.Empty; - } -} diff --git a/Agent.Installer.Win/Services/InstallerService.cs b/Agent.Installer.Win/Services/InstallerService.cs deleted file mode 100644 index d05b19360..000000000 --- a/Agent.Installer.Win/Services/InstallerService.cs +++ /dev/null @@ -1,455 +0,0 @@ -#nullable enable -using IWshRuntimeLibrary; -using Microsoft.VisualBasic.FileIO; -using Microsoft.Win32; -using Remotely.Agent.Installer.Win.Utilities; -using Remotely.Shared.Models; -using System; -using System.Configuration.Install; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net; -using System.Reflection; -using System.Security.Principal; -using System.ServiceProcess; -using System.Threading.Tasks; -using System.Web.Script.Serialization; -using System.Windows; -using FileIO = System.IO.File; - -namespace Remotely.Agent.Installer.Win.Services; - -public class InstallerService -{ - private readonly string _installPath = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "Program Files", "Remotely"); - private readonly string _platform = Environment.Is64BitOperatingSystem ? "x64" : "x86"; - private readonly JavaScriptSerializer _serializer = new JavaScriptSerializer(); - - public event EventHandler? ProgressMessageChanged; - public event EventHandler? ProgressValueChanged; - - public async Task Install(string serverUrl, - string organizationId, - string? deviceGroup, - string? deviceAlias, - string? deviceUuid, - bool createSupportShortcut) - { - try - { - Logger.Write("Install started."); - if (!CheckIsAdministrator()) - { - return false; - } - - StopService(); - - await StopProcesses(); - - BackupDirectory(); - - var connectionInfo = GetConnectionInfo(organizationId, serverUrl, deviceUuid); - - ClearInstallDirectory(); - - await DownloadRemotelyAgent(serverUrl); - - FileIO.WriteAllText(Path.Combine(_installPath, "ConnectionInfo.json"), _serializer.Serialize(connectionInfo)); - - FileIO.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(_installPath, "Remotely_Installer.exe")); - - await CreateDeviceOnServer(connectionInfo.DeviceID, serverUrl, deviceGroup, deviceAlias, organizationId); - - AddFirewallRule(); - - InstallService(); - - CreateUninstallKey(); - - CreateSupportShortcut(serverUrl, connectionInfo.DeviceID, createSupportShortcut); - - return true; - } - catch (Exception ex) - { - Logger.Write(ex); - RestoreBackup(); - return false; - } - - } - - public async Task Uninstall() - { - try - { - if (!CheckIsAdministrator()) - { - return false; - } - - StopService(); - - ProcessEx.StartHidden("cmd.exe", "/c sc delete Remotely_Service").WaitForExit(); - - await StopProcesses(); - - ProgressMessageChanged?.Invoke(this, "Deleting files."); - ClearInstallDirectory(); - ProcessEx.StartHidden("cmd.exe", $"/c timeout 5 & rd /s /q \"{_installPath}\""); - - ProcessEx.StartHidden("netsh", "advfirewall firewall delete rule name=\"Remotely Desktop Unattended\"").WaitForExit(); - - GetRegistryBaseKey().DeleteSubKeyTree(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Remotely", false); - - return true; - } - catch (Exception ex) - { - Logger.Write(ex); - return false; - } - } - - private void AddFirewallRule() - { - var desktopExePath = Path.Combine(_installPath, "Desktop", "Remotely_Desktop.exe"); - ProcessEx.StartHidden("netsh", "advfirewall firewall delete rule name=\"Remotely Desktop Unattended\"").WaitForExit(); - ProcessEx.StartHidden("netsh", $"advfirewall firewall add rule name=\"Remotely Desktop Unattended\" program=\"{desktopExePath}\" protocol=any dir=in enable=yes action=allow description=\"The agent that allows screen sharing and remote control for Remotely.\"").WaitForExit(); - } - - private void BackupDirectory() - { - if (Directory.Exists(_installPath)) - { - Logger.Write("Backing up current installation."); - ProgressMessageChanged?.Invoke(this, "Backing up current installation."); - var backupPath = Path.Combine(Path.GetTempPath(), "Remotely_Backup.zip"); - if (FileIO.Exists(backupPath)) - { - FileIO.Delete(backupPath); - } - ZipFile.CreateFromDirectory(_installPath, backupPath, CompressionLevel.Fastest, false); - } - } - - private bool CheckIsAdministrator() - { - var identity = WindowsIdentity.GetCurrent(); - var principal = new WindowsPrincipal(identity); - var result = principal.IsInRole(WindowsBuiltInRole.Administrator); - if (!result) - { - MessageBoxEx.Show("Elevated privileges are required. Please restart the installer using 'Run as administrator'.", "Elevation Required", MessageBoxButton.OK, MessageBoxImage.Warning); - } - return result; - } - - private void ClearInstallDirectory() - { - if (Directory.Exists(_installPath)) - { - foreach (var entry in Directory.GetFileSystemEntries(_installPath)) - { - try - { - if (FileIO.Exists(entry)) - { - FileIO.Delete(entry); - } - else if (Directory.Exists(entry)) - { - Directory.Delete(entry, true); - } - } - catch (Exception ex) - { - Logger.Write(ex); - } - } - } - } - - private async Task CreateDeviceOnServer(string deviceUuid, - string serverUrl, - string? deviceGroup, - string? deviceAlias, - string organizationId) - { - try - { - if (!string.IsNullOrWhiteSpace(deviceGroup) || - !string.IsNullOrWhiteSpace(deviceAlias)) - { - var setupOptions = new DeviceSetupOptions() - { - DeviceID = deviceUuid, - DeviceGroupName = deviceGroup, - DeviceAlias = deviceAlias, - OrganizationID = organizationId - }; - - var wr = WebRequest.CreateHttp(serverUrl.TrimEnd('/') + "/api/devices"); - wr.Method = "POST"; - wr.ContentType = "application/json"; - using (var rs = await wr.GetRequestStreamAsync()) - using (var sw = new StreamWriter(rs)) - { - await sw.WriteAsync(_serializer.Serialize(setupOptions)); - } - using var response = await wr.GetResponseAsync(); - if (response is HttpWebResponse httpResponse) - { - Logger.Write($"Create device response: {httpResponse.StatusCode}"); - } - } - } - catch (WebException ex) when ((ex.Response is HttpWebResponse response) && response.StatusCode == HttpStatusCode.BadRequest) - { - Logger.Write("Bad request when creating device. The device ID may already be created."); - } - catch (Exception ex) - { - Logger.Write(ex); - } - - } - - private void CreateSupportShortcut(string serverUrl, string deviceUuid, bool createSupportShortcut) - { - var shell = new WshShell(); - var shortcutLocation = Path.Combine(_installPath, "Get Support.lnk"); - var shortcut = (IWshShortcut)shell.CreateShortcut(shortcutLocation); - shortcut.Description = "Get IT support"; - shortcut.IconLocation = Path.Combine(_installPath, "Remotely_Agent.exe"); - shortcut.TargetPath = serverUrl.TrimEnd('/') + $"/get-support?deviceID={deviceUuid}"; - shortcut.Save(); - - if (createSupportShortcut) - { - var systemRoot = Path.GetPathRoot(Environment.SystemDirectory); - var publicDesktop = Path.Combine(systemRoot, "Users", "Public", "Desktop", "Get Support.lnk"); - FileIO.Copy(shortcutLocation, publicDesktop, true); - } - } - private void CreateUninstallKey() - { - var version = FileVersionInfo.GetVersionInfo(Path.Combine(_installPath, "Remotely_Agent.exe")); - var baseKey = GetRegistryBaseKey(); - - var remotelyKey = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Remotely", true); - remotelyKey.SetValue("DisplayIcon", Path.Combine(_installPath, "Remotely_Agent.exe")); - remotelyKey.SetValue("DisplayName", "Remotely"); - remotelyKey.SetValue("DisplayVersion", version.FileVersion); - remotelyKey.SetValue("InstallDate", DateTime.Now.ToShortDateString()); - remotelyKey.SetValue("Publisher", "Immense Networks"); - remotelyKey.SetValue("VersionMajor", version.FileMajorPart.ToString(), RegistryValueKind.DWord); - remotelyKey.SetValue("VersionMinor", version.FileMinorPart.ToString(), RegistryValueKind.DWord); - remotelyKey.SetValue("UninstallString", Path.Combine(_installPath, "Remotely_Installer.exe -uninstall -quiet")); - remotelyKey.SetValue("QuietUninstallString", Path.Combine(_installPath, "Remotely_Installer.exe -uninstall -quiet")); - } - - private async Task DownloadRemotelyAgent(string serverUrl) - { - var targetFile = Path.Combine(Path.GetTempPath(), $"Remotely-Agent.zip"); - - if (CommandLineParser.CommandLineArgs.TryGetValue("path", out var result) && - FileIO.Exists(result)) - { - targetFile = result; - } - else - { - ProgressMessageChanged?.Invoke(this, "Downloading Remotely agent."); - using (var client = new WebClient()) - { - client.DownloadProgressChanged += (sender, args) => - { - ProgressValueChanged?.Invoke(this, args.ProgressPercentage); - }; - - await client.DownloadFileTaskAsync($"{serverUrl}/Content/Remotely-Win-{_platform}.zip", targetFile); - } - } - - ProgressMessageChanged?.Invoke(this, "Extracting Remotely files."); - ProgressValueChanged?.Invoke(this, 0); - - var tempDir = Path.Combine(Path.GetTempPath(), "RemotelyUpdate"); - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } - - Directory.CreateDirectory(_installPath); - while (!Directory.Exists(_installPath)) - { - await Task.Delay(10); - } - - var wr = WebRequest.CreateHttp($"{serverUrl}/Content/Remotely-Win-{_platform}.zip"); - wr.Method = "Head"; - using (var response = (HttpWebResponse)await wr.GetResponseAsync()) - { - FileIO.WriteAllText(Path.Combine(_installPath, "etag.txt"), response.Headers["ETag"]); - } - - ZipFile.ExtractToDirectory(targetFile, tempDir); - var fileSystemEntries = Directory.GetFileSystemEntries(tempDir); - for (var i = 0; i < fileSystemEntries.Length; i++) - { - try - { - ProgressValueChanged?.Invoke(this, (int)((double)i / (double)fileSystemEntries.Length * 100d)); - var entry = fileSystemEntries[i]; - if (FileIO.Exists(entry)) - { - FileIO.Copy(entry, Path.Combine(_installPath, Path.GetFileName(entry)), true); - } - else if (Directory.Exists(entry)) - { - FileSystem.CopyDirectory(entry, Path.Combine(_installPath, new DirectoryInfo(entry).Name), true); - } - await Task.Delay(1); - } - catch (Exception ex) - { - Logger.Write(ex); - } - } - ProgressValueChanged?.Invoke(this, 0); - } - - private ConnectionInfo GetConnectionInfo(string organizationId, string serverUrl, string? deviceUuid) - { - ConnectionInfo connectionInfo; - var connectionInfoPath = Path.Combine(_installPath, "ConnectionInfo.json"); - if (FileIO.Exists(connectionInfoPath)) - { - connectionInfo = _serializer.Deserialize(FileIO.ReadAllText(connectionInfoPath)); - connectionInfo.ServerVerificationToken = null; - } - else - { - connectionInfo = new ConnectionInfo() - { - DeviceID = Guid.NewGuid().ToString() - }; - } - - if (!string.IsNullOrWhiteSpace(deviceUuid)) - { - // Clear the server verification token if we're installing this as a new device. - if (connectionInfo.DeviceID != deviceUuid) - { - connectionInfo.ServerVerificationToken = null; - } - connectionInfo.DeviceID = deviceUuid!; - } - connectionInfo.OrganizationID = organizationId; - connectionInfo.Host = serverUrl; - return connectionInfo; - } - - private RegistryKey GetRegistryBaseKey() - { - if (Environment.Is64BitOperatingSystem) - { - return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); - } - else - { - return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); - } - } - - private void InstallService() - { - Logger.Write("Installing service."); - ProgressMessageChanged?.Invoke(this, "Installing Remotely service."); - var serv = ServiceController.GetServices().FirstOrDefault(ser => ser.ServiceName == "Remotely_Service"); - if (serv == null) - { - var command = new string[] { "/assemblypath=" + Path.Combine(_installPath, "Remotely_Agent.exe") }; - var context = new InstallContext("", command); - var serviceInstaller = new ServiceInstaller() - { - Context = context, - DisplayName = "Remotely Service", - Description = "Background service that maintains a connection to the Remotely server. The service is used for remote support and maintenance by this computer's administrators.", - ServiceName = "Remotely_Service", - StartType = ServiceStartMode.Automatic, - Parent = new ServiceProcessInstaller() - }; - - var state = new System.Collections.Specialized.ListDictionary(); - serviceInstaller.Install(state); - Logger.Write("Service installed."); - serv = ServiceController.GetServices().FirstOrDefault(ser => ser.ServiceName == "Remotely_Service"); - - ProcessEx.StartHidden("cmd.exe", "/c sc.exe failure \"Remotely_Service\" reset= 5 actions= restart/5000"); - } - if (serv.Status != ServiceControllerStatus.Running) - { - serv.Start(); - } - Logger.Write("Service started."); - } - - private void RestoreBackup() - { - try - { - var backupPath = Path.Combine(Path.GetTempPath(), "Remotely_Backup.zip"); - if (FileIO.Exists(backupPath)) - { - Logger.Write("Restoring backup."); - ClearInstallDirectory(); - ZipFile.ExtractToDirectory(backupPath, _installPath); - var serv = ServiceController.GetServices().FirstOrDefault(ser => ser.ServiceName == "Remotely_Service"); - if (serv?.Status != ServiceControllerStatus.Running) - { - serv?.Start(); - } - } - } - catch (Exception ex) - { - Logger.Write(ex); - } - } - - private async Task StopProcesses() - { - ProgressMessageChanged?.Invoke(this, "Stopping Remotely processes."); - var procs = Process.GetProcessesByName("Remotely_Agent").Concat(Process.GetProcessesByName("Remotely_Desktop")); - - foreach (var proc in procs) - { - proc.Kill(); - } - - await Task.Delay(500); - } - private void StopService() - { - try - { - var remotelyService = ServiceController.GetServices().FirstOrDefault(x => x.ServiceName == "Remotely_Service"); - if (remotelyService != null) - { - Logger.Write("Stopping existing Remotely service."); - ProgressMessageChanged?.Invoke(this, "Stopping existing Remotely service."); - remotelyService.Stop(); - remotelyService.WaitForStatus(ServiceControllerStatus.Stopped); - } - } - catch (Exception ex) - { - Logger.Write(ex); - } - } -} diff --git a/Agent.Installer.Win/Services/RelayCommand.cs b/Agent.Installer.Win/Services/RelayCommand.cs deleted file mode 100644 index 112042d79..000000000 --- a/Agent.Installer.Win/Services/RelayCommand.cs +++ /dev/null @@ -1,39 +0,0 @@ -#nullable enable - -using System; -using System.Windows.Input; - -namespace Remotely.Agent.Installer.Win.Services; - -public class RelayCommand : ICommand -{ - private readonly Action _action; - - private readonly Predicate? _canExecute; - - public RelayCommand(Action action, Predicate? canExecute = null) - { - _action = action; - _canExecute = canExecute; - } - - public event EventHandler CanExecuteChanged - { - add { CommandManager.RequerySuggested += value; } - remove { CommandManager.RequerySuggested -= value; } - } - - public bool CanExecute(object parameter) - { - if (_canExecute is null) - { - return true; - } - return _canExecute.Invoke(parameter); - } - - public void Execute(object parameter) - { - _action?.Invoke(parameter); - } -} diff --git a/Agent.Installer.Win/Utilities/CommandLineParser.cs b/Agent.Installer.Win/Utilities/CommandLineParser.cs deleted file mode 100644 index a24e994d5..000000000 --- a/Agent.Installer.Win/Utilities/CommandLineParser.cs +++ /dev/null @@ -1,78 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Windows; - -namespace Remotely.Agent.Installer.Win.Utilities; - -public class CommandLineParser -{ - private static Dictionary? commandLineArgs; - - private static bool _invalidArgumentFound; - - public static Dictionary CommandLineArgs - { - get - { - if (commandLineArgs is null) - { - commandLineArgs = new Dictionary(); - - var args = Environment.GetCommandLineArgs(); - - for (var i = 1; i < args.Length; i += 2) - { - try - { - var key = args[i]; - if (key != null) - { - if (!key.Contains("-")) - { - _invalidArgumentFound = true; - i -= 1; - continue; - } - key = key.Trim().Replace("-", "").ToLower(); - if (i + 1 == args.Length) - { - commandLineArgs.Add(key, "true"); - continue; - } - var value = args[i + 1]; - if (value != null) - { - if (value.StartsWith("-")) - { - commandLineArgs.Add(key, "true"); - i -= 1; - } - else - { - commandLineArgs.Add(key, args[i + 1].Trim()); - } - } - } - } - catch (Exception ex) - { - Logger.Write(ex); - } - - } - } - return commandLineArgs; - } - } - - internal static void VerifyArguments() - { - if (_invalidArgumentFound) - { - Logger.Write("Command line arguments are invalid."); - MessageBoxEx.Show("Command line arguments are invalid.", "Invalid Arguments", MessageBoxButton.OK, MessageBoxImage.Error); - } - } -} diff --git a/Agent.Installer.Win/Utilities/Logger.cs b/Agent.Installer.Win/Utilities/Logger.cs deleted file mode 100644 index e5320dd12..000000000 --- a/Agent.Installer.Win/Utilities/Logger.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.IO; -using System.Linq; - -namespace Remotely.Agent.Installer.Win.Utilities; - -public class Logger -{ - public static string LogsDir { get; } = Directory.CreateDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Remotely", "Logs")).FullName; - public static string LogsPath { get; } = Path.Combine(LogsDir, "Installer.log"); - - private static readonly object _writeLock = new object(); - - public static void Debug(string message) - { - lock (_writeLock) - { -#if DEBUG - CheckLogFileExists(); - - File.AppendAllText(LogsPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Debug]\t{message}{Environment.NewLine}"); - -#endif - System.Diagnostics.Debug.WriteLine(message); - } - - } - - public static void Write(string message) - { - try - { - lock (_writeLock) - { - CheckLogFileExists(); - File.AppendAllText(LogsPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Info]\t{message}{Environment.NewLine}"); - Console.WriteLine(message); - } - } - catch { } - } - - public static void Write(Exception ex) - { - lock (_writeLock) - { - try - { - CheckLogFileExists(); - - var exception = ex; - - while (exception != null) - { - File.AppendAllText(LogsPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Error]\t{exception.Message}\t{exception.StackTrace}\t{exception.Source}{Environment.NewLine}"); - Console.WriteLine(exception.Message); - exception = exception.InnerException; - } - } - catch { } - } - } - - private static void CheckLogFileExists() - { - if (!File.Exists(LogsPath)) - { - File.Create(LogsPath).Close(); - } - - if (File.Exists(LogsPath)) - { - var fi = new FileInfo(LogsPath); - while (fi.Length > 1000000) - { - var content = File.ReadAllLines(LogsPath); - File.WriteAllLines(LogsPath, content.Skip(10)); - fi = new FileInfo(LogsPath); - } - } - } -} diff --git a/Agent.Installer.Win/Utilities/MessageBoxEx.cs b/Agent.Installer.Win/Utilities/MessageBoxEx.cs deleted file mode 100644 index 65ff2e8ae..000000000 --- a/Agent.Installer.Win/Utilities/MessageBoxEx.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Windows; - -namespace Remotely.Agent.Installer.Win.Utilities; - -public static class MessageBoxEx -{ - public static MessageBoxResult Show(string message, string caption, MessageBoxButton messageBoxButton, MessageBoxImage messageBoxImage) - { - if (!CommandLineParser.CommandLineArgs.ContainsKey("quiet")) - { - return MessageBox.Show(message, caption, messageBoxButton, messageBoxImage); - } - return MessageBoxResult.None; - } -} diff --git a/Agent.Installer.Win/Utilities/ProcessEx.cs b/Agent.Installer.Win/Utilities/ProcessEx.cs deleted file mode 100644 index 6515dfb6e..000000000 --- a/Agent.Installer.Win/Utilities/ProcessEx.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Diagnostics; - -namespace Remotely.Agent.Installer.Win.Utilities; - -public static class ProcessEx -{ - public static Process StartHidden(string filePath, string arguments) - { - var psi = new ProcessStartInfo() - { - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, - Arguments = arguments, - FileName = filePath - }; - return Process.Start(psi); - } -} diff --git a/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs b/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs deleted file mode 100644 index 86b4c1b89..000000000 --- a/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs +++ /dev/null @@ -1,512 +0,0 @@ -#nullable enable -using Remotely.Agent.Installer.Win.Models; -using Remotely.Agent.Installer.Win.Services; -using Remotely.Agent.Installer.Win.Utilities; -using Remotely.Shared.Models; -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Reflection; -using System.Security.Principal; -using System.ServiceProcess; -using System.Threading.Tasks; -using System.Web.Script.Serialization; -using System.Windows; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Net; -using Remotely.Shared; -using Remotely.Agent.Installer.Models; - -namespace Remotely.Agent.Installer.Win.ViewModels; - -public class MainWindowViewModel : ViewModelBase -{ - private readonly EmbeddedServerDataReader _embeddedDataReader; - private readonly InstallerService _installer; - private BrandingInfo? _brandingInfo; - private bool _createSupportShortcut; - private string _headerMessage = "Install the service."; - private bool _isReadyState = true; - private bool _isServiceInstalled; - - private string? _organizationID; - - private int _progress; - - private string _serverUrl = string.Empty; - - private string? _statusMessage; - - public MainWindowViewModel() - { - _installer = new InstallerService(); - _embeddedDataReader = new EmbeddedServerDataReader(); - - CopyCommandLineArgs(); - - ExtractEmbeddedServerData().Wait(); - - AddExistingConnectionInfo(); - } - - public bool CreateSupportShortcut - { - get - { - return _createSupportShortcut; - } - set - { - _createSupportShortcut = value; - FirePropertyChanged(); - } - } - - public string HeaderMessage - { - get - { - return _headerMessage; - } - set - { - _headerMessage = value; - FirePropertyChanged(); - } - } - - public BitmapImage? Icon { get; set; } - public string InstallButtonText => IsServiceMissing ? "Install" : "Reinstall"; - - public ICommand InstallCommand => new RelayCommand(async (param) => { await Install(); }); - - public bool IsProgressVisible => Progress > 0; - - public bool IsReadyState - { - get - { - return _isReadyState; - } - set - { - _isReadyState = value; - FirePropertyChanged(); - } - } - - public bool IsServiceInstalled - { - get - { - return _isServiceInstalled; - } - set - { - _isServiceInstalled = value; - FirePropertyChanged(); - FirePropertyChanged(nameof(IsServiceMissing)); - FirePropertyChanged(nameof(InstallButtonText)); - } - } - - public bool IsServiceMissing => !_isServiceInstalled; - - public ICommand OpenLogsCommand - { - get - { - return new RelayCommand(param => - { - - if (Directory.Exists(Logger.LogsDir)) - { - Process.Start(Logger.LogsDir); - } - else - { - MessageBoxEx.Show("Log directory doesn't exist.", "No Logs", MessageBoxButton.OK, MessageBoxImage.Information); - } - }); - } - } - - public string? OrganizationID - { - get - { - return _organizationID; - } - set - { - _organizationID = value; - FirePropertyChanged(); - } - } - - public string ProductName { get; set; } = "Remotely"; - - public int Progress - { - get - { - return _progress; - } - set - { - _progress = value; - FirePropertyChanged(); - FirePropertyChanged(nameof(IsProgressVisible)); - } - } - - public string ServerUrl - { - get - { - return _serverUrl; - } - set - { - _serverUrl = value?.TrimEnd('/') ?? string.Empty; - FirePropertyChanged(); - } - } - - public string? StatusMessage - { - get - { - return _statusMessage; - } - set - { - _statusMessage = value; - FirePropertyChanged(); - } - } - - public ICommand UninstallCommand => new RelayCommand(async (param) => { await Uninstall(); }); - private string? DeviceAlias { get; set; } - private string? DeviceGroup { get; set; } - private string? DeviceUuid { get; set; } - - public async Task Init() - { - _installer.ProgressMessageChanged += (sender, arg) => - { - StatusMessage = arg; - }; - - _installer.ProgressValueChanged += (sender, arg) => - { - Progress = arg; - }; - - IsServiceInstalled = ServiceController.GetServices().Any(x => x.ServiceName == "Remotely_Service"); - if (IsServiceMissing) - { - HeaderMessage = $"Install the {ProductName} service."; - } - else - { - HeaderMessage = $"Modify the {ProductName} installation."; - } - - CommandLineParser.VerifyArguments(); - - if (CommandLineParser.CommandLineArgs.ContainsKey("install")) - { - await Install(); - } - else if (CommandLineParser.CommandLineArgs.ContainsKey("uninstall")) - { - await Uninstall(); - } - - if (CommandLineParser.CommandLineArgs.ContainsKey("quiet")) - { - App.Current.Shutdown(); - } - } - - private void AddExistingConnectionInfo() - { - try - { - var connectionInfoPath = Path.Combine( - Path.GetPathRoot(Environment.SystemDirectory), - "Program Files", - "Remotely", - "ConnectionInfo.json"); - - if (File.Exists(connectionInfoPath)) - { - var serializer = new JavaScriptSerializer(); - var connectionInfo = serializer.Deserialize(File.ReadAllText(connectionInfoPath)); - - if (string.IsNullOrWhiteSpace(OrganizationID)) - { - OrganizationID = connectionInfo.OrganizationID; - } - - if (string.IsNullOrWhiteSpace(ServerUrl)) - { - ServerUrl = connectionInfo.Host ?? string.Empty; - } - } - } - catch (Exception ex) - { - Logger.Write(ex); - } - - } - - private void ApplyBranding(BrandingInfo? brandingInfo) - { - try - { - if (brandingInfo is not null && - !string.IsNullOrWhiteSpace(brandingInfo.Product)) - { - ProductName = brandingInfo.Product; - } - - Icon = GetBitmapImageIcon(brandingInfo); - } - catch (Exception ex) - { - Logger.Write(ex); - } - } - - private bool CheckIsAdministrator() - { - var identity = WindowsIdentity.GetCurrent(); - var principal = new WindowsPrincipal(identity); - var result = principal.IsInRole(WindowsBuiltInRole.Administrator); - if (!result) - { - MessageBoxEx.Show("Elevated privileges are required. Please restart the installer using 'Run as administrator'.", "Elevation Required", MessageBoxButton.OK, MessageBoxImage.Warning); - } - return result; - } - - private bool CheckParams() - { - if (string.IsNullOrWhiteSpace(OrganizationID) || string.IsNullOrWhiteSpace(ServerUrl)) - { - Logger.Write("ServerUrl or OrganizationID param is missing. Unable to install."); - MessageBoxEx.Show("Required settings are missing. Please enter a server URL and organization ID.", "Invalid Input", MessageBoxButton.OK, MessageBoxImage.Error); - return false; - } - - if (!Guid.TryParse(OrganizationID, out _)) - { - Logger.Write("OrganizationID is not a valid GUID."); - MessageBoxEx.Show("Organization ID must be a valid GUID.", "Invalid Organization ID", MessageBoxButton.OK, MessageBoxImage.Error); - return false; - } - - if (!Uri.TryCreate(ServerUrl, UriKind.Absolute, out var serverUri) || - (serverUri.Scheme != Uri.UriSchemeHttp && serverUri.Scheme != Uri.UriSchemeHttps)) - { - Logger.Write("ServerUrl is not valid."); - MessageBoxEx.Show("Server URL must be a valid Uri (e.g. https://app.example.com).", "Invalid Server URL", MessageBoxButton.OK, MessageBoxImage.Error); - return false; - } - - return true; - } - - private void CopyCommandLineArgs() - { - if (CommandLineParser.CommandLineArgs.TryGetValue("organizationid", out var orgID)) - { - OrganizationID = orgID; - } - - if (CommandLineParser.CommandLineArgs.TryGetValue("serverurl", out var serverUrl)) - { - ServerUrl = serverUrl; - } - - if (CommandLineParser.CommandLineArgs.TryGetValue("devicegroup", out var deviceGroup)) - { - DeviceGroup = deviceGroup; - } - - if (CommandLineParser.CommandLineArgs.TryGetValue("devicealias", out var deviceAlias)) - { - DeviceAlias = deviceAlias; - } - - if (CommandLineParser.CommandLineArgs.TryGetValue("deviceuuid", out var deviceUuid)) - { - DeviceUuid = deviceUuid; - } - - if (CommandLineParser.CommandLineArgs.ContainsKey("supportshortcut")) - { - CreateSupportShortcut = true; - } - } - - private async Task ExtractEmbeddedServerData() - { - - try - { - var filePath = Process.GetCurrentProcess()?.MainModule?.FileName; - - if (string.IsNullOrWhiteSpace(filePath)) - { - Logger.Write("Failed to retrieve executing file name."); - return; - } - - var embeddedData = await _embeddedDataReader.TryGetEmbeddedData(filePath!); - - if (embeddedData is null || embeddedData == EmbeddedServerData.Empty) - { - Logger.Write("Embedded server data is empty. Aborting."); - return; - } - - if (embeddedData.ServerUrl is null) - { - Logger.Write("ServerUrl is empty. Aborting."); - return; - } - - OrganizationID = embeddedData.OrganizationId; - ServerUrl = embeddedData.ServerUrl.AbsoluteUri; - - using (var httpClient = new HttpClient()) - { - var serializer = new JavaScriptSerializer(); - var brandingUrl = $"{ServerUrl.TrimEnd('/')}/api/branding/{OrganizationID}"; - using (var response = await httpClient.GetAsync(brandingUrl).ConfigureAwait(false)) - { - var responseString = await response.Content.ReadAsStringAsync(); - _brandingInfo = serializer.Deserialize(responseString); - - } - } - } - catch (Exception ex) - { - Logger.Write(ex); - } - finally - { - ApplyBranding(_brandingInfo); - } - } - private BitmapImage GetBitmapImageIcon(BrandingInfo? bi) - { - Stream imageStream; - if (bi is not null && - !string.IsNullOrWhiteSpace(bi.Icon)) - { - imageStream = new MemoryStream(Convert.FromBase64String(bi.Icon)); - } - else - { - imageStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Remotely.Agent.Installer.Win.Assets.Remotely_Icon.png"); - } - - var bitmap = new BitmapImage(); - bitmap.BeginInit(); - bitmap.StreamSource = imageStream; - bitmap.CacheOption = BitmapCacheOption.OnLoad; - bitmap.EndInit(); - bitmap.Freeze(); - imageStream.Close(); - - return bitmap; - } - private async Task Install() - { - try - { - IsReadyState = false; - if (!CheckParams()) - { - return; - } - - HeaderMessage = "Installing Remotely..."; - - if (await _installer.Install( - ServerUrl, - OrganizationID!, - DeviceGroup, - DeviceAlias, - DeviceUuid, - CreateSupportShortcut)) - { - IsServiceInstalled = true; - Progress = 0; - HeaderMessage = "Installation completed."; - StatusMessage = "Remotely has been installed. You can now close this window."; - } - else - { - Progress = 0; - HeaderMessage = "An error occurred during installation."; - StatusMessage = "There was an error during installation. Check the logs for details."; - } - if (!CheckIsAdministrator()) - { - return; - } - } - catch (Exception ex) - { - Logger.Write(ex); - } - finally - { - IsReadyState = true; - } - } - - private async Task Uninstall() - { - try - { - IsReadyState = false; - - HeaderMessage = "Uninstalling Remotely..."; - - if (await _installer.Uninstall()) - { - IsServiceInstalled = false; - Progress = 0; - HeaderMessage = "Uninstall completed."; - StatusMessage = "Remotely has been uninstalled. You can now close this window."; - } - else - { - Progress = 0; - HeaderMessage = "An error occurred during uninstall."; - StatusMessage = "There was an error during uninstall. Check the logs for details."; - } - - } - catch (Exception ex) - { - Logger.Write(ex); - } - finally - { - IsReadyState = true; - } - } -} diff --git a/Agent.Installer.Win/ViewModels/ViewModelBase.cs b/Agent.Installer.Win/ViewModels/ViewModelBase.cs deleted file mode 100644 index 15fecde76..000000000 --- a/Agent.Installer.Win/ViewModels/ViewModelBase.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable enable - -using System.ComponentModel; -using System.Runtime.CompilerServices; - -namespace Remotely.Agent.Installer.Win.ViewModels; - -public class ViewModelBase : INotifyPropertyChanged -{ - public event PropertyChangedEventHandler? PropertyChanged; - - public void FirePropertyChanged([CallerMemberName]string propertyName = "") - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } -} diff --git a/Agent.Installer.Win/app.manifest b/Agent.Installer.Win/app.manifest deleted file mode 100644 index 78e2df3c5..000000000 --- a/Agent.Installer.Win/app.manifest +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Agent/Agent.csproj b/Agent/Agent.csproj index d5ab0574a..7e383f3a4 100644 --- a/Agent/Agent.csproj +++ b/Agent/Agent.csproj @@ -39,8 +39,8 @@ + - diff --git a/Agent/Services/AgentHubConnection.cs b/Agent/Services/AgentHubConnection.cs index d2675388e..af0a43afc 100644 --- a/Agent/Services/AgentHubConnection.cs +++ b/Agent/Services/AgentHubConnection.cs @@ -1,4 +1,4 @@ -using Immense.RemoteControl.Desktop.Shared.Native.Windows; +using Remotely.Desktop.Shared.Native.Windows; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/Agent/Services/ChatClientService.cs b/Agent/Services/ChatClientService.cs index a6dfc65ce..bac65da28 100644 --- a/Agent/Services/ChatClientService.cs +++ b/Agent/Services/ChatClientService.cs @@ -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; diff --git a/Agent/Services/ConfigService.cs b/Agent/Services/ConfigService.cs index fafb8f99e..dcce37d82 100644 --- a/Agent/Services/ConfigService.cs +++ b/Agent/Services/ConfigService.cs @@ -1,13 +1,10 @@ -using Immense.RemoteControl.Shared; +using Remotely.Shared; using Microsoft.Extensions.Logging; -using Remotely.Shared; using Remotely.Shared.Models; -using Remotely.Shared.Utilities; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Text.Json; namespace Remotely.Agent.Services; diff --git a/Agent/Services/Linux/AppLauncherLinux.cs b/Agent/Services/Linux/AppLauncherLinux.cs index 2249ee5a6..e4a0e0d71 100644 --- a/Agent/Services/Linux/AppLauncherLinux.cs +++ b/Agent/Services/Linux/AppLauncherLinux.cs @@ -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; diff --git a/Agent/Services/ScriptExecutor.cs b/Agent/Services/ScriptExecutor.cs index cecdeb78e..2078791d2 100644 --- a/Agent/Services/ScriptExecutor.cs +++ b/Agent/Services/ScriptExecutor.cs @@ -1,4 +1,4 @@ -using Immense.RemoteControl.Shared.Extensions; +using Remotely.Shared.Extensions; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; diff --git a/Agent/Services/Windows/AppLauncherWin.cs b/Agent/Services/Windows/AppLauncherWin.cs index 7714a7992..bd3775992 100644 --- a/Agent/Services/Windows/AppLauncherWin.cs +++ b/Agent/Services/Windows/AppLauncherWin.cs @@ -1,4 +1,4 @@ -using Immense.RemoteControl.Desktop.Shared.Native.Windows; +using Remotely.Desktop.Shared.Native.Windows; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; using Remotely.Agent.Interfaces; diff --git a/Agent/Services/Windows/DeviceInfoGeneratorWin.cs b/Agent/Services/Windows/DeviceInfoGeneratorWin.cs index 39ae7a353..b41aade35 100644 --- a/Agent/Services/Windows/DeviceInfoGeneratorWin.cs +++ b/Agent/Services/Windows/DeviceInfoGeneratorWin.cs @@ -1,4 +1,4 @@ -using Immense.RemoteControl.Desktop.Shared.Native.Windows; +using Remotely.Desktop.Shared.Native.Windows; using Microsoft.Extensions.Logging; using Remotely.Agent.Interfaces; using Remotely.Shared.Dtos; diff --git a/Desktop.Linux/Desktop.Linux.csproj b/Desktop.Linux/Desktop.Linux.csproj index 15014a8a5..d6609caa1 100644 --- a/Desktop.Linux/Desktop.Linux.csproj +++ b/Desktop.Linux/Desktop.Linux.csproj @@ -4,7 +4,7 @@ net8.0 Assets\favicon.ico Remotely_Desktop - Remotely.Desktop.XPlat + Remotely.Desktop.Linux AnyCPU;x64;x86 @@ -56,8 +56,6 @@ - - - + diff --git a/Desktop.Linux/Program.cs b/Desktop.Linux/Program.cs index 72ee7948f..7c04131a7 100644 --- a/Desktop.Linux/Program.cs +++ b/Desktop.Linux/Program.cs @@ -1,19 +1,16 @@ -using Immense.RemoteControl.Desktop.Shared.Abstractions; -using System.Threading.Tasks; +using Remotely.Desktop.Shared.Abstractions; using System.Threading; -using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Remotely.Shared.Services; -using Immense.RemoteControl.Desktop.Shared.Services; +using Remotely.Desktop.Shared.Services; using System.Diagnostics; using Remotely.Shared.Utilities; -using Immense.RemoteControl.Desktop.Shared.Startup; -using System.Linq; -using Immense.RemoteControl.Desktop.Linux.Startup; -using Immense.RemoteControl.Desktop.UI.Services; +using Remotely.Desktop.Shared.Startup; +using Remotely.Desktop.Linux.Startup; +using Remotely.Desktop.UI.Services; using Avalonia; -using Immense.RemoteControl.Desktop.UI; +using Remotely.Desktop.UI; using Desktop.Shared.Services; namespace Remotely.Desktop.XPlat; @@ -45,14 +42,8 @@ public static async Task Main(string[] args) var services = new ServiceCollection(); - services.AddSingleton(); services.AddSingleton(); - - services.AddRemoteControlLinux( - config => - { - config.AddBrandingProvider(); - }); + services.AddRemoteControlLinux(); services.AddLogging(builder => { @@ -66,17 +57,16 @@ public static async Task Main(string[] args) var provider = services.BuildServiceProvider(); var appState = provider.GetRequiredService(); - var orgIdProvider = provider.GetRequiredService(); if (getEmbeddedResult.IsSuccess) { - orgIdProvider.OrganizationId = getEmbeddedResult.Value.OrganizationId; + appState.OrganizationId = getEmbeddedResult.Value.OrganizationId; appState.Host = getEmbeddedResult.Value.ServerUrl.AbsoluteUri; } if (appState.ArgDict.TryGetValue("org-id", out var orgId)) { - orgIdProvider.OrganizationId = orgId; + appState.OrganizationId = orgId; } var result = await provider.UseRemoteControlClient( diff --git a/Desktop.Linux/Services/AppStartup.cs b/Desktop.Linux/Services/AppStartup.cs new file mode 100644 index 000000000..5695f9f51 --- /dev/null +++ b/Desktop.Linux/Services/AppStartup.cs @@ -0,0 +1,151 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Enums; +using Remotely.Desktop.Shared.Services; +using Remotely.Desktop.UI.Services; +using Remotely.Shared.Models; +using Microsoft.Extensions.Logging; +using Desktop.Shared.Services; + +namespace Remotely.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 _logger; + + public AppStartup( + IAppState appState, + IKeyboardMouseInput inputService, + IDesktopHubConnection desktopHub, + IClipboardService clipboardService, + IChatHostService chatHostService, + ICursorIconWatcher iconWatcher, + IUiDispatcher dispatcher, + IIdleTimer idleTimer, + IShutdownService shutdownService, + IBrandingProvider brandingProvider, + ILogger 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); + } + } + } +} diff --git a/Desktop.Linux/Services/AudioCapturerLinux.cs b/Desktop.Linux/Services/AudioCapturerLinux.cs new file mode 100644 index 000000000..ae7e7603e --- /dev/null +++ b/Desktop.Linux/Services/AudioCapturerLinux.cs @@ -0,0 +1,15 @@ +using Remotely.Desktop.Shared.Abstractions; + +namespace Remotely.Desktop.Linux.Services; + +public class AudioCapturerLinux : IAudioCapturer +{ +#pragma warning disable CS0067 + public event EventHandler? AudioSampleReady; +#pragma warning restore + + public void ToggleAudio(bool toggleOn) + { + // Not implemented. + } +} diff --git a/Desktop.Linux/Services/CursorIconWatcherLinux.cs b/Desktop.Linux/Services/CursorIconWatcherLinux.cs new file mode 100644 index 000000000..6cb1105c8 --- /dev/null +++ b/Desktop.Linux/Services/CursorIconWatcherLinux.cs @@ -0,0 +1,15 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Shared.Models; +using System.Drawing; + +namespace Remotely.Desktop.Linux.Services; + +public class CursorIconWatcherLinux : ICursorIconWatcher +{ +#pragma warning disable CS0067 + public event EventHandler? OnChange; +#pragma warning restore + + + public CursorInfo GetCurrentCursor() => new(Array.Empty(), Point.Empty, "default"); +} diff --git a/Desktop.Linux/Services/FileTransferServiceLinux.cs b/Desktop.Linux/Services/FileTransferServiceLinux.cs new file mode 100644 index 000000000..ac88c5083 --- /dev/null +++ b/Desktop.Linux/Services/FileTransferServiceLinux.cs @@ -0,0 +1,160 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Services; +using Remotely.Desktop.Shared.ViewModels; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using Remotely.Desktop.UI.Controls.Dialogs; +using Remotely.Desktop.UI.Views; +using Remotely.Desktop.UI.Services; +using System.Threading; +using System.IO; + +namespace Remotely.Desktop.Linux.Services; + +public class FileTransferServiceLinux : IFileTransferService +{ + private static readonly ConcurrentDictionary _fileTransferWindows = new(); + private static readonly ConcurrentDictionary _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 _logger; + + public FileTransferServiceLinux( + IViewModelFactory viewModelFactory, + IUiDispatcher dispatcher, + IDialogProvider dialogProvider, + ILogger 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 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; + } + } +} diff --git a/Desktop.Linux/Services/KeyboardMouseInputLinux.cs b/Desktop.Linux/Services/KeyboardMouseInputLinux.cs new file mode 100644 index 000000000..fec58f4a5 --- /dev/null +++ b/Desktop.Linux/Services/KeyboardMouseInputLinux.cs @@ -0,0 +1,259 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Enums; +using Remotely.Desktop.Shared.Native.Linux; +using Remotely.Desktop.Shared.Services; +using Microsoft.Extensions.Logging; + +namespace Remotely.Desktop.Linux.Services; + +public class KeyboardMouseInputLinux : IKeyboardMouseInput +{ + private readonly ILogger _logger; + + private nint Display { get; set; } + + public KeyboardMouseInputLinux(ILogger 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."); + } + } + +} diff --git a/Desktop.Linux/Services/ScreenCapturerLinux.cs b/Desktop.Linux/Services/ScreenCapturerLinux.cs new file mode 100644 index 000000000..63d028e11 --- /dev/null +++ b/Desktop.Linux/Services/ScreenCapturerLinux.cs @@ -0,0 +1,239 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Native.Linux; +using Remotely.Desktop.Shared.Services; +using Microsoft.Extensions.Logging; +using Remotely.Shared.Primitives; +using SkiaSharp; +using System.Drawing; +using System.Runtime.InteropServices; + +namespace Remotely.Desktop.Linux.Services; + +public class ScreenCapturerLinux : IScreenCapturer +{ + private readonly IImageHelper _imageHelper; + private readonly ILogger _logger; + private readonly object _screenBoundsLock = new(); + private readonly Dictionary _x11Screens = new(); + private SKBitmap? _currentFrame; + private SKBitmap? _previousFrame; + + public ScreenCapturerLinux( + IImageHelper imageHelper, + ILogger logger) + { + _imageHelper = imageHelper; + _logger = logger; + Display = LibX11.XOpenDisplay(string.Empty); + Init(); + } + + public event EventHandler? 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 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 GetImageDiff() + { + if (_currentFrame is null) + { + return Result.Fail("Current frame is null."); + } + + return _imageHelper.GetImageDiff(_currentFrame, _previousFrame); + } + + public Result 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(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(); + + for (var i = 0; i < monitorCount; i++) + { + var monitorPtr = new nint(monitorsPtr.ToInt64() + i * monitorInfoSize); + var monitorInfo = Marshal.PtrToStructure(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(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(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); + } +} diff --git a/Desktop.Linux/Services/ShutdownServiceLinux.cs b/Desktop.Linux/Services/ShutdownServiceLinux.cs new file mode 100644 index 000000000..2c28661bd --- /dev/null +++ b/Desktop.Linux/Services/ShutdownServiceLinux.cs @@ -0,0 +1,49 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Services; +using Microsoft.Extensions.Logging; +using Remotely.Desktop.UI.Services; + +namespace Remotely.Desktop.Linux.Services; + +public class ShutdownServiceLinux : IShutdownService +{ + private readonly IDesktopHubConnection _hubConnection; + private readonly IUiDispatcher _dispatcher; + private readonly IAppState _appState; + private readonly ILogger _logger; + + public ShutdownServiceLinux( + IDesktopHubConnection hubConnection, + IUiDispatcher dispatcher, + IAppState appState, + ILogger 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."); + } + } +} diff --git a/Desktop.Linux/Startup/IServiceCollectionExtensions.cs b/Desktop.Linux/Startup/IServiceCollectionExtensions.cs new file mode 100644 index 000000000..da9d6fe08 --- /dev/null +++ b/Desktop.Linux/Startup/IServiceCollectionExtensions.cs @@ -0,0 +1,35 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Startup; +using Microsoft.Extensions.DependencyInjection; +using Remotely.Desktop.Linux.Services; +using Remotely.Desktop.UI.ViewModels; +using Remotely.Desktop.UI.Services; +using Remotely.Desktop.UI.Startup; + +namespace Remotely.Desktop.Linux.Startup; + +public static class IServiceCollectionExtensions +{ + /// + /// Adds Linux and cross-platform remote control services to the service collection. + /// All methods on must be called to register + /// required services. + /// + /// + /// + public static void AddRemoteControlLinux(this IServiceCollection services) + { + services.AddRemoteControlXplat(); + services.AddRemoteControlUi(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); + } +} diff --git a/Desktop.Linux/Usings.cs b/Desktop.Linux/Usings.cs new file mode 100644 index 000000000..c8962ecf9 --- /dev/null +++ b/Desktop.Linux/Usings.cs @@ -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; diff --git a/Desktop.Native/Desktop.Native.csproj b/Desktop.Native/Desktop.Native.csproj new file mode 100644 index 000000000..84d1cca2b --- /dev/null +++ b/Desktop.Native/Desktop.Native.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + True + + + + + + + diff --git a/Desktop.Native/Linux/LibX11.cs b/Desktop.Native/Linux/LibX11.cs new file mode 100644 index 000000000..cc16107f2 --- /dev/null +++ b/Desktop.Native/Linux/LibX11.cs @@ -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 Remotely.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; + } +} diff --git a/Desktop.Native/Linux/LibXtst.cs b/Desktop.Native/Linux/LibXtst.cs new file mode 100644 index 000000000..462691974 --- /dev/null +++ b/Desktop.Native/Linux/LibXtst.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; + +namespace Remotely.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); +} diff --git a/Desktop.Native/Linux/Libc.cs b/Desktop.Native/Linux/Libc.cs new file mode 100644 index 000000000..bd7abccbd --- /dev/null +++ b/Desktop.Native/Linux/Libc.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Remotely.Desktop.Shared.Native.Linux; + +public class Libc +{ + [DllImport("libc", SetLastError = true)] + public static extern uint geteuid(); +} diff --git a/Desktop.Native/Linux/libXrandr.cs b/Desktop.Native/Linux/libXrandr.cs new file mode 100644 index 000000000..27ce7fdd5 --- /dev/null +++ b/Desktop.Native/Linux/libXrandr.cs @@ -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 Remotely.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); +} diff --git a/Desktop.Native/Windows/ADVAPI32.cs b/Desktop.Native/Windows/ADVAPI32.cs new file mode 100644 index 000000000..8e8c661b2 --- /dev/null +++ b/Desktop.Native/Windows/ADVAPI32.cs @@ -0,0 +1,365 @@ +using System; +using System.Runtime.InteropServices; +using System.Security; + +namespace Remotely.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 + { + /// +     /// The buffer receives a TOKEN_USER structure that contains the user account of the token. +     /// + TokenUser = 1, + + /// +     /// The buffer receives a TOKEN_GROUPS structure that contains the group accounts associated with the token. +     /// + TokenGroups, + + /// +     /// The buffer receives a TOKEN_PRIVILEGES structure that contains the privileges of the token. +     /// + TokenPrivileges, + + /// +     /// The buffer receives a TOKEN_OWNER structure that contains the default owner security identifier (SID) for newly created objects. +     /// + TokenOwner, + + /// +     /// The buffer receives a TOKEN_PRIMARY_GROUP structure that contains the default primary group SID for newly created objects. +     /// + TokenPrimaryGroup, + + /// +     /// The buffer receives a TOKEN_DEFAULT_DACL structure that contains the default DACL for newly created objects. +     /// + TokenDefaultDacl, + + /// +     /// The buffer receives a TOKEN_SOURCE structure that contains the source of the token. TOKEN_QUERY_SOURCE access is needed to retrieve this information. +     /// + TokenSource, + + /// +     /// The buffer receives a TOKEN_TYPE value that indicates whether the token is a primary or impersonation token. +     /// + TokenType, + + /// +     /// 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. +     /// + TokenImpersonationLevel, + + /// +     /// The buffer receives a TOKEN_STATISTICS structure that contains various token statistics. +     /// + TokenStatistics, + + /// +     /// The buffer receives a TOKEN_GROUPS structure that contains the list of restricting SIDs in a restricted token. +     /// + TokenRestrictedSids, + + /// +     /// The buffer receives a DWORD value that indicates the Terminal Services session identifier that is associated with the token. +     /// + TokenSessionId, + + /// +     /// 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. +     /// + TokenGroupsAndPrivileges, + + /// +     /// Reserved. +     /// + TokenSessionReference, + + /// +     /// The buffer receives a DWORD value that is nonzero if the token includes the SANDBOX_INERT flag. +     /// + TokenSandBoxInert, + + /// +     /// Reserved. +     /// + TokenAuditPolicy, + + /// +     /// The buffer receives a TOKEN_ORIGIN value. +     /// + TokenOrigin, + + /// +     /// The buffer receives a TOKEN_ELEVATION_TYPE value that specifies the elevation level of the token. +     /// + TokenElevationType, + + /// +     /// The buffer receives a TOKEN_LINKED_TOKEN structure that contains a handle to another token that is linked to this token. +     /// + TokenLinkedToken, + + /// +     /// The buffer receives a TOKEN_ELEVATION structure that specifies whether the token is elevated. +     /// + TokenElevation, + + /// +     /// The buffer receives a DWORD value that is nonzero if the token has ever been filtered. +     /// + TokenHasRestrictions, + + /// +     /// The buffer receives a TOKEN_ACCESS_INFORMATION structure that specifies security information contained in the token. +     /// + TokenAccessInformation, + + /// +     /// The buffer receives a DWORD value that is nonzero if virtualization is allowed for the token. +     /// + TokenVirtualizationAllowed, + + /// +     /// The buffer receives a DWORD value that is nonzero if virtualization is enabled for the token. +     /// + TokenVirtualizationEnabled, + + /// +     /// The buffer receives a TOKEN_MANDATORY_LABEL structure that specifies the token's integrity level. +     /// + TokenIntegrityLevel, + + /// +     /// The buffer receives a DWORD value that is nonzero if the token has the UIAccess flag set. +     /// + TokenUIAccess, + + /// +     /// The buffer receives a TOKEN_MANDATORY_POLICY structure that specifies the token's mandatory integrity policy. +     /// + TokenMandatoryPolicy, + + /// +     /// The buffer receives the token's logon security identifier (SID). +     /// + TokenLogonSid, + + /// +     /// The maximum value for this enumeration +     /// + 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 +} diff --git a/Desktop.Native/Windows/GDI32.cs b/Desktop.Native/Windows/GDI32.cs new file mode 100644 index 000000000..44526e4df --- /dev/null +++ b/Desktop.Native/Windows/GDI32.cs @@ -0,0 +1,80 @@ +using System; +using System.Runtime.InteropServices; + +namespace Remotely.Desktop.Shared.Native.Windows; + +public static class GDI32 +{ + #region Enums + /// + /// 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. + /// + public enum TernaryRasterOperations : uint + { + /// dest = source + SRCCOPY = 0x00CC0020, + /// dest = source OR dest + SRCPAINT = 0x00EE0086, + /// dest = source AND dest + SRCAND = 0x008800C6, + /// dest = source XOR dest + SRCINVERT = 0x00660046, + /// dest = source AND (NOT dest) + SRCERASE = 0x00440328, + /// dest = (NOT source) + NOTSRCCOPY = 0x00330008, + /// dest = (NOT src) AND (NOT dest) + NOTSRCERASE = 0x001100A6, + /// dest = (source AND pattern) + MERGECOPY = 0x00C000CA, + /// dest = (NOT source) OR dest + MERGEPAINT = 0x00BB0226, + /// dest = pattern + PATCOPY = 0x00F00021, + /// dest = DPSnoo + PATPAINT = 0x00FB0A09, + /// dest = pattern XOR dest + PATINVERT = 0x005A0049, + /// dest = (NOT dest) + DSTINVERT = 0x00550009, + /// dest = BLACK + BLACKNESS = 0x00000042, + /// dest = WHITE + WHITENESS = 0x00FF0062, + /// + /// Capture window as seen on screen. This includes layered windows + /// such as WPF windows with AllowsTransparency="true" + /// + 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 +} diff --git a/Desktop.Native/Windows/Kernel32.cs b/Desktop.Native/Windows/Kernel32.cs new file mode 100644 index 000000000..410f6b46f --- /dev/null +++ b/Desktop.Native/Windows/Kernel32.cs @@ -0,0 +1,89 @@ +using System; +using System.Runtime.InteropServices; + +namespace Remotely.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(); + + /// + /// contains information about the current state of both physical and virtual memory, including extended memory + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public class MEMORYSTATUSEX + { + /// + /// Size of the structure, in bytes. You must set this member before calling GlobalMemoryStatusEx. + /// + public uint dwLength; + + /// + /// 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). + /// + public uint dwMemoryLoad; + + /// + /// Total size of physical memory, in bytes. + /// + public ulong ullTotalPhys; + + /// + /// Size of physical memory available, in bytes. + /// + public ulong ullAvailPhys; + + /// + /// Size of the committed memory limit, in bytes. This is physical memory plus the size of the page file, minus a small overhead. + /// + public ulong ullTotalPageFile; + + /// + /// Size of available memory to commit, in bytes. The limit is ullTotalPageFile. + /// + public ulong ullAvailPageFile; + + /// + /// Total size of the user mode portion of the virtual address space of the calling process, in bytes. + /// + public ulong ullTotalVirtual; + + /// + /// Size of unreserved and uncommitted memory in the user mode portion of the virtual address space of the calling process, in bytes. + /// + public ulong ullAvailVirtual; + + /// + /// Size of unreserved and uncommitted memory in the extended portion of the virtual address space of the calling process, in bytes. + /// + public ulong ullAvailExtendedVirtual; + + /// + /// Initializes a new instance of the class. + /// + public MEMORYSTATUSEX() + { + dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX)); + } + } +} diff --git a/Desktop.Native/Windows/SECUR32.cs b/Desktop.Native/Windows/SECUR32.cs new file mode 100644 index 000000000..2f6466327 --- /dev/null +++ b/Desktop.Native/Windows/SECUR32.cs @@ -0,0 +1,376 @@ +using Remotely.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 + { + /// +     /// The buffer receives a TOKEN_USER structure that contains the user account of the token. +     /// + TokenUser = 1, + + /// +     /// The buffer receives a TOKEN_GROUPS structure that contains the group accounts associated with the token. +     /// + TokenGroups, + + /// +     /// The buffer receives a TOKEN_PRIVILEGES structure that contains the privileges of the token. +     /// + TokenPrivileges, + + /// +     /// The buffer receives a TOKEN_OWNER structure that contains the default owner security identifier (SID) for newly created objects. +     /// + TokenOwner, + + /// +     /// The buffer receives a TOKEN_PRIMARY_GROUP structure that contains the default primary group SID for newly created objects. +     /// + TokenPrimaryGroup, + + /// +     /// The buffer receives a TOKEN_DEFAULT_DACL structure that contains the default DACL for newly created objects. +     /// + TokenDefaultDacl, + + /// +     /// The buffer receives a TOKEN_SOURCE structure that contains the source of the token. TOKEN_QUERY_SOURCE access is needed to retrieve this information. +     /// + TokenSource, + + /// +     /// The buffer receives a TOKEN_TYPE value that indicates whether the token is a primary or impersonation token. +     /// + TokenType, + + /// +     /// 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. +     /// + TokenImpersonationLevel, + + /// +     /// The buffer receives a TOKEN_STATISTICS structure that contains various token statistics. +     /// + TokenStatistics, + + /// +     /// The buffer receives a TOKEN_GROUPS structure that contains the list of restricting SIDs in a restricted token. +     /// + TokenRestrictedSids, + + /// +     /// The buffer receives a DWORD value that indicates the Terminal Services session identifier that is associated with the token. +     /// + TokenSessionId, + + /// +     /// 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. +     /// + TokenGroupsAndPrivileges, + + /// +     /// Reserved. +     /// + TokenSessionReference, + + /// +     /// The buffer receives a DWORD value that is nonzero if the token includes the SANDBOX_INERT flag. +     /// + TokenSandBoxInert, + + /// +     /// Reserved. +     /// + TokenAuditPolicy, + + /// +     /// The buffer receives a TOKEN_ORIGIN value. +     /// + TokenOrigin, + + /// +     /// The buffer receives a TOKEN_ELEVATION_TYPE value that specifies the elevation level of the token. +     /// + TokenElevationType, + + /// +     /// The buffer receives a TOKEN_LINKED_TOKEN structure that contains a handle to another token that is linked to this token. +     /// + TokenLinkedToken, + + /// +     /// The buffer receives a TOKEN_ELEVATION structure that specifies whether the token is elevated. +     /// + TokenElevation, + + /// +     /// The buffer receives a DWORD value that is nonzero if the token has ever been filtered. +     /// + TokenHasRestrictions, + + /// +     /// The buffer receives a TOKEN_ACCESS_INFORMATION structure that specifies security information contained in the token. +     /// + TokenAccessInformation, + + /// +     /// The buffer receives a DWORD value that is nonzero if virtualization is allowed for the token. +     /// + TokenVirtualizationAllowed, + + /// +     /// The buffer receives a DWORD value that is nonzero if virtualization is enabled for the token. +     /// + TokenVirtualizationEnabled, + + /// +     /// The buffer receives a TOKEN_MANDATORY_LABEL structure that specifies the token's integrity level. +     /// + TokenIntegrityLevel, + + /// +     /// The buffer receives a DWORD value that is nonzero if the token has the UIAccess flag set. +     /// + TokenUIAccess, + + /// +     /// The buffer receives a TOKEN_MANDATORY_POLICY structure that specifies the token's mandatory integrity policy. +     /// + TokenMandatoryPolicy, + + /// +     /// The buffer receives the token's logon security identifier (SID). +     /// + TokenLogonSid, + + /// +     /// The maximum value for this enumeration +     /// + 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); + } +} \ No newline at end of file diff --git a/Desktop.Native/Windows/Shlwapi.cs b/Desktop.Native/Windows/Shlwapi.cs new file mode 100644 index 000000000..f0b62fe31 --- /dev/null +++ b/Desktop.Native/Windows/Shlwapi.cs @@ -0,0 +1,51 @@ +using System.Runtime.InteropServices; + +namespace Remotely.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, +} diff --git a/Desktop.Native/Windows/User32.cs b/Desktop.Native/Windows/User32.cs new file mode 100644 index 000000000..b7170acf6 --- /dev/null +++ b/Desktop.Native/Windows/User32.cs @@ -0,0 +1,1348 @@ +using Microsoft.Win32.SafeHandles; +using System.Runtime.InteropServices; + +namespace Remotely.Desktop.Shared.Native.Windows; + +public static class User32 +{ + #region Constants + public const int CURSOR_SHOWING = 0x00000001; + public const uint MOUSEEVENTF_ABSOLUTE = 0x8000; + public const int MOUSEEVENTF_LEFTDOWN = 0x02; + public const int MOUSEEVENTF_LEFTUP = 0x04; + public const int MOUSEEVENTF_RIGHTDOWN = 0x08; + public const int MOUSEEVENTF_RIGHTUP = 0x10; + public const int MOUSEEVENTF_MOVE = 0x0001; + public const uint KEYEVENTF_EXTENDEDKEY = 0x0001; + public const uint KEYEVENTF_KEYUP = 0x0002; + + public const int SPIF_SENDWININICHANGE = 0x02; + public const int SPI_SETDESKWALLPAPER = 20; + public const int SPIF_UPDATEINIFILE = 1; + public const int SPIF_SENDCHANGE = 2; + + public static readonly int SPI_GETDESKWALLPAPER = 0x73; + public static readonly int MAX_PATH = 260; + #endregion + + #region Enums + [Flags] + public enum MouseEventFlags : uint + { + LEFTDOWN = 0x00000002, + LEFTUP = 0x00000004, + MIDDLEDOWN = 0x00000020, + MIDDLEUP = 0x00000040, + MOVE = 0x00000001, + ABSOLUTE = 0x00008000, + RIGHTDOWN = 0x00000008, + RIGHTUP = 0x00000010, + WHEEL = 0x00000800, + XDOWN = 0x00000080, + XUP = 0x00000100 + } + [Flags] + public enum MOUSEEVENTF : uint + { + ABSOLUTE = 0x8000, + HWHEEL = 0x01000, + MOVE = 0x0001, + MOVE_NOCOALESCE = 0x2000, + LEFTDOWN = 0x0002, + LEFTUP = 0x0004, + RIGHTDOWN = 0x0008, + RIGHTUP = 0x0010, + MIDDLEDOWN = 0x0020, + MIDDLEUP = 0x0040, + VIRTUALDESK = 0x4000, + WHEEL = 0x0800, + XDOWN = 0x0080, + XUP = 0x0100 + } + public enum MonitorState + { + MonitorStateOn = -1, + MonitorStateOff = 2, + MonitorStateStandBy = 1 + } + [Flags] + public enum KEYEVENTF : uint + { + EXTENDEDKEY = 0x0001, + KEYUP = 0x0002, + SCANCODE = 0x0008, + UNICODE = 0x0004 + } + + public enum VirtualKey : short + { + /// +         ///Left mouse button +         /// + LBUTTON = 0x01, + /// +         ///Right mouse button +         /// + RBUTTON = 0x02, + /// +         ///Control-break processing +         /// + CANCEL = 0x03, + /// +         ///Middle mouse button (three-button mouse) +         /// + MBUTTON = 0x04, + /// +         ///Windows 2000/XP: X1 mouse button +         /// + XBUTTON1 = 0x05, + /// +         ///Windows 2000/XP: X2 mouse button +         /// + XBUTTON2 = 0x06, + /// +         ///BACKSPACE key +         /// + BACK = 0x08, + /// +         ///TAB key +         /// + TAB = 0x09, + /// +         ///CLEAR key +         /// + CLEAR = 0x0C, + /// +         ///ENTER key +         /// + RETURN = 0x0D, + /// +         ///SHIFT key +         /// + SHIFT = 0x10, + /// +         ///CTRL key +         /// + CONTROL = 0x11, + /// +         ///ALT key +         /// + MENU = 0x12, + /// +         ///PAUSE key +         /// + PAUSE = 0x13, + /// +         ///CAPS LOCK key +         /// + CAPITAL = 0x14, + /// +         ///Input Method Editor (IME) Kana mode +         /// + KANA = 0x15, + /// +         ///IME Hangul mode +         /// + HANGUL = 0x15, + /// +         ///IME Junja mode +         /// + JUNJA = 0x17, + /// +         ///IME final mode +         /// + FINAL = 0x18, + /// +         ///IME Hanja mode +         /// + HANJA = 0x19, + /// +         ///IME Kanji mode +         /// + KANJI = 0x19, + /// +         ///ESC key +         /// + ESCAPE = 0x1B, + /// +         ///IME convert +         /// + CONVERT = 0x1C, + /// +         ///IME nonconvert +         /// + NONCONVERT = 0x1D, + /// +         ///IME accept +         /// + ACCEPT = 0x1E, + /// +         ///IME mode change request +         /// + MODECHANGE = 0x1F, + /// +         ///SPACEBAR +         /// + SPACE = 0x20, + /// +         ///PAGE UP key +         /// + PRIOR = 0x21, + /// +         ///PAGE DOWN key +         /// + NEXT = 0x22, + /// +         ///END key +         /// + END = 0x23, + /// +         ///HOME key +         /// + HOME = 0x24, + /// +         ///LEFT ARROW key +         /// + LEFT = 0x25, + /// +         ///UP ARROW key +         /// + UP = 0x26, + /// +         ///RIGHT ARROW key +         /// + RIGHT = 0x27, + /// +         ///DOWN ARROW key +         /// + DOWN = 0x28, + /// +         ///SELECT key +         /// + SELECT = 0x29, + /// +         ///PRINT key +         /// + PRINT = 0x2A, + /// +         ///EXECUTE key +         /// + EXECUTE = 0x2B, + /// +         ///PRINT SCREEN key +         /// + SNAPSHOT = 0x2C, + /// +         ///INS key +         /// + INSERT = 0x2D, + /// +         ///DEL key +         /// + DELETE = 0x2E, + /// +         ///HELP key +         /// + HELP = 0x2F, + /// +         ///0 key +         /// + KEY_0 = 0x30, + /// +         ///1 key +         /// + KEY_1 = 0x31, + /// +         ///2 key +         /// + KEY_2 = 0x32, + /// +         ///3 key +         /// + KEY_3 = 0x33, + /// +         ///4 key +         /// + KEY_4 = 0x34, + /// +         ///5 key +         /// + KEY_5 = 0x35, + /// +         ///6 key +         /// + KEY_6 = 0x36, + /// +         ///7 key +         /// + KEY_7 = 0x37, + /// +         ///8 key +         /// + KEY_8 = 0x38, + /// +         ///9 key +         /// + KEY_9 = 0x39, + /// +         ///A key +         /// + KEY_A = 0x41, + /// +         ///B key +         /// + KEY_B = 0x42, + /// +         ///C key +         /// + KEY_C = 0x43, + /// +         ///D key +         /// + KEY_D = 0x44, + /// +         ///E key +         /// + KEY_E = 0x45, + /// +         ///F key +         /// + KEY_F = 0x46, + /// +         ///G key +         /// + KEY_G = 0x47, + /// +         ///H key +         /// + KEY_H = 0x48, + /// +         ///I key +         /// + KEY_I = 0x49, + /// +         ///J key +         /// + KEY_J = 0x4A, + /// +         ///K key +         /// + KEY_K = 0x4B, + /// +         ///L key +         /// + KEY_L = 0x4C, + /// +         ///M key +         /// + KEY_M = 0x4D, + /// +         ///N key +         /// + KEY_N = 0x4E, + /// +         ///O key +         /// + KEY_O = 0x4F, + /// +         ///P key +         /// + KEY_P = 0x50, + /// +         ///Q key +         /// + KEY_Q = 0x51, + /// +         ///R key +         /// + KEY_R = 0x52, + /// +         ///S key +         /// + KEY_S = 0x53, + /// +         ///T key +         /// + KEY_T = 0x54, + /// +         ///U key +         /// + KEY_U = 0x55, + /// +         ///V key +         /// + KEY_V = 0x56, + /// +         ///W key +         /// + KEY_W = 0x57, + /// +         ///X key +         /// + KEY_X = 0x58, + /// +         ///Y key +         /// + KEY_Y = 0x59, + /// +         ///Z key +         /// + KEY_Z = 0x5A, + /// +         ///Left Windows key (Microsoft Natural keyboard) +         /// + LWIN = 0x5B, + /// +         ///Right Windows key (Natural keyboard) +         /// + RWIN = 0x5C, + /// +         ///Applications key (Natural keyboard) +         /// + APPS = 0x5D, + /// +         ///Computer Sleep key +         /// + SLEEP = 0x5F, + /// +         ///Numeric keypad 0 key +         /// + NUMPAD0 = 0x60, + /// +         ///Numeric keypad 1 key +         /// + NUMPAD1 = 0x61, + /// +         ///Numeric keypad 2 key +         /// + NUMPAD2 = 0x62, + /// +         ///Numeric keypad 3 key +         /// + NUMPAD3 = 0x63, + /// +         ///Numeric keypad 4 key +         /// + NUMPAD4 = 0x64, + /// +         ///Numeric keypad 5 key +         /// + NUMPAD5 = 0x65, + /// +         ///Numeric keypad 6 key +         /// + NUMPAD6 = 0x66, + /// +         ///Numeric keypad 7 key +         /// + NUMPAD7 = 0x67, + /// +         ///Numeric keypad 8 key +         /// + NUMPAD8 = 0x68, + /// +         ///Numeric keypad 9 key +         /// + NUMPAD9 = 0x69, + /// +         ///Multiply key +         /// + MULTIPLY = 0x6A, + /// +         ///Add key +         /// + ADD = 0x6B, + /// +         ///Separator key +         /// + SEPARATOR = 0x6C, + /// +         ///Subtract key +         /// + SUBTRACT = 0x6D, + /// +         ///Decimal key +         /// + DECIMAL = 0x6E, + /// +         ///Divide key +         /// + DIVIDE = 0x6F, + /// +         ///F1 key +         /// + F1 = 0x70, + /// +         ///F2 key +         /// + F2 = 0x71, + /// +         ///F3 key +         /// + F3 = 0x72, + /// +         ///F4 key +         /// + F4 = 0x73, + /// +         ///F5 key +         /// + F5 = 0x74, + /// +         ///F6 key +         /// + F6 = 0x75, + /// +         ///F7 key +         /// + F7 = 0x76, + /// +         ///F8 key +         /// + F8 = 0x77, + /// +         ///F9 key +         /// + F9 = 0x78, + /// +         ///F10 key +         /// + F10 = 0x79, + /// +         ///F11 key +         /// + F11 = 0x7A, + /// +         ///F12 key +         /// + F12 = 0x7B, + /// +         ///F13 key +         /// + F13 = 0x7C, + /// +         ///F14 key +         /// + F14 = 0x7D, + /// +         ///F15 key +         /// + F15 = 0x7E, + /// +         ///F16 key +         /// + F16 = 0x7F, + /// +         ///F17 key   +         /// + F17 = 0x80, + /// +         ///F18 key   +         /// + F18 = 0x81, + /// +         ///F19 key   +         /// + F19 = 0x82, + /// +         ///F20 key   +         /// + F20 = 0x83, + /// +         ///F21 key   +         /// + F21 = 0x84, + /// +         ///F22 key, (PPC only) Key used to lock device. +         /// + F22 = 0x85, + /// +         ///F23 key   +         /// + F23 = 0x86, + /// +         ///F24 key   +         /// + F24 = 0x87, + /// +         ///NUM LOCK key +         /// + NUMLOCK = 0x90, + /// +         ///SCROLL LOCK key +         /// + SCROLL = 0x91, + /// +         ///Left SHIFT key +         /// + LSHIFT = 0xA0, + /// +         ///Right SHIFT key +         /// + RSHIFT = 0xA1, + /// +         ///Left CONTROL key +         /// + LCONTROL = 0xA2, + /// +         ///Right CONTROL key +         /// + RCONTROL = 0xA3, + /// +         ///Left MENU key +         /// + LMENU = 0xA4, + /// +         ///Right MENU key +         /// + RMENU = 0xA5, + /// +         ///Windows 2000/XP: Browser Back key +         /// + BROWSER_BACK = 0xA6, + /// +         ///Windows 2000/XP: Browser Forward key +         /// + BROWSER_FORWARD = 0xA7, + /// +         ///Windows 2000/XP: Browser Refresh key +         /// + BROWSER_REFRESH = 0xA8, + /// +         ///Windows 2000/XP: Browser Stop key +         /// + BROWSER_STOP = 0xA9, + /// +         ///Windows 2000/XP: Browser Search key +         /// + BROWSER_SEARCH = 0xAA, + /// +         ///Windows 2000/XP: Browser Favorites key +         /// + BROWSER_FAVORITES = 0xAB, + /// +         ///Windows 2000/XP: Browser Start and Home key +         /// + BROWSER_HOME = 0xAC, + /// +         ///Windows 2000/XP: Volume Mute key +         /// + VOLUME_MUTE = 0xAD, + /// +         ///Windows 2000/XP: Volume Down key +         /// + VOLUME_DOWN = 0xAE, + /// +         ///Windows 2000/XP: Volume Up key +         /// + VOLUME_UP = 0xAF, + /// +         ///Windows 2000/XP: Next Track key +         /// + MEDIA_NEXT_TRACK = 0xB0, + /// +         ///Windows 2000/XP: Previous Track key +         /// + MEDIA_PREV_TRACK = 0xB1, + /// +         ///Windows 2000/XP: Stop Media key +         /// + MEDIA_STOP = 0xB2, + /// +         ///Windows 2000/XP: Play/Pause Media key +         /// + MEDIA_PLAY_PAUSE = 0xB3, + /// +         ///Windows 2000/XP: Start Mail key +         /// + LAUNCH_MAIL = 0xB4, + /// +         ///Windows 2000/XP: Select Media key +         /// + LAUNCH_MEDIA_SELECT = 0xB5, + /// +         ///Windows 2000/XP: Start Application 1 key +         /// + LAUNCH_APP1 = 0xB6, + /// +         ///Windows 2000/XP: Start Application 2 key +         /// + LAUNCH_APP2 = 0xB7, + /// +         ///Used for miscellaneous characters; it can vary by keyboard. +         /// + OEM_1 = 0xBA, + /// +         ///Windows 2000/XP: For any country/region, the '+' key +         /// + OEM_PLUS = 0xBB, + /// +         ///Windows 2000/XP: For any country/region, the ',' key +         /// + OEM_COMMA = 0xBC, + /// +         ///Windows 2000/XP: For any country/region, the '-' key +         /// + OEM_MINUS = 0xBD, + /// +         ///Windows 2000/XP: For any country/region, the '.' key +         /// + OEM_PERIOD = 0xBE, + /// +         ///Used for miscellaneous characters; it can vary by keyboard. +         /// + OEM_2 = 0xBF, + /// +         ///Used for miscellaneous characters; it can vary by keyboard. +         /// + OEM_3 = 0xC0, + /// +         ///Used for miscellaneous characters; it can vary by keyboard. +         /// + OEM_4 = 0xDB, + /// +         ///Used for miscellaneous characters; it can vary by keyboard. +         /// + OEM_5 = 0xDC, + /// +         ///Used for miscellaneous characters; it can vary by keyboard. +         /// + OEM_6 = 0xDD, + /// +         ///Used for miscellaneous characters; it can vary by keyboard. +         /// + OEM_7 = 0xDE, + /// +         ///Used for miscellaneous characters; it can vary by keyboard. +         /// + OEM_8 = 0xDF, + /// +         ///Windows 2000/XP: Either the angle bracket key or the backslash key on the RT 102-key keyboard +         /// + OEM_102 = 0xE2, + /// +         ///Windows 95/98/Me, Windows NT 4.0, Windows 2000/XP: IME PROCESS key +         /// + PROCESSKEY = 0xE5, + /// +         ///Windows 2000/XP: Used to pass Unicode characters as if they were keystrokes. +         ///The VK_PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods. For more information, +         ///see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP +         /// + PACKET = 0xE7, + /// +         ///Attn key +         /// + ATTN = 0xF6, + /// +         ///CrSel key +         /// + CRSEL = 0xF7, + /// +         ///ExSel key +         /// + EXSEL = 0xF8, + /// +         ///Erase EOF key +         /// + EREOF = 0xF9, + /// +         ///Play key +         /// + PLAY = 0xFA, + /// +         ///Zoom key +         /// + ZOOM = 0xFB, + /// +         ///Reserved +         /// + NONAME = 0xFC, + /// +         ///PA1 key +         /// + PA1 = 0xFD, + /// +         ///Clear key +         /// + OEM_CLEAR = 0xFE + } + public enum ScanCodeShort : short + { + LBUTTON = 0, + RBUTTON = 0, + CANCEL = 70, + MBUTTON = 0, + XBUTTON1 = 0, + XBUTTON2 = 0, + BACK = 14, + TAB = 15, + CLEAR = 76, + RETURN = 28, + SHIFT = 42, + CONTROL = 29, + MENU = 56, + PAUSE = 0, + CAPITAL = 58, + KANA = 0, + HANGUL = 0, + JUNJA = 0, + FINAL = 0, + HANJA = 0, + KANJI = 0, + ESCAPE = 1, + CONVERT = 0, + NONCONVERT = 0, + ACCEPT = 0, + MODECHANGE = 0, + SPACE = 57, + PRIOR = 73, + NEXT = 81, + END = 79, + HOME = 71, + LEFT = 75, + UP = 72, + RIGHT = 77, + DOWN = 80, + SELECT = 0, + PRINT = 0, + EXECUTE = 0, + SNAPSHOT = 84, + INSERT = 82, + DELETE = 83, + HELP = 99, + KEY_0 = 11, + KEY_1 = 2, + KEY_2 = 3, + KEY_3 = 4, + KEY_4 = 5, + KEY_5 = 6, + KEY_6 = 7, + KEY_7 = 8, + KEY_8 = 9, + KEY_9 = 10, + KEY_A = 30, + KEY_B = 48, + KEY_C = 46, + KEY_D = 32, + KEY_E = 18, + KEY_F = 33, + KEY_G = 34, + KEY_H = 35, + KEY_I = 23, + KEY_J = 36, + KEY_K = 37, + KEY_L = 38, + KEY_M = 50, + KEY_N = 49, + KEY_O = 24, + KEY_P = 25, + KEY_Q = 16, + KEY_R = 19, + KEY_S = 31, + KEY_T = 20, + KEY_U = 22, + KEY_V = 47, + KEY_W = 17, + KEY_X = 45, + KEY_Y = 21, + KEY_Z = 44, + LWIN = 91, + RWIN = 92, + APPS = 93, + SLEEP = 95, + NUMPAD0 = 82, + NUMPAD1 = 79, + NUMPAD2 = 80, + NUMPAD3 = 81, + NUMPAD4 = 75, + NUMPAD5 = 76, + NUMPAD6 = 77, + NUMPAD7 = 71, + NUMPAD8 = 72, + NUMPAD9 = 73, + MULTIPLY = 55, + ADD = 78, + SEPARATOR = 0, + SUBTRACT = 74, + DECIMAL = 83, + DIVIDE = 53, + F1 = 59, + F2 = 60, + F3 = 61, + F4 = 62, + F5 = 63, + F6 = 64, + F7 = 65, + F8 = 66, + F9 = 67, + F10 = 68, + F11 = 87, + F12 = 88, + F13 = 100, + F14 = 101, + F15 = 102, + F16 = 103, + F17 = 104, + F18 = 105, + F19 = 106, + F20 = 107, + F21 = 108, + F22 = 109, + F23 = 110, + F24 = 118, + NUMLOCK = 69, + SCROLL = 70, + LSHIFT = 42, + RSHIFT = 54, + LCONTROL = 29, + RCONTROL = 29, + LMENU = 56, + RMENU = 56, + BROWSER_BACK = 106, + BROWSER_FORWARD = 105, + BROWSER_REFRESH = 103, + BROWSER_STOP = 104, + BROWSER_SEARCH = 101, + BROWSER_FAVORITES = 102, + BROWSER_HOME = 50, + VOLUME_MUTE = 32, + VOLUME_DOWN = 46, + VOLUME_UP = 48, + MEDIA_NEXT_TRACK = 25, + MEDIA_PREV_TRACK = 16, + MEDIA_STOP = 36, + MEDIA_PLAY_PAUSE = 34, + LAUNCH_MAIL = 108, + LAUNCH_MEDIA_SELECT = 109, + LAUNCH_APP1 = 107, + LAUNCH_APP2 = 33, + OEM_1 = 39, + OEM_PLUS = 13, + OEM_COMMA = 51, + OEM_MINUS = 12, + OEM_PERIOD = 52, + OEM_2 = 53, + OEM_3 = 41, + OEM_4 = 26, + OEM_5 = 43, + OEM_6 = 27, + OEM_7 = 40, + OEM_8 = 0, + OEM_102 = 86, + PROCESSKEY = 0, + PACKET = 0, + ATTN = 0, + CRSEL = 0, + EXSEL = 0, + EREOF = 93, + PLAY = 0, + ZOOM = 98, + NONAME = 0, + PA1 = 0, + OEM_CLEAR = 0, + } + [Flags] + public enum ACCESS_MASK : uint + { + DELETE = 0x00010000, + READ_CONTROL = 0x00020000, + WRITE_DAC = 0x00040000, + WRITE_OWNER = 0x00080000, + SYNCHRONIZE = 0x00100000, + + STANDARD_RIGHTS_REQUIRED = 0x000F0000, + + STANDARD_RIGHTS_READ = 0x00020000, + STANDARD_RIGHTS_WRITE = 0x00020000, + STANDARD_RIGHTS_EXECUTE = 0x00020000, + + STANDARD_RIGHTS_ALL = 0x001F0000, + + SPECIFIC_RIGHTS_ALL = 0x0000FFFF, + + ACCESS_SYSTEM_SECURITY = 0x01000000, + + MAXIMUM_ALLOWED = 0x02000000, + + GENERIC_READ = 0x80000000, + GENERIC_WRITE = 0x40000000, + GENERIC_EXECUTE = 0x20000000, + GENERIC_ALL = 0x10000000, + + DESKTOP_READOBJECTS = 0x00000001, + DESKTOP_CREATEWINDOW = 0x00000002, + DESKTOP_CREATEMENU = 0x00000004, + DESKTOP_HOOKCONTROL = 0x00000008, + DESKTOP_JOURNALRECORD = 0x00000010, + DESKTOP_JOURNALPLAYBACK = 0x00000020, + DESKTOP_ENUMERATE = 0x00000040, + DESKTOP_WRITEOBJECTS = 0x00000080, + DESKTOP_SWITCHDESKTOP = 0x00000100, + + WINSTA_ENUMDESKTOPS = 0x00000001, + WINSTA_READATTRIBUTES = 0x00000002, + WINSTA_ACCESSCLIPBOARD = 0x00000004, + WINSTA_CREATEDESKTOP = 0x00000008, + WINSTA_WRITEATTRIBUTES = 0x00000010, + WINSTA_ACCESSGLOBALATOMS = 0x00000020, + WINSTA_EXITWINDOWS = 0x00000040, + WINSTA_ENUMERATE = 0x00000100, + WINSTA_READSCREEN = 0x00000200, + + WINSTA_ALL_ACCESS = 0x0000037F + } + public enum InputType : uint + { + MOUSE = 0, + KEYBOARD = 1, + HARDWARE = 2 + } + public enum MessageBoxType : long + { + MB_ABORTRETRYIGNORE = 0x00000002L, + MB_CANCELTRYCONTINUE = 0x00000006L, + MB_HELP = 0x00004000L, + MB_OK = 0x00000000L, + MB_OKCANCEL = 0x00000001L, + MB_RETRYCANCEL = 0x00000005L, + MB_YESNO = 0x00000004L, + MB_YESNOCANCEL = 0x00000003L, + MB_ICONEXCLAMATION = 0x00000030L, + MB_ICONWARNING = 0x00000030L, + MB_ICONINFORMATION = 0x00000040L, + MB_ICONASTERISK = 0x00000040L, + MB_ICONQUESTION = 0x00000020L, + MB_ICONSTOP = 0x00000010L, + MB_ICONERROR = 0x00000010L, + MB_ICONHAND = 0x00000010L, + MB_DEFBUTTON1 = 0x00000000L, + MB_DEFBUTTON2 = 0x00000100L, + MB_DEFBUTTON3 = 0x00000200L, + MB_DEFBUTTON4 = 0x00000300L, + MB_APPLMODAL = 0x00000000L, + MB_SYSTEMMODAL = 0x00001000L, + MB_TASKMODAL = 0x00002000L, + MB_DEFAULT_DESKTOP_ONLY = 0x00020000L, + MB_RIGHT = 0x00080000L, + MB_RTLREADING = 0x00100000L, + MB_SETFOREGROUND = 0x00010000L, + MB_TOPMOST = 0x00040000L, + MB_SERVICE_NOTIFICATION = 0x00200000L + } + + public enum MessageBoxResult : int + { + IDABORT = 3, + IDCANCEL = 2, + IDCONTINUE = 11, + IDIGNORE = 5, + IDNO = 7, + IDOK = 1, + IDRETRY = 4, + IDTRYAGAIN = 10, + IDYES = 6, + } + public enum SW + { + SW_HIDE = 0, + SW_SHOWNORMAL = 1, + SW_NORMAL = 1, + SW_SHOWMINIMIZED = 2, + SW_SHOWMAXIMIZED = 3, + SW_MAXIMIZE = 3, + SW_SHOWNOACTIVATE = 4, + SW_SHOW = 5, + SW_MINIMIZE = 6, + SW_SHOWMINNOACTIVE = 7, + SW_SHOWNA = 8, + SW_RESTORE = 9, + SW_SHOWDEFAULT = 10, + SW_MAX = 10 + } + public enum VkMapType : uint + { + MAPVK_VK_TO_VSC = 0, + MAPVK_VSC_TO_VK = 1, + MAPVK_VK_TO_CHAR = 2, + MAPVK_VSC_TO_VK_EX = 3, + MAPVK_VK_TO_VSC_EX = 4 + } + + #endregion + + #region Structs + [StructLayout(LayoutKind.Sequential)] + public struct ICONINFO + { + public bool fIcon; + public int xHotspot; + public int yHotspot; + public nint hbmMask; + public nint hbmColor; + } + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int x; + public int y; + } + + + [StructLayout(LayoutKind.Sequential)] + public struct CursorInfo + { + public int cbSize; + public int flags; + public nint hCursor; + public POINT ptScreenPos; + } + [StructLayout(LayoutKind.Sequential)] + public struct INPUT + { + public InputType type; + public InputUnion U; + public static int Size + { + get { return Marshal.SizeOf(typeof(INPUT)); } + } + } + + [StructLayout(LayoutKind.Explicit)] + public struct InputUnion + { + [FieldOffset(0)] + public MOUSEINPUT mi; + [FieldOffset(0)] + public KEYBDINPUT ki; + [FieldOffset(0)] + public HARDWAREINPUT hi; + } + + + + [StructLayout(LayoutKind.Sequential)] + public struct MOUSEINPUT + { + public int dx; + public int dy; + public int mouseData; + public MOUSEEVENTF dwFlags; + public uint time; + public nuint dwExtraInfo; + } + [StructLayout(LayoutKind.Sequential)] + public struct KEYBDINPUT + { + public VirtualKey wVk; + public ushort wScan; + public KEYEVENTF dwFlags; + public int time; + public nuint dwExtraInfo; + } + + + [StructLayout(LayoutKind.Sequential)] + public struct HARDWAREINPUT + { + public int uMsg; + public short wParamL; + public short wParamH; + } + #endregion + + #region DLL Imports + [DllImport("user32.dll")] + public static extern bool GetCursorInfo(out CursorInfo pci); + [DllImport("user32.dll", SetLastError = false)] + public static extern nint GetDesktopWindow(); + + [DllImport("user32.dll")] + public static extern nint GetCursor(); + + [DllImport("user32.dll")] + public static extern nint CopyIcon(nint hIcon); + + [DllImport("user32.dll")] + public static extern bool DrawIcon(nint hdc, int x, int y, nint hIcon); + + [DllImport("user32.dll")] + public static extern bool GetIconInfo(nint hIcon, out ICONINFO piconinfo); + + [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] + public static extern void Mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, nuint dwExtraInfo); + + [DllImport("user32.dll")] + public static extern void Keybd_event(byte bVk, byte bScan, uint dwFlags, nuint dwExtraInfo); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetCursorPos(out System.Drawing.Point lpPoint); + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetCursorPos(int x, int y); + + [DllImport("user32.dll")] + public static extern nint SetCursor(nint hcursor); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + static extern nint LoadImage(nint hinst, string lpszName, uint uType, + int cxDesired, int cyDesired, uint fuLoad); + + [DllImport("user32.dll")] + public static extern nint CreateCursor(nint hInst, int xHotSpot, int yHotSpot, + int nWidth, int nHeight, byte[] pvANDPlane, byte[] pvXORPlane); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool PrintWindow(nint hwnd, nint hDC, uint nFlags); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SwitchDesktop(nint hDesktop); + + public delegate bool EnumDesktopsDelegate(string desktop, nint lParam); + + [DllImport("user32.dll")] + public static extern bool EnumDesktopsA(nint hwinsta, EnumDesktopsDelegate lpEnumFunc, nint lParam); + + [DllImport("user32.dll", SetLastError = true)] + public static extern nint OpenInputDesktop(uint dwFlags, bool fInherit, ACCESS_MASK dwDesiredAccess); + + public delegate bool EnumWindowStationsDelegate(string windowsStation, nint lParam); + + [DllImport("user32.dll")] + public static extern bool EnumWindowStations(EnumWindowStationsDelegate lpEnumFunc, nint lParam); + + [DllImport("user32.dll")] + public static extern nint GetShellWindow(); + + public sealed class SafeWindowStationHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeWindowStationHandle() + : base(true) + { + } + + protected override bool ReleaseHandle() + { + return CloseWindowStation(handle); + + } + } + + [return: MarshalAs(UnmanagedType.Bool)] + [DllImport("user32", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CloseWindowStation(nint hWinsta); + + [DllImport("user32", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern SafeWindowStationHandle OpenWindowStation([MarshalAs(UnmanagedType.LPTStr)] string lpszWinSta, [MarshalAs(UnmanagedType.Bool)] bool fInherit, ACCESS_MASK dwDesiredAccess); + + [DllImport("user32", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern nint OpenWindowStationW([MarshalAs(UnmanagedType.LPTStr)] string lpszWinSta, [MarshalAs(UnmanagedType.Bool)] bool fInherit, ACCESS_MASK dwDesiredAccess); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetProcessWindowStation(nint hWinSta); + + [DllImport("user32.dll")] + public static extern nint GetWindowDC(nint hWnd); + + public delegate bool EnumWindowsProc(nint hwnd, nint lParam); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool EnumChildWindows(nint hwndParent, EnumWindowsProc lpEnumFunc, nint lParam); + + [DllImport("User32.dll")] + public static extern int ReleaseDC(nint hWnd, nint hDC); + + [DllImport("User32.dll")] + public static extern nint GetProcessWindowStation(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern nint GetThreadDesktop(uint threadId); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetThreadDesktop(nint hDesktop); + + [DllImport("user32.dll")] + public static extern nint OpenDesktop(string lpszDesktop, uint dwFlags, bool fInherit, ACCESS_MASK dwDesiredAccess); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool CloseDesktop(nint hDesktop); + + public delegate bool EnumDesktopWindowsDelegate(nint hWnd, int lParam); + + [DllImport("user32.dll")] + public static extern bool EnumDesktopWindows(nint hDesktop, EnumDesktopWindowsDelegate lpfn, nint lParam); + + [DllImport("user32.dll")] + public static extern nint GetDC(nint hWnd); + + [DllImport("user32.dll", SetLastError = true)] + public static extern nint SetActiveWindow(nint hWnd); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetForegroundWindow(nint hWnd); + + [DllImport("user32.dll")] + public static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray), In] INPUT[] pInputs, int cbSize); + + [DllImport("user32.dll", SetLastError = false)] + public static extern nuint GetMessageExtraInfo(); + [DllImport("sas.dll")] + public static extern void SendSAS(bool AsUser); + [DllImport("user32.dll")] + public static extern bool OpenClipboard(nint hWnd); + [DllImport("user32.dll")] + public static extern bool EmptyClipboard(); + [DllImport("user32.dll")] + public static extern bool CloseClipboard(); + [DllImport("user32.dll")] + public static extern nint SetClipboardData(int Format, nint hMem); + + [DllImport("user32.dll", EntryPoint = "ShowWindow", SetLastError = true)] + public static extern bool ShowWindow(nint hWnd, int nCmdShow); + /* +* SystemParametersInfo( +* SPI_SETDESKWALLPAPER, 0, "filename.bmp", +* SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); +*/ + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern int SystemParametersInfo( + int uAction, int uParam, string lpvParam, int fuWinIni); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool LockWorkStation(); + + [DllImport("user32.dll")] + public static extern short VkKeyScan(char ch); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern short VkKeyScanEx(char ch, nint dwhkl); + + [DllImport("user32.dll")] + public static extern int SendMessage(int hWnd, int hMsg, int wParam, int lParam); + + [DllImport("user32.dll", EntryPoint = "BlockInput")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool BlockInput([MarshalAs(UnmanagedType.Bool)] bool fBlockIt); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern int MessageBox(nint hWnd, string text, string caption, long type); + + [DllImport("USER32.dll")] + public static extern short GetKeyState(VirtualKey nVirtKey); + + [DllImport("user32.dll")] + public static extern uint MapVirtualKeyEx(uint uCode, VkMapType uMapType, nint dwhkl); + + [DllImport("user32.dll")] + public static extern nint GetKeyboardLayout(uint threadId = 0); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetUserObjectInformationW(nint hObj, int nIndex, + [Out] byte[] pvInfo, uint nLength, out uint lpnLengthNeeded); + #endregion +} diff --git a/Desktop.Native/Windows/WTSAPI32.cs b/Desktop.Native/Windows/WTSAPI32.cs new file mode 100644 index 000000000..f170f32e5 --- /dev/null +++ b/Desktop.Native/Windows/WTSAPI32.cs @@ -0,0 +1,79 @@ +using System; +using System.Runtime.InteropServices; + +namespace Remotely.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; + } +} diff --git a/Desktop.Native/Windows/Win32Interop.cs b/Desktop.Native/Windows/Win32Interop.cs new file mode 100644 index 000000000..c565a3f60 --- /dev/null +++ b/Desktop.Native/Windows/Win32Interop.cs @@ -0,0 +1,281 @@ +using Remotely.Shared.Models; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Text; +using static Remotely.Desktop.Shared.Native.Windows.ADVAPI32; +using static Remotely.Desktop.Shared.Native.Windows.User32; + +namespace Remotely.Desktop.Shared.Native.Windows; + +// TODO: Use https://github.com/microsoft/CsWin32 for all p/invokes. +public class Win32Interop +{ + public static List GetActiveSessions() + { + var sessions = new List(); + 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; + } +} diff --git a/Desktop.Shared/Abstractions/IAppStartup.cs b/Desktop.Shared/Abstractions/IAppStartup.cs new file mode 100644 index 000000000..a8051d794 --- /dev/null +++ b/Desktop.Shared/Abstractions/IAppStartup.cs @@ -0,0 +1,6 @@ +namespace Remotely.Desktop.Shared.Abstractions; + +public interface IAppStartup +{ + Task Run(); +} diff --git a/Desktop.Shared/Abstractions/IAudioCapturer.cs b/Desktop.Shared/Abstractions/IAudioCapturer.cs new file mode 100644 index 000000000..1080bd083 --- /dev/null +++ b/Desktop.Shared/Abstractions/IAudioCapturer.cs @@ -0,0 +1,7 @@ +namespace Remotely.Desktop.Shared.Abstractions; + +public interface IAudioCapturer +{ + event EventHandler AudioSampleReady; + void ToggleAudio(bool toggleOn); +} diff --git a/Desktop.Shared/Abstractions/IChatUiService.cs b/Desktop.Shared/Abstractions/IChatUiService.cs new file mode 100644 index 000000000..1971e1dbc --- /dev/null +++ b/Desktop.Shared/Abstractions/IChatUiService.cs @@ -0,0 +1,11 @@ +using Remotely.Shared.Models; + +namespace Remotely.Desktop.Shared.Abstractions; + +public interface IChatUiService +{ + event EventHandler ChatWindowClosed; + + void ShowChatWindow(string organizationName, StreamWriter writer); + Task ReceiveChat(ChatMessage chatMessage); +} diff --git a/Desktop.Shared/Abstractions/IClipboardService.cs b/Desktop.Shared/Abstractions/IClipboardService.cs new file mode 100644 index 000000000..2facc8e27 --- /dev/null +++ b/Desktop.Shared/Abstractions/IClipboardService.cs @@ -0,0 +1,10 @@ +namespace Remotely.Desktop.Shared.Abstractions; + +public interface IClipboardService +{ + event EventHandler ClipboardTextChanged; + + void BeginWatching(); + + Task SetText(string clipboardText); +} diff --git a/Desktop.Shared/Abstractions/ICursorIconWatcher.cs b/Desktop.Shared/Abstractions/ICursorIconWatcher.cs new file mode 100644 index 000000000..577f7308e --- /dev/null +++ b/Desktop.Shared/Abstractions/ICursorIconWatcher.cs @@ -0,0 +1,11 @@ +using Remotely.Shared.Models; + +namespace Remotely.Desktop.Shared.Abstractions; + +public interface ICursorIconWatcher +{ + [Obsolete("This should be replaced with a message published by IMessenger.")] + event EventHandler OnChange; + + CursorInfo GetCurrentCursor(); +} diff --git a/Desktop.Shared/Abstractions/IFileTransferService.cs b/Desktop.Shared/Abstractions/IFileTransferService.cs new file mode 100644 index 000000000..5ae12af38 --- /dev/null +++ b/Desktop.Shared/Abstractions/IFileTransferService.cs @@ -0,0 +1,13 @@ +using Remotely.Desktop.Shared.Services; +using Remotely.Desktop.Shared.ViewModels; + +namespace Remotely.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 progressUpdateCallback, CancellationToken cancelToken); +} diff --git a/Desktop.Shared/Abstractions/IKeyboardMouseInput.cs b/Desktop.Shared/Abstractions/IKeyboardMouseInput.cs new file mode 100644 index 000000000..693414788 --- /dev/null +++ b/Desktop.Shared/Abstractions/IKeyboardMouseInput.cs @@ -0,0 +1,17 @@ +using Remotely.Desktop.Shared.Enums; +using Remotely.Desktop.Shared.Services; + +namespace Remotely.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); +} diff --git a/Desktop.Shared/Abstractions/IRemoteControlAccessService.cs b/Desktop.Shared/Abstractions/IRemoteControlAccessService.cs new file mode 100644 index 000000000..1374e6477 --- /dev/null +++ b/Desktop.Shared/Abstractions/IRemoteControlAccessService.cs @@ -0,0 +1,10 @@ +using Remotely.Shared.Enums; + +namespace Remotely.Desktop.Shared.Abstractions; + +public interface IRemoteControlAccessService +{ + bool IsPromptOpen { get; } + + Task PromptForAccess(string requesterName, string organizationName); +} diff --git a/Desktop.Shared/Abstractions/IScreenCapturer.cs b/Desktop.Shared/Abstractions/IScreenCapturer.cs new file mode 100644 index 000000000..960c41c91 --- /dev/null +++ b/Desktop.Shared/Abstractions/IScreenCapturer.cs @@ -0,0 +1,29 @@ +using Remotely.Shared.Primitives; +using SkiaSharp; +using System.Drawing; + +namespace Remotely.Desktop.Shared.Abstractions; + +public interface IScreenCapturer : IDisposable +{ + event EventHandler ScreenChanged; + + bool CaptureFullscreen { get; set; } + Rectangle CurrentScreenBounds { get; } + bool IsGpuAccelerated { get; } + string SelectedScreen { get; } + IEnumerable GetDisplayNames(); + SKRect GetFrameDiffArea(); + + Result GetImageDiff(); + + Result GetNextFrame(); + + int GetScreenCount(); + + Rectangle GetVirtualScreenBounds(); + + void Init(); + + void SetSelectedScreen(string displayName); +} diff --git a/Desktop.Shared/Abstractions/ISessionIndicator.cs b/Desktop.Shared/Abstractions/ISessionIndicator.cs new file mode 100644 index 000000000..b69f8bd7e --- /dev/null +++ b/Desktop.Shared/Abstractions/ISessionIndicator.cs @@ -0,0 +1,6 @@ +namespace Remotely.Desktop.Shared.Abstractions; + +public interface ISessionIndicator +{ + void Show(); +} diff --git a/Desktop.Shared/Abstractions/IShutdownService.cs b/Desktop.Shared/Abstractions/IShutdownService.cs new file mode 100644 index 000000000..2510a7060 --- /dev/null +++ b/Desktop.Shared/Abstractions/IShutdownService.cs @@ -0,0 +1,6 @@ +namespace Remotely.Desktop.Shared.Abstractions; + +public interface IShutdownService +{ + Task Shutdown(); +} diff --git a/Agent.Installer.Win/Assets/favicon.ico b/Desktop.Shared/Assets/DefaultIcon.ico similarity index 100% rename from Agent.Installer.Win/Assets/favicon.ico rename to Desktop.Shared/Assets/DefaultIcon.ico diff --git a/Agent.Installer.Win/Assets/Remotely_Icon.png b/Desktop.Shared/Assets/DefaultIcon.png similarity index 100% rename from Agent.Installer.Win/Assets/Remotely_Icon.png rename to Desktop.Shared/Assets/DefaultIcon.png diff --git a/Desktop.Shared/Desktop.Shared.csproj b/Desktop.Shared/Desktop.Shared.csproj index 63d3dcaad..d3d629d21 100644 --- a/Desktop.Shared/Desktop.Shared.csproj +++ b/Desktop.Shared/Desktop.Shared.csproj @@ -4,24 +4,40 @@ net8.0 enable enable + True + Remotely.Desktop.Shared - - + - + + + + + + + + + + + + + - + + + + diff --git a/Desktop.Shared/Enums/AppMode.cs b/Desktop.Shared/Enums/AppMode.cs new file mode 100644 index 000000000..6133401c8 --- /dev/null +++ b/Desktop.Shared/Enums/AppMode.cs @@ -0,0 +1,8 @@ +namespace Remotely.Desktop.Shared.Enums; + +public enum AppMode +{ + Unattended, + Attended, + Chat +} diff --git a/Desktop.Shared/Enums/ButtonAction.cs b/Desktop.Shared/Enums/ButtonAction.cs new file mode 100644 index 000000000..8a3df55da --- /dev/null +++ b/Desktop.Shared/Enums/ButtonAction.cs @@ -0,0 +1,7 @@ +namespace Remotely.Desktop.Shared.Enums; + +public enum ButtonAction +{ + Down, + Up +} diff --git a/Desktop.Shared/Extensions/SKBitmapExtensions.cs b/Desktop.Shared/Extensions/SKBitmapExtensions.cs new file mode 100644 index 000000000..06c9aa4ea --- /dev/null +++ b/Desktop.Shared/Extensions/SKBitmapExtensions.cs @@ -0,0 +1,11 @@ +using SkiaSharp; + +namespace Remotely.Desktop.Shared.Extensions; + +public static class SKBitmapExtensions +{ + public static SKRect ToRectangle(this SKBitmap bitmap) + { + return new SKRect(0, 0, bitmap.Width, bitmap.Height); + } +} diff --git a/Desktop.Shared/Messages/AppStateHostChangedMessage.cs b/Desktop.Shared/Messages/AppStateHostChangedMessage.cs new file mode 100644 index 000000000..485771b24 --- /dev/null +++ b/Desktop.Shared/Messages/AppStateHostChangedMessage.cs @@ -0,0 +1,11 @@ +namespace Remotely.Desktop.Shared.Messages; + +public class AppStateHostChangedMessage +{ + public AppStateHostChangedMessage(string newHost) + { + NewHost = newHost; + } + + public string NewHost { get; } +} diff --git a/Desktop.Shared/Messages/DisplaySettingsChangedMessage.cs b/Desktop.Shared/Messages/DisplaySettingsChangedMessage.cs new file mode 100644 index 000000000..4be32a233 --- /dev/null +++ b/Desktop.Shared/Messages/DisplaySettingsChangedMessage.cs @@ -0,0 +1,2 @@ +namespace Remotely.Desktop.Shared.Messages; +public record DisplaySettingsChangedMessage(); diff --git a/Desktop.Shared/Messages/WindowsSessionEndingMessage.cs b/Desktop.Shared/Messages/WindowsSessionEndingMessage.cs new file mode 100644 index 000000000..893419a69 --- /dev/null +++ b/Desktop.Shared/Messages/WindowsSessionEndingMessage.cs @@ -0,0 +1,13 @@ +using Remotely.Shared.Enums; + +namespace Remotely.Desktop.Shared.Messages; + +public class WindowsSessionEndingMessage +{ + public WindowsSessionEndingMessage(SessionEndReasonsEx reason) + { + Reason = reason; + } + + public SessionEndReasonsEx Reason { get; } +} diff --git a/Desktop.Shared/Messages/WindowsSessionSwitchedMessage.cs b/Desktop.Shared/Messages/WindowsSessionSwitchedMessage.cs new file mode 100644 index 000000000..7fd6a6c4a --- /dev/null +++ b/Desktop.Shared/Messages/WindowsSessionSwitchedMessage.cs @@ -0,0 +1,15 @@ +using Remotely.Shared.Enums; + +namespace Remotely.Desktop.Shared.Messages; + +public class WindowsSessionSwitchedMessage +{ + public WindowsSessionSwitchedMessage(SessionSwitchReasonEx reason, int sessionId) + { + Reason = reason; + SessionId = sessionId; + } + + public SessionSwitchReasonEx Reason { get; } + public int SessionId { get; } +} diff --git a/Desktop.Shared/Properties/AssemblyInfo.cs b/Desktop.Shared/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..ddd26d78c --- /dev/null +++ b/Desktop.Shared/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Remotely_Desktop")] \ No newline at end of file diff --git a/Desktop.Shared/Reactive/AsyncRelayCommand.cs b/Desktop.Shared/Reactive/AsyncRelayCommand.cs new file mode 100644 index 000000000..ac57274c2 --- /dev/null +++ b/Desktop.Shared/Reactive/AsyncRelayCommand.cs @@ -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 Remotely.Desktop.Shared.Reactive; + +public class AsyncRelayCommand : ICommand +{ + private readonly Func _canExecute; + private readonly Func _execute; + public AsyncRelayCommand(Func execute) + { + _execute = execute; + _canExecute = () => true; + } + + public AsyncRelayCommand(Func execute, Func 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 : ICommand +{ + private readonly Func _canExecute; + private readonly Func _execute; + + public AsyncRelayCommand(Func execute) + { + _execute = execute; + _canExecute = (parameter) => true; + } + + public AsyncRelayCommand(Func execute, Func 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); + } +} diff --git a/Desktop.Shared/Reactive/ObservableObject.cs b/Desktop.Shared/Reactive/ObservableObject.cs new file mode 100644 index 000000000..58c721379 --- /dev/null +++ b/Desktop.Shared/Reactive/ObservableObject.cs @@ -0,0 +1,45 @@ +using System.Collections.Concurrent; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Remotely.Desktop.Shared.Reactive; + +public class ObservableObject : INotifyPropertyChanged +{ + private readonly ConcurrentDictionary _backingFields = new(); + + public event PropertyChangedEventHandler? PropertyChanged; + + public void NotifyPropertyChanged([CallerMemberName] string propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected T? Get([CallerMemberName] string propertyName = "") + { + if (_backingFields.TryGetValue(propertyName, out var value) && + value is T typedValue) + { + return typedValue; + } + + return default; + } + + protected T Get(T defaultValue, [CallerMemberName] string propertyName = "") + { + if (_backingFields.TryGetValue(propertyName, out var value) && + value is T typedValue) + { + return typedValue; + } + + return defaultValue; + } + + protected void Set(T newValue, [CallerMemberName] string propertyName = "") + { + _backingFields.AddOrUpdate(propertyName, newValue, (k, v) => newValue); + NotifyPropertyChanged(propertyName); + } +} diff --git a/Desktop.Shared/Reactive/RelayCommand.cs b/Desktop.Shared/Reactive/RelayCommand.cs new file mode 100644 index 000000000..c82a36b82 --- /dev/null +++ b/Desktop.Shared/Reactive/RelayCommand.cs @@ -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 Remotely.Desktop.Shared.Reactive; + +public class RelayCommand : ICommand +{ + private readonly Func _canExecute; + private readonly Action _execute; + public RelayCommand(Action execute) + { + _execute = execute; + _canExecute = () => true; + } + + public RelayCommand(Action execute, Func 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 : ICommand +{ + private readonly Func _canExecute; + private readonly Action _execute; + + public RelayCommand(Action execute) + { + _execute = execute; + _canExecute = (parameter) => true; + } + + public RelayCommand(Action execute, Func 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); + } +} diff --git a/Desktop.Shared/Services/AppState.cs b/Desktop.Shared/Services/AppState.cs new file mode 100644 index 000000000..522b29757 --- /dev/null +++ b/Desktop.Shared/Services/AppState.cs @@ -0,0 +1,188 @@ +using Remotely.Desktop.Shared.Enums; +using Remotely.Desktop.Shared.Messages; +using Remotely.Shared.Models; +using Microsoft.Extensions.Logging; +using Bitbound.SimpleMessenger; +using System.Collections.Concurrent; + +namespace Remotely.Desktop.Shared.Services; + +public interface IAppState +{ + event EventHandler ScreenCastRequested; + + event EventHandler ViewerAdded; + + event EventHandler ViewerRemoved; + string AccessKey { get; } + Dictionary ArgDict { get; } + string Host { get; set; } + bool IsElevate { get; } + bool IsRelaunch { get; } + AppMode Mode { get; set; } + string OrganizationId { get; set; } + string OrganizationName { get; } + string PipeName { get; } + string[] RelaunchViewers { get; } + string RequesterName { get; } + string SessionId { get; } + ConcurrentDictionary 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 _argDict = new(); + private readonly ILogger _logger; + private readonly IMessenger _messenger; + private string _host = string.Empty; + + private bool _isConfigured; + + public AppState(IMessenger messenger, ILogger logger) + { + _messenger = messenger; + _logger = logger; + } + + public event EventHandler? ScreenCastRequested; + + public event EventHandler? ViewerAdded; + + public event EventHandler? ViewerRemoved; + + public string AccessKey { get; private set; } = string.Empty; + + public Dictionary 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 OrganizationId { get; set; } = string.Empty; + public string OrganizationName { get; private set; } = string.Empty; + + public string PipeName { get; private set; } = string.Empty; + public string[] RelaunchViewers { get; private set; } = Array.Empty(); + public string RequesterName { get; private set; } = string.Empty; + public string SessionId { get; private set; } = string.Empty; + public ConcurrentDictionary 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."); + } + + } + } +} diff --git a/Desktop.Shared/Services/BrandingProvider.cs b/Desktop.Shared/Services/BrandingProvider.cs index 113cac48a..1d36a4d24 100644 --- a/Desktop.Shared/Services/BrandingProvider.cs +++ b/Desktop.Shared/Services/BrandingProvider.cs @@ -1,37 +1,41 @@ -using Immense.RemoteControl.Desktop.Shared.Abstractions; -using Immense.RemoteControl.Desktop.Shared.Services; -using Immense.RemoteControl.Shared; -using Immense.RemoteControl.Shared.Models; +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Services; +using Remotely.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; namespace Desktop.Shared.Services; +public interface IBrandingProvider +{ + BrandingInfo CurrentBranding { get; } + Task Initialize(); + void SetBrandingInfo(BrandingInfo brandingInfo); +} + public class BrandingProvider : IBrandingProvider { private readonly IAppState _appState; private readonly IEmbeddedServerDataProvider _embeddedDataSearcher; private readonly ILogger _logger; - private readonly IOrganizationIdProvider _orgIdProvider; - private BrandingInfoBase? _brandingInfo; + private BrandingInfo? _brandingInfo; public BrandingProvider( IAppState appState, - IOrganizationIdProvider orgIdProvider, IEmbeddedServerDataProvider embeddedServerDataSearcher, ILogger logger) { _appState = appState; - _orgIdProvider = orgIdProvider; _embeddedDataSearcher = embeddedServerDataSearcher; _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() @@ -56,9 +60,9 @@ public async Task Initialize() }; } - if (_brandingInfo.Icon?.Any() != true) + if (_brandingInfo.Icon is not { Length: > 0 }) { - using var mrs = typeof(BrandingProvider).Assembly.GetManifestResourceStream("Desktop.Shared.Assets.Remotely_Icon.png"); + using var mrs = typeof(BrandingProvider).Assembly.GetManifestResourceStream("Remotely.Desktop.Shared.Assets.Remotely_Icon.png"); using var ms = new MemoryStream(); mrs!.CopyTo(ms); @@ -66,7 +70,7 @@ public async Task Initialize() } } - public void SetBrandingInfo(BrandingInfoBase brandingInfo) + public void SetBrandingInfo(BrandingInfo brandingInfo) { _brandingInfo = brandingInfo; } @@ -75,7 +79,7 @@ private async Task> TryGetBrandingInfo() { try { - if (string.IsNullOrWhiteSpace(_orgIdProvider.OrganizationId) || + if (string.IsNullOrWhiteSpace(_appState.OrganizationId) || string.IsNullOrWhiteSpace(_appState.Host)) { var filePath = Process.GetCurrentProcess()?.MainModule?.FileName; @@ -87,21 +91,24 @@ private async Task> TryGetBrandingInfo() var result = _embeddedDataSearcher.TryGetEmbeddedData(filePath); - if (!result.IsSuccess) - { - return result.HadException ? - Result.Fail(result.Exception) : - Result.Fail(result.Reason); - } - - if (!string.IsNullOrWhiteSpace(result.Value.OrganizationId)) + if (result.IsSuccess) { - _orgIdProvider.OrganizationId = result.Value.OrganizationId; + if (!string.IsNullOrWhiteSpace(result.Value.OrganizationId)) + { + _appState.OrganizationId = result.Value.OrganizationId; + } + + if (result.Value.ServerUrl is not null) + { + _appState.Host = result.Value.ServerUrl.AbsoluteUri; + } } - if (result.Value.ServerUrl is not null) + if (string.IsNullOrWhiteSpace(_appState.Host)) { - _appState.Host = result.Value.ServerUrl.AbsoluteUri; + return result.HadException ? + Result.Fail(result.Exception) : + Result.Fail(result.Reason); } } @@ -112,7 +119,7 @@ private async Task> TryGetBrandingInfo() using var httpClient = new HttpClient(); - var brandingUrl = $"{_appState.Host.TrimEnd('/')}/api/branding/{_orgIdProvider.OrganizationId}"; + var brandingUrl = $"{_appState.Host.TrimEnd('/')}/api/branding/{_appState.OrganizationId}"; var httpResult = await httpClient.GetFromJsonAsync(brandingUrl).ConfigureAwait(false); if (httpResult is null) { diff --git a/Desktop.Shared/Services/ChatHostService.cs b/Desktop.Shared/Services/ChatHostService.cs new file mode 100644 index 000000000..d1780b3a6 --- /dev/null +++ b/Desktop.Shared/Services/ChatHostService.cs @@ -0,0 +1,88 @@ +using Remotely.Desktop.Shared.Abstractions; +using Microsoft.Extensions.Logging; +using Remotely.Shared.Models; +using System.IO.Pipes; +using System.Text.Json; + +namespace Remotely.Desktop.Shared.Services; + +public interface IChatHostService +{ + Task StartChat(string requesterID, string organizationName); +} +public class ChatHostService : IChatHostService +{ + private readonly IChatUiService _chatUiService; + private readonly ILogger _logger; + + private NamedPipeServerStream? _namedPipeStream; + private StreamReader? _reader; + private StreamWriter? _writer; + + public ChatHostService(IChatUiService chatUiService, ILogger 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(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."); + } + } + } +} diff --git a/Desktop.Shared/Services/DesktopEnvironment.cs b/Desktop.Shared/Services/DesktopEnvironment.cs new file mode 100644 index 000000000..ae7c796ca --- /dev/null +++ b/Desktop.Shared/Services/DesktopEnvironment.cs @@ -0,0 +1,43 @@ +using Remotely.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; + } + } +} diff --git a/Desktop.Shared/Services/DesktopHubConnection.cs b/Desktop.Shared/Services/DesktopHubConnection.cs new file mode 100644 index 000000000..1185e094d --- /dev/null +++ b/Desktop.Shared/Services/DesktopHubConnection.cs @@ -0,0 +1,461 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Messages; +using Remotely.Desktop.Shared.Native.Windows; +using Remotely.Shared.Enums; +using Remotely.Shared.Interfaces; +using Remotely.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 Remotely.Desktop.Shared.Services; + +public interface IDesktopHubConnection +{ + HubConnection? Connection { get; } + HubConnectionState ConnectionState { get; } + bool IsConnected { get; } + + Task> CheckRoundtripLatency(string viewerConnectionId); + Task Connect(TimeSpan timeout, CancellationToken cancellationToken); + Task Disconnect(); + Task DisconnectAllViewers(); + Task DisconnectViewer(IViewer viewer, bool notifyViewer); + Task GetSessionID(); + Task NotifyRequesterUnattendedReady(); + Task NotifyViewersRelaunchedScreenCasterReady(string[] viewerIDs); + Task SendAttendedSessionInfo(string machineName); + + Task SendConnectionFailedToViewers(List viewerIDs); + Task SendConnectionRequestDenied(string viewerID); + Task SendDtoToViewer(T dto, string viewerId); + + Task SendMessageToViewer(string viewerID, string message); + Task SendUnattendedSessionInfo(string sessionId, string accessKey, string machineName, string requesterName, string organizationName); +} + +public class DesktopHubConnection : IDesktopHubConnection, IDesktopHubClient +{ + private readonly IAppState _appState; + + private readonly ILogger _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 logger) + { + _messageHandler = messageHandler; + _remoteControlAccessService = remoteControlAccessService; + _serviceProvider = serviceProvider; + _appState = appState; + _logger = logger; + + messenger.Register(this, HandleWindowsSessionEnding); + messenger.Register(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> CheckRoundtripLatency(string viewerConnectionId) + { + try + { + if (Connection is null) + { + return Result.Fail("Connection is not yet established."); + } + var sw = Stopwatch.StartNew(); + var result = await Connection.InvokeAsync>("PingViewer", viewerConnectionId); + if (result.IsSuccess) + { + return Result.Ok(sw.Elapsed); + } + return Result.Fail("Latency check failed."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to check latency."); + return Result.Fail("An error occurred while checking latency."); + } + } + + public async Task 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(); + 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 GetSessionID() + { + if (Connection is null) + { + return string.Empty; + } + + return await Connection.InvokeAsync("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 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 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 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 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("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(nameof(Disconnect), Disconnect); + connection.On(nameof(GetScreenCast), GetScreenCast); + connection.On(nameof(RequestScreenCast), RequestScreenCast); + connection.On(nameof(SendDtoToClient), SendDtoToClient); + connection.On(nameof(ViewerDisconnected), ViewerDisconnected); + connection.On(nameof(PromptForAccess), PromptForAccess); + } + + private Result BuildConnection() + { + try + { + if (!Uri.TryCreate(_appState.Host, UriKind.Absolute, out _)) + { + return Result.Fail("Invalid server URI."); + } + + var builder = _serviceProvider.GetRequiredService(); + + 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(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); + } + } +} diff --git a/Desktop.Shared/Services/DtoMessageHandler.cs b/Desktop.Shared/Services/DtoMessageHandler.cs new file mode 100644 index 000000000..944c1f2e4 --- /dev/null +++ b/Desktop.Shared/Services/DtoMessageHandler.cs @@ -0,0 +1,331 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Enums; +using Remotely.Desktop.Shared.Native.Windows; +using Remotely.Shared.Helpers; +using Remotely.Shared.Models.Dtos; +using MessagePack; +using Microsoft.Extensions.Logging; + +namespace Remotely.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 _logger; + + + public DtoMessageHandler( + IKeyboardMouseInput keyboardMouseInput, + IAudioCapturer audioCapturer, + IClipboardService clipboardService, + IFileTransferService fileTransferService, + ILogger logger) + { + _keyboardMouseInput = keyboardMouseInput; + _audioCapturer = audioCapturer; + _clipboardService = clipboardService; + _fileTransferService = fileTransferService; + _logger = logger; + } + + public async Task ParseMessage(IViewer viewer, byte[] message) + { + try + { + var wrapper = MessagePackSerializer.Deserialize(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(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(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(wrapper, out var dto)) + { + return; + } + var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(dto.Timestamp); + viewer.SetLastFrameReceived(timestamp.ToLocalTime()); + } + private void KeyDown(DtoWrapper wrapper) + { + if (!DtoChunker.TryComplete(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(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(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(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(wrapper, out var dto)) + { + return; + } + + _keyboardMouseInput.SendMouseMove(dto!.PercentX, dto.PercentY, viewer); + } + + private void MouseUp(DtoWrapper wrapper, IViewer viewer) + { + if (!DtoChunker.TryComplete(wrapper, out var dto)) + { + return; + } + + _keyboardMouseInput.SendMouseButtonAction(dto!.Button, ButtonAction.Up, dto.PercentX, dto.PercentY, viewer); + } + + private void MouseWheel(DtoWrapper wrapper) + { + if (!DtoChunker.TryComplete(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(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(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(wrapper, out var dto)) + { + return; + } + + _audioCapturer.ToggleAudio(dto!.ToggleOn); + } + + private void ToggleBlockInput(DtoWrapper wrapper) + { + if (!DtoChunker.TryComplete(wrapper, out var dto)) + { + return; + } + _keyboardMouseInput.ToggleBlockInput(dto!.ToggleOn); + } +} diff --git a/Desktop.Shared/Services/IdleTimer.cs b/Desktop.Shared/Services/IdleTimer.cs new file mode 100644 index 000000000..b7b948ed8 --- /dev/null +++ b/Desktop.Shared/Services/IdleTimer.cs @@ -0,0 +1,96 @@ +using Remotely.Desktop.Shared.Abstractions; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; +using System.Timers; + +namespace Remotely.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 _logger; + private readonly SemaphoreSlim _elapseLock = new(1, 1); + private System.Timers.Timer? _timer; + + public IdleTimer( + IAppState appState, + IRemoteControlAccessService accessService, + IDesktopHubConnection desktopHubConnection, + IShutdownService shutdownService, + ILogger 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(); + } + } +} diff --git a/Desktop.Shared/Services/ImageHelper.cs b/Desktop.Shared/Services/ImageHelper.cs new file mode 100644 index 000000000..fe69dc7a5 --- /dev/null +++ b/Desktop.Shared/Services/ImageHelper.cs @@ -0,0 +1,212 @@ +using Remotely.Desktop.Shared.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.IO; +using Remotely.Shared.Primitives; +using SkiaSharp; + +namespace Remotely.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 GetImageDiff(SKBitmap currentFrame, SKBitmap? previousFrame, bool forceFullscreen = false); +} + +public class ImageHelper : IImageHelper +{ + private static readonly RecyclableMemoryStreamManager _recycleManager = new(); + private readonly ILogger _logger; + + public ImageHelper(ILogger 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 GetImageDiff(SKBitmap currentFrame, SKBitmap? previousFrame, bool forceFullscreen = false) + { + try + { + if (currentFrame is null) + { + return Result.Fail("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("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("No difference found."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while getting image diff."); + return Result.Fail(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; + } + } +} diff --git a/Desktop.Shared/Services/OrganizationIdProvider.cs b/Desktop.Shared/Services/OrganizationIdProvider.cs deleted file mode 100644 index 408824a11..000000000 --- a/Desktop.Shared/Services/OrganizationIdProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Desktop.Shared.Services; - -public interface IOrganizationIdProvider -{ - string OrganizationId { get; set; } -} -public class OrganizationIdProvider : IOrganizationIdProvider -{ - public string OrganizationId { get; set; } = string.Empty; -} diff --git a/Desktop.Shared/Services/ScreenCaster.cs b/Desktop.Shared/Services/ScreenCaster.cs new file mode 100644 index 000000000..ec9040c54 --- /dev/null +++ b/Desktop.Shared/Services/ScreenCaster.cs @@ -0,0 +1,283 @@ +using Microsoft.Extensions.DependencyInjection; +using SkiaSharp; +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Enums; +using Remotely.Shared.Models; +using Microsoft.Extensions.Logging; +using Remotely.Shared.Helpers; +using Remotely.Shared.Models.Dtos; +using MessagePack; +using Remotely.Shared.Services; +using Microsoft.IO; +using System.Diagnostics; +using Bitbound.SimpleMessenger; +using Remotely.Desktop.Shared.Messages; + +namespace Remotely.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 _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 logger) + { + _appState = appState; + _cursorIconWatcher = cursorIconWatcher; + _sessionIndicator = sessionIndicator; + _shutdownService = shutdownService; + _imageHelper = imageHelper; + _systemTime = systemTime; + _viewerFactory = viewerFactory; + _logger = logger; + + _messengerRegistrations = + [ + messenger.Register(this, HandleWindowsSessionSwitchedMessage), + messenger.Register(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 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); + } + } +} diff --git a/Desktop.Shared/Services/Viewer.cs b/Desktop.Shared/Services/Viewer.cs new file mode 100644 index 000000000..d16f00ed9 --- /dev/null +++ b/Desktop.Shared/Services/Viewer.cs @@ -0,0 +1,374 @@ +using System.Collections.Concurrent; +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Shared.Models; +using Microsoft.Extensions.Logging; +using Remotely.Shared.Helpers; +using Remotely.Shared.Models.Dtos; +using Remotely.Desktop.Shared.ViewModels; +using Microsoft.AspNetCore.SignalR.Client; +using Remotely.Shared.Services; +using Remotely.Desktop.Shared.Native.Windows; + +namespace Remotely.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 asyncEnumerable, Guid streamId); + Task SendFile(FileUpload fileUpload, Action progressUpdateCallback, CancellationToken cancelToken); + Task SendScreenData(string selectedDisplay, IEnumerable displayNames, int screenWidth, int screenHeight); + Task SendScreenSize(int width, int height); + Task SendSessionMetrics(SessionMetricsDto metrics); + Task SendWindowsSessions(); + void SetLastFrameReceived(DateTimeOffset timestamp); + Task 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 _fpsQueue = new(); + private readonly ILogger _logger; + private readonly ConcurrentQueue _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 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 stream, Guid streamId) + { + if (_desktopHubConnection.Connection is not null) + { + await _desktopHubConnection.Connection.SendAsync("SendDesktopStream", stream, streamId); + } + } + + public async Task SendFile( + FileUpload fileUpload, + Action 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 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 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 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); + } + } +} diff --git a/Desktop.Shared/Services/ViewerFactory.cs b/Desktop.Shared/Services/ViewerFactory.cs new file mode 100644 index 000000000..952f95318 --- /dev/null +++ b/Desktop.Shared/Services/ViewerFactory.cs @@ -0,0 +1,46 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.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 Remotely.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(); + var screenCapturer = _serviceProvider.GetRequiredService(); + var clipboardService = _serviceProvider.GetRequiredService(); + var audioCapturer = _serviceProvider.GetRequiredService(); + var systemTime = _serviceProvider.GetRequiredService(); + var logger = _serviceProvider.GetRequiredService>(); + + return new Viewer( + viewerName, + viewerConnectionId, + desktopHubConnection, + screenCapturer, + clipboardService, + audioCapturer, + systemTime, + logger); + } +} diff --git a/Desktop.Shared/Startup/CommandProvider.cs b/Desktop.Shared/Startup/CommandProvider.cs new file mode 100644 index 000000000..281051911 --- /dev/null +++ b/Desktop.Shared/Startup/CommandProvider.cs @@ -0,0 +1,99 @@ +using System.CommandLine; +using CommunityToolkit.Diagnostics; +using Remotely.Desktop.Shared.Enums; + +namespace Remotely.Desktop.Shared.Startup; +public static class CommandProvider +{ + /// + /// Creates a for starting the remote control client. + /// + /// Whether to create a or . + /// The description for the command. + /// The name used to invoke the command. Required if not a root command. + /// + 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( + new[] { "-h", "--host" }, + "The hostname of the server to which to connect (e.g. https://example.com)."); + rootCommand.AddOption(hostOption); + + var modeOption = new Option( + new[] { "-m", "--mode" }, + () => AppMode.Attended, + "The remote control mode to use. Either Attended, Unattended, or Chat."); + rootCommand.AddOption(modeOption); + + + var pipeNameOption = new Option( + 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())) + { + context.ErrorMessage = "A pipe name must be specified when AppMode is Chat."; + } + }); + rootCommand.AddOption(pipeNameOption); + + var sessionIdOption = new Option( + 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( + new[] { "-a", "--access-key" }, + "In Unattended mode, secures access to the connection using the provided key."); + rootCommand.AddOption(accessKeyOption); + + var requesterNameOption = new Option( + new[] { "-r", "--requester-name" }, + "The name of the technician requesting to connect."); + rootCommand.AddOption(requesterNameOption); + + var organizationNameOption = new Option( + new[] { "-o", "--org-name" }, + "The organization name of the technician requesting to connect."); + rootCommand.AddOption(organizationNameOption); + + var relaunchOption = new Option( + "--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( + "--viewers", + "Used with --relaunch. Should be a comma-separated list of viewers' " + + "SignalR connection IDs."); + rootCommand.AddOption(viewersOption); + + var elevateOption = new Option( + "--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; + } +} diff --git a/Desktop.Shared/Startup/IServiceCollectionExtensions.cs b/Desktop.Shared/Startup/IServiceCollectionExtensions.cs new file mode 100644 index 000000000..7d5f1fe81 --- /dev/null +++ b/Desktop.Shared/Startup/IServiceCollectionExtensions.cs @@ -0,0 +1,36 @@ +using Remotely.Desktop.Shared.Services; +using Remotely.Shared.Services; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Bitbound.SimpleMessenger; +using Desktop.Shared.Services; +using Remotely.Desktop.Shared.Abstractions; + +namespace Remotely.Desktop.Shared.Startup; + +public static class IServiceCollectionExtensions +{ + internal static void AddRemoteControlXplat( + this IServiceCollection services) + { + services.AddLogging(builder => + { + builder.AddConsole().AddDebug(); + }); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(s => WeakReferenceMessenger.Default); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(s => new HubConnectionBuilder()); + } +} diff --git a/Desktop.Shared/Startup/IServiceProviderExtensions.cs b/Desktop.Shared/Startup/IServiceProviderExtensions.cs new file mode 100644 index 000000000..de8e3ed37 --- /dev/null +++ b/Desktop.Shared/Startup/IServiceProviderExtensions.cs @@ -0,0 +1,163 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Enums; +using Remotely.Desktop.Shared.Native.Windows; +using Remotely.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 Remotely.Desktop.Shared.Startup; + +public static class IServiceProviderExtensions +{ + /// + /// Runs the remote control startup with the specified arguments. + /// + public static async Task 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>(); + TaskScheduler.UnobservedTaskException += (object? sender, UnobservedTaskExceptionEventArgs e) => + { + HandleUnobservedTask(e, logger); + }; + + if (OperatingSystem.IsWindows() && elevate) + { + RelaunchElevated(); + return Result.Ok(); + } + + var appState = services.GetRequiredService(); + 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(); + await appStartup.Run(); + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(ex); + } + + } + + /// + /// Runs the remote control startup as a root command. This uses the System.CommandLine package. + /// + /// The service provider fo rthe app using this library. + /// The original command line arguments passed into the app. + /// The description to use for the remote control command. + /// If provided, will be used as a fallback if --host option is missing. + /// + public static async Task 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 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); + } +} diff --git a/Desktop.Shared/StaticServiceProvider.cs b/Desktop.Shared/StaticServiceProvider.cs new file mode 100644 index 000000000..8b7be2efa --- /dev/null +++ b/Desktop.Shared/StaticServiceProvider.cs @@ -0,0 +1,6 @@ +namespace Remotely.Desktop.Shared; + +public static class StaticServiceProvider +{ + public static IServiceProvider? Instance { get; set; } +} diff --git a/Desktop.Shared/ViewModels/FileUpload.cs b/Desktop.Shared/ViewModels/FileUpload.cs new file mode 100644 index 000000000..0ab05fbd1 --- /dev/null +++ b/Desktop.Shared/ViewModels/FileUpload.cs @@ -0,0 +1,23 @@ +using Remotely.Desktop.Shared.Reactive; + +namespace Remotely.Desktop.Shared.ViewModels; + +public partial class FileUpload : ObservableObject +{ + public string FilePath + { + get => Get(defaultValue: string.Empty); + set => Set(value); + } + + + public double PercentProgress + { + get => Get(); + set => Set(value); + } + + public CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource(); + + public string DisplayName => Path.GetFileName(FilePath); +} diff --git a/Desktop.UI/App.axaml b/Desktop.UI/App.axaml new file mode 100644 index 000000000..0f69af773 --- /dev/null +++ b/Desktop.UI/App.axaml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/Desktop.UI/App.axaml.cs b/Desktop.UI/App.axaml.cs new file mode 100644 index 000000000..f11a65ff3 --- /dev/null +++ b/Desktop.UI/App.axaml.cs @@ -0,0 +1,32 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; + +namespace Remotely.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(); + } +} diff --git a/Desktop.UI/Assets/DefaultIcon.ico b/Desktop.UI/Assets/DefaultIcon.ico new file mode 100644 index 000000000..55aaa8ca3 Binary files /dev/null and b/Desktop.UI/Assets/DefaultIcon.ico differ diff --git a/Desktop.UI/Assets/DefaultIcon.png b/Desktop.UI/Assets/DefaultIcon.png new file mode 100644 index 000000000..987309020 Binary files /dev/null and b/Desktop.UI/Assets/DefaultIcon.png differ diff --git a/Desktop.UI/Assets/Gear.png b/Desktop.UI/Assets/Gear.png new file mode 100644 index 000000000..bb38d5969 Binary files /dev/null and b/Desktop.UI/Assets/Gear.png differ diff --git a/Desktop.UI/Assets/avalonia-logo.ico b/Desktop.UI/Assets/avalonia-logo.ico new file mode 100644 index 000000000..da8d49ff9 Binary files /dev/null and b/Desktop.UI/Assets/avalonia-logo.ico differ diff --git a/Desktop.UI/Controls/Dialogs/MessageBox.axaml b/Desktop.UI/Controls/Dialogs/MessageBox.axaml new file mode 100644 index 000000000..2606d6a13 --- /dev/null +++ b/Desktop.UI/Controls/Dialogs/MessageBox.axaml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Desktop.UI/Controls/Dialogs/MessageBox.axaml.cs b/Desktop.UI/Controls/Dialogs/MessageBox.axaml.cs new file mode 100644 index 000000000..9c4fab839 --- /dev/null +++ b/Desktop.UI/Controls/Dialogs/MessageBox.axaml.cs @@ -0,0 +1,51 @@ +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using CommunityToolkit.Diagnostics; +using Remotely.Desktop.Shared; +using Microsoft.Extensions.DependencyInjection; +using System.Threading; + +namespace Remotely.Desktop.UI.Controls.Dialogs; + +public partial class MessageBox : Window +{ + public MessageBox() + { + InitializeComponent(); + } + + public static async Task Show(string message, string caption, MessageBoxType type) + { + Guard.IsNotNull(StaticServiceProvider.Instance, nameof(StaticServiceProvider.Instance)); + + var dispatcher = StaticServiceProvider.Instance.GetRequiredService(); + + return await dispatcher.InvokeAsync(async () => + { + var viewModel = StaticServiceProvider.Instance.GetRequiredService(); + 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; + }); + } +} \ No newline at end of file diff --git a/Desktop.UI/Controls/Dialogs/MessageBoxResult.cs b/Desktop.UI/Controls/Dialogs/MessageBoxResult.cs new file mode 100644 index 000000000..6b582390a --- /dev/null +++ b/Desktop.UI/Controls/Dialogs/MessageBoxResult.cs @@ -0,0 +1,9 @@ +namespace Remotely.Desktop.UI.Controls.Dialogs; + +public enum MessageBoxResult +{ + Cancel, + OK, + Yes, + No +} diff --git a/Desktop.UI/Controls/Dialogs/MessageBoxType.cs b/Desktop.UI/Controls/Dialogs/MessageBoxType.cs new file mode 100644 index 000000000..80a4f0bac --- /dev/null +++ b/Desktop.UI/Controls/Dialogs/MessageBoxType.cs @@ -0,0 +1,7 @@ +namespace Remotely.Desktop.UI.Controls.Dialogs; + +public enum MessageBoxType +{ + OK, + YesNo +} diff --git a/Desktop.UI/Desktop.UI.csproj b/Desktop.UI/Desktop.UI.csproj new file mode 100644 index 000000000..fe86b4a4a --- /dev/null +++ b/Desktop.UI/Desktop.UI.csproj @@ -0,0 +1,51 @@ + + + + net8.0 + enable + enable + + copyused + true + true + true + false + Remotely.Desktop.UI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Desktop.UI/GlobalUsings.cs b/Desktop.UI/GlobalUsings.cs new file mode 100644 index 000000000..fb2edcb29 --- /dev/null +++ b/Desktop.UI/GlobalUsings.cs @@ -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 Remotely.Desktop.UI.Services; +global using Remotely.Desktop.UI.ViewModels; +global using Remotely.Desktop.UI.Views; \ No newline at end of file diff --git a/Desktop.UI/Services/ChatUiService.cs b/Desktop.UI/Services/ChatUiService.cs new file mode 100644 index 000000000..2c6a0fe0b --- /dev/null +++ b/Desktop.UI/Services/ChatUiService.cs @@ -0,0 +1,67 @@ +using Avalonia.Controls; +using Remotely.Desktop.Shared.Abstractions; +using System.ComponentModel; +using Remotely.Desktop.UI.Controls.Dialogs; +using CommunityToolkit.Diagnostics; +using Remotely.Shared.Models; + +namespace Remotely.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); + } +} diff --git a/Desktop.UI/Services/ClipboardService.cs b/Desktop.UI/Services/ClipboardService.cs new file mode 100644 index 000000000..6d07edf80 --- /dev/null +++ b/Desktop.UI/Services/ClipboardService.cs @@ -0,0 +1,88 @@ +using Remotely.Desktop.Shared.Abstractions; +using Microsoft.Extensions.Logging; +using System.Threading; + +namespace Remotely.Desktop.UI.Services; + +public class ClipboardService : IClipboardService +{ + private readonly IUiDispatcher _dispatcher; + private readonly ILogger _logger; + private Task? _watcherTask; + + public event EventHandler? ClipboardTextChanged; + + public ClipboardService( + IUiDispatcher dispatcher, + ILogger 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); + } + } + } +} diff --git a/Desktop.UI/Services/DialogProvider.cs b/Desktop.UI/Services/DialogProvider.cs new file mode 100644 index 000000000..cbc01d199 --- /dev/null +++ b/Desktop.UI/Services/DialogProvider.cs @@ -0,0 +1,22 @@ +using Remotely.Desktop.UI.Controls.Dialogs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Remotely.Desktop.UI.Services; + +public interface IDialogProvider +{ + Task Show(string message, string caption, MessageBoxType type); +} + +internal class DialogProvider : IDialogProvider +{ + public async Task Show(string message, string caption, MessageBoxType type) + { + return await MessageBox.Show(message, caption, type); + } +} diff --git a/Desktop.UI/Services/RemoteControlAccessService.cs b/Desktop.UI/Services/RemoteControlAccessService.cs new file mode 100644 index 000000000..167d25b4a --- /dev/null +++ b/Desktop.UI/Services/RemoteControlAccessService.cs @@ -0,0 +1,62 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Shared.Enums; +using Microsoft.Extensions.Logging; +using System.Threading; + +namespace Remotely.Desktop.UI.Services; + +public class RemoteControlAccessService : IRemoteControlAccessService +{ + private readonly IViewModelFactory _viewModelFactory; + private readonly IUiDispatcher _dispatcher; + private readonly ILogger _logger; + private volatile int _promptCount = 0; + + public RemoteControlAccessService( + IViewModelFactory viewModelFactory, + IUiDispatcher dispatcher, + ILogger logger) + { + _viewModelFactory = viewModelFactory; + _dispatcher = dispatcher; + _logger = logger; + } + + public bool IsPromptOpen => _promptCount > 0; + + public async Task 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); + } + }); + } +} diff --git a/Desktop.UI/Services/SessionIndicator.cs b/Desktop.UI/Services/SessionIndicator.cs new file mode 100644 index 000000000..42d35aa2b --- /dev/null +++ b/Desktop.UI/Services/SessionIndicator.cs @@ -0,0 +1,26 @@ +using Remotely.Desktop.Shared; +using Remotely.Desktop.Shared.Abstractions; +using Microsoft.Extensions.DependencyInjection; + +namespace Remotely.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() + }; + _dispatcher.ShowMainWindow(indicatorWindow); + }); + } +} diff --git a/Desktop.UI/Services/UiDispatcher.cs b/Desktop.UI/Services/UiDispatcher.cs new file mode 100644 index 000000000..35b30834a --- /dev/null +++ b/Desktop.UI/Services/UiDispatcher.cs @@ -0,0 +1,255 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Input.Platform; +using Avalonia.Threading; +using Remotely.Shared.Helpers; +using Microsoft.Extensions.Logging; +using Remotely.Shared.Primitives; +using System.Threading; + +namespace Remotely.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 func, DispatcherPriority priority = default); + Task InvokeAsync(Func> func, DispatcherPriority priority = default); + void Post(Action action, DispatcherPriority priority = default); + Task Show(Window window, TimeSpan timeout); + + Task ShowDialog(Window window); + void ShowMainWindow(Window window); + + void ShowWindow(Window window); + void Shutdown(); + void StartClassicDesktop(); + Task StartHeadless(); +} + +internal class UiDispatcher : IUiDispatcher +{ + private static readonly CancellationTokenSource _appCts = new(); + private static Application? _currentApp; + private readonly ILogger _logger; + private AppBuilder? _appBuilder; + private Window? _headlessMainWindow; + + public UiDispatcher(ILogger 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 func, DispatcherPriority priority = default) + { + + return Dispatcher.UIThread.InvokeAsync(func, priority); + } + + public Task InvokeAsync(Func> 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 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 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() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); + + private static void RunHeadless(Application app, string[] args) + { + _currentApp = app; + app.Run(_appCts.Token); + } +} diff --git a/Desktop.UI/Services/ViewModelFactory.cs b/Desktop.UI/Services/ViewModelFactory.cs new file mode 100644 index 000000000..be9157613 --- /dev/null +++ b/Desktop.UI/Services/ViewModelFactory.cs @@ -0,0 +1,61 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.IO; +using Desktop.Shared.Services; + +namespace Remotely.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(); + var dispatcher = _serviceProvider.GetRequiredService(); + var logger = _serviceProvider.GetRequiredService>(); + return new ChatWindowViewModel(streamWriter, organizationName, branding, dispatcher, logger); + } + + public IFileTransferWindowViewModel CreateFileTransferWindowViewModel(IViewer viewer) + { + var brandingProvider = _serviceProvider.GetRequiredService(); + var dispatcher = _serviceProvider.GetRequiredService(); + var logger = _serviceProvider.GetRequiredService>(); + var fileTransfer = _serviceProvider.GetRequiredService(); + return new FileTransferWindowViewModel(viewer, brandingProvider, dispatcher, fileTransfer, logger); + } + + public IPromptForAccessWindowViewModel CreatePromptForAccessViewModel(string requesterName, string organizationName) + { + var brandingProvider = _serviceProvider.GetRequiredService(); + var dispatcher = _serviceProvider.GetRequiredService(); + var logger = _serviceProvider.GetRequiredService>(); + return new PromptForAccessWindowViewModel(requesterName, organizationName, brandingProvider, dispatcher, logger); + } + + public IHostNamePromptViewModel CreateHostNamePromptViewModel() + { + var brandingProvider = _serviceProvider.GetRequiredService(); + var dispatcher = _serviceProvider.GetRequiredService(); + var logger = _serviceProvider.GetRequiredService>(); + return new HostNamePromptViewModel(brandingProvider, dispatcher, logger); + } +} diff --git a/Desktop.UI/Startup/IServiceCollectionExtensions.cs b/Desktop.UI/Startup/IServiceCollectionExtensions.cs new file mode 100644 index 000000000..a227db4bf --- /dev/null +++ b/Desktop.UI/Startup/IServiceCollectionExtensions.cs @@ -0,0 +1,23 @@ +using Remotely.Desktop.Shared.Abstractions; +using Microsoft.Extensions.DependencyInjection; + +namespace Remotely.Desktop.UI.Startup; + +public static class IServiceCollectionExtensions +{ + public static void AddRemoteControlUi( + this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddSingleton(); + } +} diff --git a/Desktop.UI/ViewModels/BrandedViewModelBase.cs b/Desktop.UI/ViewModels/BrandedViewModelBase.cs new file mode 100644 index 000000000..927d4f882 --- /dev/null +++ b/Desktop.UI/ViewModels/BrandedViewModelBase.cs @@ -0,0 +1,93 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Reactive; +using Remotely.Shared.Models; +using Microsoft.Extensions.Logging; +using Remotely.Shared.Entities; +using System.IO; +using System.Reflection; +using Desktop.Shared.Services; + +namespace Remotely.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 _logger; + protected readonly IUiDispatcher _dispatcher; + private readonly IBrandingProvider _brandingProvider; + + + public BrandedViewModelBase( + IBrandingProvider brandingProvider, + IUiDispatcher dispatcher, + ILogger logger) + { + _brandingProvider = brandingProvider; + _dispatcher = dispatcher; + _logger = logger; + + ApplyBrandingImpl(); + } + + public Bitmap? Icon + { + get => Get(); + set => Set(value); + } + + public string ProductName + { + get => Get() ?? "Remote Control"; + set => Set(value ?? "Remote Control"); + } + + public WindowIcon? WindowIcon + { + get => Get(); + set => Set(value); + } + + private void ApplyBrandingImpl() + { + _dispatcher.Invoke(() => + { + try + { + _brandingInfo ??= _brandingProvider.CurrentBranding; + + ProductName = _brandingInfo.Product; + + if (_brandingInfo.Icon is { Length: > 0 }) + { + using var imageStream = new MemoryStream(_brandingInfo.Icon); + Icon = new Bitmap(imageStream); + } + else + { + using var imageStream = + Assembly + .GetExecutingAssembly() + .GetManifestResourceStream("Remotely.Desktop.Shared.Assets.DefaultIcon.png") ?? new MemoryStream(); + + Icon = new Bitmap(imageStream); + } + + WindowIcon = new WindowIcon(Icon); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error applying branding."); + } + }); + } +} diff --git a/Desktop.UI/ViewModels/ChatWindowViewModel.cs b/Desktop.UI/ViewModels/ChatWindowViewModel.cs new file mode 100644 index 000000000..04cdf4cba --- /dev/null +++ b/Desktop.UI/ViewModels/ChatWindowViewModel.cs @@ -0,0 +1,116 @@ +using Avalonia.Controls; +using System.Collections.ObjectModel; +using System.IO; +using System.Text.Json; +using System.Windows.Input; +using Remotely.Desktop.Shared.Abstractions; +using Microsoft.Extensions.Logging; +using Remotely.Desktop.Shared.Reactive; +using Microsoft.Extensions.DependencyInjection; +using Remotely.Shared.Models; +using Desktop.Shared.Services; + +namespace Remotely.Desktop.UI.ViewModels; + +public interface IChatWindowViewModel : IBrandedViewModelBase +{ + ObservableCollection 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 logger) + : base(brandingProvider, dispatcher, logger) + { + _streamWriter = streamWriter; + if (!string.IsNullOrWhiteSpace(organizationName)) + { + OrganizationName = organizationName; + } + CloseCommand = new RelayCommand(CloseWindow); + MinimizeCommand = new RelayCommand(MinimizeWindow); + } + + + public ObservableCollection ChatMessages { get; } = new ObservableCollection(); + + public string ChatSessionHeader => $"Chat session with {OrganizationName}"; + + public ICommand CloseCommand { get; } + + public string InputText + { + get => Get() ?? string.Empty; + set => Set(value); + } + + public ICommand MinimizeCommand { get; } + + public string OrganizationName + { + get => Get() ?? "your IT provider"; + set + { + Set(value); + NotifyPropertyChanged(nameof(ChatSessionHeader)); + } + } + + public string SenderName + { + get => Get() ?? "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; + } + } +} diff --git a/Desktop.UI/ViewModels/Fakes/FakeBrandedViewModelBase.cs b/Desktop.UI/ViewModels/Fakes/FakeBrandedViewModelBase.cs new file mode 100644 index 000000000..ece7a67b8 --- /dev/null +++ b/Desktop.UI/ViewModels/Fakes/FakeBrandedViewModelBase.cs @@ -0,0 +1,51 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Remotely.Desktop.UI.Controls.Dialogs; +using Remotely.Shared.Models; +using Remotely.Shared.Entities; +using System.Diagnostics; +using System.IO; + +namespace Remotely.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("Remotely.Desktop.Shared.Assets.DefaultIcon.png") ?? new MemoryStream(); + + return new Bitmap(imageStream); + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + return null; + } + } +} diff --git a/Desktop.UI/ViewModels/Fakes/FakeChatWindowViewModel.cs b/Desktop.UI/ViewModels/Fakes/FakeChatWindowViewModel.cs new file mode 100644 index 000000000..35f934946 --- /dev/null +++ b/Desktop.UI/ViewModels/Fakes/FakeChatWindowViewModel.cs @@ -0,0 +1,41 @@ +using Remotely.Desktop.Shared.Reactive; +using Remotely.Shared.Models; +using System.Collections.ObjectModel; +using System.Windows.Input; + +namespace Remotely.Desktop.UI.ViewModels.Fakes; + +public class FakeChatWindowViewModel : FakeBrandedViewModelBase, IChatWindowViewModel +{ + public ObservableCollection 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; + } +} diff --git a/Desktop.UI/ViewModels/Fakes/FakeFileTransferViewModel.cs b/Desktop.UI/ViewModels/Fakes/FakeFileTransferViewModel.cs new file mode 100644 index 000000000..e9951a97e --- /dev/null +++ b/Desktop.UI/ViewModels/Fakes/FakeFileTransferViewModel.cs @@ -0,0 +1,33 @@ +using Remotely.Desktop.Shared.Reactive; +using Remotely.Desktop.Shared.ViewModels; +using System.Collections.ObjectModel; +using System.Windows.Input; + +namespace Remotely.Desktop.UI.ViewModels.Fakes; + +public class FakeFileTransferViewModel : FakeBrandedViewModelBase, IFileTransferWindowViewModel +{ + public ObservableCollection 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; + } +} diff --git a/Desktop.UI/ViewModels/Fakes/FakeHostNamePromptViewModel.cs b/Desktop.UI/ViewModels/Fakes/FakeHostNamePromptViewModel.cs new file mode 100644 index 000000000..8917174ac --- /dev/null +++ b/Desktop.UI/ViewModels/Fakes/FakeHostNamePromptViewModel.cs @@ -0,0 +1,11 @@ +using Remotely.Desktop.Shared.Reactive; +using System.Windows.Input; + +namespace Remotely.Desktop.UI.ViewModels.Fakes; + +public class FakeHostNamePromptViewModel : FakeBrandedViewModelBase, IHostNamePromptViewModel +{ + public string Host { get; set; } = "https://localhost:7024"; + + public ICommand OKCommand => new RelayCommand(() => { }); +} diff --git a/Desktop.UI/ViewModels/Fakes/FakeMainViewViewModel.cs b/Desktop.UI/ViewModels/Fakes/FakeMainViewViewModel.cs new file mode 100644 index 000000000..7cd5ef4d5 --- /dev/null +++ b/Desktop.UI/ViewModels/Fakes/FakeMainViewViewModel.cs @@ -0,0 +1,169 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Reactive; +using Remotely.Desktop.Shared.Services; +using Remotely.Desktop.Shared.ViewModels; +using Remotely.Shared.Models; +using Remotely.Shared.Models.Dtos; +using System.Collections.ObjectModel; +using System.Threading; +using System.Windows.Input; + +namespace Remotely.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 Viewers { get; } = new() { new FakeViewer() }; + + public IList SelectedViewers { get; } = new List(); + + 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 asyncEnumerable, Guid streamId) + { + return Task.CompletedTask; + } + + public Task SendFile(FileUpload fileUpload, Action progressUpdateCallback, CancellationToken cancelToken) + { + return Task.CompletedTask; + } + + public Task SendScreenData(string selectedDisplay, IEnumerable 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 WaitForViewer() + { + return Task.FromResult(true); + } + } +} diff --git a/Desktop.UI/ViewModels/Fakes/FakeMainWindowViewModel.cs b/Desktop.UI/ViewModels/Fakes/FakeMainWindowViewModel.cs new file mode 100644 index 000000000..9b040d1bf --- /dev/null +++ b/Desktop.UI/ViewModels/Fakes/FakeMainWindowViewModel.cs @@ -0,0 +1,5 @@ +namespace Remotely.Desktop.UI.ViewModels.Fakes; + +public class FakeMainWindowViewModel : FakeBrandedViewModelBase, IMainWindowViewModel +{ +} diff --git a/Desktop.UI/ViewModels/Fakes/FakeMessageBoxViewModel.cs b/Desktop.UI/ViewModels/Fakes/FakeMessageBoxViewModel.cs new file mode 100644 index 000000000..013479435 --- /dev/null +++ b/Desktop.UI/ViewModels/Fakes/FakeMessageBoxViewModel.cs @@ -0,0 +1,21 @@ +using Remotely.Desktop.Shared.Reactive; +using System.Windows.Input; +using Remotely.Desktop.UI.Controls.Dialogs; + +namespace Remotely.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(() => { }); +} diff --git a/Desktop.UI/ViewModels/Fakes/FakePromptForAccessViewModel.cs b/Desktop.UI/ViewModels/Fakes/FakePromptForAccessViewModel.cs new file mode 100644 index 000000000..81cb758d3 --- /dev/null +++ b/Desktop.UI/ViewModels/Fakes/FakePromptForAccessViewModel.cs @@ -0,0 +1,23 @@ +using Remotely.Desktop.Shared.Reactive; +using System.Windows.Input; + +namespace Remotely.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(() => { }); +} diff --git a/Desktop.UI/ViewModels/Fakes/FakeSessionIndicatorWindowViewModel.cs b/Desktop.UI/ViewModels/Fakes/FakeSessionIndicatorWindowViewModel.cs new file mode 100644 index 000000000..14aac159a --- /dev/null +++ b/Desktop.UI/ViewModels/Fakes/FakeSessionIndicatorWindowViewModel.cs @@ -0,0 +1,8 @@ +namespace Remotely.Desktop.UI.ViewModels.Fakes; +internal class FakeSessionIndicatorWindowViewModel : FakeBrandedViewModelBase, ISessionIndicatorWindowViewModel +{ + public Task PromptForExit() + { + return Task.CompletedTask; + } +} diff --git a/Desktop.UI/ViewModels/FileTransferWindowViewModel.cs b/Desktop.UI/ViewModels/FileTransferWindowViewModel.cs new file mode 100644 index 000000000..98b1a4556 --- /dev/null +++ b/Desktop.UI/ViewModels/FileTransferWindowViewModel.cs @@ -0,0 +1,131 @@ +using Avalonia.Threading; +using Remotely.Desktop.Shared.Services; +using Remotely.Desktop.Shared.ViewModels; +using System.Collections.ObjectModel; +using System.IO; +using System.Windows.Input; +using Remotely.Desktop.Shared.Abstractions; +using Microsoft.Extensions.Logging; +using Remotely.Desktop.Shared.Reactive; +using Desktop.Shared.Services; + +namespace Remotely.Desktop.UI.ViewModels; + +public interface IFileTransferWindowViewModel : IBrandedViewModelBase +{ + ObservableCollection FileUploads { get; } + ICommand OpenFileUploadDialogCommand { get; } + ICommand RemoveFileUploadCommand { get; } + string ViewerConnectionId { get; set; } + string ViewerName { get; set; } + + void RemoveFileUpload(FileUpload? fileUpload); + Task UploadFile(string filePath); +} + +public class FileTransferWindowViewModel : BrandedViewModelBase, IFileTransferWindowViewModel +{ + private readonly IFileTransferService _fileTransferService; + private readonly IViewer _viewer; + + public FileTransferWindowViewModel( + IViewer viewer, + IBrandingProvider brandingProvider, + IUiDispatcher dispatcher, + IFileTransferService fileTransferService, + ILogger logger) + : base(brandingProvider, dispatcher, logger) + { + _viewer = viewer; + _fileTransferService = fileTransferService; + ViewerName = viewer.Name; + ViewerConnectionId = viewer.ViewerConnectionId; + + OpenFileUploadDialogCommand = new AsyncRelayCommand(OpenFileUploadDialog); + RemoveFileUploadCommand = new RelayCommand(RemoveFileUpload); + } + + public ObservableCollection FileUploads { get; } = new ObservableCollection(); + + public ICommand OpenFileUploadDialogCommand { get; } + + public ICommand RemoveFileUploadCommand { get; } + + public string ViewerConnectionId + { + get => Get() ?? string.Empty; + set => Set(value); + } + + public string ViewerName + { + get => Get() ?? string.Empty; + set => Set(value); + } + + public void RemoveFileUpload(FileUpload? fileUpload) + { + if (fileUpload is null) + { + return; + } + FileUploads.Remove(fileUpload); + fileUpload.CancellationTokenSource.Cancel(); + } + + public async Task UploadFile(string filePath) + { + var fileUpload = new FileUpload() + { + FilePath = filePath + }; + + await Dispatcher.UIThread.InvokeAsync(() => + { + FileUploads.Add(fileUpload); + }); + + await _fileTransferService.UploadFile( + fileUpload, + _viewer, + async progress => + { + await Dispatcher.UIThread.InvokeAsync(() => + { + fileUpload.PercentProgress = progress; + }); + }, + fileUpload.CancellationTokenSource.Token); + } + + private async Task OpenFileUploadDialog(FileTransferWindow? window) + { + if (window is null) + { + return; + } + + var initialDir = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); + if (!Directory.Exists(initialDir)) + { + initialDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "RemoteControl")).FullName; + } + + var startFolder = await window.StorageProvider.TryGetFolderFromPathAsync(new Uri(initialDir)); + var result = await window.StorageProvider.OpenFilePickerAsync(new() + { + Title = "Upload File via Remotely", + AllowMultiple = true, + SuggestedStartLocation = startFolder + }); + + if (result?.Any() != true) + { + return; + } + foreach (var file in result) + { + await UploadFile($"{file.Path.LocalPath}"); + } + } +} diff --git a/Desktop.UI/ViewModels/HostNamePromptViewModel.cs b/Desktop.UI/ViewModels/HostNamePromptViewModel.cs new file mode 100644 index 000000000..eefdeab96 --- /dev/null +++ b/Desktop.UI/ViewModels/HostNamePromptViewModel.cs @@ -0,0 +1,34 @@ +using Avalonia.Controls; +using System.Windows.Input; +using Remotely.Desktop.Shared.Abstractions; +using Microsoft.Extensions.Logging; +using Remotely.Desktop.Shared.Reactive; +using Desktop.Shared.Services; + +namespace Remotely.Desktop.UI.ViewModels; + +public interface IHostNamePromptViewModel : IBrandedViewModelBase +{ + string Host { get; set; } + ICommand OKCommand { get; } +} + +public class HostNamePromptViewModel : BrandedViewModelBase, IHostNamePromptViewModel +{ + public HostNamePromptViewModel( + IBrandingProvider brandingProvider, + IUiDispatcher dispatcher, + ILogger logger) + : base(brandingProvider, dispatcher, logger) + { + OKCommand = new RelayCommand(x => x?.Close()); + } + + public string Host + { + get => Get() ?? "https://"; + set => Set(value); + } + + public ICommand OKCommand { get; } +} diff --git a/Desktop.UI/ViewModels/MainViewViewModel.cs b/Desktop.UI/ViewModels/MainViewViewModel.cs new file mode 100644 index 000000000..4e2cbf866 --- /dev/null +++ b/Desktop.UI/ViewModels/MainViewViewModel.cs @@ -0,0 +1,350 @@ +using Avalonia.Controls; +using Avalonia.Threading; +using Remotely.Desktop.Shared.Services; +using Remotely.Shared.Models; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Windows.Input; +using Remotely.Desktop.Shared.Abstractions; +using Microsoft.Extensions.Logging; +using Remotely.Desktop.Shared.Reactive; +using Microsoft.Extensions.DependencyInjection; +using Remotely.Desktop.UI.Controls.Dialogs; +using Remotely.Desktop.Shared.Native.Linux; +using Desktop.Shared.Services; + +namespace Remotely.Desktop.UI.ViewModels; + +public interface IMainViewViewModel : IBrandedViewModelBase +{ + AsyncRelayCommand ChangeServerCommand { get; } + AsyncRelayCommand CopyLinkCommand { get; } + double CopyMessageOpacity { get; set; } + string Host { get; set; } + bool IsCopyMessageVisible { get; set; } + ICommand OpenOptionsMenu { get; } + AsyncRelayCommand RemoveViewersCommand { get; } + IList SelectedViewers { get; } + string StatusMessage { get; set; } + ObservableCollection Viewers { get; } + Task ChangeServer(); + Task CopyLink(); + Task GetSessionID(); + Task Init(); + Task PromptForHostName(); + Task RemoveViewers(); +} + +public class MainViewViewModel : BrandedViewModelBase, IMainViewViewModel +{ + private readonly IAppState _appState; + private readonly IDesktopEnvironment _environment; + private readonly IDialogProvider _dialogProvider; + private readonly IDesktopHubConnection _hubConnection; + private readonly IServiceProvider _serviceProvider; + private readonly IViewModelFactory _viewModelFactory; + private IList _selectedViewers = new List(); + + public MainViewViewModel( + IBrandingProvider brandingProvider, + IUiDispatcher dispatcher, + IAppState appState, + IDesktopHubConnection hubConnection, + IServiceProvider serviceProvider, + IViewModelFactory viewModelFactory, + IDesktopEnvironment environmentHelper, + IDialogProvider dialogProvider, + ILogger logger) + : base(brandingProvider, dispatcher, logger) + { + _appState = appState; + _hubConnection = hubConnection; + _serviceProvider = serviceProvider; + _viewModelFactory = viewModelFactory; + _environment = environmentHelper; + _dialogProvider = dialogProvider; + + _appState.ViewerRemoved += ViewerRemoved; + _appState.ViewerAdded += ViewerAdded; + _appState.ScreenCastRequested += ScreenCastRequested; + + Host = appState.Host; + ChangeServerCommand = new AsyncRelayCommand(ChangeServer); + CopyLinkCommand = new AsyncRelayCommand(CopyLink); + RemoveViewersCommand = new AsyncRelayCommand(RemoveViewers, CanRemoveViewers); + } + + public AsyncRelayCommand ChangeServerCommand { get; } + + public AsyncRelayCommand CopyLinkCommand { get; } + + public double CopyMessageOpacity + { + get => Get(); + set => Set(value); + } + + public string Host + { + get => Get() ?? string.Empty; + set => Set(value); + } + + public bool IsCopyMessageVisible + { + get => Get(); + set => Set(value); + } + + public ICommand OpenOptionsMenu { get; } = new RelayCommand + + + diff --git a/Desktop.UI/Views/FileTransferWindow.axaml.cs b/Desktop.UI/Views/FileTransferWindow.axaml.cs new file mode 100644 index 000000000..812d0ead9 --- /dev/null +++ b/Desktop.UI/Views/FileTransferWindow.axaml.cs @@ -0,0 +1,27 @@ +using Avalonia; +using Avalonia.Controls; + +namespace Remotely.Desktop.UI.Views; + +public partial class FileTransferWindow : Window +{ + public FileTransferWindow() + { + InitializeComponent(); + Opened += FileTransferWindow_Opened; + } + + public IFileTransferWindowViewModel? ViewModel => DataContext as IFileTransferWindowViewModel; + + private void FileTransferWindow_Opened(object? sender, EventArgs e) + { + Topmost = false; + + if (Screens.Primary is not null) + { + var left = Screens.Primary.WorkingArea.Right - FrameSize?.Width ?? Width; + var top = Screens.Primary.WorkingArea.Bottom - FrameSize?.Height ?? Height; + Position = new PixelPoint((int)left, (int)top); + } + } +} diff --git a/Desktop.UI/Views/HostNamePrompt.axaml b/Desktop.UI/Views/HostNamePrompt.axaml new file mode 100644 index 000000000..6b610da7b --- /dev/null +++ b/Desktop.UI/Views/HostNamePrompt.axaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + Copied to clipboard! + + + + Viewers + + Name + Has Control + + + + + + + + + + + + + + + + + diff --git a/Desktop.UI/Views/MainView.axaml.cs b/Desktop.UI/Views/MainView.axaml.cs new file mode 100644 index 000000000..8d98c7421 --- /dev/null +++ b/Desktop.UI/Views/MainView.axaml.cs @@ -0,0 +1,38 @@ +using Avalonia.Controls; +using Remotely.Desktop.Shared; +using Remotely.Desktop.Shared.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Remotely.Desktop.UI.Views; +public partial class MainView : UserControl +{ + public MainView() + { + if (!Design.IsDesignMode) + { + DataContext = StaticServiceProvider.Instance?.GetService(); + } + + InitializeComponent(); + ViewerListBox.SelectionChanged += ViewerListBox_SelectionChanged; + Loaded += MainView_Loaded; + } + + private void ViewerListBox_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (DataContext is MainViewViewModel viewModel && + sender is ListBox viewerListBox && + viewerListBox.SelectedItems is not null) + { + viewModel.SelectedViewers = viewerListBox.SelectedItems.OfType().ToList(); + } + } + + private async void MainView_Loaded(object? sender, System.EventArgs e) + { + if (DataContext is MainViewViewModel viewModel) + { + await viewModel.Init(); + } + } +} diff --git a/Desktop.UI/Views/MainWindow.axaml b/Desktop.UI/Views/MainWindow.axaml new file mode 100644 index 000000000..7b09cbb57 --- /dev/null +++ b/Desktop.UI/Views/MainWindow.axaml @@ -0,0 +1,16 @@ + + + + diff --git a/Desktop.UI/Views/MainWindow.axaml.cs b/Desktop.UI/Views/MainWindow.axaml.cs new file mode 100644 index 000000000..5f4c44312 --- /dev/null +++ b/Desktop.UI/Views/MainWindow.axaml.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Remotely.Desktop.Shared; +using Microsoft.Extensions.DependencyInjection; + +namespace Remotely.Desktop.UI.Views; + +public partial class MainWindow : Window +{ + public MainWindow() + { + if (!Design.IsDesignMode) + { + DataContext = StaticServiceProvider.Instance?.GetService(); + } + + InitializeComponent(); + Closed += MainWindow_Closed; + } + + private void MainWindow_Closed(object? sender, EventArgs e) + { + var dispatcher = StaticServiceProvider.Instance?.GetService(); + dispatcher?.Shutdown(); + } +} diff --git a/Desktop.UI/Views/PromptForAccessWindow.axaml b/Desktop.UI/Views/PromptForAccessWindow.axaml new file mode 100644 index 000000000..5eabcde8a --- /dev/null +++ b/Desktop.UI/Views/PromptForAccessWindow.axaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + A remote control session has been requested. + + + + + + + + + + + + + diff --git a/Desktop.UI/Views/PromptForAccessWindow.axaml.cs b/Desktop.UI/Views/PromptForAccessWindow.axaml.cs new file mode 100644 index 000000000..72fce41f3 --- /dev/null +++ b/Desktop.UI/Views/PromptForAccessWindow.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace Remotely.Desktop.UI.Views; + +public partial class PromptForAccessWindow : Window +{ + public PromptForAccessWindow() + { + InitializeComponent(); + Loaded += Window_Loaded; + } + + private void Window_Loaded(object? sender, RoutedEventArgs e) + { + Topmost = false; + } +} diff --git a/Desktop.UI/Views/SessionIndicatorWindow.axaml b/Desktop.UI/Views/SessionIndicatorWindow.axaml new file mode 100644 index 000000000..cb003bdd8 --- /dev/null +++ b/Desktop.UI/Views/SessionIndicatorWindow.axaml @@ -0,0 +1,33 @@ + + + + + + + + + + + Remote Control Active + + + A remote control session has started. + + + + diff --git a/Desktop.UI/Views/SessionIndicatorWindow.axaml.cs b/Desktop.UI/Views/SessionIndicatorWindow.axaml.cs new file mode 100644 index 000000000..6cfecf3f9 --- /dev/null +++ b/Desktop.UI/Views/SessionIndicatorWindow.axaml.cs @@ -0,0 +1,56 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Microsoft.Extensions.DependencyInjection; +using Remotely.Desktop.Shared; +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.UI.Controls.Dialogs; + +namespace Remotely.Desktop.UI.Views; + +public partial class SessionIndicatorWindow : Window +{ + public SessionIndicatorWindow() + { + InitializeComponent(); + + Closing += SessionIndicatorWindow_Closing; + PointerPressed += SessionIndicatorWindow_PointerPressed; + Opened += SessionIndicatorWindow_Opened; + } + + private void SessionIndicatorWindow_Opened(object? sender, EventArgs e) + { + Topmost = false; + + if (Screens.Primary is not null) + { + var left = Screens.Primary.WorkingArea.Right - FrameSize?.Width ?? Width; + var top = Screens.Primary.WorkingArea.Bottom - FrameSize?.Height ?? Height; + Position = new PixelPoint((int)left, (int)top); + } + } + + private void SessionIndicatorWindow_PointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == Avalonia.Input.PointerUpdateKind.LeftButtonPressed) + { + BeginMoveDrag(e); + } + } + + private async void SessionIndicatorWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e) + { + // This event appears to fire in design mode too. + if (Design.IsDesignMode) + { + return; + } + + if (DataContext is ISessionIndicatorWindowViewModel viewModel) + { + e.Cancel = true; + await viewModel.PromptForExit(); + } + } +} diff --git a/Desktop.Win/Desktop.Win.csproj b/Desktop.Win/Desktop.Win.csproj index bf4a04b5f..b5a566a8c 100644 --- a/Desktop.Win/Desktop.Win.csproj +++ b/Desktop.Win/Desktop.Win.csproj @@ -44,13 +44,13 @@ + + - - diff --git a/Desktop.Win/Helpers/DisplayEnumerationHelper.cs b/Desktop.Win/Helpers/DisplayEnumerationHelper.cs new file mode 100644 index 000000000..288e00036 --- /dev/null +++ b/Desktop.Win/Helpers/DisplayEnumerationHelper.cs @@ -0,0 +1,66 @@ +using Remotely.Shared.Models; +using System.Drawing; +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Remotely.Desktop.Win.Helpers; + +internal static class DisplaysEnumerationHelper +{ + delegate bool EnumMonitorsDelegate(nint hMonitor, nint hdcMonitor, ref RECT lprcMonitor, nint dwData); + + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + private const int CCHDEVICENAME = 32; + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + internal struct MonitorInfoEx + { + public int Size; + public RECT Monitor; + public RECT WorkArea; + public uint Flags; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)] + public string DeviceName; + } + + [DllImport("user32.dll")] + static extern bool EnumDisplayMonitors(nint hdc, nint lprcClip, EnumMonitorsDelegate lpfnEnum, nint dwData); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + static extern bool GetMonitorInfo(nint hMonitor, ref MonitorInfoEx lpmi); + + public static IEnumerable GetDisplays() + { + var result = new List(); + + EnumDisplayMonitors(nint.Zero, nint.Zero, + delegate (nint hMonitor, nint hdcMonitor, ref RECT lprcMonitor, nint dwData) + { + var mi = new MonitorInfoEx(); + mi.Size = Marshal.SizeOf(mi); + bool success = GetMonitorInfo(hMonitor, ref mi); + if (success) + { + var info = new DisplayInfo + { + ScreenSize = new Vector2(mi.Monitor.Right - mi.Monitor.Left, mi.Monitor.Bottom - mi.Monitor.Top), + MonitorArea = new Rectangle(mi.Monitor.Left, mi.Monitor.Top, mi.Monitor.Right - mi.Monitor.Left, mi.Monitor.Bottom - mi.Monitor.Top), + WorkArea = new Rectangle(mi.WorkArea.Left, mi.WorkArea.Top, mi.WorkArea.Right - mi.WorkArea.Left, mi.WorkArea.Bottom - mi.WorkArea.Top), + IsPrimary = mi.Flags > 0, + Hmon = hMonitor, + DeviceName = mi.DeviceName + }; + result.Add(info); + } + return true; + }, nint.Zero); + return result; + } +} \ No newline at end of file diff --git a/Desktop.Win/Models/DirectXOutput.cs b/Desktop.Win/Models/DirectXOutput.cs new file mode 100644 index 000000000..f41715f4c --- /dev/null +++ b/Desktop.Win/Models/DirectXOutput.cs @@ -0,0 +1,36 @@ +using Remotely.Shared.Helpers; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using System.Drawing; + +namespace Remotely.Desktop.Win.Models; + +public class DirectXOutput : IDisposable +{ + public DirectXOutput(Adapter1 adapter, + SharpDX.Direct3D11.Device device, + OutputDuplication outputDuplication, + Texture2D texture2D, + DisplayModeRotation rotation) + { + Adapter = adapter; + Device = device; + OutputDuplication = outputDuplication; + Texture2D = texture2D; + Rotation = rotation; + Bounds = new Rectangle(0, 0, texture2D.Description.Width, texture2D.Description.Height); + } + + public Adapter1 Adapter { get; } + public Rectangle Bounds { get; set; } + public SharpDX.Direct3D11.Device Device { get; } + public OutputDuplication OutputDuplication { get; } + public DisplayModeRotation Rotation { get; } + public Texture2D Texture2D { get; } + public void Dispose() + { + OutputDuplication.ReleaseFrame(); + Disposer.TryDisposeAll(OutputDuplication, Texture2D, Adapter, Device); + GC.SuppressFinalize(this); + } +} diff --git a/Desktop.Win/Program.cs b/Desktop.Win/Program.cs index 42e63e994..952664775 100644 --- a/Desktop.Win/Program.cs +++ b/Desktop.Win/Program.cs @@ -1,20 +1,19 @@ using Avalonia; using Desktop.Shared.Services; -using Immense.RemoteControl.Desktop.Shared.Services; -using Immense.RemoteControl.Desktop.Shared.Startup; -using Immense.RemoteControl.Desktop.UI; -using Immense.RemoteControl.Desktop.UI.Services; -using Immense.RemoteControl.Desktop.Windows.Startup; +using Remotely.Desktop.Shared.Services; +using Remotely.Desktop.Shared.Startup; +using Remotely.Desktop.UI; +using Remotely.Desktop.UI.Services; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using Remotely.Desktop.Win.Startup; using Remotely.Shared.Services; using Remotely.Shared.Utilities; -using System; using System.Diagnostics; -using System.Linq; using System.Runtime.Versioning; using System.Threading; -using System.Threading.Tasks; +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.UI.Startup; +using Remotely.Desktop.Win.Services; namespace Remotely.Desktop.Win; @@ -46,14 +45,11 @@ public static async Task Main(string[] args) } var services = new ServiceCollection(); - services.AddSingleton(); services.AddSingleton(EmbeddedServerDataProvider.Instance); - services.AddRemoteControlWindows( - config => - { - config.AddBrandingProvider(); - }); + services.AddRemoteControlXplat(); + services.AddRemoteControlUi(); + services.AddRemoteControlWindows(); services.AddLogging(builder => { @@ -67,17 +63,16 @@ public static async Task Main(string[] args) var provider = services.BuildServiceProvider(); var appState = provider.GetRequiredService(); - var orgIdProvider = provider.GetRequiredService(); if (getEmbeddedResult.IsSuccess) { - orgIdProvider.OrganizationId = getEmbeddedResult.Value.OrganizationId; + appState.OrganizationId = getEmbeddedResult.Value.OrganizationId; appState.Host = getEmbeddedResult.Value.ServerUrl.AbsoluteUri; } if (appState.ArgDict.TryGetValue("org-id", out var orgId)) { - orgIdProvider.OrganizationId = orgId; + appState.OrganizationId = orgId; } var result = await provider.UseRemoteControlClient( @@ -98,7 +93,7 @@ public static async Task Main(string[] args) { await Task.Delay(Timeout.InfiniteTimeSpan, dispatcher.ApplicationExitingToken); } - catch (TaskCanceledException) { } + catch (OperationCanceledException) { } // Output type is WinExe, so we need to explicitly exit. Environment.Exit(0); diff --git a/Desktop.Win/Services/AppStartup.cs b/Desktop.Win/Services/AppStartup.cs new file mode 100644 index 000000000..42160ee02 --- /dev/null +++ b/Desktop.Win/Services/AppStartup.cs @@ -0,0 +1,168 @@ +using Desktop.Shared.Services; +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Enums; +using Remotely.Desktop.Shared.Native.Windows; +using Remotely.Desktop.Shared.Services; +using Remotely.Desktop.UI.Services; +using Remotely.Shared.Models; + +namespace Remotely.Desktop.Win.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 IMessageLoop _messageLoop; + private readonly IUiDispatcher _uiDispatcher; + private readonly IIdleTimer _idleTimer; + private readonly IShutdownService _shutdownService; + private readonly IBrandingProvider _brandingProvider; + private readonly ILogger _logger; + + public AppStartup( + IAppState appState, + IKeyboardMouseInput inputService, + IDesktopHubConnection desktopHub, + IClipboardService clipboardService, + IChatHostService chatHostService, + ICursorIconWatcher iconWatcher, + IMessageLoop messageLoop, + IUiDispatcher uiDispatcher, + IIdleTimer idleTimer, + IShutdownService shutdownService, + IBrandingProvider brandingProvider, + ILogger logger) + { + _appState = appState; + _inputService = inputService; + _desktopHub = desktopHub; + _clipboardService = clipboardService; + _chatHostService = chatHostService; + _cursorIconWatcher = iconWatcher; + _messageLoop = messageLoop; + _uiDispatcher = uiDispatcher; + _idleTimer = idleTimer; + _shutdownService = shutdownService; + _brandingProvider = brandingProvider; + _logger = logger; + } + + public async Task Run() + { + await _brandingProvider.Initialize(); + + _messageLoop.StartMessageLoop(); + + 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 _uiDispatcher.StartHeadless().ConfigureAwait(false); + if (!result.IsSuccess) + { + return; + } + await StartScreenCasting().ConfigureAwait(false); + break; + } + case AppMode.Attended: + { + _uiDispatcher.StartClassicDesktop(); + break; + } + case AppMode.Chat: + { + var result = await _uiDispatcher.StartHeadless().ConfigureAwait(false); + 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), _uiDispatcher.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 (Win32Interop.GetCurrentDesktop(out var currentDesktopName)) + { + _logger.LogInformation("Setting initial desktop to {currentDesktopName}.", currentDesktopName); + } + else + { + _logger.LogWarning("Failed to get initial desktop name."); + } + + if (!Win32Interop.SwitchToInputDesktop()) + { + _logger.LogWarning("Failed to set initial desktop."); + } + + if (_appState.IsRelaunch) + { + _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); + } + } + } +} diff --git a/Desktop.Win/Services/AudioCapturerWin.cs b/Desktop.Win/Services/AudioCapturerWin.cs new file mode 100644 index 000000000..7a2a67356 --- /dev/null +++ b/Desktop.Win/Services/AudioCapturerWin.cs @@ -0,0 +1,101 @@ +using Remotely.Desktop.Shared.Abstractions; +using NAudio.Wave; +using System.IO; + +namespace Remotely.Desktop.Win.Services; + +public class AudioCapturerWin : IAudioCapturer +{ + private readonly ILogger _logger; + private readonly SemaphoreSlim _sendLock = new(1, 1); + private WasapiLoopbackCapture? _capturer; + private WaveFormat? _targetFormat; + public AudioCapturerWin(ILogger logger) + { + _logger = logger; + } + + public event EventHandler? AudioSampleReady; + + public void ToggleAudio(bool toggleOn) + { + if (toggleOn) + { + Start(); + } + else + { + Stop(); + } + } + + private async void Capturer_DataAvailable(object? sender, WaveInEventArgs args) + { + if (args.Buffer.All(x => x == 0)) + { + return; + } + + try + { + await _sendLock.WaitAsync(); + + if (args.BytesRecorded > 0) + { + await SendTempBuffer(args.Buffer); + } + } + catch { } + finally + { + _sendLock.Release(); + } + } + + private async Task SendTempBuffer(byte[] buffer) + { + if (_capturer is null) + { + _logger.LogWarning("Audio capturer is unexpectedly null."); + return; + } + + using var ms1 = new MemoryStream(); + using (var wfw = new WaveFileWriter(ms1, _capturer.WaveFormat)) + { + await wfw.WriteAsync(buffer); + } + + // Resample to 16-bit. + using var ms2 = new MemoryStream(ms1.ToArray()); + using var wfr = new WaveFileReader(ms2); + using var ms3 = new MemoryStream(); + using (var resampler = new MediaFoundationResampler(wfr, _targetFormat)) + { + WaveFileWriter.WriteWavFileToStream(ms3, resampler); + } + AudioSampleReady?.Invoke(this, ms3.ToArray()); + } + + private void Start() + { + try + { + _capturer?.Dispose(); + _capturer = new WasapiLoopbackCapture(); + _targetFormat ??= new WaveFormat(16000, 8, 1); + _capturer.DataAvailable += Capturer_DataAvailable; + + _capturer.StartRecording(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while creating audio capturer. Make sure a sound device is installed and working."); + } + } + + private void Stop() + { + _capturer?.StopRecording(); + } +} diff --git a/Desktop.Win/Services/CursorIconWatcherWin.cs b/Desktop.Win/Services/CursorIconWatcherWin.cs new file mode 100644 index 000000000..4c263cfc3 --- /dev/null +++ b/Desktop.Win/Services/CursorIconWatcherWin.cs @@ -0,0 +1,132 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Native.Windows; +using Remotely.Shared.Models; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Timers; +using Timer = System.Timers.Timer; + +namespace Remotely.Desktop.Win.Services; + +// TODO: Change to IHostedService and emit through IMessenger. +/// +/// A class that can be used to watch for cursor icon changes. +/// +[SupportedOSPlatform("windows")] +public class CursorIconWatcherWin : ICursorIconWatcher +{ + private readonly SemaphoreSlim _cursorLock = new(1, 1); + private readonly Timer _changeTimer; + private const int IBeamHandle = 65541; + private User32.CursorInfo _cursorInfo; + private int _previousCursorHandle; + + public CursorIconWatcherWin() + { + _changeTimer = new Timer(25); + _changeTimer.Elapsed += ChangeTimer_Elapsed; + _changeTimer.Start(); + } + + // TODO: Emit through IMessenger. + public event EventHandler? OnChange; + + public CursorInfo GetCurrentCursor() + { + try + { + var ci = new User32.CursorInfo(); + ci.cbSize = Marshal.SizeOf(ci); + User32.GetCursorInfo(out ci); + if (ci.flags == User32.CURSOR_SHOWING) + { + if (ci.hCursor.ToInt32() == IBeamHandle) + { + return new CursorInfo(Array.Empty(), Point.Empty, "text"); + } + + var hotspot = Point.Empty; + + if (User32.GetIconInfo(ci.hCursor, out var iconInfo)) + { + hotspot = new Point(iconInfo.xHotspot, iconInfo.yHotspot); + } + + using var icon = Icon.FromHandle(ci.hCursor); + using var ms = new MemoryStream(); + icon.ToBitmap().Save(ms, ImageFormat.Png); + return new CursorInfo(ms.ToArray(), hotspot); + } + else + { + return new CursorInfo(Array.Empty(), Point.Empty, "default"); + } + } + catch + { + return new CursorInfo(Array.Empty(), Point.Empty, "default"); + } + } + + private void ChangeTimer_Elapsed(object? sender, ElapsedEventArgs e) + { + if (!_cursorLock.Wait(0)) + { + return; + } + + try + { + if (OnChange is null) + { + return; + } + + _cursorInfo = new User32.CursorInfo(); + _cursorInfo.cbSize = Marshal.SizeOf(_cursorInfo); + User32.GetCursorInfo(out _cursorInfo); + if (_cursorInfo.flags == User32.CURSOR_SHOWING) + { + var currentCursor = _cursorInfo.hCursor.ToInt32(); + if (currentCursor != _previousCursorHandle) + { + if (currentCursor == IBeamHandle) + { + OnChange?.Invoke(this, new CursorInfo(Array.Empty(), Point.Empty, "text")); + } + else + { + using var icon = Icon.FromHandle(_cursorInfo.hCursor); + using var ms = new MemoryStream(); + var hotspot = Point.Empty; + + if (User32.GetIconInfo(_cursorInfo.hCursor, out var iconInfo)) + { + hotspot = new Point(iconInfo.xHotspot, iconInfo.yHotspot); + } + icon.ToBitmap().Save(ms, ImageFormat.Png); + OnChange?.Invoke(this, new CursorInfo(ms.ToArray(), hotspot)); + } + _previousCursorHandle = currentCursor; + } + } + else if (_previousCursorHandle != 0) + { + _previousCursorHandle = 0; + OnChange?.Invoke(this, new CursorInfo(Array.Empty(), Point.Empty, "default")); + } + } + catch + { + OnChange?.Invoke(this, new CursorInfo(Array.Empty(), Point.Empty, "default")); + } + finally + { + _cursorLock.Release(); + } + } + +} diff --git a/Desktop.Win/Services/FileTransferServiceWin.cs b/Desktop.Win/Services/FileTransferServiceWin.cs new file mode 100644 index 000000000..a5cd37b0d --- /dev/null +++ b/Desktop.Win/Services/FileTransferServiceWin.cs @@ -0,0 +1,222 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Services; +using Remotely.Desktop.Shared.ViewModels; +using Remotely.Desktop.UI.Controls.Dialogs; +using Remotely.Desktop.UI.Services; +using Remotely.Desktop.UI.Views; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Security.AccessControl; +using System.Security.Principal; +using Remotely.Shared.Extensions; +using System.IO; + +namespace Remotely.Desktop.Win.Services; + +public class FileTransferServiceWin : IFileTransferService +{ + private static readonly ConcurrentDictionary _fileTransferWindows = + new(); + + private static readonly ConcurrentDictionary _partialTransfers = + new(); + + private static readonly SemaphoreSlim _writeLock = new(1, 1); + private static MessageBoxResult? _result; + private readonly IUiDispatcher _dispatcher; + private readonly ILogger _logger; + private readonly IViewModelFactory _viewModelFactory; + private readonly IDialogProvider _dialogProvider; + + public FileTransferServiceWin( + IUiDispatcher dispatcher, + IViewModelFactory viewModelFactory, + IDialogProvider dialogProvider, + ILogger logger) + { + _dispatcher = dispatcher; + _viewModelFactory = viewModelFactory; + _dialogProvider = dialogProvider; + _logger = logger; + } + + public string GetBaseDirectory() + { + var programDataPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); + return Directory.CreateDirectory(Path.Combine(programDataPath, "RemoteControl", "Shared")).FullName; + } + + public void OpenFileTransferWindow(IViewer viewer) + { + _dispatcher.Invoke(() => + { + if (_fileTransferWindows.TryGetValue(viewer.ViewerConnectionId, out var window)) + { + window.Activate(); + } + else + { + var viewModel = _viewModelFactory.CreateFileTransferWindowViewModel(viewer); + window = new FileTransferWindow() + { + DataContext = viewModel + }; + window.Closed += (sender, arg) => + { + _fileTransferWindows.Remove(viewer.ViewerConnectionId, out _); + }; + _fileTransferWindows.AddOrUpdate(viewer.ViewerConnectionId, window, (k, v) => window); + window.Show(); + } + }); + } + + [SupportedOSPlatform("windows")] + public async Task ReceiveFile(byte[] buffer, string fileName, string messageId, bool endOfFile, bool startOfFile) + { + try + { + await _writeLock.WaitAsync(); + + var baseDir = GetBaseDirectory(); + + SetFileOrFolderPermissions(baseDir); + + 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(); + SetFileOrFolderPermissions(filePath); + 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) + { + // We're currently in the context of an RPC call from the + // SignalR hub. We don't want to block it, which will prevent + // subsequent messages from being received. + ShowTransferComplete().Forget(); + } + } + + public async Task UploadFile( + FileUpload fileUpload, + IViewer viewer, + Action progressUpdateCallback, + CancellationToken cancelToken) + { + try + { + await viewer.SendFile(fileUpload, progressUpdateCallback, cancelToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while uploading file."); + } + } + + [SupportedOSPlatform("windows")] + private static void SetFileOrFolderPermissions(string path) + { + FileSystemSecurity ds; + + var aclSections = AccessControlSections.Access | AccessControlSections.Group | AccessControlSections.Owner; + if (File.Exists(path)) + { + ds = new FileSecurity(path, aclSections); + } + else if (Directory.Exists(path)) + { + ds = new DirectorySecurity(path, aclSections); + } + else + { + return; + } + + var sid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); + var account = (NTAccount)sid.Translate(typeof(NTAccount)); + + var accessAlreadySet = false; + + foreach (FileSystemAccessRule rule in ds.GetAccessRules(true, true, typeof(NTAccount))) + { + if (rule.IdentityReference == account && + rule.FileSystemRights.HasFlag(FileSystemRights.Modify) && + rule.AccessControlType == AccessControlType.Allow) + { + accessAlreadySet = true; + break; + } + } + + if (!accessAlreadySet) + { + ds.AddAccessRule(new FileSystemAccessRule(account, FileSystemRights.Modify, AccessControlType.Allow)); + if (File.Exists(path)) + { + new FileInfo(path).SetAccessControl((FileSecurity)ds); + } + else if (Directory.Exists(path)) + { + new DirectoryInfo(path).SetAccessControl((DirectorySecurity)ds); + } + } + } + + private async Task ShowTransferComplete() + { + // Prevent multiple dialogs from popping up. + if (_result is null) + { + _result = await _dialogProvider.Show("File transfer complete. Show folder?", + "Transfer Complete", + MessageBoxType.YesNo); + + if (_result == MessageBoxResult.Yes) + { + Process.Start("explorer.exe", GetBaseDirectory()); + } + + _result = null; + } + } +} diff --git a/Desktop.Win/Services/KeyboardMouseInputWin.cs b/Desktop.Win/Services/KeyboardMouseInputWin.cs new file mode 100644 index 000000000..8261c6d94 --- /dev/null +++ b/Desktop.Win/Services/KeyboardMouseInputWin.cs @@ -0,0 +1,582 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Enums; +using Remotely.Desktop.Shared.Native.Windows; +using Remotely.Desktop.Shared.Services; +using Remotely.Desktop.UI.Services; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using static Remotely.Desktop.Shared.Native.Windows.User32; + +namespace Remotely.Desktop.Win.Services; + +[SupportedOSPlatform("windows")] +public class KeyboardMouseInputWin( + IUiDispatcher _dispatcher, + ILogger _logger) : IKeyboardMouseInput +{ + private readonly ConcurrentQueue _inputActions = new(); + private readonly AutoResetEvent _inputReadySignal = new(false); + private volatile bool _inputBlocked; + private Thread? _inputProcessingThread; + + [Flags] + private enum ShiftState : byte + { + None = 0, + ShiftPressed = 1 << 0, + CtrlPressed = 1 << 1, + AltPressed = 1 << 2, + HankakuPressed = 1 << 3, + Reserved1 = 1 << 4, + Reserved2 = 1 << 5, + } + + public void Init() + { + StartInputProcessingThread(); + } + + public void SendKeyDown(string key) + { + TryOnInputDesktop(() => + { + try + { + try + { + if (!ConvertJavaScriptKeyToVirtualKey(key, out var vk)) + { + _logger.LogWarning("Unable to simulate key input {key}.", key); + return; + }; + + + var input = CreateKeyboardInput(vk.Value, true); + var sent = SendInput(1, [input], INPUT.Size); + if (sent == 0) + { + _logger.LogWarning( + "Failed to send input for key {Key}. Last Win32 Error: {Win32Error}", + key, + Marshal.GetLastPInvokeError()); + } + + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while sending key up."); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while sending key down."); + } + }); + } + + public void SendKeyUp(string key) + { + TryOnInputDesktop(() => + { + try + { + if (!ConvertJavaScriptKeyToVirtualKey(key, out var vk)) + { + _logger.LogWarning("Unable to simulate key input {key}.", key); + return; + }; + + + var input = CreateKeyboardInput(vk.Value, false); + var sent = SendInput(1, [input], INPUT.Size); + if (sent == 0) + { + _logger.LogWarning( + "Failed to send input for key {Key}. Last Win32 Error: {Win32Error}", + key, + Marshal.GetLastPInvokeError()); + } + + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while sending key up."); + } + }); + } + + public void SendMouseButtonAction(int button, ButtonAction buttonAction, double percentX, double percentY, IViewer viewer) + { + TryOnInputDesktop(() => + { + try + { + MOUSEEVENTF mouseEvent; + switch (button) + { + case 0: + switch (buttonAction) + { + case ButtonAction.Down: + mouseEvent = MOUSEEVENTF.LEFTDOWN; + break; + case ButtonAction.Up: + mouseEvent = MOUSEEVENTF.LEFTUP; + break; + default: + return; + } + break; + case 1: + switch (buttonAction) + { + case ButtonAction.Down: + mouseEvent = MOUSEEVENTF.MIDDLEDOWN; + break; + case ButtonAction.Up: + mouseEvent = MOUSEEVENTF.MIDDLEUP; + break; + default: + return; + } + break; + case 2: + switch (buttonAction) + { + case ButtonAction.Down: + mouseEvent = MOUSEEVENTF.RIGHTDOWN; + break; + case ButtonAction.Up: + mouseEvent = MOUSEEVENTF.RIGHTUP; + break; + default: + return; + } + break; + default: + return; + } + var xyPercent = GetAbsolutePercentFromRelativePercent(percentX, percentY, viewer.Capturer); + // Coordinates must be normalized. The bottom-right coordinate is mapped to 65535. + var normalizedX = xyPercent.Item1 * 65535D; + var normalizedY = xyPercent.Item2 * 65535D; + var union = new InputUnion() + { + mi = new MOUSEINPUT() + { + dwFlags = MOUSEEVENTF.ABSOLUTE | mouseEvent | MOUSEEVENTF.VIRTUALDESK, + dx = (int)normalizedX, + dy = (int)normalizedY, + time = 0, + mouseData = 0, + dwExtraInfo = GetMessageExtraInfo() + } + }; + var input = new INPUT() { type = InputType.MOUSE, U = union }; + var sent = SendInput(1, [input], INPUT.Size); + if (sent == 0) + { + _logger.LogWarning( + "Failed to send input for button {Button}. Last Win32 Error: {Win32Error}", + button, + Marshal.GetLastPInvokeError()); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while sending mouse button."); + } + }); + } + + public void SendMouseMove(double percentX, double percentY, IViewer viewer) + { + TryOnInputDesktop(() => + { + try + { + if (!Win32Interop.SwitchToInputDesktop()) + { + _logger.LogWarning("Desktop switch failed during mouse move."); + } + var xyPercent = GetAbsolutePercentFromRelativePercent(percentX, percentY, viewer.Capturer); + // Coordinates must be normalized. The bottom-right coordinate is mapped to 65535. + var normalizedX = xyPercent.Item1 * 65535D; + var normalizedY = xyPercent.Item2 * 65535D; + var union = new InputUnion() { mi = new MOUSEINPUT() { dwFlags = MOUSEEVENTF.ABSOLUTE | MOUSEEVENTF.MOVE | MOUSEEVENTF.VIRTUALDESK, dx = (int)normalizedX, dy = (int)normalizedY, time = 0, mouseData = 0, dwExtraInfo = GetMessageExtraInfo() } }; + var input = new INPUT() { type = InputType.MOUSE, U = union }; + var sent = SendInput(1, [input], INPUT.Size); + if (sent == 0) + { + _logger.LogWarning( + "Failed to send mouse move. Last Win32 Error: {Win32Error}", + Marshal.GetLastPInvokeError()); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while sending mouse move."); + } + }); + } + + public void SendMouseWheel(int deltaY) + { + TryOnInputDesktop(() => + { + try + { + if (deltaY < 0) + { + deltaY = -120; + } + else if (deltaY > 0) + { + deltaY = 120; + } + var union = new InputUnion() { mi = new MOUSEINPUT() { dwFlags = MOUSEEVENTF.WHEEL, dx = 0, dy = 0, time = 0, mouseData = deltaY, dwExtraInfo = GetMessageExtraInfo() } }; + var input = new INPUT() { type = InputType.MOUSE, U = union }; + var sent = SendInput(1, [input], INPUT.Size); + if (sent == 0) + { + _logger.LogWarning( + "Failed to send mouse wheel. Last Win32 Error: {Win32Error}", + Marshal.GetLastPInvokeError()); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while sending mouse wheel."); + } + }); + } + + public void SendText(string transferText) + { + TryOnInputDesktop(() => + { + try + { + foreach (var character in transferText) + { + var keyCode = Convert.ToUInt16(character); + + var keyDown = CreateKeyboardInput(keyCode, true); + var result = SendInput(1, [keyDown], INPUT.Size); + if (result != 1) + { + _logger.LogWarning( + "Send text failed. Failed to simulate input for character {Character}.", + character); + break; + } + + var keyUp = CreateKeyboardInput(keyCode, false); + result = SendInput(1, [keyUp], INPUT.Size); + if (result != 1) + { + _logger.LogWarning( + "Send text failed. Failed to simulate input for character {Character}.", + character); + break; + } + + Thread.Sleep(1); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while sending text."); + } + }); + } + + public void SetKeyStatesUp() + { + TryOnInputDesktop(() => + { + foreach (VirtualKey key in Enum.GetValues(typeof(VirtualKey))) + { + try + { + // Skip mouse buttons and toggleable keys. + switch (key) + { + case VirtualKey.LBUTTON: + case VirtualKey.RBUTTON: + case VirtualKey.MBUTTON: + case VirtualKey.NUMLOCK: + case VirtualKey.CAPITAL: + case VirtualKey.SCROLL: + continue; + default: + break; + } + var (isPressed, isToggled) = GetKeyPressState(key); + if (isPressed || isToggled) + { + var input = CreateKeyboardInput(key, false); + var sent = SendInput(1, [input], INPUT.Size); + if (sent == 0) + { + _logger.LogWarning( + "Failed to set key up for key {Key}. Last Win32 Error: {Win32Error}", + key, + Marshal.GetLastPInvokeError()); + } + Thread.Sleep(1); + } + } + catch { } + } + }); + } + + public void ToggleBlockInput(bool toggleOn) + { + TryOnInputDesktop(() => + { + _inputBlocked = toggleOn; + var result = BlockInput(toggleOn); + _logger.LogInformation("Result of ToggleBlockInput set to {toggleOn}: {result}", toggleOn, result); + }); + } + + private static INPUT CreateKeyboardInput( + VirtualKey virtualKey, + bool isPressed) + { + KEYEVENTF flags = 0; + + if (IsExtendedKey(virtualKey)) + { + flags |= KEYEVENTF.EXTENDEDKEY; + } + + if (!isPressed) + { + flags |= KEYEVENTF.KEYUP; + } + + return new INPUT() + { + type = InputType.KEYBOARD, + U = new InputUnion() + { + ki = new KEYBDINPUT() + { + wVk = virtualKey, + wScan = (ushort)MapVirtualKeyEx((uint)virtualKey, VkMapType.MAPVK_VK_TO_VSC_EX, GetKeyboardLayout((uint)Environment.CurrentManagedThreadId)), + dwExtraInfo = GetMessageExtraInfo(), + dwFlags = flags, + time = 0 + } + } + }; + } + + private static INPUT CreateKeyboardInput(ushort unicodeKey, bool isPressed) + { + var flags = KEYEVENTF.UNICODE; + if (!isPressed) + { + flags |= KEYEVENTF.KEYUP; + } + + return new INPUT() + { + type = InputType.KEYBOARD, + U = new InputUnion() + { + ki = new KEYBDINPUT() + { + wVk = 0, + wScan = unicodeKey, + dwFlags = flags, + dwExtraInfo = GetMessageExtraInfo() + } + } + }; + } + + private static Tuple GetAbsolutePercentFromRelativePercent(double percentX, double percentY, IScreenCapturer capturer) + { + var absoluteX = capturer.CurrentScreenBounds.Width * percentX + capturer.CurrentScreenBounds.Left - capturer.GetVirtualScreenBounds().Left; + var absoluteY = capturer.CurrentScreenBounds.Height * percentY + capturer.CurrentScreenBounds.Top - capturer.GetVirtualScreenBounds().Top; + return new Tuple(absoluteX / capturer.GetVirtualScreenBounds().Width, absoluteY / capturer.GetVirtualScreenBounds().Height); + } + + private static (bool Pressed, bool Toggled) GetKeyPressState(VirtualKey vkey) + { + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getkeystate#return-value + var state = GetKeyState(vkey); + var pressed = state < 0; + var toggled = (state & 1) != 0; + return (pressed, toggled); + } + + private static bool IsExtendedKey(VirtualKey virtualKey) + { + return virtualKey switch + { + VirtualKey.SHIFT or + VirtualKey.CONTROL or + VirtualKey.MENU or + VirtualKey.RCONTROL or + VirtualKey.RMENU or + VirtualKey.INSERT or + VirtualKey.DELETE or + VirtualKey.HOME or + VirtualKey.END or + VirtualKey.PRIOR or + VirtualKey.NEXT or + VirtualKey.LEFT or + VirtualKey.RIGHT or + VirtualKey.UP or + VirtualKey.DOWN or + VirtualKey.NUMLOCK or + VirtualKey.CANCEL or + VirtualKey.DIVIDE or + VirtualKey.SNAPSHOT or + VirtualKey.RETURN => true, + _ => false + }; + } + + private bool ConvertJavaScriptKeyToVirtualKey(string key, [NotNullWhen(true)] out VirtualKey? result) + { + result = key switch + { + " " => VirtualKey.SPACE, + "Down" or "ArrowDown" => VirtualKey.DOWN, + "Up" or "ArrowUp" => VirtualKey.UP, + "Left" or "ArrowLeft" => VirtualKey.LEFT, + "Right" or "ArrowRight" => VirtualKey.RIGHT, + "Enter" => VirtualKey.RETURN, + "Esc" or "Escape" => VirtualKey.ESCAPE, + "Alt" => VirtualKey.MENU, + "Control" => VirtualKey.CONTROL, + "Shift" => VirtualKey.SHIFT, + "PAUSE" => VirtualKey.PAUSE, + "BREAK" => VirtualKey.PAUSE, + "Backspace" => VirtualKey.BACK, + "Tab" => VirtualKey.TAB, + "CapsLock" => VirtualKey.CAPITAL, + "Delete" => VirtualKey.DELETE, + "Home" => VirtualKey.HOME, + "End" => VirtualKey.END, + "PageUp" => VirtualKey.PRIOR, + "PageDown" => VirtualKey.NEXT, + "NumLock" => VirtualKey.NUMLOCK, + "Insert" => VirtualKey.INSERT, + "ScrollLock" => VirtualKey.SCROLL, + "F1" => VirtualKey.F1, + "F2" => VirtualKey.F2, + "F3" => VirtualKey.F3, + "F4" => VirtualKey.F4, + "F5" => VirtualKey.F5, + "F6" => VirtualKey.F6, + "F7" => VirtualKey.F7, + "F8" => VirtualKey.F8, + "F9" => VirtualKey.F9, + "F10" => VirtualKey.F10, + "F11" => VirtualKey.F11, + "F12" => VirtualKey.F12, + "Meta" => VirtualKey.LWIN, + "ContextMenu" => VirtualKey.MENU, + _ => key.Length == 1 ? + (VirtualKey)VkKeyScan(Convert.ToChar(key)) : + null + }; + + if (result is null) + { + _logger.LogWarning("Unable to parse key input: {key}.", key); + return false; + } + return true; + } + + private void ProcessQueue(CancellationToken cancelToken) + { + while (!cancelToken.IsCancellationRequested) + { + try + { + _inputReadySignal.WaitOne(); + while (_inputActions.TryDequeue(out var action)) + { + action(); + Thread.Sleep(1); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during input queue processing."); + } + } + + _logger.LogInformation("Stopping input processing on thread."); + } + private void StartInputProcessingThread() + { + // After BlockInput is enabled, only simulated input coming from the same thread + // will work. So we have to start a new thread that runs continuously and + // processes a queue of input events. + _inputProcessingThread = new Thread(() => + { + _logger.LogInformation("New input processing thread started on thread {threadId}.", Environment.CurrentManagedThreadId); + + if (_inputBlocked && !BlockInput(true)) + { + _logger.LogWarning("Failed to block input on input processing start."); + } + ProcessQueue(_dispatcher.ApplicationExitingToken); + }); + + _inputProcessingThread.SetApartmentState(ApartmentState.MTA); + _inputProcessingThread.Start(); + } + + private void TryOnInputDesktop(Action inputAction) + { + _inputActions.Enqueue(() => + { + try + { + var switchResult = Win32Interop.SwitchToInputDesktop(); + + // Try to perform the dequeued action whether or not the switch was successful. + inputAction(); + + if (!switchResult) + { + _logger.LogWarning("Desktop switch failed during input processing."); + + // Thread likely has hooks in current desktop. SendKeys will create one with no way to unhook it. + // Start a new thread for processing input. + StartInputProcessingThread(); + return; + } + + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during input queue processing."); + } + }); + _inputReadySignal.Set(); + } + [StructLayout(LayoutKind.Explicit)] + private struct ShortHelper(short value) + { + [FieldOffset(0)] + public short Value = value; + [FieldOffset(0)] + public byte Low; + [FieldOffset(1)] + public byte High; + } +} diff --git a/Desktop.Win/Services/MessageLoop.cs b/Desktop.Win/Services/MessageLoop.cs new file mode 100644 index 000000000..e30b8a467 --- /dev/null +++ b/Desktop.Win/Services/MessageLoop.cs @@ -0,0 +1,122 @@ +using Remotely.Desktop.Shared.Messages; +using Remotely.Desktop.UI.Services; +using Remotely.Shared.Enums; +using Bitbound.SimpleMessenger; +using Microsoft.Win32; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Remotely.Desktop.Win.Services; + +public interface IMessageLoop +{ + void StartMessageLoop(); +} + +[SupportedOSPlatform("windows")] +public class MessageLoop : IMessageLoop +{ + private readonly CancellationToken _exitToken; + private readonly ILogger _logger; + private readonly IMessenger _messenger; + private Thread? _messageLoopThread; + + public MessageLoop( + IMessenger messenger, + IUiDispatcher uiDispatcher, + ILogger logger) + { + _messenger = messenger; + _logger = logger; + _exitToken = uiDispatcher.ApplicationExitingToken; + } + + + public void StartMessageLoop() + { + if (_messageLoopThread is not null) + { + throw new InvalidOperationException("Message loop already started."); + } + + _messageLoopThread = new Thread(() => + { + SystemEvents.SessionSwitch += SystemEvents_SessionSwitch; + SystemEvents.SessionEnding += SystemEvents_SessionEnding; + SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; + + while (!_exitToken.IsCancellationRequested) + { + try + { + while (GetMessage(out var msg, IntPtr.Zero, 0, 0) > 0) + { + DispatchMessage(ref msg); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in message loop."); + } + } + + SystemEvents.SessionSwitch -= SystemEvents_SessionSwitch; + SystemEvents.SessionEnding -= SystemEvents_SessionEnding; + SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged; + }); + _messageLoopThread.SetApartmentState(ApartmentState.STA); + _messageLoopThread.Start(); + } + + [DllImport("user32.dll")] + private static extern bool DispatchMessage([In] ref MSG lpmsg); + + [DllImport("user32.dll")] + private static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); + + + private void SystemEvents_DisplaySettingsChanged(object? sender, EventArgs e) + { + _messenger.Send(new DisplaySettingsChangedMessage()); + } + + private void SystemEvents_SessionEnding(object sender, SessionEndingEventArgs e) + { + _logger.LogInformation("Session ending. Reason: {reason}", e.Reason); + + var reason = (SessionEndReasonsEx)e.Reason; + _messenger.Send(new WindowsSessionEndingMessage(reason)); + } + + private void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e) + { + _logger.LogInformation("Session changing. Reason: {reason}", e.Reason); + + var reason = (SessionSwitchReasonEx)(int)e.Reason; + _messenger.Send(new WindowsSessionSwitchedMessage(reason, Process.GetCurrentProcess().SessionId)); + } + [StructLayout(LayoutKind.Sequential)] + private struct MSG + { + public IntPtr hwnd; + public uint message; + public IntPtr wParam; + public IntPtr lParam; + public uint time; + public POINT pt; + } + + [StructLayout(LayoutKind.Sequential)] + private struct POINT + { + public int X; + public int Y; + + public POINT(int x, int y) + { + X = x; + Y = y; + } + } +} diff --git a/Desktop.Win/Services/ScreenCapturerWin.cs b/Desktop.Win/Services/ScreenCapturerWin.cs new file mode 100644 index 000000000..96e668308 --- /dev/null +++ b/Desktop.Win/Services/ScreenCapturerWin.cs @@ -0,0 +1,556 @@ +// The DirectX capture code is based off examples from the +// SharpDX Samples at https://github.com/sharpdx/SharpDX. + +// Copyright (c) 2010-2013 SharpDX - Alexandre Mutel +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// 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 +// AUTHORS OR COPYRIGHT HOLDERS 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. + +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Native.Windows; +using Remotely.Desktop.Shared.Services; +using Remotely.Shared.Models; +using Bitbound.SimpleMessenger; +using Remotely.Desktop.Win.Helpers; +using Remotely.Desktop.Win.Models; +using SharpDX; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using SkiaSharp; +using SkiaSharp.Views.Desktop; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using Result = Remotely.Shared.Primitives.Result; +using Remotely.Desktop.Shared.Messages; +using Remotely.Shared.Primitives; + +namespace Remotely.Desktop.Win.Services; + +[SupportedOSPlatform("windows")] +public class ScreenCapturerWin : IScreenCapturer +{ + private readonly ConcurrentDictionary _displays = new(); + private readonly Dictionary _directxScreens = new(); + private readonly IImageHelper _imageHelper; + private readonly ILogger _logger; + private readonly object _screenBoundsLock = new(); + private SKBitmap? _currentFrame; + private bool _needsInit; + private SKBitmap? _previousFrame; + + public ScreenCapturerWin( + IImageHelper imageHelper, + IMessenger messenger, + ILogger logger) + { + _imageHelper = imageHelper; + _logger = logger; + + Init(); + + // Registration is automatically removed when subscriber is disposed. + _ = messenger.Register(this, HandleDisplaySettingsChanged); + } + + public event EventHandler? ScreenChanged; + + public bool CaptureFullscreen { get; set; } = true; + + public Rectangle CurrentScreenBounds { get; private set; } + + public bool IsGpuAccelerated { get; private set; } + + public string SelectedScreen { get; private set; } = string.Empty; + + private SKBitmap? CurrentFrame + { + get => _currentFrame; + set + { + if (_currentFrame != null) + { + _previousFrame?.Dispose(); + _previousFrame = _currentFrame; + } + _currentFrame = value; + } + } + + public void Dispose() + { + try + { + ClearDirectXOutputs(); + GC.SuppressFinalize(this); + } + catch { } + } + + public IEnumerable GetDisplayNames() + { + return DisplaysEnumerationHelper + .GetDisplays() + .Select(x => x.DeviceName); + } + + public SKRect GetFrameDiffArea() + { + if (CurrentFrame is null) + { + return SKRect.Empty; + } + return _imageHelper.GetDiffArea(CurrentFrame, _previousFrame, CaptureFullscreen); + } + + public Result GetImageDiff() + { + + if (CurrentFrame is null) + { + return Result.Fail("Current frame cannot be empty."); + } + return _imageHelper.GetImageDiff(CurrentFrame, _previousFrame); + } + + public Result GetNextFrame() + { + lock (_screenBoundsLock) + { + try + { + if (!Win32Interop.SwitchToInputDesktop()) + { + // Something will occasionally prevent this from succeeding after active + // desktop has changed to/from WinLogon (err code 170). I'm guessing a hook + // is getting put in the desktop, which causes SetThreadDesktop to fail. + // The caller can start a new thread, which seems to resolve it. + var errCode = Marshal.GetLastWin32Error(); + _logger.LogError("Failed to switch to input desktop. Last Win32 error code: {errCode}", errCode); + return Result.Fail($"Failed to switch to input desktop. Last Win32 error code: {errCode}"); + } + + if (_needsInit) + { + _logger.LogWarning("Init needed in GetNextFrame."); + Init(); + } + + var result = GetDirectXFrame(); + + if (result.IsSuccess && !result.HadChanges) + { + return Result.Fail("No screen changes occurred."); + } + + if (result.HadChanges && !IsEmpty(result.Bitmap)) + { + CurrentFrame = result.Bitmap; + } + else + { + var bitBltResult = GetBitBltFrame(); + if (!bitBltResult.IsSuccess) + { + var ex = bitBltResult.Exception ?? new("Unknown error."); + _logger.LogError(ex, "Error while getting next frame."); + return Result.Fail(ex); + } + CurrentFrame = bitBltResult.Value; + } + + return Result.Ok(CurrentFrame); + } + catch (Exception e) + { + _logger.LogError(e, "Error while getting next frame."); + _needsInit = true; + return Result.Fail(e); + } + } + } + + public int GetScreenCount() + { + return DisplaysEnumerationHelper.GetDisplays().Count(); + } + + public Rectangle GetVirtualScreenBounds() + { + var displays = DisplaysEnumerationHelper.GetDisplays(); + var lowestX = 0; + var highestX = 0; + var lowestY = 0; + var highestY = 0; + + foreach (var display in displays) + { + lowestX = Math.Min(display.MonitorArea.Left, lowestX); + highestX = Math.Max(display.MonitorArea.Right, highestX); + lowestY = Math.Min(display.MonitorArea.Top, lowestY); + highestY = Math.Max(display.MonitorArea.Bottom, highestY); + } + + return new Rectangle(lowestX, lowestY, highestX - lowestX, highestY - lowestY); + } + + public void Init() + { + Win32Interop.SwitchToInputDesktop(); + + CaptureFullscreen = true; + InitDisplays(); + InitDirectX(); + + ScreenChanged?.Invoke(this, CurrentScreenBounds); + + _needsInit = false; + } + + public void SetSelectedScreen(string displayName) + { + lock (_screenBoundsLock) + { + if (displayName == SelectedScreen) + { + return; + } + + if (!_displays.TryGetValue(displayName, out var display)) + { + display = _displays.First().Value; + } + + SelectedScreen = displayName; + CurrentScreenBounds = display.MonitorArea; + CaptureFullscreen = true; + ScreenChanged?.Invoke(this, CurrentScreenBounds); + } + } + + internal Result GetBitBltFrame() + { + try + { + using var bitmap = new Bitmap(CurrentScreenBounds.Width, CurrentScreenBounds.Height, PixelFormat.Format32bppArgb); + using (var graphic = Graphics.FromImage(bitmap)) + { + graphic.CopyFromScreen(CurrentScreenBounds.Left, CurrentScreenBounds.Top, 0, 0, new Size(CurrentScreenBounds.Width, CurrentScreenBounds.Height)); + } + return Result.Ok(bitmap.ToSKBitmap()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Capturer error in BitBltCapture."); + _needsInit = true; + return Result.Fail("Error while capturing BitBlt frame."); + } + } + + private void ClearDirectXOutputs() + { + foreach (var screen in _directxScreens.Values) + { + try + { + screen.Dispose(); + } + catch { } + } + _directxScreens.Clear(); + } + + private DxCaptureResult GetDirectXFrame() + { + if (!_directxScreens.TryGetValue(SelectedScreen, out var dxOutput)) + { + return DxCaptureResult.Fail("DirectX output not found."); + } + + try + { + var outputDuplication = dxOutput.OutputDuplication; + var device = dxOutput.Device; + var texture2D = dxOutput.Texture2D; + var bounds = dxOutput.Bounds; + + var result = outputDuplication.TryAcquireNextFrame(timeoutInMilliseconds: 25, out var duplicateFrameInfo, out var screenResource); + + if (!result.Success) + { + return DxCaptureResult.TryAcquireFailed(result); + } + + if (duplicateFrameInfo.AccumulatedFrames == 0) + { + try + { + outputDuplication.ReleaseFrame(); + } + catch { } + return DxCaptureResult.NoAccumulatedFrames(result); + } + + using Texture2D screenTexture2D = screenResource.QueryInterface(); + device.ImmediateContext.CopyResource(screenTexture2D, texture2D); + var dataBox = device.ImmediateContext.MapSubresource(texture2D, 0, MapMode.Read, SharpDX.Direct3D11.MapFlags.None); + using var bitmap = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb); + var bitmapData = bitmap.LockBits(bounds, ImageLockMode.WriteOnly, bitmap.PixelFormat); + var dataBoxPointer = dataBox.DataPointer; + var bitmapDataPointer = bitmapData.Scan0; + for (var y = 0; y < bounds.Height; y++) + { + Utilities.CopyMemory(bitmapDataPointer, dataBoxPointer, bounds.Width * 4); + dataBoxPointer = nint.Add(dataBoxPointer, dataBox.RowPitch); + bitmapDataPointer = nint.Add(bitmapDataPointer, bitmapData.Stride); + } + bitmap.UnlockBits(bitmapData); + device.ImmediateContext.UnmapSubresource(texture2D, 0); + screenResource?.Dispose(); + + switch (dxOutput.Rotation) + { + case DisplayModeRotation.Unspecified: + case DisplayModeRotation.Identity: + break; + case DisplayModeRotation.Rotate90: + bitmap.RotateFlip(RotateFlipType.Rotate270FlipNone); + break; + case DisplayModeRotation.Rotate180: + bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone); + break; + case DisplayModeRotation.Rotate270: + bitmap.RotateFlip(RotateFlipType.Rotate90FlipNone); + break; + default: + break; + } + IsGpuAccelerated = true; + return DxCaptureResult.Ok(bitmap.ToSKBitmap(), result); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while getting DirectX frame."); + IsGpuAccelerated = false; + } + finally + { + try + { + dxOutput.OutputDuplication.ReleaseFrame(); + } + catch { } + } + + return DxCaptureResult.Fail("Failed to get DirectX frame."); + } + + private Task HandleDisplaySettingsChanged(object subscriber, DisplaySettingsChangedMessage message) + { + _needsInit = true; + return Task.CompletedTask; + } + + private void InitDisplays() + { + _displays.Clear(); + + var displays = DisplaysEnumerationHelper.GetDisplays().ToArray(); + foreach (var display in displays) + { + _displays.AddOrUpdate(display.DeviceName, display, (k, v) => display); + } + + var primary = displays.FirstOrDefault(x => x.IsPrimary) ?? displays.First(); + SelectedScreen = primary.DeviceName; + CurrentScreenBounds = primary.MonitorArea; + } + + private void InitDirectX() + { + try + { + ClearDirectXOutputs(); + + using var factory = new Factory1(); + foreach (var adapter in factory.Adapters1.Where(x => (x.Outputs?.Length ?? 0) > 0)) + { + foreach (var output in adapter.Outputs) + { + try + { + var device = new SharpDX.Direct3D11.Device(adapter); + var output1 = output.QueryInterface(); + + var bounds = output1.Description.DesktopBounds; + var width = bounds.Right - bounds.Left; + var height = bounds.Bottom - bounds.Top; + + // Create Staging texture CPU-accessible + var textureDesc = new Texture2DDescription + { + CpuAccessFlags = CpuAccessFlags.Read, + BindFlags = BindFlags.None, + Format = Format.B8G8R8A8_UNorm, + Width = width, + Height = height, + OptionFlags = ResourceOptionFlags.None, + MipLevels = 1, + ArraySize = 1, + SampleDescription = { Count = 1, Quality = 0 }, + Usage = ResourceUsage.Staging + }; + + var texture2D = new Texture2D(device, textureDesc); + + _directxScreens.Add( + output1.Description.DeviceName, + new DirectXOutput(adapter, + device, + output1.DuplicateOutput(device), + texture2D, + output1.Description.Rotation)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while initializing DirectX."); + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while initializing DirectX."); + } + } + + private bool IsEmpty(SKBitmap bitmap) + { + if (bitmap is null) + { + return true; + } + + var height = bitmap.Height; + var width = bitmap.Width; + var bytesPerPixel = bitmap.BytesPerPixel; + + try + { + unsafe + { + byte* scan = (byte*)bitmap.GetPixels(); + + for (var row = 0; row < height; row++) + { + for (var column = 0; column < width; column++) + { + var index = row * width * bytesPerPixel + column * bytesPerPixel; + + byte* data = scan + index; + + for (var i = 0; i < bytesPerPixel; i++) + { + if (data[i] != 0) + { + return false; + } + } + } + } + + return true; + } + } + catch + { + return true; + } + } + + private class DxCaptureResult + { + public SKBitmap? Bitmap { get; init; } + public SharpDX.Result? DxResult { get; init; } + public string FailureReason { get; init; } = string.Empty; + + [MemberNotNull(nameof(Bitmap))] + public bool HadChanges { get; init; } + + public bool IsSuccess { get; init; } + + internal static DxCaptureResult Fail(string failureReason) + { + return new DxCaptureResult() + { + FailureReason = failureReason + }; + } + + internal static DxCaptureResult Fail(string failureReason, SharpDX.Result dxResult) + { + return new DxCaptureResult() + { + FailureReason = failureReason, + DxResult = dxResult + }; + } + + internal static DxCaptureResult NoAccumulatedFrames(SharpDX.Result dxResult) + { + return new DxCaptureResult() + { + FailureReason = "No frames were accumulated.", + DxResult = dxResult, + IsSuccess = true + }; + } + + internal static DxCaptureResult Ok(SKBitmap sKBitmap, SharpDX.Result result) + { + return new DxCaptureResult() + { + Bitmap = sKBitmap, + DxResult = result, + HadChanges = true, + IsSuccess = true, + }; + } + + internal static DxCaptureResult TryAcquireFailed(SharpDX.Result dxResult) + { + if (dxResult.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Code) + { + return new DxCaptureResult() + { + FailureReason = "Timed out while waiting for the next frame.", + DxResult = dxResult, + IsSuccess = true + }; + } + return new DxCaptureResult() + { + FailureReason = "TryAcquireFrame returned failure.", + DxResult = dxResult + }; + } + } +} diff --git a/Desktop.Win/Services/ShutdownServiceWin.cs b/Desktop.Win/Services/ShutdownServiceWin.cs new file mode 100644 index 000000000..4b8ca8ff2 --- /dev/null +++ b/Desktop.Win/Services/ShutdownServiceWin.cs @@ -0,0 +1,80 @@ +using Remotely.Desktop.Shared.Abstractions; +using Remotely.Desktop.Shared.Services; +using Remotely.Desktop.UI.Services; +using Remotely.Shared.Extensions; + +namespace Remotely.Desktop.Win.Services; + +public class ShutdownServiceWin : IShutdownService +{ + private readonly IDesktopHubConnection _hubConnection; + private readonly IUiDispatcher _dispatcher; + private readonly IAppState _appState; + private readonly ILogger _logger; + private readonly SemaphoreSlim _shutdownLock = new(1, 1); + + public ShutdownServiceWin( + IDesktopHubConnection hubConnection, + IUiDispatcher dispatcher, + IAppState appState, + ILogger logger) + { + _hubConnection = hubConnection; + _dispatcher = dispatcher; + _appState = appState; + _logger = logger; + } + + public async Task Shutdown() + { + using var _ = _logger.Enter(LogLevel.Information); + + try + { + if (!await _shutdownLock.WaitAsync(0)) + { + // We've made our best effort to shutdown gracefully, but WPF will + // sometimes hang indefinitely. In that case, we'll forcefully close. + _logger.LogInformation( + "Shutdown was called more than once. Forcing process exit."); + Environment.FailFast("Process hung during shutdown. Forcefully quitting on second call."); + return; + } + + _logger.LogInformation("Starting process shutdown."); + + _logger.LogInformation("Disconnecting viewers."); + await TryDisconnectViewers(); + + _logger.LogInformation("Shutting down UI dispatchers."); + _dispatcher.Shutdown(); + + Environment.Exit(0); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while shutting down."); + Environment.Exit(1); + } + finally + { + _shutdownLock.Release(); + } + } + + 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."); + } + } +} diff --git a/Desktop.Win/Startup/IServiceCollectionExtensions.cs b/Desktop.Win/Startup/IServiceCollectionExtensions.cs new file mode 100644 index 000000000..15440e8b8 --- /dev/null +++ b/Desktop.Win/Startup/IServiceCollectionExtensions.cs @@ -0,0 +1,34 @@ +using Remotely.Desktop.Shared.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Remotely.Desktop.Shared.Startup; +using Remotely.Desktop.UI.Startup; +using System.Runtime.Versioning; +using Remotely.Desktop.Win.Services; + +namespace Remotely.Desktop.Win.Startup; + +public static class IServiceCollectionExtensions +{ + /// + /// Adds Windows and cross-platform remote control services to the service collection. + /// All methods on must be called to register + /// required services. + /// + /// + /// + [SupportedOSPlatform("windows")] + public static void AddRemoteControlWindows(this IServiceCollection services) + { + services.AddRemoteControlXplat(); + services.AddRemoteControlUi(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + } +} diff --git a/Desktop.Win/Usings.cs b/Desktop.Win/Usings.cs new file mode 100644 index 000000000..fc2c0ddf1 --- /dev/null +++ b/Desktop.Win/Usings.cs @@ -0,0 +1,6 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; +global using Microsoft.Extensions.Logging; \ No newline at end of file diff --git a/README.md b/README.md index 6a2e21747..c34297e35 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,6 @@ All other configuration is done in the Server Config page once you're logged in. - Set this to -1 or increase it to a specific number to allow multi-tenancy. - RedirectToHttps: Whether ASP.NET Core will redirect all traffic from HTTP to HTTPS. This is independent of Caddy, Nginx, and IIS configurations that do the same. - RemoteControlNotifyUsers: Whether to show a notification to the end user when an unattended remote control session starts. -- RemoteControlSessionLimit: How many concurrent remote control sessions are allowed per organization. - RemoteControlRequiresAuthentication: Whether the remote control page requires authentication to establish a connection. - Require2FA: Require users to set up 2FA before they can use the main app. - Smpt-: SMTP settings for auto-generated system emails (such as registration and password reset). diff --git a/Remotely.sln b/Remotely.sln index 1c6bb4216..872e15657 100644 --- a/Remotely.sln +++ b/Remotely.sln @@ -31,8 +31,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Desktop.Linux", "Desktop.Li EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Desktop.Win", "Desktop.Win\Desktop.Win.csproj", "{6B726FC4-A907-4813-BF38-3342E02AA8D2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agent.Installer.Win", "Agent.Installer.Win\Agent.Installer.Win.csproj", "{A3D0368C-0850-4614-B5B5-41B9D5135AA9}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server.Tests", "Tests\Server.Tests\Server.Tests.csproj", "{48D9D0E6-5781-44A9-84C0-56F56C2A1193}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoadTester", "Tests\LoadTester\LoadTester.csproj", "{6C25240C-613D-4A86-A04E-784BA6726094}" @@ -51,24 +49,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0754E195 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared.Tests", "Tests\Shared.Tests\Shared.Tests.csproj", "{B6C1030D-1F74-4143-BB70-FC79C0274653}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Submodules", "Submodules", "{48C738FB-359E-43DB-B338-FD7CB1CCF6A8}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Desktop.Shared", "Desktop.Shared\Desktop.Shared.csproj", "{38099844-F6B6-4975-BEC5-D58A547145F0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Immense.RemoteControl.Desktop.Shared", "submodules\Immense.RemoteControl\Immense.RemoteControl.Desktop.Shared\Immense.RemoteControl.Desktop.Shared.csproj", "{3EB48B01-A672-4658-868B-8CA21FF73929}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Immense.RemoteControl.Desktop.Windows", "submodules\Immense.RemoteControl\Immense.RemoteControl.Desktop.Windows\Immense.RemoteControl.Desktop.Windows.csproj", "{7FA4456D-8695-4990-B20A-B897CF9DF0EF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Immense.RemoteControl.Server", "submodules\Immense.RemoteControl\Immense.RemoteControl.Server\Immense.RemoteControl.Server.csproj", "{8CBED18D-64A8-44C0-8433-EE14E93B472A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Immense.RemoteControl.Shared", "submodules\Immense.RemoteControl\Immense.RemoteControl.Shared\Immense.RemoteControl.Shared.csproj", "{FEF0D431-EB2F-4C08-A125-8DF59AFDA525}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E4D83C37-8B98-44FB-898B-9AA1BB223C66}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Immense.RemoteControl.Desktop.Linux", "submodules\Immense.RemoteControl\Immense.RemoteControl.Desktop.Linux\Immense.RemoteControl.Desktop.Linux.csproj", "{2FF27827-1F43-474E-A0E3-DA76BC598BCC}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Immense.RemoteControl.Desktop.UI", "submodules\Immense.RemoteControl\Immense.RemoteControl.Desktop.UI\Immense.RemoteControl.Desktop.UI.csproj", "{3095BA44-D5E0-42B4-9161-7F7AB8E68A10}" -EndProject Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose\docker-compose.dcproj", "{90EC49B2-B56A-4ECD-8F63-2162DD140F7C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pipelines", "pipelines", "{9484AB47-2F99-43DE-9F5D-5B8679B01B3B}" @@ -76,6 +58,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pipelines", "pipelines", "{ .azure-pipelines\Release Build.yml = .azure-pipelines\Release Build.yml EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Desktop.UI", "Desktop.UI\Desktop.UI.csproj", "{167EDC38-E455-4564-A61C-784A57B2AB0A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Desktop.Native", "Desktop.Native\Desktop.Native.csproj", "{DB48AE81-BFD4-4005-8693-365B045C8D18}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -134,18 +120,6 @@ Global {6B726FC4-A907-4813-BF38-3342E02AA8D2}.Release|x64.Build.0 = Release|x64 {6B726FC4-A907-4813-BF38-3342E02AA8D2}.Release|x86.ActiveCfg = Release|x86 {6B726FC4-A907-4813-BF38-3342E02AA8D2}.Release|x86.Build.0 = Release|x86 - {A3D0368C-0850-4614-B5B5-41B9D5135AA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A3D0368C-0850-4614-B5B5-41B9D5135AA9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A3D0368C-0850-4614-B5B5-41B9D5135AA9}.Debug|x64.ActiveCfg = Debug|Any CPU - {A3D0368C-0850-4614-B5B5-41B9D5135AA9}.Debug|x64.Build.0 = Debug|Any CPU - {A3D0368C-0850-4614-B5B5-41B9D5135AA9}.Debug|x86.ActiveCfg = Debug|Any CPU - {A3D0368C-0850-4614-B5B5-41B9D5135AA9}.Debug|x86.Build.0 = Debug|Any CPU - {A3D0368C-0850-4614-B5B5-41B9D5135AA9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A3D0368C-0850-4614-B5B5-41B9D5135AA9}.Release|Any CPU.Build.0 = Release|Any CPU - {A3D0368C-0850-4614-B5B5-41B9D5135AA9}.Release|x64.ActiveCfg = Release|Any CPU - {A3D0368C-0850-4614-B5B5-41B9D5135AA9}.Release|x64.Build.0 = Release|Any CPU - {A3D0368C-0850-4614-B5B5-41B9D5135AA9}.Release|x86.ActiveCfg = Release|Any CPU - {A3D0368C-0850-4614-B5B5-41B9D5135AA9}.Release|x86.Build.0 = Release|Any CPU {48D9D0E6-5781-44A9-84C0-56F56C2A1193}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {48D9D0E6-5781-44A9-84C0-56F56C2A1193}.Debug|Any CPU.Build.0 = Debug|Any CPU {48D9D0E6-5781-44A9-84C0-56F56C2A1193}.Debug|x64.ActiveCfg = Debug|x64 @@ -206,78 +180,6 @@ Global {38099844-F6B6-4975-BEC5-D58A547145F0}.Release|x64.Build.0 = Release|Any CPU {38099844-F6B6-4975-BEC5-D58A547145F0}.Release|x86.ActiveCfg = Release|Any CPU {38099844-F6B6-4975-BEC5-D58A547145F0}.Release|x86.Build.0 = Release|Any CPU - {3EB48B01-A672-4658-868B-8CA21FF73929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3EB48B01-A672-4658-868B-8CA21FF73929}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3EB48B01-A672-4658-868B-8CA21FF73929}.Debug|x64.ActiveCfg = Debug|Any CPU - {3EB48B01-A672-4658-868B-8CA21FF73929}.Debug|x64.Build.0 = Debug|Any CPU - {3EB48B01-A672-4658-868B-8CA21FF73929}.Debug|x86.ActiveCfg = Debug|Any CPU - {3EB48B01-A672-4658-868B-8CA21FF73929}.Debug|x86.Build.0 = Debug|Any CPU - {3EB48B01-A672-4658-868B-8CA21FF73929}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3EB48B01-A672-4658-868B-8CA21FF73929}.Release|Any CPU.Build.0 = Release|Any CPU - {3EB48B01-A672-4658-868B-8CA21FF73929}.Release|x64.ActiveCfg = Release|Any CPU - {3EB48B01-A672-4658-868B-8CA21FF73929}.Release|x64.Build.0 = Release|Any CPU - {3EB48B01-A672-4658-868B-8CA21FF73929}.Release|x86.ActiveCfg = Release|Any CPU - {3EB48B01-A672-4658-868B-8CA21FF73929}.Release|x86.Build.0 = Release|Any CPU - {7FA4456D-8695-4990-B20A-B897CF9DF0EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7FA4456D-8695-4990-B20A-B897CF9DF0EF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7FA4456D-8695-4990-B20A-B897CF9DF0EF}.Debug|x64.ActiveCfg = Debug|Any CPU - {7FA4456D-8695-4990-B20A-B897CF9DF0EF}.Debug|x64.Build.0 = Debug|Any CPU - {7FA4456D-8695-4990-B20A-B897CF9DF0EF}.Debug|x86.ActiveCfg = Debug|Any CPU - {7FA4456D-8695-4990-B20A-B897CF9DF0EF}.Debug|x86.Build.0 = Debug|Any CPU - {7FA4456D-8695-4990-B20A-B897CF9DF0EF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7FA4456D-8695-4990-B20A-B897CF9DF0EF}.Release|Any CPU.Build.0 = Release|Any CPU - {7FA4456D-8695-4990-B20A-B897CF9DF0EF}.Release|x64.ActiveCfg = Release|Any CPU - {7FA4456D-8695-4990-B20A-B897CF9DF0EF}.Release|x64.Build.0 = Release|Any CPU - {7FA4456D-8695-4990-B20A-B897CF9DF0EF}.Release|x86.ActiveCfg = Release|Any CPU - {7FA4456D-8695-4990-B20A-B897CF9DF0EF}.Release|x86.Build.0 = Release|Any CPU - {8CBED18D-64A8-44C0-8433-EE14E93B472A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8CBED18D-64A8-44C0-8433-EE14E93B472A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8CBED18D-64A8-44C0-8433-EE14E93B472A}.Debug|x64.ActiveCfg = Debug|Any CPU - {8CBED18D-64A8-44C0-8433-EE14E93B472A}.Debug|x64.Build.0 = Debug|Any CPU - {8CBED18D-64A8-44C0-8433-EE14E93B472A}.Debug|x86.ActiveCfg = Debug|Any CPU - {8CBED18D-64A8-44C0-8433-EE14E93B472A}.Debug|x86.Build.0 = Debug|Any CPU - {8CBED18D-64A8-44C0-8433-EE14E93B472A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8CBED18D-64A8-44C0-8433-EE14E93B472A}.Release|Any CPU.Build.0 = Release|Any CPU - {8CBED18D-64A8-44C0-8433-EE14E93B472A}.Release|x64.ActiveCfg = Release|Any CPU - {8CBED18D-64A8-44C0-8433-EE14E93B472A}.Release|x64.Build.0 = Release|Any CPU - {8CBED18D-64A8-44C0-8433-EE14E93B472A}.Release|x86.ActiveCfg = Release|Any CPU - {8CBED18D-64A8-44C0-8433-EE14E93B472A}.Release|x86.Build.0 = Release|Any CPU - {FEF0D431-EB2F-4C08-A125-8DF59AFDA525}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FEF0D431-EB2F-4C08-A125-8DF59AFDA525}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FEF0D431-EB2F-4C08-A125-8DF59AFDA525}.Debug|x64.ActiveCfg = Debug|Any CPU - {FEF0D431-EB2F-4C08-A125-8DF59AFDA525}.Debug|x64.Build.0 = Debug|Any CPU - {FEF0D431-EB2F-4C08-A125-8DF59AFDA525}.Debug|x86.ActiveCfg = Debug|Any CPU - {FEF0D431-EB2F-4C08-A125-8DF59AFDA525}.Debug|x86.Build.0 = Debug|Any CPU - {FEF0D431-EB2F-4C08-A125-8DF59AFDA525}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FEF0D431-EB2F-4C08-A125-8DF59AFDA525}.Release|Any CPU.Build.0 = Release|Any CPU - {FEF0D431-EB2F-4C08-A125-8DF59AFDA525}.Release|x64.ActiveCfg = Release|Any CPU - {FEF0D431-EB2F-4C08-A125-8DF59AFDA525}.Release|x64.Build.0 = Release|Any CPU - {FEF0D431-EB2F-4C08-A125-8DF59AFDA525}.Release|x86.ActiveCfg = Release|Any CPU - {FEF0D431-EB2F-4C08-A125-8DF59AFDA525}.Release|x86.Build.0 = Release|Any CPU - {2FF27827-1F43-474E-A0E3-DA76BC598BCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2FF27827-1F43-474E-A0E3-DA76BC598BCC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2FF27827-1F43-474E-A0E3-DA76BC598BCC}.Debug|x64.ActiveCfg = Debug|Any CPU - {2FF27827-1F43-474E-A0E3-DA76BC598BCC}.Debug|x64.Build.0 = Debug|Any CPU - {2FF27827-1F43-474E-A0E3-DA76BC598BCC}.Debug|x86.ActiveCfg = Debug|Any CPU - {2FF27827-1F43-474E-A0E3-DA76BC598BCC}.Debug|x86.Build.0 = Debug|Any CPU - {2FF27827-1F43-474E-A0E3-DA76BC598BCC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2FF27827-1F43-474E-A0E3-DA76BC598BCC}.Release|Any CPU.Build.0 = Release|Any CPU - {2FF27827-1F43-474E-A0E3-DA76BC598BCC}.Release|x64.ActiveCfg = Release|Any CPU - {2FF27827-1F43-474E-A0E3-DA76BC598BCC}.Release|x64.Build.0 = Release|Any CPU - {2FF27827-1F43-474E-A0E3-DA76BC598BCC}.Release|x86.ActiveCfg = Release|Any CPU - {2FF27827-1F43-474E-A0E3-DA76BC598BCC}.Release|x86.Build.0 = Release|Any CPU - {3095BA44-D5E0-42B4-9161-7F7AB8E68A10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3095BA44-D5E0-42B4-9161-7F7AB8E68A10}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3095BA44-D5E0-42B4-9161-7F7AB8E68A10}.Debug|x64.ActiveCfg = Debug|Any CPU - {3095BA44-D5E0-42B4-9161-7F7AB8E68A10}.Debug|x64.Build.0 = Debug|Any CPU - {3095BA44-D5E0-42B4-9161-7F7AB8E68A10}.Debug|x86.ActiveCfg = Debug|Any CPU - {3095BA44-D5E0-42B4-9161-7F7AB8E68A10}.Debug|x86.Build.0 = Debug|Any CPU - {3095BA44-D5E0-42B4-9161-7F7AB8E68A10}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3095BA44-D5E0-42B4-9161-7F7AB8E68A10}.Release|Any CPU.Build.0 = Release|Any CPU - {3095BA44-D5E0-42B4-9161-7F7AB8E68A10}.Release|x64.ActiveCfg = Release|Any CPU - {3095BA44-D5E0-42B4-9161-7F7AB8E68A10}.Release|x64.Build.0 = Release|Any CPU - {3095BA44-D5E0-42B4-9161-7F7AB8E68A10}.Release|x86.ActiveCfg = Release|Any CPU - {3095BA44-D5E0-42B4-9161-7F7AB8E68A10}.Release|x86.Build.0 = Release|Any CPU {90EC49B2-B56A-4ECD-8F63-2162DD140F7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {90EC49B2-B56A-4ECD-8F63-2162DD140F7C}.Debug|Any CPU.Build.0 = Debug|Any CPU {90EC49B2-B56A-4ECD-8F63-2162DD140F7C}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -290,6 +192,30 @@ Global {90EC49B2-B56A-4ECD-8F63-2162DD140F7C}.Release|x64.Build.0 = Release|Any CPU {90EC49B2-B56A-4ECD-8F63-2162DD140F7C}.Release|x86.ActiveCfg = Release|Any CPU {90EC49B2-B56A-4ECD-8F63-2162DD140F7C}.Release|x86.Build.0 = Release|Any CPU + {167EDC38-E455-4564-A61C-784A57B2AB0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {167EDC38-E455-4564-A61C-784A57B2AB0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {167EDC38-E455-4564-A61C-784A57B2AB0A}.Debug|x64.ActiveCfg = Debug|Any CPU + {167EDC38-E455-4564-A61C-784A57B2AB0A}.Debug|x64.Build.0 = Debug|Any CPU + {167EDC38-E455-4564-A61C-784A57B2AB0A}.Debug|x86.ActiveCfg = Debug|Any CPU + {167EDC38-E455-4564-A61C-784A57B2AB0A}.Debug|x86.Build.0 = Debug|Any CPU + {167EDC38-E455-4564-A61C-784A57B2AB0A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {167EDC38-E455-4564-A61C-784A57B2AB0A}.Release|Any CPU.Build.0 = Release|Any CPU + {167EDC38-E455-4564-A61C-784A57B2AB0A}.Release|x64.ActiveCfg = Release|Any CPU + {167EDC38-E455-4564-A61C-784A57B2AB0A}.Release|x64.Build.0 = Release|Any CPU + {167EDC38-E455-4564-A61C-784A57B2AB0A}.Release|x86.ActiveCfg = Release|Any CPU + {167EDC38-E455-4564-A61C-784A57B2AB0A}.Release|x86.Build.0 = Release|Any CPU + {DB48AE81-BFD4-4005-8693-365B045C8D18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB48AE81-BFD4-4005-8693-365B045C8D18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB48AE81-BFD4-4005-8693-365B045C8D18}.Debug|x64.ActiveCfg = Debug|Any CPU + {DB48AE81-BFD4-4005-8693-365B045C8D18}.Debug|x64.Build.0 = Debug|Any CPU + {DB48AE81-BFD4-4005-8693-365B045C8D18}.Debug|x86.ActiveCfg = Debug|Any CPU + {DB48AE81-BFD4-4005-8693-365B045C8D18}.Debug|x86.Build.0 = Debug|Any CPU + {DB48AE81-BFD4-4005-8693-365B045C8D18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB48AE81-BFD4-4005-8693-365B045C8D18}.Release|Any CPU.Build.0 = Release|Any CPU + {DB48AE81-BFD4-4005-8693-365B045C8D18}.Release|x64.ActiveCfg = Release|Any CPU + {DB48AE81-BFD4-4005-8693-365B045C8D18}.Release|x64.Build.0 = Release|Any CPU + {DB48AE81-BFD4-4005-8693-365B045C8D18}.Release|x86.ActiveCfg = Release|Any CPU + {DB48AE81-BFD4-4005-8693-365B045C8D18}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -298,13 +224,6 @@ Global {48D9D0E6-5781-44A9-84C0-56F56C2A1193} = {0754E195-7080-4AAC-B5A3-A9923B1283CE} {6C25240C-613D-4A86-A04E-784BA6726094} = {0754E195-7080-4AAC-B5A3-A9923B1283CE} {B6C1030D-1F74-4143-BB70-FC79C0274653} = {0754E195-7080-4AAC-B5A3-A9923B1283CE} - {3EB48B01-A672-4658-868B-8CA21FF73929} = {48C738FB-359E-43DB-B338-FD7CB1CCF6A8} - {7FA4456D-8695-4990-B20A-B897CF9DF0EF} = {48C738FB-359E-43DB-B338-FD7CB1CCF6A8} - {8CBED18D-64A8-44C0-8433-EE14E93B472A} = {48C738FB-359E-43DB-B338-FD7CB1CCF6A8} - {FEF0D431-EB2F-4C08-A125-8DF59AFDA525} = {48C738FB-359E-43DB-B338-FD7CB1CCF6A8} - {E4D83C37-8B98-44FB-898B-9AA1BB223C66} = {48C738FB-359E-43DB-B338-FD7CB1CCF6A8} - {2FF27827-1F43-474E-A0E3-DA76BC598BCC} = {48C738FB-359E-43DB-B338-FD7CB1CCF6A8} - {3095BA44-D5E0-42B4-9161-7F7AB8E68A10} = {48C738FB-359E-43DB-B338-FD7CB1CCF6A8} {9484AB47-2F99-43DE-9F5D-5B8679B01B3B} = {2176596E-12DA-4766-96E1-4D23EA7DBEC8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/Server/API/AlertsController.cs b/Server/API/AlertsController.cs index 278a94d29..11aa3e15b 100644 --- a/Server/API/AlertsController.cs +++ b/Server/API/AlertsController.cs @@ -1,4 +1,4 @@ -using Immense.RemoteControl.Shared.Extensions; +using Remotely.Shared.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Build.Framework; using Microsoft.Extensions.Logging; diff --git a/Server/API/BrandingController.cs b/Server/API/BrandingController.cs index 696bf0359..d455cf9ce 100644 --- a/Server/API/BrandingController.cs +++ b/Server/API/BrandingController.cs @@ -1,4 +1,4 @@ -using Immense.RemoteControl.Shared.Extensions; +using Remotely.Shared.Extensions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Build.Framework; diff --git a/Server/API/DevicesController.cs b/Server/API/DevicesController.cs index 40fe8c0b7..8934d25a5 100644 --- a/Server/API/DevicesController.cs +++ b/Server/API/DevicesController.cs @@ -1,4 +1,4 @@ -using Immense.RemoteControl.Shared.Extensions; +using Remotely.Shared.Extensions; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; diff --git a/Server/API/LoginController.cs b/Server/API/LoginController.cs index e497f1b70..45e247d77 100644 --- a/Server/API/LoginController.cs +++ b/Server/API/LoginController.cs @@ -1,17 +1,10 @@ -using Immense.RemoteControl.Server.Hubs; -using Immense.RemoteControl.Server.Services; +using Remotely.Server.Hubs; +using Remotely.Server.Services; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; -using Microsoft.Build.Framework; -using Microsoft.Extensions.Logging; -using Remotely.Server.Hubs; using Remotely.Server.Models; -using Remotely.Server.Services; using Remotely.Shared.Entities; -using System; -using System.Linq; -using System.Threading.Tasks; namespace Remotely.Server.API; diff --git a/Server/API/OrganizationManagementController.cs b/Server/API/OrganizationManagementController.cs index ee804c29c..66942521e 100644 --- a/Server/API/OrganizationManagementController.cs +++ b/Server/API/OrganizationManagementController.cs @@ -1,4 +1,4 @@ -using Immense.RemoteControl.Shared.Extensions; +using Remotely.Shared.Extensions; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; diff --git a/Server/API/RemoteControlController.cs b/Server/API/RemoteControlController.cs index 8cd83ca16..15c58953b 100644 --- a/Server/API/RemoteControlController.cs +++ b/Server/API/RemoteControlController.cs @@ -4,14 +4,8 @@ using Remotely.Server.Hubs; using Remotely.Server.Models; using Remotely.Server.Services; -using System; -using System.Linq; -using System.Threading.Tasks; using Remotely.Server.Auth; -using Immense.RemoteControl.Server.Services; -using Immense.RemoteControl.Server.Abstractions; -using Immense.RemoteControl.Shared.Helpers; -using Microsoft.Extensions.Logging; +using Remotely.Shared.Helpers; using Remotely.Server.Extensions; using Remotely.Shared.Entities; using Remotely.Shared.Interfaces; @@ -29,7 +23,6 @@ public class RemoteControlController : ControllerBase private readonly IAgentHubSessionCache _serviceSessionCache; private readonly IDataService _dataService; private readonly IOtpProvider _otpProvider; - private readonly IHubEventHandler _hubEvents; private readonly SignInManager _signInManager; private readonly ILogger _logger; @@ -40,7 +33,6 @@ public RemoteControlController( IHubContext agentHub, IAgentHubSessionCache serviceSessionCache, IOtpProvider otpProvider, - IHubEventHandler hubEvents, ILogger logger) { _dataService = dataService; @@ -48,7 +40,6 @@ public RemoteControlController( _remoteControlSessionCache = remoteControlSessionCache; _serviceSessionCache = serviceSessionCache; _otpProvider = otpProvider; - _hubEvents = hubEvents; _signInManager = signInManager; _logger = logger; } @@ -139,20 +130,12 @@ private async Task InitiateRemoteControl(string deviceID, string } } - var sessionCount = _remoteControlSessionCache.Sessions - .OfType() - .Count(x => x.OrganizationId == orgId); - - var settings = await _dataService.GetSettings(); - if (sessionCount > settings.RemoteControlSessionLimit) - { - return BadRequest("There are already the maximum amount of active remote control sessions for your organization."); - } + var sessionCount = _remoteControlSessionCache.Sessions.Count(x => x.OrganizationId == orgId); var sessionId = Guid.NewGuid(); var accessKey = RandomGenerator.GenerateAccessKey(); - var session = new RemoteControlSessionEx() + var session = new RemoteControlSession() { UnattendedSessionId = sessionId, UserConnectionId = HttpContext.Connection.Id, @@ -163,13 +146,8 @@ private async Task InitiateRemoteControl(string deviceID, string _remoteControlSessionCache.AddOrUpdate($"{sessionId}", session, (k, v) => { - if (v is RemoteControlSessionEx ex) - { - ex.AgentConnectionId = HttpContext.Connection.Id; - return ex; - } - v.Dispose(); - return session; + v.AgentConnectionId = HttpContext.Connection.Id; + return v; }); var orgNameResult = await _dataService.GetOrganizationNameById(orgId); @@ -195,6 +173,6 @@ await _agentHub.Clients.Client(serviceConnectionId).RemoteControl( var otp = _otpProvider.GetOtp(targetDevice.ID); - return Ok($"{HttpContext.Request.Scheme}://{Request.Host}/RemoteControl/Viewer?mode=Unattended&sessionId={sessionId}&accessKey={accessKey}&otp={otp}"); + return Ok($"{HttpContext.Request.Scheme}://{Request.Host}/Viewer?mode=Unattended&sessionId={sessionId}&accessKey={accessKey}&otp={otp}"); } } diff --git a/Server/API/ScriptResultsController.cs b/Server/API/ScriptResultsController.cs index be417443a..662e82072 100644 --- a/Server/API/ScriptResultsController.cs +++ b/Server/API/ScriptResultsController.cs @@ -1,4 +1,4 @@ -using Immense.RemoteControl.Shared.Extensions; +using Remotely.Shared.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Build.Framework; using Microsoft.EntityFrameworkCore; diff --git a/Server/API/ScriptingController.cs b/Server/API/ScriptingController.cs index 59c6f2710..06dfc96cd 100644 --- a/Server/API/ScriptingController.cs +++ b/Server/API/ScriptingController.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Remotely.Shared.Enums; using Remotely.Server.Auth; -using Immense.RemoteControl.Shared.Helpers; +using Remotely.Shared.Helpers; using Remotely.Shared; using Remotely.Server.Extensions; using Remotely.Shared.Entities; diff --git a/Server/Auth/ApiAuthorizationFilter.cs b/Server/Auth/ApiAuthorizationFilter.cs index 3eb306c16..d09f5d2c6 100644 --- a/Server/Auth/ApiAuthorizationFilter.cs +++ b/Server/Auth/ApiAuthorizationFilter.cs @@ -1,4 +1,4 @@ -using Immense.RemoteControl.Shared.Extensions; +using Remotely.Shared.Extensions; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; diff --git a/Server/Components/Devices/ChatCard.razor.cs b/Server/Components/Devices/ChatCard.razor.cs index 3379a6120..c6de7557c 100644 --- a/Server/Components/Devices/ChatCard.razor.cs +++ b/Server/Components/Devices/ChatCard.razor.cs @@ -1,5 +1,4 @@ -using Immense.SimpleMessenger; -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Remotely.Server.Hubs; using Remotely.Server.Models.Messages; @@ -7,8 +6,6 @@ using Remotely.Server.Services.Stores; using Remotely.Shared.Enums; using Remotely.Shared.ViewModels; -using System; -using System.Threading.Tasks; namespace Remotely.Server.Components.Devices; diff --git a/Server/Components/Devices/ChatFrame.razor.cs b/Server/Components/Devices/ChatFrame.razor.cs index db8b98ece..9120447d9 100644 --- a/Server/Components/Devices/ChatFrame.razor.cs +++ b/Server/Components/Devices/ChatFrame.razor.cs @@ -1,4 +1,4 @@ -using Immense.SimpleMessenger; +using Bitbound.SimpleMessenger; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Remotely.Server.Hubs; diff --git a/Server/Components/Devices/DeviceCard.razor.cs b/Server/Components/Devices/DeviceCard.razor.cs index df5a66913..ccd14236c 100644 --- a/Server/Components/Devices/DeviceCard.razor.cs +++ b/Server/Components/Devices/DeviceCard.razor.cs @@ -1,4 +1,4 @@ -using Immense.SimpleMessenger; +using Bitbound.SimpleMessenger; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Web; @@ -334,7 +334,7 @@ private async Task StartRemoteControl(bool viewOnly) } JsInterop.OpenWindow( - $"/RemoteControl/Viewer" + + $"/Viewer" + $"?mode=Unattended&sessionId={session.UnattendedSessionId}" + $"&accessKey={session.AccessKey}" + $"&viewonly={viewOnly}", diff --git a/Server/Components/Devices/DevicesFrame.razor.cs b/Server/Components/Devices/DevicesFrame.razor.cs index acb2ac3fc..29a249d10 100644 --- a/Server/Components/Devices/DevicesFrame.razor.cs +++ b/Server/Components/Devices/DevicesFrame.razor.cs @@ -1,4 +1,4 @@ -using Immense.SimpleMessenger; +using Bitbound.SimpleMessenger; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components; using Microsoft.Build.Framework; diff --git a/Server/Components/Devices/Terminal.razor.cs b/Server/Components/Devices/Terminal.razor.cs index 199046092..7608d2d65 100644 --- a/Server/Components/Devices/Terminal.razor.cs +++ b/Server/Components/Devices/Terminal.razor.cs @@ -1,9 +1,6 @@ -using Immense.RemoteControl.Server.Abstractions; -using Immense.SimpleMessenger; -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Web; -using Microsoft.Extensions.Logging; using Remotely.Server.Components.ModalContents; using Remotely.Server.Hubs; using Remotely.Server.Models.Messages; @@ -13,10 +10,6 @@ using Remotely.Shared.Enums; using Remotely.Shared.Models; using Remotely.Shared.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Remotely.Server.Components.Devices; @@ -122,7 +115,7 @@ private void ApplyCompletion(PwshCommandCompletion completion) var match = completion.CompletionMatches[completion.CurrentMatchIndex]; var replacementText = string.Concat( - _lastCompletionInput.Substring(0, completion.ReplacementIndex), + _lastCompletionInput[..completion.ReplacementIndex], match.CompletionText, _lastCompletionInput[(completion.ReplacementIndex + completion.ReplacementLength)..]); @@ -170,7 +163,7 @@ private void EvaluateInputKeypress(KeyboardEventArgs ev) } var devices = CardStore.SelectedDevices.ToArray(); - if (!devices.Any()) + if (devices.Length == 0) { ToastService.ShowToast("You must select at least one device.", classString: "bg-warning"); return; @@ -281,7 +274,7 @@ private async Task ShowQuickScripts() EnsureUserSet(); var quickScripts = await DataService.GetQuickScripts(User.Id); - if (quickScripts?.Any() != true) + if (quickScripts.Count == 0) { ToastService.ShowToast("No quick scripts saved.", classString: "bg-warning"); return; @@ -307,8 +300,8 @@ void showModal(RenderTreeBuilder builder) private void ShowTerminalHelp() { - ModalService.ShowModal("Terminal Help", new[] - { + ModalService.ShowModal("Terminal Help", + [ "Enter terminal commands that will execute on all selected devices.", "Tab completion is available for PowerShell Core (PSCore) and Windows PowerShell (WinPS). Tab and Shift + Tab " + @@ -324,7 +317,7 @@ private void ShowTerminalHelp() "Note: The first PS Core command or tab completion takes a few moments while the service is " + "starting on the remote device." - }); + ]); } private async void TerminalStore_TerminalLinesChanged(object? sender, EventArgs e) diff --git a/Server/Components/Layout/NavMenu.razor b/Server/Components/Layout/NavMenu.razor index 585ab1352..10c1e3286 100644 --- a/Server/Components/Layout/NavMenu.razor +++ b/Server/Components/Layout/NavMenu.razor @@ -40,7 +40,7 @@