From bae86ce75d2ef08b64985521755e8e8420f3e63d Mon Sep 17 00:00:00 2001 From: Mike Crowley Date: Fri, 31 Jul 2026 14:04:21 -0400 Subject: [PATCH] Fix Open Registry actions on non-English Windows (#11) regedit.exe resolves its LastKey value against the *localized* name of the tree root node. That name comes from a string resource in regedit.exe.mui, so it is "Computer" on en-US but "Ordinateur" on fr-FR, and so on. Because every call site hard coded "Computer\", regedit silently fell back to the root node instead of navigating to the requested key on any Windows whose display language is not English. regedit also accepts a LastKey without a root node prefix and prepends the correct localized name itself, so the prefix is now stripped rather than guessed: * add Helper.NormalizeRegistryPath(), which drops a leading root node segment unless the path already starts at a hive (HKEY_*, HKLM, ...) * drop the hard coded "Computer\" from all 8 Helper.OpenRegistry() call sites in MainWindow.xaml.cs Also switch the Applets\Regedit key from OpenSubKey to CreateSubKey. The key does not exist on a profile where regedit.exe has never been started, and the null conditional SetValue silently did nothing in that case. Second part of #11: add a "-> Device Inventory Agent folder" entry to the Open Folder menu, pointing at %ProgramW6432%\Microsoft Device Inventory Agent. Co-Authored-By: Claude Opus 5 --- SyncMLViewer/Helper.cs | 49 +++++++++++++++++++++++++++++++-- SyncMLViewer/MainWindow.xaml | 1 + SyncMLViewer/MainWindow.xaml.cs | 26 +++++++++++------ 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/SyncMLViewer/Helper.cs b/SyncMLViewer/Helper.cs index 88d3347..9e80f8e 100644 --- a/SyncMLViewer/Helper.cs +++ b/SyncMLViewer/Helper.cs @@ -18,12 +18,57 @@ namespace SyncMLViewer { internal static class Helper { + // The tree root node shown by regedit.exe comes from a localized string resource in + // regedit.exe.mui, it reads "Computer" on en-US and "Ordinateur" on fr-FR for example. + // Everything below the root node (the hive names) is not localized. + private static readonly string[] RegistryHiveNames = + { + "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER", "HKEY_CLASSES_ROOT", "HKEY_USERS", + "HKEY_CURRENT_CONFIG", "HKEY_PERFORMANCE_DATA", "HKEY_DYN_DATA", + "HKLM", "HKCU", "HKCR", "HKU", "HKCC" + }; + + /// + /// Removes the tree root node from a registry path so that the path becomes independent of + /// the Windows display language. + /// + /// + /// regedit.exe matches the first segment of its LastKey value against the localized name of + /// its tree root node. A hard coded English "Computer\" therefore only resolves on an + /// English Windows, on every other display language regedit silently falls back to the root + /// node instead of navigating to the requested key. regedit happily accepts a LastKey value + /// without any root node prefix and prepends the correct localized name itself, so dropping + /// the prefix works on all languages. + /// + internal static string NormalizeRegistryPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return string.Empty; + } + + var normalized = path.Trim().Trim('\\'); + var separatorIndex = normalized.IndexOf('\\'); + var firstSegment = separatorIndex < 0 ? normalized : normalized.Substring(0, separatorIndex); + + // already rooted at a hive, nothing to strip + if (RegistryHiveNames.Contains(firstSegment, StringComparer.OrdinalIgnoreCase)) + { + return normalized; + } + + // the first segment is the localized root node, drop it + return separatorIndex < 0 ? string.Empty : normalized.Substring(separatorIndex + 1); + } + public static void OpenRegistry(string path) { + // CreateSubKey instead of OpenSubKey, the Regedit key does not exist yet on a profile + // where regedit.exe has never been started using (var registryKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default) - .OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Applets\Regedit", true)) + .CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Applets\Regedit")) { - registryKey?.SetValue("LastKey", path); + registryKey?.SetValue("LastKey", NormalizeRegistryPath(path)); } var processes = Process.GetProcessesByName("regedit"); diff --git a/SyncMLViewer/MainWindow.xaml b/SyncMLViewer/MainWindow.xaml index df065d2..c0ad2cc 100644 --- a/SyncMLViewer/MainWindow.xaml +++ b/SyncMLViewer/MainWindow.xaml @@ -158,6 +158,7 @@ + diff --git a/SyncMLViewer/MainWindow.xaml.cs b/SyncMLViewer/MainWindow.xaml.cs index 24255f2..8f18e39 100644 --- a/SyncMLViewer/MainWindow.xaml.cs +++ b/SyncMLViewer/MainWindow.xaml.cs @@ -1962,37 +1962,37 @@ private async void MenuItemCheckUpdate_OnClick(object sender, RoutedEventArgs e) private void MenuItemRegistryEnrollments_Click(object sender, RoutedEventArgs e) { - Helper.OpenRegistry(@"Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Enrollments"); + Helper.OpenRegistry(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Enrollments"); } private void MenuItemRegistryProvisioning_Click(object sender, RoutedEventArgs e) { - Helper.OpenRegistry(@"Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning"); + Helper.OpenRegistry(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning"); } private void MenuItemRegistryPolicyManager_OnClick(object sender, RoutedEventArgs e) { - Helper.OpenRegistry(@"Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PolicyManager"); + Helper.OpenRegistry(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PolicyManager"); } private void MenuItemRegistryRebootRequiredUris_Click(object sender, RoutedEventArgs e) { - Helper.OpenRegistry(@"Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\SyncML\RebootRequiredURIs"); + Helper.OpenRegistry(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\SyncML\RebootRequiredURIs"); } private void MenuItemRegistryDeclaredConfiguration_Click(object sender, RoutedEventArgs e) { - Helper.OpenRegistry(@"Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\DeclaredConfiguration"); + Helper.OpenRegistry(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\DeclaredConfiguration"); } private void MenuItemRegistryEnterpriseDesktopAppManagement_Click(object sender, RoutedEventArgs e) { - Helper.OpenRegistry(@"Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\EnterpriseDesktopAppManagement"); + Helper.OpenRegistry(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\EnterpriseDesktopAppManagement"); } private void MenuItemRegistryIntuneManagementExtension_Click(object sender, RoutedEventArgs e) { - Helper.OpenRegistry(@"Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\IntuneManagementExtension"); + Helper.OpenRegistry(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\IntuneManagementExtension"); } private async void MenuItemMdmDiagnostics_OnClick(object sender, RoutedEventArgs e) @@ -2351,6 +2351,16 @@ private void MenuItemOpenDeclaredConfigurationHostOSFolder_Click(object sender, Helper.OpenFolder(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), @"Microsoft\DC\HostOS")); } + private void MenuItemOpenDeviceInventoryAgentFolder_Click(object sender, RoutedEventArgs e) + { + // ProgramW6432 always points to the native Program Files, SpecialFolder.ProgramFiles + // would resolve to Program Files (x86) when running as a 32-bit process + var programFiles = Environment.GetEnvironmentVariable("ProgramW6432") + ?? Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + + Helper.OpenFolder(Path.Combine(programFiles, "Microsoft Device Inventory Agent")); + } + private void MenuItemClearHistoryItems_Click(object sender, RoutedEventArgs e) { resultStack.Children.Clear(); @@ -2732,7 +2742,7 @@ private void MenuItemLookupNodeCache_Click(object sender, RoutedEventArgs e) if (int.TryParse(text, out _)) { - Helper.OpenRegistry($@"Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\NodeCache\CSP\Device\MS DM Server\Nodes\{text}"); + Helper.OpenRegistry($@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\NodeCache\CSP\Device\MS DM Server\Nodes\{text}"); } }