Squirrel.Windows icon indicating copy to clipboard operation
Squirrel.Windows copied to clipboard

AppSettings lost across upgrade

Open AArnott opened this issue 4 years ago • 13 comments

Squirrel version(s) 2.0.2-netcore.3 (a private build with minimal changes based on the official 2.0.1 version).

Description

Updating a squirrel .NET app causes it to lose all its AppSettings.

Steps to recreate

  1. In a WPF app (or maybe any .NET app) that uses Squirrel, consume appsettings like this.
  2. Within the app, set a value to the setting
  3. Update the app with Squirrel
  4. Re-run the app and read the setting.

Expected behavior

The app setting's value should be what was set in the prior version.

Actual behavior

The app setting's value has been cleared.

AArnott avatar Nov 12 '21 16:11 AArnott

This has been a longstanding bug because AppSettings (despite being entirely appropriate to store in roaming app profiles) gets put in LocalAppData. The current recommendation is to not use AppSettings 😅

anaisbetts avatar Nov 12 '21 20:11 anaisbetts

Thanks for looking so quickly. Where is "local app settings"? Is that something that squirrel can copy from an old location to a new one?

AArnott avatar Nov 13 '21 02:11 AArnott

Or heck... given Squirrel offers callbacks around upgrade time, maybe I could do something to preserve them. If I didn't use AppSettings, what would I use? Anything I store in the app dir itself I suppose would be wiped out in the upgrade as well, wouldn't it?

AArnott avatar Nov 13 '21 02:11 AArnott

I typically store settings and other stuff one directory up from my EXE's directory, this doesn't get wiped across upgrades, but does get wiped on uninstall

anaisbetts avatar Nov 13 '21 05:11 anaisbetts

Thanks for looking so quickly. Where is "local app settings"?

This was me getting confused 😓 I'm talking about %LocalAppData%

anaisbetts avatar Nov 13 '21 15:11 anaisbetts

And somehow the app upgrade makes .NET read/write app settings in a different path under LocalAppData?

AArnott avatar Nov 14 '21 00:11 AArnott

It took Process Monitor to figure it out since I couldn't find any documentation for it, but in my case the settings are saved in C:\Users\andarno\AppData\Local\Andrew_Arnott\MoneyMan.WPF_Url_zor3ou4qwd14tf0bxfj3xami0oyfzk5t\0.2.0.0\user.config

I wonder what goes into that zor... character sequence. Whatever it is, I guess it must change with each update. Maybe the path of the app itself.

AArnott avatar Nov 14 '21 03:11 AArnott

Yup. When I rename the directory the app is in, the AppSettings writes to a new path, and all prior settings are unreachable.

As AppSettings has a Providers property, I may be able to replace the default provider with another that does as you say and writes the settings to the parent directory.

AArnott avatar Nov 14 '21 03:11 AArnott

I think it's theoretically possible, but the .NET BCL doesn't expose the types required to make it remotely easy. I'm going to go with your original suggestion and serialize my settings another way.

AArnott avatar Nov 14 '21 04:11 AArnott

It looks like the logic for generating this path is here https://referencesource.microsoft.com/#System.Configuration/System/Configuration/ClientConfigPaths.cs,343 - and it will use a SHA1 hash of the strong name if your assembly has one, and fall back to the exe path if not. If it's simple enough to sn sign your assembly, it may fix this for you.

Depending on your framework version, the following may also be useful as it forces a custom application path. If set before you use the configuration manager for the first time, it will be used instead of your app path.

AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", path); (from the dotnet runtime), or;

AppDomain.CurrentDomain.SetupInformation.ConfigurationFile (from reference source)

caesay avatar Dec 06 '21 23:12 caesay

@caesay: I wasn't able to set APP_CONFIG_FILE and detect any difference in where the file was saved. But strong name signing did seem to give it a new and stable path for settings. Thank you.

AArnott avatar Dec 08 '21 23:12 AArnott

This isn't the most elegant solution, but strong naming produced too many other build errors. I dug a bit on that Providers property in Reference Source and settled on this workaround:

public Settings()
{
    LocalFileSettingsProvider localProvider = Providers
        .OfType<LocalFileSettingsProvider>()
        .FirstOrDefault();

    if (localProvider is not null)
    {
        Configuration userConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);

        /* User config is landing in: %LocalAppData%\Publisher\<exe-name><hash>\<version>\user.config
            * This "hash" includes the file path of the *executable* meaning it changes with each Squirrel version.
            * For example:
            *                                                 |****** THIS PART CHANGES *******|  
            * %LocalAppData%\<company-name>\<exe-filename>_Url_fvqct42v5v155z0imoat5bi2twry5min\1.2.3.4\user.config
            * 
            * The framework looks for other version folders (e.g. 1.2.2.0) in that same location and will upgrade from them.
            * We can override that behavior by forcing its search one level higher into the <company-name> folder.
            */

        DirectoryInfo? parent = Directory.GetParent(userConfig.FilePath) // e.g. 1.2.3.4
            ?.Parent    // e.g. <exe-filename>_Url_fvqct42v5v155z0imoat5bi2twry5min
            ?.Parent;   // e.g. <company-name>

        FileInfo? previous = parent?.EnumerateFiles("*user.config", SearchOption.AllDirectories)
            .Where(f => f.FullName != userConfig.FilePath)
            .OrderByDescending(f =>
            {
                try
                {
                    return new Version(f.Directory.Name);
                }
                catch
                {
                    return new Version();
                }
            }, new VersionComparison())
            .FirstOrDefault();

        // .NET Framework resolves this field to previous folders, we can bypass that behavior by
        // pre-calculating it ourselves before calling Upgrade()
        FieldInfo fileNameField = typeof(LocalFileSettingsProvider).GetField("_prevLocalConfigFileName",
            bindingAttr: BindingFlags.Instance | BindingFlags.NonPublic);

        fileNameField.SetValue(localProvider, previous?.FullName);
    }
}

adamhewitt627 avatar Dec 22 '21 05:12 adamhewitt627

Thanks for sharing an alternative. Strong naming was definitely the simplest solution for me but I can see where some folks wouldn't have that option.

AArnott avatar Dec 22 '21 14:12 AArnott