【问题标题】:ApplicationSettingsBase ConfigurationErrorsException multiple assembliesApplicationSettingsBase ConfigurationErrorsException 多个程序集
【发布时间】:2016-12-19 10:46:54
【问题描述】:

更新问题(正确指出问题)

我正在使用一个库,它实现了一个派生自ApplicationSettingsBase 的类。

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

现在我在另一个项目中使用这个库。这些项目还包含至少一个派生自ApplicationSettingsBase 的类。

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

现在派生自ApplicationSettingsBase 的两个类都将它们的属性存储到同一个user.config 文件中。应用程序和 lib 使用多个任务,如果两个任务同时执行(例如)属性设置器,我得到以下异常。两个任务都尝试同时执行写入操作...

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)

我可以用以下场景重现它:

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

现在我正在寻找一种实现来保护对user.config 文件的访问。

解决方案:

我找到了一个可行的解决方案。 CustomApplicationSettingsBase 锁定并发任务。

public sealed class GlobalLibSettings : CustomApplicationSettingsBase

public sealed class ProjectSettings : CustomApplicationSettingsBase 与:

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();
            }
        }
    }
}

感谢您的帮助!

【问题讨论】:

  • 只有当所有线程都使用你的类的同一个实例时,你的锁实现才会有所帮助。
  • 如何实例化 GlobalAppSettings?您是否有一个或多个 GlobalAppSettings 实例?
  • @Mathew Jibin 和 Michal Komorowski:你们都是对的,现在我明白我的错了。我需要一个全局锁对象来管理多个实现的文件访问。我下午测试一下。
  • @Mathew Jibin:感谢您的评论。这有帮助。您可以在我上次的编辑中找到有效的解决方案。为赏金创造一个答案。
  • @Michal Komorowski:感谢您的评论。这有帮助。您可以在我上次的编辑中找到有效的解决方案。为赏金创造一个答案。

标签: c# .net wpf .net-assembly applicationsettingsbase


【解决方案1】:

很多不喜欢 System.Configuration,但它并没有弄错这个细节。您可以在Reference Source 中看到的内容,文件打开方式为:

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

如此简单的读取访问和使用 FileShare.Read 允许其他任何人也可以从文件中读取。所以任何线程都可能触发这段代码,你不能得到文件共享异常。

所以您尝试过的解决方案无法解决问题。使用提供的信息很难找到另一种解释。很难在隐藏得如此之好的文件上发生共享冲突。唯一合理的解释是另一个线程正在同时写入文件。也就是说,执行 Save() 方法。

你一定非常倒霉。但这在技术上是可行的。使用 Debug > Windows > Threads 调试器窗口查看其他线程在做什么,您应该在它们的堆栈跟踪之一中看到 Save() 方法调用。唯一可能的其他怪癖是环境,不稳定的反恶意软件可以在扫描文件时任意使文件无法访问。

最后但并非最不重要的一点是,你做了什么来完成这项工作并不明显。只有解决方案的 EXE 项目可以使用 .config 文件。要让 DLL 使用设置,需要大量不明显的 hanky-panky。我们不知道您做了什么,请务必使用这些详细信息更新您的问题。真的最好不要这样做。

【讨论】:

  • 感谢您的建议。现在我发现了问题并用 Matthew 和 Michal 的 cmets 解决了它。没错,从不同的Tasks(用while 循环测试它)在不同程序集中的ApplicationSettingsBase 的不同实现上并行调用Save()(例如)会导致并发文件访问。如果我将锁放入静态类,我可以按照我的问题中所述使用lock 来防止这种情况。我会用解决方案更新我的答案,这是有效的。谢谢你的解释
  • 以正确的方式解决此问题,在程序退出以外的任何时间调用 Save() 都是一个非常糟糕的主意。故障模式非常难看,您可能正在保存导致程序崩溃的设置。现在,当用户重新启动它时,它会总是崩溃。他也没有什么好的办法来解决这个问题,因为他不知道有坏数据的文件,他必须重新格式化磁盘。
  • 为什么在更改属性值后调用Save() 不好?如果我在由其他项目共享的库中使用来自ApplicationSettingsBase 的派生类,我不会从我的项目中调用lib 类的Save()。因此,我将保护对文件的访问。仅在程序退出时从多个Threads 调用Save() 会导致相同的问题,并且如果对文件的访问不安全,则会以相同的方式危及文件。
【解决方案2】:

问题可能在于您有多个GlobalAppSettings 实例同时使用。这意味着可能有多个线程试图读/写同一个文件,这会导致异常。

由于_locker 对象未在GlobalAppSettings 的不同实例之间共享,因此您的带锁解决方案不起作用。

我看到以下解决方案。首先,您可以在第二次编辑中执行类似操作,即使用静态对象来同步对设置的访问。但是,我更喜欢第二种解决方案。尝试将CustomApplicationSettingsBase 实现为单例。或者甚至更好地使用依赖注入在需要的地方注入CustomApplicationSettingsBase 的实例,并告诉DI 容器CustomApplicationSettingsBase 应该是单例。

【讨论】:

  • 我发现单例实现存在问题。实例的问题较少。我有多个ApplicationSettingsBase 的实现(例如在 assemblyA.GlobalAppSettings、assemblyA.SpecificToolSettings、assemblyB.Settings ...)。它们都指向相同的 user.config。对多个属性(读/写)的多线程并行访问会导致 Save() 调用,例如通过设置属性。然后并发线程尝试写入同一个文件并产生Exception。单例实现在这里无济于事。
【解决方案3】:

这是一个允许程序的多个副本同时运行的解决方案,例如,它在您的多线程支持之上添加了多进程支持。最后保存的程序将“获胜”。如果程序 1 保存后程序 2 保存了相同的设置。程序 1 不会收到有关新值的通知。

根据您使用的这些设置,这可能不是问题。例如,如果您要保存最大化的窗口状态,或者类似这样的微不足道的事情,这个解决方案效果很好。

如果您希望将新保存的值加载回第二个实例,您可以在每个 getter 函数中手动调用 Settings.Default.Reload() 以确保每次访问时都重新加载它。这显然会增加相当多的开销,因此请仅在您确实需要时才这样做。

如果当前有异常,我什么都不做,但你也可以添加一些错误处理。

这使用了一个命名的 eventWaitHandle。通过使用命名事件句柄,它将允许您执行多进程锁定。通过将其设置为 AutoReset,它将在程序崩溃/退出等时自动解锁,使其使用起来非常安全。

锁名称是 .config 文件的名称。如果您使用的是共享的 .config 文件,则应将锁的名称更新为常量值,而不是使用执行路径。

我还在 LockConfigSettings() 函数中包含了一个超时,这样它就不会无限锁定,但最终会尝试导致崩溃(如果它确实仍然被锁定)或者它将继续。

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

}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-20
    • 1970-01-01
    • 2011-03-30
    • 2016-06-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多