简体   繁体   中英

ApplicationSettingsBase ConfigurationErrorsException multiple assemblies

Updated Question (to point out the problem correctly)

I'm using a lib, which implements a class that derives from ApplicationSettingsBase .

namespace MyLib {
    public sealed class GlobalLibSettings : ApplicationSettingsBase
    {
        [UserScopedSetting, DefaultSettingValue("true")]
        public bool SimpleProperty{
            get { return (bool) this["SimpleProperty"]; }
            set {
                this["SimpleProperty"] = value;
                Save();
            }
        }
    }
}

Now I use this lib in another project. The projects also contains at least one class that derives from ApplicationSettingsBase .

namespace MyProject {
    public sealed class ProjectSettings : ApplicationSettingsBase
    {
        [UserScopedSetting, DefaultSettingValue("true")]
        public bool AnotherProperty{
            get { return (bool) this["AnotherProperty"]; }
            set {
                this["AnotherProperty"] = value;
                Save();
            }
        }
    }
}

Now both classes deriving from ApplicationSettingsBase storing their properties to same user.config file. The application and lib uses multiple tasks and if two tasks perform (for example) the properties setter at the same time I get the following exception. Both tasks try to perform write action at same time...

System.Configuration.ConfigurationErrorsException occurred
  BareMessage=Beim Laden einer Konfigurationsdatei ist ein Fehler aufgetreten.: Der Prozess kann nicht auf die Datei "... _xneb3g43uoxqiagk4ge5e4hea1vxlina\1.0.4.862\user.config" zugreifen, da sie von einem anderen Prozess verwendet wird.
  Filename=..._xneb3g43uoxqiagk4ge5e4hea1vxlina\1.0.4.862\user.config
  HResult=-2146232062
  Line=0
  Message=Beim Laden einer Konfigurationsdatei ist ein Fehler aufgetreten.: Der Prozess kann nicht auf die Datei "..._xneb3g43uoxqiagk4ge5e4hea1vxlina\1.0.4.862\user.config" zugreifen, da sie von einem anderen Prozess verwendet wird. (..._xneb3g43uoxqiagk4ge5e4hea1vxlina\1.0.4.862\user.config)
  Source=System.Configuration
  StackTrace:
       bei System.Configuration.ConfigurationSchemaErrors.ThrowIfErrors(Boolean ignoreLocal)
  InnerException: 
       HResult=-2147024864
       Message=Der Prozess kann nicht auf die Datei "_xneb3g43uoxqiagk4ge5e4hea1vxlina\1.0.4.862\user.config" zugreifen, da sie von einem anderen Prozess verwendet wird.
       Source=mscorlib
       StackTrace:
            bei System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
            bei System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
            bei System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
            bei System.Configuration.Internal.InternalConfigHost.StaticOpenStreamForRead(String streamName)
            bei System.Configuration.Internal.InternalConfigHost.System.Configuration.Internal.IInternalConfigHost.OpenStreamForRead(String streamName, Boolean assertPermissions)
            bei System.Configuration.Internal.InternalConfigHost.System.Configuration.Internal.IInternalConfigHost.OpenStreamForRead(String streamName)
            bei System.Configuration.ClientConfigurationHost.OpenStreamForRead(String streamName)
            bei System.Configuration.BaseConfigurationRecord.RefreshFactoryRecord(String configKey)

I can reproduce it with the following scenario:

var settings1 = new GlobalLibSettings ();
var settings2 = new ProjectSettings ();
Task.Factory.StartNew(()=>{
    while(true) settings1.SimpleProperty = !settings1.SimpleProperty;
});
Task.Factory.StartNew(()=>{
    while(true) settings2.AnotherProperty = !settings2.AnotherProperty;
});

Now I was looking for an implementation to protect the access to user.config file.

Solution:

I found a working solution. The CustomApplicationSettingsBase locks concurrent tasks.

public sealed class GlobalLibSettings : CustomApplicationSettingsBase and

public sealed class ProjectSettings : CustomApplicationSettingsBase with:

namespace MyLib {
    public static class LockProvider
    {
        public static object AppSettingsLock { get; } = new object();
    }

    public class CustomApplicationSettingsBase : ApplicationSettingsBase
    {
        public override object this[string propertyName] {
            get {
                lock (LockProvider.AppSettingsLock) {
                    return base[propertyName];
                }
            }
            set {
                lock (LockProvider.AppSettingsLock) {
                    base[propertyName] = value;
                }
            }
        }
        public override void Save() {
            lock (LockProvider.AppSettingsLock) {
                base.Save();
            }
        }
        public override void Upgrade() {
            lock (LockProvider.AppSettingsLock) {
                base.Upgrade();
            }
        }
    }
}

Thanks for your help!

There is a lot to dislike about System.Configuration, but it doesn't get this detail wrong. Something you can see in the Reference Source , the file is opened with:

 return new FileStream(streamName, FileMode.Open, FileAccess.Read, FileShare.Read);

So simple read access and using FileShare.Read to allow anybody else to read from the file as well. So any thread may trigger this code, you can't get the file sharing exception.

