From 8ed322cce381c88867f7ca4b14308e2486bf7ef4 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Fri, 17 Jan 2025 03:58:16 -0600 Subject: [PATCH] Use localappdata to store settings, instead of in WindowsApps (#304) I'm smarter than that, really. As described in #302. You can't write into `WindowsApps`, where actual packages are installed. Instead, you need to use the local app data path. This replicates logic that we've got in the Terminal, for getting the right LocalAppData path, without using Windows.Storage. Original code looks like: ```c++ _TIL_INLINEPREFIX bool IsPackaged() { static const auto isPackaged = []() { UINT32 bufferLength = 0; const auto hr = GetCurrentPackageId(&bufferLength, nullptr); return hr != APPMODEL_ERROR_NO_PACKAGE; }(); return isPackaged; } std::filesystem::path GetBaseSettingsPath() { static auto baseSettingsPath = []() { /* some portable mode code we don't need */ wil::unique_cotaskmem_string localAppDataFolder; // KF_FLAG_FORCE_APP_DATA_REDIRECTION, when engaged, causes SHGet... to return // the new AppModel paths (Packages/xxx/RoamingState, etc.) for standard path requests. // Using this flag allows us to avoid Windows.Storage.ApplicationData completely. THROW_IF_FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_FORCE_APP_DATA_REDIRECTION, nullptr, &localAppDataFolder)); std::filesystem::path parentDirectoryForSettingsFile{ localAppDataFolder.get() }; if (!IsPackaged()) { parentDirectoryForSettingsFile /= UnpackagedSettingsFolderName; } // Create the directory if it doesn't exist std::filesystem::create_directories(parentDirectoryForSettingsFile); return parentDirectoryForSettingsFile; }(); return baseSettingsPath; } ``` I stuck this in a `Helpers.Utilities` class, because we will not be the only ones hitting this. Closes #302 --- .../BookmarkDataContext.cs | 1 - .../BookmarksCommandProvider.cs | 9 +- .../Helpers/SettingsManager.cs | 7 +- .../Pages/SettingsPage.cs | 2 +- .../Helpers/SettingsManager.cs | 86 ++++++------------- .../Pages/SettingsPage.cs | 2 +- .../Helpers/SettingsManager.cs | 7 +- .../Pages/SettingsPage.cs | 2 +- .../Helpers/SettingsManager.cs | 7 +- .../Pages/SettingsPage.cs | 2 +- .../Exts/SpongebotExtension/SpongebotPage.cs | 7 +- .../YouTubeExtension/Helper/YouTubeHelper.cs | 13 +-- .../SettingsModel.cs | 8 +- .../JsonSettingsManager.cs | 9 +- .../NativeMethods.txt | 5 +- .../Utilities.cs | 83 ++++++++++++++++++ 16 files changed, 137 insertions(+), 113 deletions(-) create mode 100644 src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/Utilities.cs diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkDataContext.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkDataContext.cs index 31fcb37123..2083045e89 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkDataContext.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkDataContext.cs @@ -8,7 +8,6 @@ namespace Microsoft.CmdPal.Ext.Bookmarks; [JsonSerializable(typeof(BookmarkData))] [JsonSerializable(typeof(string))] -[JsonSerializable(typeof(string))] internal sealed partial class BookmarkDataContext : JsonSerializerContext { } diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs index d6b24f1ece..7f0aefb4d9 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs @@ -111,13 +111,10 @@ public partial class BookmarksCommandProvider : CommandProvider internal static string StateJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = System.IO.Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe - return System.IO.Path.Combine(directory, "state.json"); + return System.IO.Path.Combine(directory, "bookmarks.json"); } } diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs index f0156b516e..01511c3d1c 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs @@ -53,11 +53,8 @@ public class SettingsManager : JsonSettingsManager internal static string SettingsJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe return Path.Combine(directory, "settings.json"); diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Pages/SettingsPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Pages/SettingsPage.cs index 2cdfd6b9cb..d6533ef9fb 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Pages/SettingsPage.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Pages/SettingsPage.cs @@ -23,7 +23,7 @@ internal sealed partial class SettingsPage : FormPage { Name = Properties.Resources.settings_page_name; Icon = new("\uE713"); // Settings icon - _settings = settingsManager.GetSettings(); + _settings = settingsManager.Settings; _settingsManager = settingsManager; _settings.SettingsChanged += SettingsChanged; diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs index 21b8b4507d..96d812a4f7 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs @@ -8,20 +8,21 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text.Json; -using System.Text.Json.Nodes; using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Extensions.Helpers; namespace Microsoft.CmdPal.Ext.WebSearch.Helpers; -public class SettingsManager +public class SettingsManager : JsonSettingsManager { - private readonly string _filePath; private readonly string _historyPath; - private readonly Microsoft.CmdPal.Extensions.Helpers.Settings _settings = new(); - private readonly List _choices = new() + private static readonly string _namespace = "websearch"; + + private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; + + private static readonly List _choices = new() { new ChoiceSetSetting.Choice(Resources.history_none, Resources.history_none), new ChoiceSetSetting.Choice(Resources.history_1, Resources.history_1), @@ -30,8 +31,17 @@ public class SettingsManager new ChoiceSetSetting.Choice(Resources.history_20, Resources.history_20), }; - private readonly ToggleSetting _globalIfURI = new(nameof(GlobalIfURI), Resources.plugin_global_if_uri, Resources.plugin_global_if_uri, false); - private readonly ChoiceSetSetting _showHistory; + private readonly ToggleSetting _globalIfURI = new( + Namespaced(nameof(GlobalIfURI)), + Resources.plugin_global_if_uri, + Resources.plugin_global_if_uri, + false); + + private readonly ChoiceSetSetting _showHistory = new( + Namespaced(nameof(ShowHistory)), + Resources.plugin_show_history, + Resources.plugin_show_history, + _choices); public bool GlobalIfURI => _globalIfURI.Value; @@ -39,23 +49,17 @@ public class SettingsManager internal static string SettingsJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe - return Path.Combine(directory, "websearch_state.json"); + return Path.Combine(directory, "settings.json"); } internal static string HistoryStateJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe return Path.Combine(directory, "websearch_history.json"); @@ -142,12 +146,11 @@ public class SettingsManager public SettingsManager() { - _filePath = SettingsJsonPath(); + FilePath = SettingsJsonPath(); _historyPath = HistoryStateJsonPath(); - _showHistory = new(nameof(ShowHistory), Resources.plugin_show_history, Resources.plugin_show_history, _choices); - _settings.Add(_globalIfURI); - _settings.Add(_showHistory); + Settings.Add(_globalIfURI); + Settings.Add(_showHistory); // Load settings from file upon initialization LoadSettings(); @@ -178,17 +181,11 @@ public class SettingsManager } } - public Microsoft.CmdPal.Extensions.Helpers.Settings GetSettings() => _settings; - - public void SaveSettings() + public override void SaveSettings() { + base.SaveSettings(); try { - // Serialize the main dictionary to JSON and save it to the file - var settingsJson = _settings.ToJson(); - - File.WriteAllText(_filePath, settingsJson); - if (ShowHistory == Resources.history_none) { ClearHistory(); @@ -219,33 +216,4 @@ public class SettingsManager ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); } } - - public void LoadSettings() - { - if (!File.Exists(_filePath)) - { - ExtensionHost.LogMessage(new LogMessage() { Message = "The provided settings file does not exist" }); - return; - } - - try - { - // Read the JSON content from the file - var jsonContent = File.ReadAllText(_filePath); - - // Is it valid JSON? - if (JsonNode.Parse(jsonContent) is JsonObject savedSettings) - { - _settings.Update(jsonContent); - } - else - { - ExtensionHost.LogMessage(new LogMessage() { Message = "Failed to parse settings file as JsonObject." }); - } - } - catch (Exception ex) - { - ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); - } - } } diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Pages/SettingsPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Pages/SettingsPage.cs index 986b7d634b..6d753d3b07 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Pages/SettingsPage.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Pages/SettingsPage.cs @@ -24,7 +24,7 @@ internal sealed partial class SettingsPage : FormPage { Name = Resources.settings_page_name; Icon = new("\uE713"); // Settings icon - _settings = settingsManager.GetSettings(); + _settings = settingsManager.Settings; _settingsManager = settingsManager; _settings.SettingsChanged += SettingsChanged; diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs index c86d3a2354..b872e522e9 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs @@ -90,11 +90,8 @@ public class SettingsManager : JsonSettingsManager internal static string SettingsJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe return Path.Combine(directory, "settings.json"); diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/SettingsPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/SettingsPage.cs index a5836dcaa1..e8f8d62cc0 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/SettingsPage.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/SettingsPage.cs @@ -25,7 +25,7 @@ internal sealed partial class SettingsPage : FormPage Name = Resources.windowwalker_settings_name; Icon = new("\uE713"); // Settings icon _settingsManager = SettingsManager.Instance; - _settings = _settingsManager.GetSettings(); + _settings = _settingsManager.Settings; _settings.SettingsChanged += SettingsChanged; } diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs index d386cd3dca..858ccbf704 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs @@ -42,11 +42,8 @@ public class SettingsManager : JsonSettingsManager internal static string SettingsJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe return Path.Combine(directory, "settings.json"); diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/SettingsPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/SettingsPage.cs index 29091583d8..70bb45f0ac 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/SettingsPage.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/SettingsPage.cs @@ -24,7 +24,7 @@ internal sealed partial class SettingsPage : FormPage { Name = Resources.settings_page_name; Icon = new("\uE713"); // Settings icon - _settings = settingsManager.GetSettings(); + _settings = settingsManager.Settings; _settingsManager = settingsManager; _settings.SettingsChanged += SettingsChanged; diff --git a/src/modules/cmdpal/Exts/SpongebotExtension/SpongebotPage.cs b/src/modules/cmdpal/Exts/SpongebotExtension/SpongebotPage.cs index 4270458c84..e93dce1189 100644 --- a/src/modules/cmdpal/Exts/SpongebotExtension/SpongebotPage.cs +++ b/src/modules/cmdpal/Exts/SpongebotExtension/SpongebotPage.cs @@ -90,11 +90,8 @@ public partial class SpongebotPage : MarkdownPage, IFallbackHandler internal static string StateJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe return Path.Combine(directory, "state.json"); diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeHelper.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeHelper.cs index f8ae9c16b2..65450cd995 100644 --- a/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeHelper.cs +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeHelper.cs @@ -2,12 +2,8 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Microsoft.CmdPal.Extensions.Helpers; namespace YouTubeExtension.Actions; @@ -15,11 +11,8 @@ internal sealed class YouTubeHelper { internal static string StateJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe return Path.Combine(directory, "state.json"); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs index 3f6ed42eb5..3735f128ec 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs @@ -7,6 +7,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CmdPal.Extensions.Helpers; using Microsoft.CmdPal.UI.ViewModels.Settings; using Windows.Foundation; @@ -119,11 +120,8 @@ public partial class SettingsModel : ObservableObject internal static string SettingsJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the settings is just next to the exe return Path.Combine(directory, "settings.json"); diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/JsonSettingsManager.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/JsonSettingsManager.cs index 21ce5ee48d..da88a30912 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/JsonSettingsManager.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/JsonSettingsManager.cs @@ -22,12 +22,7 @@ public abstract class JsonSettingsManager Converters = { new JsonStringEnumConverter() }, }; - public Settings GetSettings() - { - return _settings; - } - - public void LoadSettings() + public virtual void LoadSettings() { if (string.IsNullOrEmpty(FilePath)) { @@ -62,7 +57,7 @@ public abstract class JsonSettingsManager } } - public void SaveSettings() + public virtual void SaveSettings() { if (string.IsNullOrEmpty(FilePath)) { diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/NativeMethods.txt b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/NativeMethods.txt index 8423d9d31e..942650356e 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/NativeMethods.txt +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/NativeMethods.txt @@ -2,4 +2,7 @@ GetCurrentThread OpenThreadToken GetPackageFamilyNameFromToken -CoRevertToSelf \ No newline at end of file +CoRevertToSelf +SHGetKnownFolderPath +KNOWN_FOLDER_FLAG +GetCurrentPackageId diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/Utilities.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/Utilities.cs new file mode 100644 index 0000000000..d5c73d4971 --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/Utilities.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; + +namespace Microsoft.CmdPal.Extensions.Helpers; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1312:Variable names should begin with lower-case letter", Justification = "This file has more than a couple Windows constants in it, which don't make sense to rename")] +public class Utilities +{ + /// + /// Used to produce a path to a settings folder which your app can use. + /// If your app is running packaged, this will return the redirected local + /// app data path (Packages/{your_pfn}/LocalState). If not, it'll return + /// %LOCALAPPDATA%\{settingsFolderName}. + /// + /// Does not ensure that the directory exists. Callers should call + /// CreateDirectory before writing settings files to this directory. + /// + /// + /// var directory = Utilities.BaseSettingsPath("Some.Unique.String.Here"); + /// Directory.CreateDirectory(directory); + /// + /// A fallback directory name to use + /// inside of %LocalAppData%, in the case this app is not currently running + /// in a package context + /// The path to a folder to use for storing settings. + public static string BaseSettingsPath(string settingsFolderName) + { + // KF_FLAG_FORCE_APP_DATA_REDIRECTION, when engaged, causes SHGet... to return + // the new AppModel paths (Packages/xxx/RoamingState, etc.) for standard path requests. + // Using this flag allows us to avoid Windows.Storage.ApplicationData completely. + var FOLDERID_LocalAppData = new Guid("F1B32785-6FBA-4FCF-9D55-7B8E7F157091"); + var hr = PInvoke.SHGetKnownFolderPath( + FOLDERID_LocalAppData, + (uint)KNOWN_FOLDER_FLAG.KF_FLAG_FORCE_APP_DATA_REDIRECTION, + null, + out var localAppDataFolder); + + if (hr.Succeeded) + { + var basePath = new string(localAppDataFolder.ToString()); + if (!IsPackaged()) + { + basePath = Path.Combine(basePath, settingsFolderName); + } + + return basePath; + } + else + { + throw Marshal.GetExceptionForHR(hr.Value)!; + } + } + + /// + /// Can be used to quickly determine if this process is running with package identity. + /// + /// true iff the process is running with package identity + public static bool IsPackaged() + { + uint bufferSize = 0; + var bytes = Array.Empty(); + + // CsWinRT apparently won't generate this constant + var APPMODEL_ERROR_NO_PACKAGE = (WIN32_ERROR)15700; + unsafe + { + fixed (byte* p = bytes) + { + // We don't actually need the package ID. We just need to know + // if we have a package or not, and APPMODEL_ERROR_NO_PACKAGE + // is a quick way to find out. + var win32Error = PInvoke.GetCurrentPackageId(ref bufferSize, p); + return win32Error != APPMODEL_ERROR_NO_PACKAGE; + } + } + } +}