diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index c484361..58eb125 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -64,6 +64,16 @@ + + + + symsrv.dll + PreserveNewest + False + + + + + + diff --git a/src/SizeBench.AnalysisEngine/DIAInterop/DIAAdapter.cs b/src/SizeBench.AnalysisEngine/DIAInterop/DIAAdapter.cs index ace6de1..d656c3f 100644 --- a/src/SizeBench.AnalysisEngine/DIAInterop/DIAAdapter.cs +++ b/src/SizeBench.AnalysisEngine/DIAInterop/DIAAdapter.cs @@ -56,6 +56,13 @@ internal sealed class DIAAdapter : IDIAAdapter, IDisposable private uint _sectionAlignment; private readonly int _affinitizedThreadId; + /// + /// The path DIA actually loaded the PDB from. For explicit-PDB sessions this echoes the path we passed + /// to loadDataFromPdb. For symbol-server sessions this is the cached/downloaded location DIA + /// resolved via loadDataForExe. + /// + internal string? LoadedPdbPath { get; private set; } + private static readonly string[] debugFastlinkSwitchNames = ["/debug:fastlink"]; [ThreadStatic] @@ -124,6 +131,16 @@ private PEFile PEFile #region Construction, opening, all the startup-y things internal DIAAdapter(Session session, string pdbPath) + : this(session, source => ((IDiaDataSourceEx2)source).loadDataFromPdbEx(pdbPath, fPdbPrefetching: 1), pdbPath) + { + } + + internal DIAAdapter(Session session, string binaryPath, string symbolSearchPath) + : this(session, source => source.loadDataForExe(binaryPath, symbolSearchPath, pCallback: null), binaryPath) + { + } + + private DIAAdapter(Session session, Action loadFromSource, string sourceDescriptionForErrors) { this._session = session; this._cache = session.DataCache; @@ -135,7 +152,7 @@ internal DIAAdapter(Session session, string pdbPath) { this._diaDataSource = CoClassLoaderRegFree.CreateInstance(_diaLibraryModule, Dia140Clsid); - this._diaDataSource.loadDataFromPdbEx(pdbPath, fPdbPrefetching: 1); + loadFromSource(this._diaDataSource); this._diaDataSource.openSession(out var diaSession); this._diaSession = (IDiaSessionEx)diaSession; @@ -149,7 +166,7 @@ internal DIAAdapter(Session session, string pdbPath) { if (((uint)comException.HResult) == Convert.ToUInt32(diaHRESULTValues.GetValue(i), CultureInfo.InvariantCulture.NumberFormat)) { - throw new PDBNotSuitableForAnalysisException($"Unable to open PDB from '{session.PdbPath}'" + Environment.NewLine + + throw new PDBNotSuitableForAnalysisException($"Unable to open PDB from '{sourceDescriptionForErrors}'" + Environment.NewLine + $"DIA returned this error: {Enum.GetName((DIAHRESULTs)comException.HResult)}", comException); } @@ -158,6 +175,16 @@ internal DIAAdapter(Session session, string pdbPath) } this._globalScope = this._diaSession.globalScope; + + try + { + this.LoadedPdbPath = this._globalScope.symbolsFileName; + } + catch (COMException) + { + // Leave LoadedPdbPath null if DIA can't report it for this PDB type + } + var machineType = this._globalScope.machineType; // See http://msdn.microsoft.com/en-us/library/windows/desktop/ms680313(v=vs.85).aspx diff --git a/src/SizeBench.AnalysisEngine/Session.cs b/src/SizeBench.AnalysisEngine/Session.cs index 4598eb4..7d9463c 100644 --- a/src/SizeBench.AnalysisEngine/Session.cs +++ b/src/SizeBench.AnalysisEngine/Session.cs @@ -20,7 +20,9 @@ public sealed class Session : ISession { private readonly string _originalPDBPathMayBeRemote; private GuaranteedLocalFile? _guaranteedLocalPDBFile; - public string PdbPath => this._guaranteedLocalPDBFile?.OriginalPath ?? "No pdb opened yet"; + public string PdbPath => this._guaranteedLocalPDBFile?.OriginalPath + ?? this._diaAdapter?.LoadedPdbPath + ?? (String.IsNullOrEmpty(this._originalPDBPathMayBeRemote) ? "No pdb opened yet" : this._originalPDBPathMayBeRemote); private readonly string _originalBinaryPathMayBeRemote; public string BinaryPath => this._peFile?.GuaranteedLocalCopyOfBinary.OriginalPath ?? "No binary opened yet"; @@ -141,8 +143,14 @@ internal Session(string binaryPath, string pdbPath, SessionOptions options, ILog this.SessionOptions = options; this.DataCache = new SessionDataCache(options.SymbolSourcesSupported); - Debug.Assert(File.Exists(pdbPath)); - this._originalPDBPathMayBeRemote = pdbPath; + var hasExplicitPdbPath = !String.IsNullOrEmpty(pdbPath); + var hasSymbolServer = !String.IsNullOrWhiteSpace(options.SymbolServerSearchPath); + if (!hasExplicitPdbPath && !hasSymbolServer) + { + throw new ArgumentException("Either a PDB path or a SymbolServerSearchPath in SessionOptions must be supplied.", nameof(pdbPath)); + } + Debug.Assert(!hasExplicitPdbPath || File.Exists(pdbPath)); + this._originalPDBPathMayBeRemote = pdbPath ?? String.Empty; Debug.Assert(File.Exists(binaryPath)); this._originalBinaryPathMayBeRemote = binaryPath; @@ -160,10 +168,22 @@ private void InitializeDIAThread() using var initializeDiaThreadLog = this._logger.StartTaskLog($"Setting up initial data needed to open the session (symbol sources: {this.SessionOptions.SymbolSourcesSupported})"); this._diaManagedThreadId = Environment.CurrentManagedThreadId; - this.ProgressReporter?.Report(new SessionTaskProgress("Copying PDB file locally if necessary.", 0, null)); - this._guaranteedLocalPDBFile = new GuaranteedLocalFile(this._originalPDBPathMayBeRemote, initializeDiaThreadLog); + var hasExplicitPdb = !String.IsNullOrEmpty(this._originalPDBPathMayBeRemote); + if (hasExplicitPdb) + { + this.ProgressReporter?.Report(new SessionTaskProgress("Copying PDB file locally if necessary.", 0, null)); + this._guaranteedLocalPDBFile = new GuaranteedLocalFile(this._originalPDBPathMayBeRemote, initializeDiaThreadLog); + + this._diaAdapter = new DIAAdapter(this, this._guaranteedLocalPDBFile.GuaranteedLocalPath); + } + else + { + var searchPath = this.SessionOptions.SymbolServerSearchPath!; + initializeDiaThreadLog.Log($"No explicit PDB supplied - using symbol server search path: {searchPath}"); + this.ProgressReporter?.Report(new SessionTaskProgress("Downloading PDB from symbol server if necessary.", 0, null)); - this._diaAdapter = new DIAAdapter(this, this._guaranteedLocalPDBFile.GuaranteedLocalPath); + this._diaAdapter = new DIAAdapter(this, this._originalBinaryPathMayBeRemote, searchPath); + } this._taskParameters = new SessionTaskParameters(this, this._diaAdapter, this.DataCache); this._peFile = new PEFile(this._originalBinaryPathMayBeRemote, this.SessionOptions.SymbolSourcesSupported, initializeDiaThreadLog); diff --git a/src/SizeBench.AnalysisEngine/SessionOptions.cs b/src/SizeBench.AnalysisEngine/SessionOptions.cs index 5abb953..2a9ea21 100644 --- a/src/SizeBench.AnalysisEngine/SessionOptions.cs +++ b/src/SizeBench.AnalysisEngine/SessionOptions.cs @@ -3,4 +3,18 @@ public sealed record class SessionOptions { public SymbolSourcesSupported SymbolSourcesSupported { get; init; } = SymbolSourcesSupported.All; + + /// + /// Optional symsrv-style symbol search path used to locate (and download) PDBs when an explicit + /// PDB path is not supplied. The string is passed verbatim to DIA's loadDataForExe, so it follows + /// the standard format used by dbghelp / symsrv. Examples: + /// + /// srv*https://msdl.microsoft.com/download/symbols + /// srv*C:\Symbols*https://msdl.microsoft.com/download/symbols + /// C:\local\symbols;srv*C:\cache*https://internal.symbol.server + /// + /// When this is non-empty and the PDB path passed to + /// is empty, the symbol server will be used to locate the PDB for the binary. + /// + public string? SymbolServerSearchPath { get; init; } } diff --git a/src/SizeBench.AnalysisEngine/SizeBench.AnalysisEngine.csproj b/src/SizeBench.AnalysisEngine/SizeBench.AnalysisEngine.csproj index 23e6d44..107e26f 100644 --- a/src/SizeBench.AnalysisEngine/SizeBench.AnalysisEngine.csproj +++ b/src/SizeBench.AnalysisEngine/SizeBench.AnalysisEngine.csproj @@ -8,6 +8,7 @@ + diff --git a/src/SizeBench.GUI.Tests/Windows/OpenSingleBinaryWindowViewModelTests.cs b/src/SizeBench.GUI.Tests/Windows/OpenSingleBinaryWindowViewModelTests.cs index a822c9f..bd5ba9b 100644 --- a/src/SizeBench.GUI.Tests/Windows/OpenSingleBinaryWindowViewModelTests.cs +++ b/src/SizeBench.GUI.Tests/Windows/OpenSingleBinaryWindowViewModelTests.cs @@ -1,4 +1,5 @@ -using SizeBench.LocalBuild; +using System.IO; +using SizeBench.LocalBuild; using SizeBench.PathLocators; namespace SizeBench.GUI.Windows.Tests; @@ -27,4 +28,39 @@ public void OKButtonRemainsDisabledWithJustBinaryPathSet() Assert.IsFalse(vm.OKEnabled); Assert.IsEmpty(propertiesChanged); } + + [TestMethod] + public void OKButtonEnabledWhenBinaryExistsAndSymbolServerConfigured() + { + // Pick an existing binary that we know does not have a matching PDB next to it on disk. + // notepad.exe is always present in System32 on test machines and won't have a sibling .pdb. + var existingBinaryPath = Path.Combine(Environment.SystemDirectory, "notepad.exe"); + if (!File.Exists(existingBinaryPath)) + { + Assert.Inconclusive($"Cannot run test - expected sentinel binary at {existingBinaryPath}"); + } + + var sso = new SelectSessionOptionsControlViewModel(); + var vm = new OpenSingleBinaryWindowViewModel(new SelectSingleBinaryAndPDBControlViewModel(new IBinaryLocator[] { new LocalBuildPathLocator() }), + sso); + + // Just the binary, no PDB, no symbol server -> still disabled + vm.SelectSingleBinaryAndPDBControlViewModel.BinaryPath = existingBinaryPath; + // Defensive: blow away any inferred PDB so the symbol-server branch is the only way to enable OK + vm.SelectSingleBinaryAndPDBControlViewModel.PDBPath = String.Empty; + Assert.IsFalse(vm.OKEnabled); + + // Enabling the symbol server checkbox without any paths -> still disabled + sso.UseSymbolServer = true; + sso.SymbolServerPathsText = String.Empty; + Assert.IsFalse(vm.OKEnabled); + + // Once a path is provided, OK becomes enabled even without a local PDB + sso.SymbolServerPathsText = "srv*https://msdl.microsoft.com/download/symbols"; + Assert.IsTrue(vm.OKEnabled); + + // Disabling the symbol server toggle should drop the OK enablement again + sso.UseSymbolServer = false; + Assert.IsFalse(vm.OKEnabled); + } } diff --git a/src/SizeBench.GUI.Tests/Windows/SelectSessionOptionsControlViewModelTests.cs b/src/SizeBench.GUI.Tests/Windows/SelectSessionOptionsControlViewModelTests.cs index f0cffcf..a660f7f 100644 --- a/src/SizeBench.GUI.Tests/Windows/SelectSessionOptionsControlViewModelTests.cs +++ b/src/SizeBench.GUI.Tests/Windows/SelectSessionOptionsControlViewModelTests.cs @@ -91,4 +91,36 @@ public void ChangingIndividualSymbolTypesUpdatesSymbolSourcesSupported() Assert.HasCount(4, propertiesChanged); Assert.AreSequenceEqual(new[] { "", "", "", "" }, propertiesChanged, Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder.InAnyOrder); } + + [TestMethod] + public void SymbolServerToggleFlowsThroughToSessionOptions() + { + var vm = new SelectSessionOptionsControlViewModel(); + + // Default - no symbol server + Assert.IsFalse(vm.UseSymbolServer); + Assert.IsNull(vm.SessionOptions.SymbolServerSearchPath); + Assert.IsFalse(vm.HasAnySymbolServerPaths); + + // Setting the text only does not enable usage; SessionOptions still has null search path + vm.SymbolServerPathsText = "srv*https://contoso/symbols"; + Assert.IsTrue(vm.HasAnySymbolServerPaths); + Assert.IsNull(vm.SessionOptions.SymbolServerSearchPath); + + // Enabling the checkbox surfaces the search path to SessionOptions + vm.UseSymbolServer = true; + Assert.AreEqual("srv*https://contoso/symbols", vm.SessionOptions.SymbolServerSearchPath); + + // Multiple lines are joined with ';' for symsrv + vm.SymbolServerPathsText = "C:\\local\r\nsrv*C:\\cache*https://msdl"; + Assert.AreEqual("C:\\local;srv*C:\\cache*https://msdl", vm.SessionOptions.SymbolServerSearchPath); + + // Blank lines are ignored + vm.SymbolServerPathsText = "\r\n\r\nC:\\local\r\n\r\n"; + Assert.AreEqual("C:\\local", vm.SessionOptions.SymbolServerSearchPath); + + // Disabling the toggle removes the search path even if text remains + vm.UseSymbolServer = false; + Assert.IsNull(vm.SessionOptions.SymbolServerSearchPath); + } } diff --git a/src/SizeBench.GUI/Settings/AppSettings.cs b/src/SizeBench.GUI/Settings/AppSettings.cs new file mode 100644 index 0000000..71eb64f --- /dev/null +++ b/src/SizeBench.GUI/Settings/AppSettings.cs @@ -0,0 +1,115 @@ +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SizeBench.GUI.Settings; + +internal sealed class AppSettings : IAppSettings +{ + private const string DefaultPublicMsftSymbolServer = "srv*https://msdl.microsoft.com/download/symbols"; + + private readonly string _filePath; + private readonly PersistedShape _data; + + public AppSettings() : this(DefaultSettingsFilePath()) { } + + internal AppSettings(string filePath) + { + this._filePath = filePath; + this._data = Load(filePath); + } + + public bool UseSymbolServer + { + get => this._data.UseSymbolServer; + set + { + if (this._data.UseSymbolServer != value) + { + this._data.UseSymbolServer = value; + Save(); + } + } + } + + public IList SymbolServerPaths => this._data.SymbolServerPaths; + + public void SetSymbolServerPaths(IEnumerable paths) + { + ArgumentNullException.ThrowIfNull(paths); + + this._data.SymbolServerPaths = paths.Where(p => !String.IsNullOrWhiteSpace(p)) + .Select(p => p.Trim()) + .ToList(); + Save(); + } + + public string BuildSymbolSearchPath() + => String.Join(";", this._data.SymbolServerPaths.Where(p => !String.IsNullOrWhiteSpace(p))); + + private void Save() + { + try + { + var dir = Path.GetDirectoryName(this._filePath); + if (!String.IsNullOrEmpty(dir)) + { + Directory.CreateDirectory(dir); + } + var json = JsonSerializer.Serialize(this._data, SerializerOptions); + File.WriteAllText(this._filePath, json); + } +#pragma warning disable CA1031 // Do not catch general exception types - settings persistence failures should not crash the app + catch + { + // Best-effort persistence: if we can't save settings we still want the app to keep working. + } +#pragma warning restore CA1031 + } + + internal static string DefaultSettingsFilePath() + => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "SizeBench", + "settings.json"); + + private static PersistedShape Load(string filePath) + { + try + { + if (File.Exists(filePath)) + { + var json = File.ReadAllText(filePath); + var loaded = JsonSerializer.Deserialize(json, SerializerOptions); + if (loaded != null) + { + loaded.SymbolServerPaths ??= new List(); + return loaded; + } + } + } +#pragma warning disable CA1031 // Do not catch general exception types - corrupt settings should fall back to defaults + catch + { + // Fall through to defaults on any read/parse failure + } +#pragma warning restore CA1031 + + return new PersistedShape + { + UseSymbolServer = false, + SymbolServerPaths = new List { DefaultPublicMsftSymbolServer }, + }; + } + + private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + internal sealed class PersistedShape + { + public bool UseSymbolServer { get; set; } + public List SymbolServerPaths { get; set; } = new(); + } +} diff --git a/src/SizeBench.GUI/Settings/IAppSettings.cs b/src/SizeBench.GUI/Settings/IAppSettings.cs new file mode 100644 index 0000000..b38a82b --- /dev/null +++ b/src/SizeBench.GUI/Settings/IAppSettings.cs @@ -0,0 +1,10 @@ +namespace SizeBench.GUI.Settings; + +public interface IAppSettings +{ + bool UseSymbolServer { get; set; } + + IList SymbolServerPaths { get; } + + void SetSymbolServerPaths(IEnumerable paths); +} diff --git a/src/SizeBench.GUI/SizeBench.GUI.csproj b/src/SizeBench.GUI/SizeBench.GUI.csproj index 954980f..1732fa7 100644 --- a/src/SizeBench.GUI/SizeBench.GUI.csproj +++ b/src/SizeBench.GUI/SizeBench.GUI.csproj @@ -27,7 +27,10 @@ + + + diff --git a/src/SizeBench.GUI/Windows/OpenSingleBinaryWindowViewModel.cs b/src/SizeBench.GUI/Windows/OpenSingleBinaryWindowViewModel.cs index 1310fff..ed2e9cd 100644 --- a/src/SizeBench.GUI/Windows/OpenSingleBinaryWindowViewModel.cs +++ b/src/SizeBench.GUI/Windows/OpenSingleBinaryWindowViewModel.cs @@ -13,6 +13,7 @@ public OpenSingleBinaryWindowViewModel(SelectSingleBinaryAndPDBControlViewModel this.SelectSingleBinaryAndPDBControlViewModel.PropertyChanged += (s, e) => EnableOKButtonIfReady(); this.SelectSessionOptionsControlViewModel = ssoViewModel; + this.SelectSessionOptionsControlViewModel.PropertyChanged += (s, e) => EnableOKButtonIfReady(); } public SelectSingleBinaryAndPDBControlViewModel SelectSingleBinaryAndPDBControlViewModel { get; } @@ -44,8 +45,22 @@ public bool OKEnabled private void EnableOKButtonIfReady() { - this.OKEnabled = File.Exists(this.SelectSingleBinaryAndPDBControlViewModel.PDBPath) && - File.Exists(this.SelectSingleBinaryAndPDBControlViewModel.BinaryPath); + if (!File.Exists(this.SelectSingleBinaryAndPDBControlViewModel.BinaryPath)) + { + this.OKEnabled = false; + return; + } + + if (File.Exists(this.SelectSingleBinaryAndPDBControlViewModel.PDBPath)) + { + this.OKEnabled = true; + return; + } + + // No explicit PDB on disk - allow proceeding only when the symbol server fallback is configured + // with at least one path, so that DIA has somewhere to look for the matching PDB. + this.OKEnabled = this.SelectSessionOptionsControlViewModel.UseSymbolServer && + this.SelectSessionOptionsControlViewModel.HasAnySymbolServerPaths; } } diff --git a/src/SizeBench.GUI/Windows/SelectSessionOptionsControl.xaml b/src/SizeBench.GUI/Windows/SelectSessionOptionsControl.xaml index e09fa34..22fa656 100644 --- a/src/SizeBench.GUI/Windows/SelectSessionOptionsControl.xaml +++ b/src/SizeBench.GUI/Windows/SelectSessionOptionsControl.xaml @@ -7,31 +7,59 @@ d:DesignWidth="500" d:Background="White" MaxWidth="600"> - - - - - - + + + + + + + - - - - - - - - - - - Some sources of symbols (like PDATA and XDATA) can take a long time to parse out of large binaries, so - you can disable them to open a binary up much faster if you're only looking for certain things like code. - Hover over any of the checkboxes to see a description of this type of symbol. - - - Note: if some symbol sources are disabled, then SizeBench will show a subset of its normal data. - - - + + + + + + + + + + + Some sources of symbols (like PDATA and XDATA) can take a long time to parse out of large binaries, so + you can disable them to open a binary up much faster if you're only looking for certain things like code. + Hover over any of the checkboxes to see a description of this type of symbol. + + + Note: if some symbol sources are disabled, then SizeBench will show a subset of its normal data. + + + + + + + + + + One symbol server path per line. Each line is a standard symsrv-style entry, for example: + + srv*C:\Symbols*https://msdl.microsoft.com/download/symbols + + The list below is saved across sessions. + + + + + + \ No newline at end of file diff --git a/src/SizeBench.GUI/Windows/SelectSessionOptionsControlViewModel.cs b/src/SizeBench.GUI/Windows/SelectSessionOptionsControlViewModel.cs index 6a1cf5c..c7d03bb 100644 --- a/src/SizeBench.GUI/Windows/SelectSessionOptionsControlViewModel.cs +++ b/src/SizeBench.GUI/Windows/SelectSessionOptionsControlViewModel.cs @@ -1,14 +1,34 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using SizeBench.AnalysisEngine; +using SizeBench.GUI.Settings; namespace SizeBench.GUI.Windows; public sealed class SelectSessionOptionsControlViewModel : INotifyPropertyChanged { + private readonly IAppSettings? _appSettings; + + public SelectSessionOptionsControlViewModel() : this(null) { } + + public SelectSessionOptionsControlViewModel(IAppSettings? appSettings) + { + this._appSettings = appSettings; + + if (appSettings != null) + { + this._useSymbolServer = appSettings.UseSymbolServer; + this._symbolServerPathsText = String.Join(Environment.NewLine, appSettings.SymbolServerPaths); + } + } + public SymbolSourcesSupported SymbolSourcesSupported { get; private set; } = SymbolSourcesSupported.All; - public SessionOptions SessionOptions => new SessionOptions() { SymbolSourcesSupported = this.SymbolSourcesSupported }; + public SessionOptions SessionOptions => new SessionOptions() + { + SymbolSourcesSupported = this.SymbolSourcesSupported, + SymbolServerSearchPath = this.UseSymbolServer ? BuildSymbolSearchPath() : null, + }; public bool CodeSymbolsSupported { @@ -46,6 +66,48 @@ public bool OtherPESymbolsSupported set => SetFlagOnSymbolsSupported(SymbolSourcesSupported.OtherPESymbols, value); } + private bool _useSymbolServer; + public bool UseSymbolServer + { + get => this._useSymbolServer; + set + { + if (this._useSymbolServer != value) + { + this._useSymbolServer = value; + this._appSettings?.UseSymbolServer = value; + RaiseOnPropertyChanged(String.Empty); + } + } + } + + private string _symbolServerPathsText = String.Empty; + public string SymbolServerPathsText + { + get => this._symbolServerPathsText; + set + { + value ??= String.Empty; + if (this._symbolServerPathsText != value) + { + this._symbolServerPathsText = value; + this._appSettings?.SetSymbolServerPaths(ParseLines(value)); + RaiseOnPropertyChanged(String.Empty); + } + } + } + + public bool HasAnySymbolServerPaths => ParseLines(this._symbolServerPathsText).Any(); + + private string BuildSymbolSearchPath() + => String.Join(";", ParseLines(this._symbolServerPathsText)); + + private static IEnumerable ParseLines(string text) + => (text ?? String.Empty) + .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) + .Select(p => p.Trim()) + .Where(p => p.Length > 0); + private void SetFlagOnSymbolsSupported(SymbolSourcesSupported flag, bool flagValue) { if (flagValue) diff --git a/src/SizeBench.GUI/WindsorInstaller.cs b/src/SizeBench.GUI/WindsorInstaller.cs index 1ad78a9..3e88387 100644 --- a/src/SizeBench.GUI/WindsorInstaller.cs +++ b/src/SizeBench.GUI/WindsorInstaller.cs @@ -52,6 +52,10 @@ public void Install(IWindsorContainer container, IConfigurationStore store) .Instance(new SessionFactory()) .LifestyleSingleton()); + container.Register(Component.For() + .ImplementedBy() + .LifestyleSingleton()); + container.Register(Classes.FromAssembly(typeof(MainWindow).Assembly) .IncludeNonPublicTypes() .Where(c => c.Name.EndsWith("ViewModel", StringComparison.Ordinal) &&