.Net version detection fix

optimization
This commit is contained in:
Dimitrij
2025-09-15 23:32:06 +01:00
parent 6580e1b6db
commit 31ecbaa977
4 changed files with 233 additions and 144 deletions

View File

@@ -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");
/// <summary>
/// The main entry point for the application.
/// </summary>
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<bool> 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<bool> 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<bool> 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;
}
}
}
}

View File

@@ -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
/// <summary>
/// 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
/// </summary>
public static async Task<bool> IsDotnetRuntimeInstalled(string version)
/// <returns>The version string (e.g., "v9.0.0") or null if not found</returns>
[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
}
}

View File

@@ -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;
}
}
}
}

View File

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