diff --git a/mRemoteNG/App/ProgramRoot.cs b/mRemoteNG/App/ProgramRoot.cs index 848b4fe7c..25736af7e 100644 --- a/mRemoteNG/App/ProgramRoot.cs +++ b/mRemoteNG/App/ProgramRoot.cs @@ -1,4 +1,6 @@ -using mRemoteNG.Config.Settings; +using mRemoteNG.App.Update; +using mRemoteNG.Config.Settings; +using mRemoteNG.DotNet.Update; using mRemoteNG.DotNet.Update; using mRemoteNG.UI.Forms; @@ -10,9 +12,8 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Threading; -using System.Windows.Forms; using System.Threading.Tasks; -using mRemoteNG.DotNet.Update; +using System.Windows.Forms; @@ -21,102 +22,85 @@ namespace mRemoteNG.App [SupportedOSPlatform("windows")] public static class ProgramRoot { - private static Mutex _mutex; + private static Mutex? _mutex; private static FrmSplashScreenNew _frmSplashScreen = null; private static string customResourcePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Languages"); - /// - /// The main entry point for the application. - /// + private static System.Threading.Thread? _wpfSplashThread; + private static FrmSplashScreenNew? _wpfSplash; + [STAThread] public static void Main(string[] args) { - try + // Ensure the real entry point is definitely STA + MainAsync(args).GetAwaiter().GetResult(); + } + + private static Task MainAsync(string[] args) + { + AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve; + + string? installedVersion = DotNetRuntimeCheck.GetLatestDotNetRuntimeVersion(); + + if (InternetConnection.IsPosible()) { - // FIX: Awaited Task synchronously to obtain bool result - bool isInstalled = DotNetRuntimeCheck - .IsDotnetRuntimeInstalled(DotNetRuntimeCheck.RequiredDotnetVersion) - .GetAwaiter() - .GetResult(); + var (latestRuntimeVersion, downloadUrl) = DotNetRuntimeCheck.GetLatestAvailableDotNetVersionAsync().GetAwaiter().GetResult(); - if (!isInstalled) + if (string.IsNullOrEmpty(installedVersion)) { - Trace.WriteLine($".NET Desktop Runtime {DotNetRuntimeCheck.RequiredDotnetVersion} is NOT installed."); - Trace.WriteLine("Please download and install it from:"); - Trace.WriteLine("https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-9.0.8-windows-x64-installer"); - try { - MessageBox.Show( - $".NET Desktop Runtime {DotNetRuntimeCheck.RequiredDotnetVersion} is required.\n" + - "The application will now exit.\n\nDownload:\nhttps://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-9.0.8-windows-x64-installer", - "Missing .NET Runtime", + _ = MessageBox.Show( + $".NET Desktop Runtime at least {DotNetRuntimeCheck.RequiredDotnetVersion}.0 is required.\n" + + "The application will now exit.\n\nPlease download and install latest desktop runtime:\n" + downloadUrl, + "Missing .NET " + DotNetRuntimeCheck.RequiredDotnetVersion + " Runtime", MessageBoxButtons.OK, MessageBoxIcon.Error); - } catch { - // Ignore UI issues + + try + { + Process.Start(new ProcessStartInfo(fileName: downloadUrl) { UseShellExecute = true }); + } + catch (Exception ex) + { + MessageBox.Show($"Unable to open download link: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + Environment.Exit(0); } - - Environment.Exit(1); - return; + catch { } } - - Trace.WriteLine($".NET Desktop Runtime {DotNetRuntimeCheck.RequiredDotnetVersion} is installed."); - } catch (Exception ex) { - Trace.WriteLine("Runtime check failed: " + ex); - Environment.Exit(1); - return; } - Trace.WriteLine("!!!!!!=============== TEST ==================!!!!!!!!!!!!!"); + Lazy singleInstanceOption = new(() => Properties.OptionsStartupExitPage.Default.SingleInstance); + if (singleInstanceOption.Value) + StartApplicationAsSingleInstance(); + else + StartApplication(); + + return Task.CompletedTask; + } + + // Assembly resolve handler + private static Assembly? OnAssemblyResolve(object? sender, ResolveEventArgs args) + { try { - string assemblyFile = "System.Configuration.ConfigurationManager.dll"; + string assemblyName = new AssemblyName(args.Name).Name ?? string.Empty; + if (assemblyName.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) + return null; + + string assemblyFile = assemblyName + ".dll"; string assemblyPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assemblies", assemblyFile); if (File.Exists(assemblyPath)) - { - Assembly.LoadFrom(assemblyPath); - } + return Assembly.LoadFrom(assemblyPath); } - catch (FileNotFoundException ex) + catch { - Trace.WriteLine("Error occured: " + ex.Message); - } - - AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - string runtimeVersion = RuntimeInformation.FrameworkDescription; - if (runtimeVersion.Contains(".NET 9.0.2")) - { - Console.WriteLine(".NET Desktop Runtime 9.0.2 is already installed."); - } - else - { - Console.WriteLine(".NET Desktop Runtime 9.0.2 is not installed. Please download and install it from the following link:"); - Console.WriteLine("https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-9.0.2-windows-x64-installer"); - Console.WriteLine("After installation, please restart the application."); - } - } - else - { - Console.WriteLine("This application requires the .NET Desktop Runtime 9.0.2 on Windows."); - } - - CheckLockalDB(); - - Lazy singleInstanceOption = new(() => Properties.OptionsStartupExitPage.Default.SingleInstance); - - if (singleInstanceOption.Value) - { - StartApplicationAsSingleInstance(); - } - else - { - StartApplication(); + // Suppress resolution exceptions; return null to continue standard probing } + return null; } private static void CheckLockalDB() @@ -124,36 +108,13 @@ namespace mRemoteNG.App LocalDBManager settingsManager = new LocalDBManager(dbPath: "mRemoteNG.appSettings", useEncryption: false, schemaFilePath: ""); } - private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs resolveArgs) - { - string assemblyName = new AssemblyName(resolveArgs.Name).Name.Replace(".resources", string.Empty); - string assemblyFile = assemblyName + ".dll"; - string assemblyPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assemblies", assemblyFile); - - if (File.Exists(assemblyPath)) - { - return Assembly.LoadFrom(assemblyPath); - } - return null; - } - private static void StartApplication() { CatchAllUnhandledExceptions(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); - _frmSplashScreen = FrmSplashScreenNew.GetInstance(); - - Screen targetScreen = Screen.PrimaryScreen; - - Rectangle viewport = targetScreen.WorkingArea; - _frmSplashScreen.Top = viewport.Top; - _frmSplashScreen.Left = viewport.Left; - _frmSplashScreen.Left = viewport.Left + (targetScreen.Bounds.Size.Width - _frmSplashScreen.Width) / 2; - _frmSplashScreen.Top = viewport.Top + (targetScreen.Bounds.Size.Height - _frmSplashScreen.Height) / 2; - _frmSplashScreen.ShowInTaskbar = false; - _frmSplashScreen.Show(); + ShowSplashOnStaThread(); Application.Run(FrmMain.Default); } @@ -210,10 +171,8 @@ namespace mRemoteNG.App private static void ApplicationOnThreadException(object sender, ThreadExceptionEventArgs e) { - FrmSplashScreenNew.GetInstance().Close(); - + CloseSplash(); if (FrmMain.Default.IsDisposed) return; - FrmUnhandledException window = new(e.Exception, false); window.ShowDialog(FrmMain.Default); } @@ -223,5 +182,38 @@ namespace mRemoteNG.App FrmUnhandledException window = new(e.ExceptionObject as Exception, e.IsTerminating); window.ShowDialog(FrmMain.Default); } + + private static void ShowSplashOnStaThread() + { + _wpfSplashThread = new System.Threading.Thread(() => + { + _wpfSplash = FrmSplashScreenNew.GetInstance(); + + // Center the splash screen on the primary screen before showing it + _wpfSplash.WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen; + + _wpfSplash.ShowInTaskbar = false; + _wpfSplash.Show(); + System.Windows.Forms.Integration.ElementHost.EnableModelessKeyboardInterop(_wpfSplash); + System.Windows.Threading.Dispatcher.Run(); // WPF message loop + }) + { IsBackground = true }; + _wpfSplashThread.SetApartmentState(System.Threading.ApartmentState.STA); + _wpfSplashThread.Start(); + } + + private static void CloseSplash() + { + if (_wpfSplash != null) + { + _wpfSplash.Dispatcher.Invoke(() => _wpfSplash.Close()); + _wpfSplash = null; + } + if (_wpfSplashThread != null) + { + _wpfSplashThread.Join(); + _wpfSplashThread = null; + } + } } } \ No newline at end of file diff --git a/mRemoteNG/App/Update/DotNetRuntimeCheck.cs b/mRemoteNG/App/Update/DotNetRuntimeCheck.cs index 019343a82..369ea42f4 100644 --- a/mRemoteNG/App/Update/DotNetRuntimeCheck.cs +++ b/mRemoteNG/App/Update/DotNetRuntimeCheck.cs @@ -1,66 +1,107 @@ -using System; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; +using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http; +using System.Runtime.Versioning; using System.Threading.Tasks; +using System.Windows.Forms; namespace mRemoteNG.DotNet.Update { + [SupportedOSPlatform("windows")] public class DotNetRuntimeCheck { - public const string RequiredDotnetVersion = "9.0.8"; - public const string DotnetInstallerUrl = "https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-9.0.8-windows-x64-installer"; - public const string DotnetInstallerFileName = "windowsdesktop-runtime-9.0.8-win-x64.exe"; + public const string RequiredDotnetVersion = "9.0"; + private const string ReleaseFeedUrl = "https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json"; - public static async Task Main(string[] args) - { - if (await IsDotnetRuntimeInstalled(RequiredDotnetVersion)) - { - Console.WriteLine($".NET Desktop Runtime {RequiredDotnetVersion} is installed. Launching application..."); - } - else - { - Console.WriteLine($".NET Desktop Runtime {RequiredDotnetVersion} is not installed."); - } - } + #region Installed Version Check /// - /// Checks if a specific version of the .NET runtime is installed by running `dotnet --list-runtimes`. + /// Gets the installed .NET 9 runtime version if present /// - public static async Task IsDotnetRuntimeInstalled(string version) + /// The version string (e.g., "v9.0.0") or null if not found + [SupportedOSPlatform("windows")] + public static string? GetLatestDotNetRuntimeVersion() + { + string[] registryPaths = new[] + { + @"SOFTWARE\dotnet\Setup\InstalledVersions\x86", + @"SOFTWARE\dotnet\Setup\InstalledVersions\x64" + }; + + foreach (string path in registryPaths) { try { - // Set up a process to run the 'dotnet' command. - var process = new Process + using RegistryKey? key = Registry.LocalMachine.OpenSubKey(path); + if (key == null) { - StartInfo = new ProcessStartInfo + continue; + } + + // Check for the "sharedhost" subkey + using (RegistryKey? sharedHostKey = key.OpenSubKey("sharedhost")) + { + if (sharedHostKey == null) { + continue; + }; + + // Look for the "Version" value in sharedhost + object? versionValue = sharedHostKey.GetValue("Version"); + if (versionValue != null) { - FileName = "dotnet", - Arguments = "--list-runtimes", - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, + string? version = versionValue.ToString(); + if (!string.IsNullOrWhiteSpace(version)) + { + return version; + } } - }; - - process.Start(); - - // Read the output from the command. - var output = await process.StandardOutput.ReadToEndAsync(); - process.WaitForExit(); - - // Check if the output contains the required runtime and version. - // The format is typically: Microsoft.NETCore.App 9.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] - return output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) - .Any(line => line.Trim().StartsWith($"Microsoft.NETCore.App {version}") || - line.Trim().StartsWith($"Microsoft.WindowsDesktop.App {version}")); + } } catch (Exception ex) { - Console.WriteLine($"Could not check .NET runtimes. Please ensure 'dotnet' is in your PATH. Error: {ex.Message}"); - return false; + Console.WriteLine($"Error checking registry fallback: {ex.Message}"); } } - } //Check + + return null; + } + #endregion Installed Version Check + #region Latest Online Version Check + public static async Task<(string latestRuntimeVersion, string downloadUrl)> GetLatestAvailableDotNetVersionAsync() + { + try + { + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("User-Agent", "DotNetRuntimeChecker"); + + string jsonContent = await httpClient.GetStringAsync(ReleaseFeedUrl); + JObject releasesIndex = JObject.Parse(jsonContent); + + // Find the entry for .NET matching RequiredDotnetVersion + JToken? dotnetEntry = releasesIndex["releases-index"]?.FirstOrDefault(entry => entry["channel-version"]?.ToString() == RequiredDotnetVersion); + + if (dotnetEntry != null && dotnetEntry["latest-runtime"] != null) + { + string? latestRuntimeVersion = dotnetEntry["latest-runtime"]?.ToString(); + if (!string.IsNullOrEmpty(latestRuntimeVersion)) + { + // Construct the download URL using the latest version + string downloadUrl = $"https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-{latestRuntimeVersion}-windows-x64-installer"; + return (latestRuntimeVersion, downloadUrl); + } + } + + return ("Unknown", ""); + } + catch (Exception ex) + { + Console.WriteLine($"Error fetching latest version: {ex.Message}"); + return ("Unknown", ""); + } + } + #endregion Latest Online Version Check + } } diff --git a/mRemoteNG/App/Update/InternetConnection.cs b/mRemoteNG/App/Update/InternetConnection.cs new file mode 100644 index 000000000..b92a8d25d --- /dev/null +++ b/mRemoteNG/App/Update/InternetConnection.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace mRemoteNG.App.Update +{ + public class InternetConnection + { + public static bool IsPosible() + { + try + { + using var client = new HttpClient(); + client.Timeout = TimeSpan.FromSeconds(5); + return client.GetAsync("https://www.microsoft.com").Result.IsSuccessStatusCode; + } + catch (Exception) + { + return false; + } + } + } +} diff --git a/mRemoteNG/UI/Forms/frmMain.cs b/mRemoteNG/UI/Forms/frmMain.cs index 23e5ac965..789191055 100644 --- a/mRemoteNG/UI/Forms/frmMain.cs +++ b/mRemoteNG/UI/Forms/frmMain.cs @@ -34,6 +34,7 @@ using mRemoteNG.UI.Controls; using mRemoteNG.Resources.Language; using System.Runtime.Versioning; using mRemoteNG.Config.Settings.Registry; +using System.Threading; // ADDED #endregion // ReSharper disable MemberCanBePrivate.Global @@ -43,7 +44,32 @@ namespace mRemoteNG.UI.Forms [SupportedOSPlatform("windows")] public partial class FrmMain { - public static FrmMain Default { get; } = new FrmMain(); + // CHANGED: lazy, thread-safe, STA-enforced initialization + private static readonly Lazy s_default = + new(InitializeOnSta, LazyThreadSafetyMode.ExecutionAndPublication); + + public static FrmMain Default => s_default.Value; + + public static bool IsCreated => s_default.IsValueCreated; + + private static FrmMain InitializeOnSta() + { + // Enforce STA to avoid OLE/WinForms threading violations + if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) + { + // If we're already on a WinForms UI thread with a sync context, marshal to it + if (SynchronizationContext.Current is WindowsFormsSynchronizationContext ctx) + { + FrmMain created = null; + ctx.Send(_ => created = new FrmMain(), null); + return created!; + } + + throw new ThreadStateException("FrmMain must be created on an STA thread."); + } + + return new FrmMain(); + } private static ClipboardchangeEventHandler _clipboardChangedEvent; private bool _inSizeMove; @@ -208,7 +234,11 @@ namespace mRemoteNG.UI.Forms pnlDock.ShowDocumentIcon = true; - FrmSplashScreenNew.GetInstance().Close(); + FrmSplashScreenNew splash = FrmSplashScreenNew.GetInstance(); + if (splash.Dispatcher.CheckAccess()) + splash.Close(); + else + splash.Dispatcher.Invoke(() => splash.Close()); if (Properties.OptionsStartupExitPage.Default.StartMinimized) {