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)
{