So the solution you've tried can't solve the problem. Finding another explanation is going to be difficult with the provided info. It is very hard to get a sharing conflict on a file that is so well hidden. The only plausible explanation is that another thread is writing to the file at the exact same time. In other words, executing the Save() method.

You'd have to be extraordinary unlucky. But it is technically possible. Use the Debug > Windows > Threads debugger window to see what other threads are doing, you ought to see the Save() method call in one of their stack traces. The only other possible quirk is environmental, wonky anti-malware can arbitrarily make a file inaccessible while it is scanning the file.

And last but not least, it isn't obvious what you did to make this work at all. Only the EXE project of a solution can use a .config file. Lots of non-obvious hanky-panky is needed to get a DLL to use settings. We don't know what you did, be sure to update your question with those details. Really rather best to not do this at all.

The problem probably lays in the fact that you have more than one instances of GlobalAppSettings that are used at the same time. It means that there may be more than one thread trying to read/write the same file and this leads to the excpetion.

Your solution with lock does not work because _locker object is not shared between different instances of GlobalAppSettings .

I see the following solutions. Firstly, you can do something like in your second edit ie to use static object to synchronize access to settings. However, I like more the second solution. Try to implement CustomApplicationSettingsBase as the singleton. Or even better use dependency injection to inject an instance of CustomApplicationSettingsBase where it is needed and tell DI container that CustomApplicationSettingsBase should be the singleton.

Here is a solution that allows multiple copies of the program to run simultaneously, eg it adds multi process support on top of your multi thread support. Which ever program saves last will "win". If program 2 saves the same setting after program 1 has saved. Program 1 won't be notified about the new value.

Depending on what you are using these settings for this may not be an issue. Eg if you are saving maximised window state, or something trivial like that this solution works great.

If you want the new saved values to load back into the second instance you could manually call Settings.Default.Reload() in each of the getter functions to ensure it is reloaded each time it is accessed. This would obviously add quite a bit of overhead, so only do it if you actually want this.

I do nothing if there is an exception currently, but you could add some error handling as well.

This uses a named eventWaitHandle. By using a named event handle it will allow you to perform a multi process lock. By setting it to AutoReset it will automatically unlock on program crash/exit etc making it very safe to use.

The lock name is the name of the .config file. If you are using a shared .config file, then you should update the name of the lock to a constant value rather than using the executing path.

I have included a timeout in the LockConfigSettings() function as well so that it won't lock infinitely but will eventually try anyway causing a crash (if it really is still locked) or it will continue.

public class ConfigSettings : IConfigSettings
{
    private readonly EventWaitHandle _configSettingsLock = new EventWaitHandle(true, EventResetMode.AutoReset, MakeStringPathSafe(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "program.exe.config")));


    public string SettingOne
    {
        set
        {
            LockConfigSettings();
            try 
            { 
                Properties.Settings.Default.SettingOne = value; 
                Properties.Settings.Default.Save();
            } 
            catch { }
            finally { ReleaseSettingsLock(); }
        }
        get
        {
            LockConfigSettings();
            try 
            { 
                return Properties.Settings.Default.SettingOne; 
            } 
            catch 
            { 
                return null; 
            }
            finally 
            { 
                ReleaseSettingsLock(); 
            }
                
        }
    }
    
    public string SettingTwo
    {
        set
        {
            LockConfigSettings();
            try 
            { 
                Properties.Settings.Default.SettingTwo = value; 
                Properties.Settings.Default.Save();
            } 
            catch { }
            finally { ReleaseSettingsLock(); }
        }
        get
        {
            LockConfigSettings();
            try 
            { 
                return Properties.Settings.Default.SettingTwo; 
            } 
            catch 
            { 
                return null; 
            }
            finally 
            { 
                ReleaseSettingsLock(); 
            }
                
        }
    }
    
    private void LockConfigSettings()
    {
        //In case we are debugging we make it infinite as the event handle will still count when the debugger is paused
        if (Debugger.IsAttached)
        {
            if (!_configSettingsLock.WaitOne())
                throw new InvalidOperationException($"Failed to lock program.exe.config due to timeout. Please try closing all instances of program.exe via task manager.");
        }
        else
        {
            //After 15 seconds stop waiting. This will ensure you get a crash or something other than an infinite wait.
            //This should only occur if the other application holding the lock has been paused in a debugger etc.
            if (!_configSettingsLock.WaitOne(15000))
                throw new InvalidOperationException($"Failed to lock program.exe.config due to timeout. Please try closing all instances of program.exe via task manager.");
        }
    }
    
    private void ReleaseSettingsLock()
    {
        try
        {
            _configSettingsLock.Set();
        }
        catch (Exception e)
        {
            throw new Exception($"Failed to release program.exe.config lock due to error");
        }
    }

    public static string MakeStringPathSafe(string path)
    {
        if (path == null) return path;

        path = path.Replace("//", "_");
        path = path.Replace("/", "_");
        path = path.Replace("\\", "_");
        path = path.Replace(@"\", "_");
        path = path.Replace(@"\\", "_");
        path = path.Replace(":", "-");
        path = path.Replace(" ", "-");
        return path;
    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM