diff --git a/Source/Microsoft.Gateway/Microsoft.InnerEye.Gateway.Logging/ServiceStatus.cs b/Source/Microsoft.Gateway/Microsoft.InnerEye.Gateway.Logging/ServiceStatus.cs index be9be32..65c3a93 100644 --- a/Source/Microsoft.Gateway/Microsoft.InnerEye.Gateway.Logging/ServiceStatus.cs +++ b/Source/Microsoft.Gateway/Microsoft.InnerEye.Gateway.Logging/ServiceStatus.cs @@ -56,6 +56,11 @@ /// NewConfigurationError, + /// + /// Configuration files have changed. + /// + NewConfigurationDetetected, + /// /// Error in ping. /// diff --git a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/AETConfigProvider.cs b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/AETConfigProvider.cs index 59c6536..24d3f93 100644 --- a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/AETConfigProvider.cs +++ b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/AETConfigProvider.cs @@ -6,17 +6,17 @@ using Microsoft.Extensions.Logging; /// - /// Monitor a JSON file containing a list of AETConfigModels. + /// Monitor a JSON file or folder containing a list of . /// - public class AETConfigProvider : BaseConfigProvider> + public class AETConfigProvider : BaseConfigProvider> { /// - /// File name for JSON file containing a list of AETConfigModels. + /// File name for JSON file containing a list of . /// public static readonly string AETConfigFileName = "GatewayModelRulesConfig.json"; /// - /// Folder name for folder containing JSON files, each containing a list of AETConfigModels. + /// Folder name for folder containing JSON files, each containing a list of . /// public static readonly string AETConfigFolderName = "GatewayModelRulesConfig"; @@ -30,25 +30,18 @@ ILogger logger, string configurationsPathRoot, bool useFile = false) : base(logger, - Path.Combine(configurationsPathRoot, useFile ? AETConfigFileName : AETConfigFolderName)) + Path.Combine(configurationsPathRoot, useFile ? string.Empty : AETConfigFolderName), + useFile ? AETConfigFileName : string.Empty, + MergeModels) { } /// - /// Lookup list of AETConfigModels from a JSON file. + /// Helper to create a for returning list of from cache. /// - /// List of AETConfigModels. - public IEnumerable GetAETConfigs() - { - Load(); - - _t = _ts != null ? MergeModels(_ts) : _t; - - // no need to keep two copies of all the config data. - _ts = null; - - return _t; - } + /// Cached list of . + public IEnumerable AETConfigModels() => + Config; /// /// Merge a list of lists of AET config models into one list. @@ -64,7 +57,7 @@ /// /// List of lists of AET config models. /// List of AET config models. - private static List MergeModels(IEnumerable> modelLists) + private static List MergeModels(IEnumerable> modelLists) { var mergedModels = new List(); diff --git a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/BaseConfigProvider.cs b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/BaseConfigProvider.cs index 6c70e9c..858182a 100644 --- a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/BaseConfigProvider.cs +++ b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/BaseConfigProvider.cs @@ -12,8 +12,50 @@ /// Class that monitors a JSON settings file or folder. /// /// Data type underlying the JSON settings. - public class BaseConfigProvider + public class BaseConfigProvider : IDisposable { + /// + /// Class to hold results from trying to open and parse a possible JSON file. + /// + private class LoadJsonResult + { + /// + /// True if the file has loaded, false otherwise. + /// + /// + /// The FileSystemWatcher can report file changed events whilst another process is saving + /// a file. In this case there may be a System.IO.IOException (“File used by another process”). + /// + public bool Loaded { get; } + + /// + /// True if the file has been parsed, false otherwise. + /// + /// + /// In the case of loading from a folder, all files are loaded and parsed. There may be other files + /// there and they are to be ignored. + /// + public bool Parsed { get; } + + /// + /// An instance of type T if the file has been loaded and parsed correctly. Default(T) otherwise. + /// + public T Result { get; } + + /// + /// Initialize a new instance of the class. + /// + /// True if file loaded. + /// True if file parsed. + /// Instance of type T if loaded and parsed. + public LoadJsonResult(bool loaded, bool parsed, T result) + { + Loaded = loaded; + Parsed = parsed; + Result = result; + } + } + /// /// Logger for errors loading or parsing JSON. /// @@ -25,60 +67,139 @@ private readonly string _settingsFileOrFolderName; /// - /// Cached copy of data as last loaded from JSON file. + /// Optional flat map to handle folders. /// - protected T _t; + private readonly Func, T> _flatMap; /// - /// Cached copy of data as last loaded from folder of JSON files. + /// File system watcher to monitor changes to file or folder. /// - protected IEnumerable _ts; + private readonly FileSystemWatcher _fileSystemWatcher; + + /// + /// Disposed flag for IDisposable. + /// + private bool disposedValue; + + /// + /// Config as last loaded from file or folder. + /// + public T Config { get; private set; } + + /// + /// Called when the config has changed. + /// + public event EventHandler ConfigChanged; /// /// Initialize a new instance of the class. /// /// Logger. - /// JSON settings file or folder. + /// Settings folder name. + /// Optional settings file, use String.Empty to monitor a folder. + /// Optional flat map to handle folders. This should merge a T from each file in the folder + /// into a single new T. This is required if monitoring a folder. public BaseConfigProvider( ILogger logger, - string settingsFileOrFolderName) + string folderName, + string settingsFile, + Func, T> flatMap = null) { _logger = logger; - _settingsFileOrFolderName = settingsFileOrFolderName; + _settingsFileOrFolderName = Path.Combine(folderName, settingsFile); + _flatMap = flatMap; + + if (string.IsNullOrWhiteSpace(settingsFile) && flatMap == null) + { + throw new ArgumentNullException(nameof(flatMap), "If monitoring a folder, flatMap must be supplied"); + } + + _fileSystemWatcher = new FileSystemWatcher(folderName) + { + Filter = !string.IsNullOrWhiteSpace(settingsFile) ? settingsFile : "*.json", + NotifyFilter = NotifyFilters.LastWrite, + }; + + _fileSystemWatcher.Changed += OnChanged; + _fileSystemWatcher.EnableRaisingEvents = true; + + Load(); } /// - /// Load T or Ts from a JSON file or folder. + /// File watcher Changed event handler. Filter the events, reload the config and if successful invoke ConfigChanged. /// - protected void Load() + /// Sender. + /// File system event args. + private void OnChanged(object sender, FileSystemEventArgs e) + { + if (e.ChangeType != WatcherChangeTypes.Changed) + { + return; + } + + var logEntry = LogEntry.Create(ServiceStatus.NewConfigurationDetetected, + string.Format("Settings have changed: {0}", e.FullPath)); + logEntry.Log(_logger, LogLevel.Information); + + if (!Load()) + { + return; + } + + ConfigChanged?.Invoke(this, new EventArgs()); + } + + /// + /// Load T from a JSON file or folder. + /// + /// True if new config has been loaded, false otherwise. + private bool Load() { if (File.Exists(_settingsFileOrFolderName)) { - _ts = null; + var loadJsonResult = LoadFile(_settingsFileOrFolderName); - (_t, _) = LoadFile(_settingsFileOrFolderName); + if (!loadJsonResult.Loaded || !loadJsonResult.Parsed) + { + return false; + } + + Config = loadJsonResult.Result; + + return true; } else if (Directory.Exists(_settingsFileOrFolderName)) { - _t = default(T); var ts = new List(); foreach (var file in Directory.EnumerateFiles(_settingsFileOrFolderName, "*.json")) { - var (t, loaded) = LoadFile(file); - if (loaded) + var loadJsonResult = LoadFile(file); + if (!loadJsonResult.Loaded) { - ts.Add(t); + // File still in use, FileWatcher has reported file changed but + // the other process has not finished yet. + return false; + } + + if (loadJsonResult.Parsed) + { + ts.Add(loadJsonResult.Result); } } - _ts = ts.ToArray(); + Config = _flatMap(ts); + + return true; } else { var logEntry = LogEntry.Create(ServiceStatus.NewConfigurationError, string.Format("Settings is neither a file nor a folder: {0}", _settingsFileOrFolderName)); logEntry.Log(_logger, LogLevel.Error); + + return false; } } @@ -86,49 +207,62 @@ /// Update settings file, according to an update callback function. /// /// Callback to update the settings. Return new settings for update, or the same object to not update. - /// How to compare objects. - protected void UpdateFile(Func updater, IEqualityComparer equalityComparer) + /// Optional, how to compare objects. + public (T, bool) Update(Func updater, IEqualityComparer equalityComparer = null) { if (!File.Exists(_settingsFileOrFolderName)) { throw new NotImplementedException(string.Format("Can only update single settings files: {0}", _settingsFileOrFolderName)); } - var (t, loaded) = LoadFile(_settingsFileOrFolderName); - if (!loaded) + var loadJsonResult = LoadFile(_settingsFileOrFolderName); + if (!loadJsonResult.Loaded || !loadJsonResult.Parsed) { - return; + return (default(T), false); } - var newt = updater.Invoke(t); - if (equalityComparer.Equals(newt, t)) + var newt = updater(loadJsonResult.Result); + + equalityComparer = equalityComparer ?? EqualityComparer.Default; + + if (equalityComparer.Equals(newt, loadJsonResult.Result)) { - return; + return (default(T), false); } SaveFile(newt, _settingsFileOrFolderName); + + return (newt, true); } /// /// Load T from a JSON file. /// /// Path to file. - /// Pair of T and true if file loaded correctly, false otherwise. - private (T, bool) LoadFile(string path) + /// New . + private LoadJsonResult LoadFile(string path) { try { var jsonText = File.ReadAllText(path); - return (JsonConvert.DeserializeObject(jsonText), true); + return new LoadJsonResult(true, true, JsonConvert.DeserializeObject(jsonText)); } - catch (Exception e) + catch (JsonSerializationException e) + { + var logEntry = LogEntry.Create(ServiceStatus.NewConfigurationError, + string.Format("Unable to parse settings file {0}", path)); + logEntry.Log(_logger, LogLevel.Error, e); + + return new LoadJsonResult(true, false, default(T)); + } + catch (IOException e) { var logEntry = LogEntry.Create(ServiceStatus.NewConfigurationError, string.Format("Unable to load settings file {0}", path)); logEntry.Log(_logger, LogLevel.Error, e); - return (default(T), false); + return new LoadJsonResult(false, false, default(T)); } } @@ -149,5 +283,33 @@ var jsonText = JsonConvert.SerializeObject(t, serializerSettings); File.WriteAllText(path, jsonText); } + + /// + /// Disposes of all managed resources. + /// + /// If we are disposing. + protected virtual void Dispose(bool disposing) + { + if (disposedValue) + { + return; + } + + if (disposing) + { + _fileSystemWatcher.Dispose(); + } + + disposedValue = true; + } + + /// + /// Implements the disposable pattern. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } } } diff --git a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/GatewayProcessorConfigProvider.cs b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/GatewayProcessorConfigProvider.cs index 74ec29a..c8cfc96 100644 --- a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/GatewayProcessorConfigProvider.cs +++ b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/GatewayProcessorConfigProvider.cs @@ -1,20 +1,18 @@ namespace Microsoft.InnerEye.Listener.Common.Providers { using System; - using System.Collections.Generic; - using System.IO; using Microsoft.Extensions.Logging; using Microsoft.InnerEye.Azure.Segmentation.Client; using Microsoft.InnerEye.Gateway.Logging; using Microsoft.InnerEye.Gateway.Models; /// - /// Monitor a JSON file containing a GatewayProcessorConfig. + /// Monitor a JSON file containing a . /// public class GatewayProcessorConfigProvider : BaseConfigProvider { /// - /// File name for JSON file containing a GatewayProcessorConfig. + /// File name for JSON file containing a . /// public static readonly string GatewayProcessorConfigFileName = "GatewayProcessorConfig.json"; @@ -26,27 +24,10 @@ public GatewayProcessorConfigProvider( ILogger logger, string configurationsPathRoot) : base(logger, - Path.Combine(configurationsPathRoot, GatewayProcessorConfigFileName)) + configurationsPathRoot, GatewayProcessorConfigFileName) { } - /// - /// Load GatewayProcessorConfig from a JSON file. - /// - /// Loaded GatewayProcessorConfig. - public GatewayProcessorConfig GatewayProcessorConfig() - { - Load(); - return _t; - } - - /// - /// Update GatewayProcessorConfig file, according to an update callback function. - /// - /// Callback to update the settings. Return new settings for update, or the same object to not update. - public void Update(Func updater) => - UpdateFile(updater, EqualityComparer.Default); - /// /// Set ServiceSettings.RunAsConsole. /// @@ -55,7 +36,7 @@ Update(gatewayProcessorConfig => gatewayProcessorConfig.With(new ServiceSettings(runAsConsole))); /// - /// Update ProcessorSettings. + /// Update . /// /// Optional new inference API Uri. /// Optional new license key. @@ -78,45 +59,45 @@ } /// - /// Load ServiceSettings from a JSON file. + /// Helper to create a for returning from cached . /// - /// Loaded ServiceSettings. + /// Cached . public ServiceSettings ServiceSettings() => - GatewayProcessorConfig().ServiceSettings; + Config.ServiceSettings; /// - /// Load ProcessorSettings from a JSON file. + /// Helper to create a for returning from cached . /// - /// Loaded ProcessorSettings. + /// Cached . public ProcessorSettings ProcessorSettings() => - GatewayProcessorConfig().ProcessorSettings; + Config.ProcessorSettings; /// - /// Load DequeueServiceConfig from a JSON file. + /// Helper to create a for returning from cached . /// - /// Loaded DequeueServiceConfig. + /// Cached . public DequeueServiceConfig DequeueServiceConfig() => - GatewayProcessorConfig().DequeueServiceConfig; + Config.DequeueServiceConfig; /// - /// Load DownloadServiceConfig from a JSON file. + /// Helper to create a for returning from cached . /// - /// Loaded DownloadServiceConfig. + /// Cached . public DownloadServiceConfig DownloadServiceConfig() => - GatewayProcessorConfig().DownloadServiceConfig; + Config.DownloadServiceConfig; /// - /// Load ConfigurationServiceConfig from a JSON file. + /// Helper to create a for returning from cached . /// - /// Loaded ConfigurationServiceConfig. + /// Cached . public ConfigurationServiceConfig ConfigurationServiceConfig() => - GatewayProcessorConfig().ConfigurationServiceConfig; + Config.ConfigurationServiceConfig; /// - /// Create a new segmentation client based on settings in JSON file. + /// Create a new based on settings in JSON file. /// /// Optional logger for client. - /// New IInnerEyeSegmentationClient. + /// New . public Func CreateInnerEyeSegmentationClient(ILogger logger = null) => () => { diff --git a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/GatewayReceiveConfigProvider.cs b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/GatewayReceiveConfigProvider.cs index e56e242..1731529 100644 --- a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/GatewayReceiveConfigProvider.cs +++ b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Common/Providers/GatewayReceiveConfigProvider.cs @@ -1,18 +1,15 @@ namespace Microsoft.InnerEye.Listener.Common.Providers { - using System; - using System.Collections.Generic; - using System.IO; using Microsoft.Extensions.Logging; using Microsoft.InnerEye.Gateway.Models; /// - /// Monitor a JSON file containing a GatewayReceiveConfig. + /// Monitor a JSON file containing a . /// public class GatewayReceiveConfigProvider : BaseConfigProvider { /// - /// File name for JSON file containing a GatewayReceiveConfig. + /// File name for JSON file containing a . /// public static readonly string GatewayReceiveConfigFileName = "GatewayReceiveConfig.json"; @@ -24,27 +21,10 @@ public GatewayReceiveConfigProvider( ILogger logger, string configurationsPathRoot) : base(logger, - Path.Combine(configurationsPathRoot, GatewayReceiveConfigFileName)) + configurationsPathRoot, GatewayReceiveConfigFileName) { } - /// - /// Load GatewayReceiveConfig from a JSON file. - /// - /// Loaded GatewayReceiveConfig. - public GatewayReceiveConfig GatewayReceiveConfig() - { - Load(); - return _t; - } - - /// - /// Update GatewayReceiveConfig file, according to an update callback function. - /// - /// Callback to update the settings. Return new settings for update, or the same object to not update. - public void Update(Func updater) => - UpdateFile(updater, EqualityComparer.Default); - /// /// Set ServiceSettings.RunAsConsole. /// @@ -53,24 +33,24 @@ Update(gatewayReceiveConfig => gatewayReceiveConfig.With(new ServiceSettings(runAsConsole))); /// - /// Load ServiceSettings from a JSON file. + /// Helper to create a for returning from cached . /// - /// Loaded ServiceSettings. + /// Cached . public ServiceSettings ServiceSettings() => - GatewayReceiveConfig().ServiceSettings; + Config.ServiceSettings; /// - /// Load ConfigurationServiceConfig from a JSON file. + /// Helper to create a for returning from cached . /// - /// Loaded ConfigurationServiceConfig. + /// Cached . public ConfigurationServiceConfig ConfigurationServiceConfig() => - GatewayReceiveConfig().ConfigurationServiceConfig; + Config.ConfigurationServiceConfig; /// - /// Load ReceiveServiceConfig from a JSON file. + /// Helper to create a for returning from cached . /// - /// Loaded ReceiveServiceConfig. + /// Cached . public ReceiveServiceConfig ReceiveServiceConfig() => - GatewayReceiveConfig().ReceiveServiceConfig; + Config.ReceiveServiceConfig; } } diff --git a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Processor/Program.cs b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Processor/Program.cs index b69a6ca..c528fe8 100644 --- a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Processor/Program.cs +++ b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Processor/Program.cs @@ -34,55 +34,55 @@ var configurationsPathRoot = ConfigurationService.FindRelativeDirectory(relativePaths, loggerFactory.CreateLogger("Main")); - var aetConfigurationProvider = new AETConfigProvider( - loggerFactory.CreateLogger("ModelSettings"), - configurationsPathRoot); + using (var aetConfigurationProvider = new AETConfigProvider( + loggerFactory.CreateLogger("ModelSettings"), + configurationsPathRoot)) + using (var gatewayProcessorConfigProvider = new GatewayProcessorConfigProvider( + loggerFactory.CreateLogger("ProcessorSettings"), + configurationsPathRoot)) + { + var segmentationClientLogger = loggerFactory.CreateLogger("SegmentationClient"); - var gatewayProcessorConfigProvider = new GatewayProcessorConfigProvider( - loggerFactory.CreateLogger("ProcessorSettings"), - configurationsPathRoot); - - var segmentationClientLogger = loggerFactory.CreateLogger("SegmentationClient"); - - // The ProjectInstaller.cs uses the service name to install the service. - // If you change it please update the ProjectInstaller.cs - ServiceHelpers.RunServices( - ServiceName, - gatewayProcessorConfigProvider.ServiceSettings(), - new ConfigurationService( - gatewayProcessorConfigProvider.CreateInnerEyeSegmentationClient(segmentationClientLogger), - gatewayProcessorConfigProvider.ConfigurationServiceConfig, - loggerFactory.CreateLogger("ConfigurationService"), - new UploadService( + // The ProjectInstaller.cs uses the service name to install the service. + // If you change it please update the ProjectInstaller.cs + ServiceHelpers.RunServices( + ServiceName, + gatewayProcessorConfigProvider.ServiceSettings(), + new ConfigurationService( gatewayProcessorConfigProvider.CreateInnerEyeSegmentationClient(segmentationClientLogger), - aetConfigurationProvider.GetAETConfigs, - GatewayMessageQueue.UploadQueuePath, - GatewayMessageQueue.DownloadQueuePath, - GatewayMessageQueue.DeleteQueuePath, - gatewayProcessorConfigProvider.DequeueServiceConfig, - loggerFactory.CreateLogger("UploadService"), - instances: 2), - new DownloadService( - gatewayProcessorConfigProvider.CreateInnerEyeSegmentationClient(segmentationClientLogger), - GatewayMessageQueue.DownloadQueuePath, - GatewayMessageQueue.PushQueuePath, - GatewayMessageQueue.DeleteQueuePath, - gatewayProcessorConfigProvider.DownloadServiceConfig, - gatewayProcessorConfigProvider.DequeueServiceConfig, - loggerFactory.CreateLogger("DownloadService"), - instances: 1), - new PushService( - aetConfigurationProvider.GetAETConfigs, - new DicomDataSender(), - GatewayMessageQueue.PushQueuePath, - GatewayMessageQueue.DeleteQueuePath, - gatewayProcessorConfigProvider.DequeueServiceConfig, - loggerFactory.CreateLogger("PushService"), - instances: 1), - new DeleteService( - GatewayMessageQueue.DeleteQueuePath, - gatewayProcessorConfigProvider.DequeueServiceConfig, - loggerFactory.CreateLogger("DeleteService")))); + gatewayProcessorConfigProvider.ConfigurationServiceConfig, + loggerFactory.CreateLogger("ConfigurationService"), + new UploadService( + gatewayProcessorConfigProvider.CreateInnerEyeSegmentationClient(segmentationClientLogger), + aetConfigurationProvider.AETConfigModels, + GatewayMessageQueue.UploadQueuePath, + GatewayMessageQueue.DownloadQueuePath, + GatewayMessageQueue.DeleteQueuePath, + gatewayProcessorConfigProvider.DequeueServiceConfig, + loggerFactory.CreateLogger("UploadService"), + instances: 2), + new DownloadService( + gatewayProcessorConfigProvider.CreateInnerEyeSegmentationClient(segmentationClientLogger), + GatewayMessageQueue.DownloadQueuePath, + GatewayMessageQueue.PushQueuePath, + GatewayMessageQueue.DeleteQueuePath, + gatewayProcessorConfigProvider.DownloadServiceConfig, + gatewayProcessorConfigProvider.DequeueServiceConfig, + loggerFactory.CreateLogger("DownloadService"), + instances: 1), + new PushService( + aetConfigurationProvider.AETConfigModels, + new DicomDataSender(), + GatewayMessageQueue.PushQueuePath, + GatewayMessageQueue.DeleteQueuePath, + gatewayProcessorConfigProvider.DequeueServiceConfig, + loggerFactory.CreateLogger("PushService"), + instances: 1), + new DeleteService( + GatewayMessageQueue.DeleteQueuePath, + gatewayProcessorConfigProvider.DequeueServiceConfig, + loggerFactory.CreateLogger("DeleteService")))); + } } } } diff --git a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Receiver/Program.cs b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Receiver/Program.cs index a0cff82..4b18a08 100644 --- a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Receiver/Program.cs +++ b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Receiver/Program.cs @@ -33,23 +33,24 @@ var configurationsPathRoot = ConfigurationService.FindRelativeDirectory(relativePaths, loggerFactory.CreateLogger("Main")); - var gatewayReceiveConfigProvider = new GatewayReceiveConfigProvider( + using (var gatewayReceiveConfigProvider = new GatewayReceiveConfigProvider( loggerFactory.CreateLogger("ProcessorSettings"), - configurationsPathRoot); - - // The ProjectInstaller.cs uses the service name to install the service. - // If you change it please update the ProjectInstaller.cs - ServiceHelpers.RunServices( - ServiceName, - gatewayReceiveConfigProvider.ServiceSettings(), - new ConfigurationService( - null, - gatewayReceiveConfigProvider.ConfigurationServiceConfig, - loggerFactory.CreateLogger("ConfigurationService"), - new ReceiveService( - gatewayReceiveConfigProvider.ReceiveServiceConfig, - GatewayMessageQueue.UploadQueuePath, - loggerFactory.CreateLogger("ReceiveService")))); + configurationsPathRoot)) + { + // The ProjectInstaller.cs uses the service name to install the service. + // If you change it please update the ProjectInstaller.cs + ServiceHelpers.RunServices( + ServiceName, + gatewayReceiveConfigProvider.ServiceSettings(), + new ConfigurationService( + null, + gatewayReceiveConfigProvider.ConfigurationServiceConfig, + loggerFactory.CreateLogger("ConfigurationService"), + new ReceiveService( + gatewayReceiveConfigProvider.ReceiveServiceConfig, + GatewayMessageQueue.UploadQueuePath, + loggerFactory.CreateLogger("ReceiveService")))); + } } } } diff --git a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/BaseTestClass.cs b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/BaseTestClass.cs index a9d2db6..0067ed1 100644 --- a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/BaseTestClass.cs +++ b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/BaseTestClass.cs @@ -34,7 +34,7 @@ /// The base test class. /// [TestClass] - public class BaseTestClass + public class BaseTestClass : IDisposable { /// /// List of chars to use for random string generation. @@ -44,12 +44,12 @@ /// /// LoggerFactory for creating more ILoggers. /// - protected readonly Microsoft.Extensions.Logging.ILoggerFactory _loggerFactory; + private readonly Microsoft.Extensions.Logging.ILoggerFactory _loggerFactory; /// /// Logger for common use. /// - protected readonly ILogger _baseTestLogger; + private readonly ILogger _baseTestLogger; /// /// Gets or sets the test context. @@ -110,7 +110,12 @@ /// /// GatewayReceiveConfigProvider as loaded from _basePathConfigs. /// - private GatewayReceiveConfigProvider _testGatewayReceiveConfigProvider; + protected GatewayReceiveConfigProvider TestGatewayReceiveConfigProvider { get; } + + /// + /// Disposed flag for IDisposable. + /// + private bool disposedValue; /// /// Initializes a new instance of the class. @@ -125,9 +130,9 @@ // Set a logger for fo-dicom network operations so that they show up in VS output when debugging Dicom.Log.LogManager.SetImplementation(new Dicom.Log.TextWriterLogManager(new DataProviderTests.DebugTextWriter())); - _testAETConfigProvider = new AETConfigProvider(_loggerFactory.CreateLogger("ModelSettings"), _basePathConfigs); - TestGatewayProcessorConfigProvider = new GatewayProcessorConfigProvider(_loggerFactory.CreateLogger("ProcessorSettings"), _basePathConfigs); - _testGatewayReceiveConfigProvider = new GatewayReceiveConfigProvider(_loggerFactory.CreateLogger("ProcessorSettings"), _basePathConfigs); + _testAETConfigProvider = CreateAETConfigProvider(_basePathConfigs); + TestGatewayProcessorConfigProvider = CreateGatewayProcessorConfigProvider(_basePathConfigs); + TestGatewayReceiveConfigProvider = CreateGatewayReceiveConfigProvider(_basePathConfigs); } [TestInitialize] @@ -170,6 +175,35 @@ TryKillAnyZombieProcesses(); } + /// + /// Create a new based on the given folder. + /// + /// Path to folder containing config folder. + /// True to use config file, false to use folder of files. + /// New . + protected AETConfigProvider CreateAETConfigProvider( + string configurationsPathRoot, + bool useFile = false) => + new AETConfigProvider(_loggerFactory.CreateLogger("ModelSettings"), configurationsPathRoot, useFile); + + /// + /// Create a new based on the given folder. + /// + /// Path to folder containing config file. + /// New . + protected GatewayProcessorConfigProvider CreateGatewayProcessorConfigProvider( + string configurationsPathRoot) => + new GatewayProcessorConfigProvider(_loggerFactory.CreateLogger("ProcessorSettings"), configurationsPathRoot); + + /// + /// Create a new based on the given folder. + /// + /// Path to folder containing config file. + /// New . + protected GatewayReceiveConfigProvider CreateGatewayReceiveConfigProvider( + string configurationsPathRoot) => + new GatewayReceiveConfigProvider(_loggerFactory.CreateLogger("ReceiveSettings"), configurationsPathRoot); + protected void WriteDicomFileForBuildPackage(string fileName, DicomFile dicomFile) { var path = GetBuildPackageResultPath(fileName, "AnonymisationProtocols"); @@ -312,7 +346,7 @@ } protected AETConfigModel GetTestAETConfigModel() => - _testAETConfigProvider.GetAETConfigs().First(); + _testAETConfigProvider.Config.First(); /// /// Create ReceiveServiceConfig from test files, but overwrite the port and rootDicomFolder. @@ -324,7 +358,7 @@ int port, DirectoryInfo rootDicomFolder = null) { - var gatewayConfig = _testGatewayReceiveConfigProvider.GatewayReceiveConfig().ReceiveServiceConfig; + var gatewayConfig = TestGatewayReceiveConfigProvider.Config.ReceiveServiceConfig; return gatewayConfig.With( new DicomEndPoint(gatewayConfig.GatewayDicomEndPoint.Title, port, gatewayConfig.GatewayDicomEndPoint.Ip), @@ -522,7 +556,7 @@ protected PushService CreatePushService( Func> aetConfigProvider = null) => new PushService( - aetConfigProvider ?? _testAETConfigProvider.GetAETConfigs, + aetConfigProvider ?? _testAETConfigProvider.AETConfigModels, new DicomDataSender(), TestPushQueuePath, TestDeleteQueuePath, @@ -560,7 +594,7 @@ int instances = 1) => new UploadService( innerEyeSegmentationClient != null ? () => innerEyeSegmentationClient : TestGatewayProcessorConfigProvider.CreateInnerEyeSegmentationClient(), - aetConfigProvider ?? _testAETConfigProvider.GetAETConfigs, + aetConfigProvider ?? _testAETConfigProvider.AETConfigModels, TestUploadQueuePath, TestDownloadQueuePath, TestDeleteQueuePath, @@ -824,5 +858,35 @@ /// public static readonly Func RandomDicomTime = (tag, random) => new DicomTime(tag, DateTime.UtcNow.AddSeconds(random.NextDouble() * 1000.0)); + + /// + /// Disposes of all managed resources. + /// + /// If we are disposing. + protected virtual void Dispose(bool disposing) + { + if (disposedValue) + { + return; + } + + if (disposing) + { + _testAETConfigProvider.Dispose(); + TestGatewayProcessorConfigProvider.Dispose(); + TestGatewayReceiveConfigProvider.Dispose(); + } + + disposedValue = true; + } + + /// + /// Implements the disposable pattern. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } } } diff --git a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/Models/MockConfigurationProvider.cs b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/Models/MockConfigurationProvider.cs index 8a54e26..77e7828 100644 --- a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/Models/MockConfigurationProvider.cs +++ b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/Models/MockConfigurationProvider.cs @@ -1,18 +1,17 @@ namespace Microsoft.InnerEye.Listener.Tests.Models { using System; - using System.Collections.Generic; /// - /// Mock implementation of IConfigurationProvider. + /// Mock returning configuration, but sometimes throwing an exception. /// - /// + /// Configuration type. public class MockConfigurationProvider { /// - /// Previous configuration, if there was a queue. + /// Configuration. /// - private T _previousConfiguration; + private T _configuration; /// /// TestException to throw on GetConfiguration to mock failure. @@ -20,11 +19,18 @@ public Exception TestException { get; set; } /// - /// Mock queue of configurations. + /// Initialize a new instance of the. /// - public Queue ConfigurationQueue { get; } = new Queue(); + /// Configuration. + public MockConfigurationProvider(T configuration) + { + _configuration = configuration; + } - /// + /// + /// Return configuration or throw an exception. + /// + /// Configuration. public T GetConfiguration() { if (TestException != null) @@ -32,12 +38,7 @@ throw TestException; } - if (ConfigurationQueue.Count > 0) - { - _previousConfiguration = ConfigurationQueue.Dequeue(); - } - - return _previousConfiguration; + return _configuration; } } } diff --git a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/ServiceTests/ConfigurationProviderTests.cs b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/ServiceTests/ConfigurationProviderTests.cs index c6d4a40..7ef6c6f 100644 --- a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/ServiceTests/ConfigurationProviderTests.cs +++ b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/ServiceTests/ConfigurationProviderTests.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; + using System.Threading; using Microsoft.InnerEye.Azure.Segmentation.API.Common; using Microsoft.InnerEye.DicomConstraints; using Microsoft.InnerEye.Gateway.Models; @@ -503,8 +504,10 @@ [TestCategory("ConfigurationProvider")] [Description("Creates a random gateway receive config, saves it, and checks it loads correctly.")] + [DataRow(false, DisplayName = "Load GatewayReceiveConfig")] + [DataRow(true, DisplayName = "Reload GatewayReceiveConfig")] [TestMethod] - public void TestLoadGatewayReceiveConfig() + public void TestLoadGatewayReceiveConfig(bool reload) { var configurationDirectory = CreateTemporaryDirectory().FullName; var random = new Random(); @@ -512,16 +515,68 @@ var expectedGatewayReceiveConfig = RandomGatewayReceiveConfig(random); Serialise(expectedGatewayReceiveConfig, configurationDirectory, GatewayReceiveConfigProvider.GatewayReceiveConfigFileName); - var gatewayReceiveConfigProvider = new GatewayReceiveConfigProvider(_baseTestLogger, configurationDirectory); - var actualGatewayReceiveConfig = gatewayReceiveConfigProvider.GatewayReceiveConfig(); + using (var gatewayReceiveConfigProvider = CreateGatewayReceiveConfigProvider(configurationDirectory)) + { + Assert.AreEqual(expectedGatewayReceiveConfig, gatewayReceiveConfigProvider.Config); - Assert.AreEqual(expectedGatewayReceiveConfig, actualGatewayReceiveConfig); + if (reload) + { + var configReloadedCount = 0; + + gatewayReceiveConfigProvider.ConfigChanged += (s, e) => + { + Interlocked.Increment(ref configReloadedCount); + }; + + var expectedGatewayReceiveConfig2 = RandomGatewayReceiveConfig(random); + Serialise(expectedGatewayReceiveConfig2, configurationDirectory, GatewayReceiveConfigProvider.GatewayReceiveConfigFileName); + + SpinWait.SpinUntil(() => configReloadedCount > 0, TimeSpan.FromSeconds(10)); + + Assert.AreEqual(expectedGatewayReceiveConfig2, gatewayReceiveConfigProvider.Config); + } + } + } + + [TestCategory("ConfigurationProvider")] + [Description("Creates a random gateway receive config, saves it, and checks it loads correctly. Then toggles runAsConsole.")] + [TestMethod] + public void TestUpdateGatewayReceiveConfigRunAsConsole() + { + var configurationDirectory = CreateTemporaryDirectory().FullName; + var random = new Random(); + + var expectedGatewayReceiveConfig = RandomGatewayReceiveConfig(random); + Serialise(expectedGatewayReceiveConfig, configurationDirectory, GatewayReceiveConfigProvider.GatewayReceiveConfigFileName); + + using (var gatewayReceiveConfigProvider = CreateGatewayReceiveConfigProvider(configurationDirectory)) + { + Assert.AreEqual(expectedGatewayReceiveConfig, gatewayReceiveConfigProvider.Config); + + var configReloadedCount = 0; + + gatewayReceiveConfigProvider.ConfigChanged += (s, e) => + { + Interlocked.Increment(ref configReloadedCount); + }; + + var runAsConsole = gatewayReceiveConfigProvider.Config.ServiceSettings.RunAsConsole; + + gatewayReceiveConfigProvider.SetRunAsConsole(!runAsConsole); + + SpinWait.SpinUntil(() => configReloadedCount > 0, TimeSpan.FromSeconds(10)); + + // RunAsConsole should have now toggled. + Assert.AreEqual(!runAsConsole, gatewayReceiveConfigProvider.Config.ServiceSettings.RunAsConsole); + } } [TestCategory("ConfigurationProvider")] [Description("Creates a random gateway processor config, saves it, and checks it loads correctly.")] + [DataRow(false, DisplayName = "Load GatewayProcessorConfig")] + [DataRow(true, DisplayName = "Reload GatewayProcessorConfig")] [TestMethod] - public void TestLoadGatewayProcessorConfig() + public void TestLoadGatewayProcessorConfig(bool reload) { var configurationDirectory = CreateTemporaryDirectory().FullName; var random = new Random(); @@ -529,112 +584,191 @@ var expectedGatewayProcessorConfig = RandomGatewayProcessorConfig(random); Serialise(expectedGatewayProcessorConfig, configurationDirectory, GatewayProcessorConfigProvider.GatewayProcessorConfigFileName); - var gatewayProcessorConfigProvider = new GatewayProcessorConfigProvider(_baseTestLogger, configurationDirectory); - var actualGatewayProcessorConfig = gatewayProcessorConfigProvider.GatewayProcessorConfig(); + using (var gatewayProcessorConfigProvider = CreateGatewayProcessorConfigProvider(configurationDirectory)) + { + Assert.AreEqual(expectedGatewayProcessorConfig, gatewayProcessorConfigProvider.Config); - Assert.AreEqual(expectedGatewayProcessorConfig, actualGatewayProcessorConfig); + if (reload) + { + var configReloadedCount = 0; + + gatewayProcessorConfigProvider.ConfigChanged += (s, e) => + { + Interlocked.Increment(ref configReloadedCount); + }; + + var expectedGatewayProcessorConfig2 = RandomGatewayProcessorConfig(random); + Serialise(expectedGatewayProcessorConfig2, configurationDirectory, GatewayProcessorConfigProvider.GatewayProcessorConfigFileName); + + SpinWait.SpinUntil(() => configReloadedCount > 0, TimeSpan.FromSeconds(10)); + + Assert.AreEqual(expectedGatewayProcessorConfig2, gatewayProcessorConfigProvider.Config); + } + } + } + + [TestCategory("ConfigurationProvider")] + [Description("Creates a random gateway processor config, saves it, and checks it loads correctly. Then toggles runAsConsole.")] + [TestMethod] + public void TestUpdateGatewayProcessorConfigRunAsConsole() + { + var configurationDirectory = CreateTemporaryDirectory().FullName; + var random = new Random(); + + var expectedGatewayProcessorConfig = RandomGatewayProcessorConfig(random); + Serialise(expectedGatewayProcessorConfig, configurationDirectory, GatewayProcessorConfigProvider.GatewayProcessorConfigFileName); + + using (var gatewayProcessorConfigProvider = CreateGatewayProcessorConfigProvider(configurationDirectory)) + { + Assert.AreEqual(expectedGatewayProcessorConfig, gatewayProcessorConfigProvider.Config); + + var configReloadedCount = 0; + + gatewayProcessorConfigProvider.ConfigChanged += (s, e) => + { + Interlocked.Increment(ref configReloadedCount); + }; + + var runAsConsole = gatewayProcessorConfigProvider.Config.ServiceSettings.RunAsConsole; + + gatewayProcessorConfigProvider.SetRunAsConsole(!runAsConsole); + + SpinWait.SpinUntil(() => configReloadedCount > 0, TimeSpan.FromSeconds(10)); + + // RunAsConsole should have now toggled. + Assert.AreEqual(!runAsConsole, gatewayProcessorConfigProvider.Config.ServiceSettings.RunAsConsole); + } + } + + [TestCategory("ConfigurationProvider")] + [Description("Creates a random gateway processor config, saves it, and checks it loads correctly. Then toggles updates processor settings.")] + [TestMethod] + public void TestUpdateGatewayProcessorConfigProcessorSettings() + { + var configurationDirectory = CreateTemporaryDirectory().FullName; + var random = new Random(); + + var expectedGatewayProcessorConfig = RandomGatewayProcessorConfig(random); + Serialise(expectedGatewayProcessorConfig, configurationDirectory, GatewayProcessorConfigProvider.GatewayProcessorConfigFileName); + + using (var gatewayProcessorConfigProvider = CreateGatewayProcessorConfigProvider(configurationDirectory)) + { + Assert.AreEqual(expectedGatewayProcessorConfig, gatewayProcessorConfigProvider.Config); + + var configReloadedCount = 0; + + gatewayProcessorConfigProvider.ConfigChanged += (s, e) => + { + Interlocked.Increment(ref configReloadedCount); + }; + + var processorSettings = RandomProcessorSettings(random); + + gatewayProcessorConfigProvider.SetProcessorSettings(processorSettings.InferenceUri); + + SpinWait.SpinUntil(() => configReloadedCount > 0, TimeSpan.FromSeconds(10)); + + // InferenceUri should have now changed. + Assert.AreEqual(processorSettings.InferenceUri, gatewayProcessorConfigProvider.Config.ProcessorSettings.InferenceUri); + } } /// - /// Create a list of random AET config models, save them to a single file, and check they load correctly. + /// Create an Action for saving a set of AETConfigModels to a single file. /// - /// True to use AETConfigProvider in single file mode, false to use it in folder mode. - public void TestLoadAETConfigCommon(bool useFile) - { - var configurationDirectory = CreateTemporaryDirectory().FullName; - var random = new Random(); + /// Folder to store file in. + /// Action. + public static Action SaveFileAETConfigModels(string configurationDirectory) => + (random, aetConfigModels) => Serialise(aetConfigModels, configurationDirectory, AETConfigProvider.AETConfigFileName); - var expectedAETConfigModels = RandomArray(random, 2, 10, RandomAETConfigModel); - var folder = string.Empty; - var filename = string.Empty; - - if (useFile) + /// + /// Create an Action for saving a set of AETConfigModels to a folder. + /// + /// Folder to store folder in. + /// True to add junk files that should be ignored. + /// True to split config across multiple files. + /// Action. + public static Action SaveFolderAETConfigModels(string configurationDirectory, bool addJunk, bool multipleFiles) => + (random, aetConfigModels) => { - folder = configurationDirectory; - filename = AETConfigProvider.AETConfigFileName; - } - else - { - folder = Path.Combine(configurationDirectory, AETConfigProvider.AETConfigFolderName); + var folder = Path.Combine(configurationDirectory, AETConfigProvider.AETConfigFolderName); Directory.CreateDirectory(folder); - filename = "test1.json"; - } - Serialise(expectedAETConfigModels, folder, filename); + // Add in two extra files that should be ignored. + if (addJunk) + { + // Write a random GatewayProcessorConfig + var gatewayProcessorConfig = RandomGatewayProcessorConfig(random); + Serialise(gatewayProcessorConfig, folder, GatewayProcessorConfigProvider.GatewayProcessorConfigFileName); - var aetConfigProvider = new AETConfigProvider(_baseTestLogger, configurationDirectory, useFile); - var actualAETConfigModels = aetConfigProvider.GetAETConfigs().ToArray(); + // Write a random GatewayReceiverConfig + var gatewayReceiveConfig = RandomGatewayReceiveConfig(random); + Serialise(gatewayReceiveConfig, folder, GatewayReceiveConfigProvider.GatewayReceiveConfigFileName); + } - Assert.IsTrue(expectedAETConfigModels.SequenceEqual(actualAETConfigModels)); - } + if (multipleFiles) + { + for (var i = 0; i < aetConfigModels.Length; i++) + { + var expectedAETConfig = new[] { aetConfigModels[i] }; + Serialise(expectedAETConfig, folder, string.Format("test{0}.json", i + 1)); + } + } + else + { + Serialise(aetConfigModels, folder, "test1.json"); + } + }; + + public static AETConfigModel[] OrderAETConfigModels(IEnumerable aetConfigModels) => + aetConfigModels.OrderBy(m => m.CalledAET).ThenBy(m => m.CallingAET).ToArray(); + + public static void AssertAETConfigModelsEqual(IEnumerable expectedAETConfigModels, IEnumerable actualAETConfigModels) => + Assert.IsTrue(OrderAETConfigModels(expectedAETConfigModels).SequenceEqual(OrderAETConfigModels(actualAETConfigModels))); [TestCategory("ConfigurationProvider")] - [Description("Creates a list of AET config models, saves it to a file, and checks it loads correctly.")] + [Description("Creates a list of AET config models, saves it to a file or folder, and checks it loads correctly.")] + [DataRow(false, false, false, false, DisplayName = "Load folder AETConfigModels")] + [DataRow(false, false, false, true, DisplayName = "Load folder AETConfigModels, split files")] + [DataRow(false, false, true, false, DisplayName = "Load folder AETConfigModels, ignore junk")] + [DataRow(false, true, false, false, DisplayName = "Reload folder AETConfigModels")] + [DataRow(false, true, true, false, DisplayName = "Reload folder AETConfigModels, ignore junk")] + [DataRow(true, false, false, false, DisplayName = "Load file AETConfigModels")] + [DataRow(true, true, false, false, DisplayName = "Reload file AETConfigModels")] [TestMethod] - public void TestLoadAETConfigFile() - { - TestLoadAETConfigCommon(true); - } - - [TestCategory("ConfigurationProvider")] - [Description("Creates a list of AET config models, saves it to a file in a folder, and checks it loads correctly.")] - [TestMethod] - public void TestLoadAETConfigFolder() - { - TestLoadAETConfigCommon(false); - } - - [TestCategory("ConfigurationProvider")] - [Description("Creates a list of AET config models, saves them along with two other config files, and checks the models load correctly" + - "and the other configs are ignored.")] - [TestMethod] - public void TestLoadAETConfigInvalidFiles() + public void TestLoadAETConfigModels(bool useFile, bool reload, bool addJunk, bool multipleFiles) { var configurationDirectory = CreateTemporaryDirectory().FullName; var random = new Random(); - var aetConfigFolder = Path.Combine(configurationDirectory, AETConfigProvider.AETConfigFolderName); - Directory.CreateDirectory(aetConfigFolder); + var saveAETConfigModels = useFile ? + SaveFileAETConfigModels(configurationDirectory) : + SaveFolderAETConfigModels(configurationDirectory, addJunk, multipleFiles); var expectedAETConfigModels = RandomArray(random, 3, 10, RandomAETConfigModel); - Serialise(expectedAETConfigModels, aetConfigFolder, "test1.json"); + saveAETConfigModels.Invoke(random, expectedAETConfigModels); - // Write a random GatewayProcessorConfig - var gatewayProcessorConfig = RandomGatewayProcessorConfig(random); - Serialise(gatewayProcessorConfig, aetConfigFolder, "test2.json"); - - // Write a random GatewayReceiverConfig - var gatewayReceiveConfig = RandomGatewayReceiveConfig(random); - Serialise(gatewayReceiveConfig, aetConfigFolder, "test3.json"); - - var aetConfigProvider = new AETConfigProvider(_baseTestLogger, configurationDirectory); - var actualAETConfigModels = aetConfigProvider.GetAETConfigs().ToArray(); - - Assert.IsTrue(expectedAETConfigModels.SequenceEqual(actualAETConfigModels)); - } - - [TestCategory("ConfigurationProvider")] - [Description("Creates a list of AET config models, saves them to one file per called/calling pair, and checks they all load correctly.")] - [TestMethod] - public void TestLoadAETConfigConcatenate() - { - var configurationDirectory = CreateTemporaryDirectory().FullName; - var random = new Random(); - - var aetConfigFolder = Path.Combine(configurationDirectory, AETConfigProvider.AETConfigFolderName); - Directory.CreateDirectory(aetConfigFolder); - - var expectedAETConfigModels = RandomArray(random, 3, 10, RandomAETConfigModel); - for (var i = 0; i < expectedAETConfigModels.Length; i++) + using (var aetConfigProvider = CreateAETConfigProvider(configurationDirectory, useFile)) { - var expectedAETConfig = new[] { expectedAETConfigModels[i] }; - Serialise(expectedAETConfig, aetConfigFolder, string.Format("GatewayModelRulesConfig{0}.json", i)); + AssertAETConfigModelsEqual(expectedAETConfigModels, aetConfigProvider.Config); + + if (reload) + { + var configReloadedCount = 0; + + aetConfigProvider.ConfigChanged += (s, e) => + { + Interlocked.Increment(ref configReloadedCount); + }; + + var expectedAETConfigModels2 = RandomArray(random, 3, 10, RandomAETConfigModel); + saveAETConfigModels.Invoke(random, expectedAETConfigModels2); + + SpinWait.SpinUntil(() => configReloadedCount > (addJunk ? 4 : 0), TimeSpan.FromSeconds(10)); + + AssertAETConfigModelsEqual(expectedAETConfigModels2, aetConfigProvider.Config); + } } - - var aetConfigProvider = new AETConfigProvider(_baseTestLogger, configurationDirectory); - var actualAETConfigModels = aetConfigProvider.GetAETConfigs().ToArray(); - - Assert.IsTrue(expectedAETConfigModels.SequenceEqual(actualAETConfigModels)); } [TestCategory("ConfigurationProvider")] @@ -669,10 +803,10 @@ Serialise(expectedAETConfig, aetConfigFolder, string.Format("GatewayModelRulesConfig{0}.json", i), true); } - var aetConfigProvider = new AETConfigProvider(_baseTestLogger, configurationDirectory); - var actualAETConfigModels = aetConfigProvider.GetAETConfigs().ToArray(); - - Assert.IsTrue(expectedAETConfigModels.SequenceEqual(actualAETConfigModels)); + using (var aetConfigProvider = CreateAETConfigProvider(configurationDirectory)) + { + AssertAETConfigModelsEqual(expectedAETConfigModels, aetConfigProvider.Config); + } } [TestCategory("ConfigurationProvider")] @@ -714,10 +848,10 @@ } } - var aetConfigProvider = new AETConfigProvider(_baseTestLogger, configurationDirectory); - var actualAETConfigModels = aetConfigProvider.GetAETConfigs().ToArray(); - - Assert.IsTrue(expectedAETConfigModels.SequenceEqual(actualAETConfigModels)); + using (var aetConfigProvider = CreateAETConfigProvider(configurationDirectory)) + { + AssertAETConfigModelsEqual(expectedAETConfigModels, aetConfigProvider.Config); + } } } } diff --git a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/ServiceTests/ReceiveServiceTests.cs b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/ServiceTests/ReceiveServiceTests.cs index 224df2b..b72e647 100644 --- a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/ServiceTests/ReceiveServiceTests.cs +++ b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/ServiceTests/ReceiveServiceTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.InnerEye.Gateway.MessageQueueing.Exceptions; using Microsoft.InnerEye.Gateway.Models; + using Microsoft.InnerEye.Listener.Common.Providers; using Microsoft.InnerEye.Listener.DataProvider.Implementations; using Microsoft.InnerEye.Listener.Tests.Common.Helpers; using Microsoft.InnerEye.Listener.Tests.Models; @@ -20,38 +21,25 @@ [TestMethod] public void ReceiveServiceRestartTest() { - var callingAet = "ProstateRTMl"; + var testAETConfigModel = GetTestAETConfigModel(); + + var configurationDirectory = CreateTemporaryDirectory().FullName; + + var expectedGatewayReceiveConfig1 = TestGatewayReceiveConfigProvider.Config.With( + receiveServiceConfig: GetTestGatewayReceiveServiceConfig(110), + configurationServiceConfig: new ConfigurationServiceConfig( + configurationRefreshDelaySeconds: 1)); + + ConfigurationProviderTests.Serialise(expectedGatewayReceiveConfig1, configurationDirectory, GatewayReceiveConfigProvider.GatewayReceiveConfigFileName); var client = GetMockInnerEyeSegmentationClient(); - - var mockConfigurationServiceConfigProvider = new MockConfigurationProvider(); - - var configurationServiceConfig1 = new ConfigurationServiceConfig( - configurationRefreshDelaySeconds: 1); - - var configurationServiceConfig2 = new ConfigurationServiceConfig( - configurationServiceConfig1.ConfigCreationDateTime.AddSeconds(5), - configurationServiceConfig1.ApplyConfigDateTime.AddSeconds(10)); - - mockConfigurationServiceConfigProvider.ConfigurationQueue.Clear(); - mockConfigurationServiceConfigProvider.ConfigurationQueue.Enqueue(configurationServiceConfig1); - mockConfigurationServiceConfigProvider.ConfigurationQueue.Enqueue(configurationServiceConfig2); - - var mockReceiverConfigurationProvider2 = new MockConfigurationProvider(); - - var testReceiveServiceConfig1 = GetTestGatewayReceiveServiceConfig(110); - var testReceiveServiceConfig2 = GetTestGatewayReceiveServiceConfig(111); - - mockReceiverConfigurationProvider2.ConfigurationQueue.Clear(); - mockReceiverConfigurationProvider2.ConfigurationQueue.Enqueue(testReceiveServiceConfig1); - mockReceiverConfigurationProvider2.ConfigurationQueue.Enqueue(testReceiveServiceConfig2); - - using (var receiveService = CreateReceiveService(mockReceiverConfigurationProvider2.GetConfiguration)) + using (var gatewayReceiveConfigProvider = CreateGatewayReceiveConfigProvider(configurationDirectory)) + using (var receiveService = CreateReceiveService(gatewayReceiveConfigProvider.ReceiveServiceConfig)) using (var uploadQueue = receiveService.UploadQueue) using (var configurationService = CreateConfigurationService( client, - mockConfigurationServiceConfigProvider.GetConfiguration, + gatewayReceiveConfigProvider.ConfigurationServiceConfig, receiveService)) { // Start the service @@ -59,28 +47,36 @@ uploadQueue.Clear(); // Clear the message queue + var expectedGatewayReceiveConfig2 = TestGatewayReceiveConfigProvider.Config.With( + receiveServiceConfig: GetTestGatewayReceiveServiceConfig(111), + configurationServiceConfig: new ConfigurationServiceConfig( + expectedGatewayReceiveConfig1.ConfigurationServiceConfig.ConfigCreationDateTime.AddSeconds(5), + expectedGatewayReceiveConfig1.ConfigurationServiceConfig.ApplyConfigDateTime.AddSeconds(10))); + + ConfigurationProviderTests.Serialise(expectedGatewayReceiveConfig2, configurationDirectory, GatewayReceiveConfigProvider.GatewayReceiveConfigFileName); + SpinWait.SpinUntil(() => receiveService.StartCount == 2); - // Send on the new config + // Send on the old config var result = DcmtkHelpers.SendFileUsingDCMTK( @"Images\1ValidSmall\1.dcm", - testReceiveServiceConfig1.GatewayDicomEndPoint.Port, + expectedGatewayReceiveConfig1.ReceiveServiceConfig.GatewayDicomEndPoint.Port, ScuProfile.LEExplicitCT, TestContext, - applicationEntityTitle: callingAet, - calledAETitle: testReceiveServiceConfig1.GatewayDicomEndPoint.Title); + applicationEntityTitle: testAETConfigModel.CallingAET, + calledAETitle: testAETConfigModel.CalledAET); - // Check this did send on the old config + // Check this did not send on the old config Assert.IsFalse(string.IsNullOrWhiteSpace(result)); // Send on the new config result = DcmtkHelpers.SendFileUsingDCMTK( @"Images\1ValidSmall\1.dcm", - testReceiveServiceConfig2.GatewayDicomEndPoint.Port, + expectedGatewayReceiveConfig2.ReceiveServiceConfig.GatewayDicomEndPoint.Port, ScuProfile.LEExplicitCT, TestContext, - applicationEntityTitle: callingAet, - calledAETitle: testReceiveServiceConfig2.GatewayDicomEndPoint.Title); + applicationEntityTitle: testAETConfigModel.CallingAET, + calledAETitle: testAETConfigModel.CalledAET); // Check this did send on the new config Assert.IsTrue(string.IsNullOrWhiteSpace(result)); @@ -88,8 +84,8 @@ var receiveQueueItem = TransactionalDequeue(uploadQueue); Assert.IsNotNull(receiveQueueItem); - Assert.AreEqual(callingAet, receiveQueueItem.CallingApplicationEntityTitle); - Assert.AreEqual(testReceiveServiceConfig2.GatewayDicomEndPoint.Title, receiveQueueItem.CalledApplicationEntityTitle); + Assert.AreEqual(testAETConfigModel.CallingAET, receiveQueueItem.CallingApplicationEntityTitle); + Assert.AreEqual(testAETConfigModel.CalledAET, receiveQueueItem.CalledApplicationEntityTitle); Assert.IsFalse(string.IsNullOrEmpty(receiveQueueItem.AssociationFolderPath)); @@ -113,15 +109,12 @@ [TestMethod] public void ReceiveServiceAPIDownTest() { - var callingAet = "ProstateRTMl"; + var testAETConfigModel = GetTestAETConfigModel(); - var mockReceiverConfigurationProvider = new MockConfigurationProvider(); var client = GetMockInnerEyeSegmentationClient(); var gatewayReceiveConfig = GetTestGatewayReceiveServiceConfig(140); - - mockReceiverConfigurationProvider.ConfigurationQueue.Clear(); - mockReceiverConfigurationProvider.ConfigurationQueue.Enqueue(gatewayReceiveConfig); + var mockReceiverConfigurationProvider = new MockConfigurationProvider(gatewayReceiveConfig); using (var receiveService = CreateReceiveService(mockReceiverConfigurationProvider.GetConfiguration)) using (var uploadQueue = receiveService.UploadQueue) @@ -138,14 +131,14 @@ gatewayReceiveConfig.GatewayDicomEndPoint.Port, ScuProfile.LEExplicitCT, TestContext, - applicationEntityTitle: callingAet, - calledAETitle: gatewayReceiveConfig.GatewayDicomEndPoint.Title); + applicationEntityTitle: testAETConfigModel.CallingAET, + calledAETitle: testAETConfigModel.CalledAET); var receiveQueueItem = TransactionalDequeue(uploadQueue, 10000); Assert.IsNotNull(receiveQueueItem); - Assert.AreEqual(callingAet, receiveQueueItem.CallingApplicationEntityTitle); - Assert.AreEqual(gatewayReceiveConfig.GatewayDicomEndPoint.Title, receiveQueueItem.CalledApplicationEntityTitle); + Assert.AreEqual(testAETConfigModel.CallingAET, receiveQueueItem.CallingApplicationEntityTitle); + Assert.AreEqual(testAETConfigModel.CalledAET, receiveQueueItem.CalledApplicationEntityTitle); Assert.IsFalse(string.IsNullOrEmpty(receiveQueueItem.AssociationFolderPath)); @@ -169,11 +162,9 @@ [TestMethod] public void ReceiveServiceLiveEndToEndTest() { + var testAETConfigModel = GetTestAETConfigModel(); var receivePort = 160; - var callingAet = "ProstateRTMl"; - var calledAet = "testname"; - using (var receiveService = CreateReceiveService(receivePort)) using (var uploadQueue = receiveService.UploadQueue) { @@ -185,14 +176,14 @@ receivePort, ScuProfile.LEExplicitCT, TestContext, - applicationEntityTitle: callingAet, - calledAETitle: calledAet); + applicationEntityTitle: testAETConfigModel.CallingAET, + calledAETitle: testAETConfigModel.CalledAET); var receiveQueueItem = TransactionalDequeue(uploadQueue, timeoutMs: 20 * 1000); Assert.IsNotNull(receiveQueueItem); - Assert.AreEqual(callingAet, receiveQueueItem.CallingApplicationEntityTitle); - Assert.AreEqual(calledAet, receiveQueueItem.CalledApplicationEntityTitle); + Assert.AreEqual(testAETConfigModel.CallingAET, receiveQueueItem.CallingApplicationEntityTitle); + Assert.AreEqual(testAETConfigModel.CalledAET, receiveQueueItem.CalledApplicationEntityTitle); Assert.IsFalse(string.IsNullOrEmpty(receiveQueueItem.AssociationFolderPath)); @@ -216,7 +207,7 @@ [TestMethod] public async Task ReceiveServiceEchoTest() { - var calledAet = "testname"; + var testAETConfigModel = GetTestAETConfigModel(); var receivePort = 180; using (var receiveService = CreateReceiveService(receivePort)) @@ -229,7 +220,7 @@ await sender.DicomEchoAsync( "Hello", - calledAet, + testAETConfigModel.CalledAET, receivePort, "127.0.0.1"); @@ -242,15 +233,15 @@ receivePort, ScuProfile.LEExplicitCT, TestContext, - applicationEntityTitle: "ProstateRTMl", - calledAETitle: calledAet); + applicationEntityTitle: testAETConfigModel.CallingAET, + calledAETitle: testAETConfigModel.CalledAET); Assert.IsNotNull(TransactionalDequeue(uploadQueue, timeoutMs: 1000)); // Now try another Dicom echo await sender.DicomEchoAsync( "Hello", - calledAet, + testAETConfigModel.CalledAET, receivePort, "127.0.0.1"); diff --git a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/ServiceTests/SystemTests.cs b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/ServiceTests/SystemTests.cs index 93badc4..ee3dbca 100644 --- a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/ServiceTests/SystemTests.cs +++ b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Tests/ServiceTests/SystemTests.cs @@ -8,10 +8,10 @@ using Dicom; using Microsoft.InnerEye.Gateway.MessageQueueing.Exceptions; using Microsoft.InnerEye.Gateway.Models; + using Microsoft.InnerEye.Listener.Common.Providers; using Microsoft.InnerEye.Listener.DataProvider.Implementations; using Microsoft.InnerEye.Listener.DataProvider.Models; using Microsoft.InnerEye.Listener.Tests.Common.Helpers; - using Microsoft.InnerEye.Listener.Tests.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] @@ -23,6 +23,14 @@ [TestMethod] public async Task ProcessorServiceRestartTest() { + var configurationDirectory = CreateTemporaryDirectory().FullName; + + var expectedGatewayProcessorConfig1 = TestGatewayProcessorConfigProvider.Config.With( + configurationServiceConfig: new ConfigurationServiceConfig( + configurationRefreshDelaySeconds: 1)); + + ConfigurationProviderTests.Serialise(expectedGatewayProcessorConfig1, configurationDirectory, GatewayProcessorConfigProvider.GatewayProcessorConfigFileName); + var tempFolder = CreateTemporaryDirectory(); foreach (var file in new DirectoryInfo(@"Images\1ValidSmall\").GetFiles()) @@ -32,21 +40,7 @@ var client = GetMockInnerEyeSegmentationClient(); - var mockConfigurationServiceConfigProvider = new MockConfigurationProvider(); - - var configurationServiceConfig1 = new ConfigurationServiceConfig( - configurationRefreshDelaySeconds: 1); - - var configurationServiceConfig2 = new ConfigurationServiceConfig( - configurationServiceConfig1.ConfigCreationDateTime.AddSeconds(5), - configurationServiceConfig1.ApplyConfigDateTime.AddSeconds(10)); - - mockConfigurationServiceConfigProvider.ConfigurationQueue.Enqueue(configurationServiceConfig1); - mockConfigurationServiceConfigProvider.ConfigurationQueue.Enqueue(configurationServiceConfig2); - - var resultDirectory = CreateTemporaryDirectory(); - - using (var dicomDataReceiver = new ListenerDataReceiver(new ListenerDicomSaver(resultDirectory.FullName))) + using (var dicomDataReceiver = new ListenerDataReceiver(new ListenerDicomSaver(CreateTemporaryDirectory().FullName))) { var eventCount = 0; var folderPath = string.Empty; @@ -65,9 +59,10 @@ using (var uploadService = CreateUploadService(client)) using (var uploadQueue = uploadService.UploadQueue) using (var downloadService = CreateDownloadService(client)) + using (var gatewayProcessorConfigProvider = CreateGatewayProcessorConfigProvider(configurationDirectory)) using (var configurationService = CreateConfigurationService( client, - mockConfigurationServiceConfigProvider.GetConfiguration, + gatewayProcessorConfigProvider.ConfigurationServiceConfig, downloadService, uploadService, pushService)) @@ -77,6 +72,14 @@ uploadQueue.Clear(); // Clear the message queue + // Save a new config, this should be picked up and the services restart in 10 seconds. + var expectedGatewayProcessorConfig2 = TestGatewayProcessorConfigProvider.Config.With( + configurationServiceConfig: new ConfigurationServiceConfig( + expectedGatewayProcessorConfig1.ConfigurationServiceConfig.ConfigCreationDateTime.AddSeconds(5), + expectedGatewayProcessorConfig1.ConfigurationServiceConfig.ApplyConfigDateTime.AddSeconds(10))); + + ConfigurationProviderTests.Serialise(expectedGatewayProcessorConfig2, configurationDirectory, GatewayProcessorConfigProvider.GatewayProcessorConfigFileName); + SpinWait.SpinUntil(() => pushService.StartCount == 2); SpinWait.SpinUntil(() => uploadService.StartCount == 2); SpinWait.SpinUntil(() => downloadService.StartCount == 2); diff --git a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Wix.Actions/CustomActions.cs b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Wix.Actions/CustomActions.cs index 6b8023e..2f2f297 100644 --- a/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Wix.Actions/CustomActions.cs +++ b/Source/Microsoft.Gateway/Microsoft.InnerEye.Listener.Wix.Actions/CustomActions.cs @@ -53,40 +53,42 @@ namespace Microsoft.InnerEye.Listener.Wix.Actions #endif // Make sure that the applications are run as services. - var gatewayProcessorConfigProvider = new GatewayProcessorConfigProvider( - null, - ConfigInstallDirectory); - gatewayProcessorConfigProvider.SetRunAsConsole(false); - - var gatewayReceiveConfigProvider = new GatewayReceiveConfigProvider( - null, - ConfigInstallDirectory); - gatewayReceiveConfigProvider.SetRunAsConsole(false); - - // Check if the installer is running unattended - lets skip the UI if true - if (session.CustomActionData[UILevelCustomActionKey] == "2") + using (var gatewayProcessorConfigProvider = new GatewayProcessorConfigProvider(null, ConfigInstallDirectory)) { - return ActionResult.Success; - } + gatewayProcessorConfigProvider.SetRunAsConsole(false); - // In the context of the installer, this may have a different SecurityProtocol to the application. - // In testing it was: SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls - // but it may vary. In order to value the uri and license key, we need TLS 1.2 - ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; - - // First time install so lets display a form to grab the license key. - using (var form = new LicenseKeyForm(gatewayProcessorConfigProvider)) - { - var licenseKeyDialogResult = form.ShowDialog(); - - switch (licenseKeyDialogResult) + using (var gatewayReceiveConfigProvider = new GatewayReceiveConfigProvider( + null, + ConfigInstallDirectory)) { - case DialogResult.Cancel: - return ActionResult.UserExit; - case DialogResult.No: - return ActionResult.NotExecuted; - default: - return ActionResult.Success; + gatewayReceiveConfigProvider.SetRunAsConsole(false); + } + + // Check if the installer is running unattended - lets skip the UI if true + if (session.CustomActionData[UILevelCustomActionKey] == "2") + { + return ActionResult.Success; + } + + // In the context of the installer, this may have a different SecurityProtocol to the application. + // In testing it was: SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls + // but it may vary. In order to value the uri and license key, we need TLS 1.2 + ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; + + // First time install so lets display a form to grab the license key. + using (var form = new LicenseKeyForm(gatewayProcessorConfigProvider)) + { + var licenseKeyDialogResult = form.ShowDialog(); + + switch (licenseKeyDialogResult) + { + case DialogResult.Cancel: + return ActionResult.UserExit; + case DialogResult.No: + return ActionResult.NotExecuted; + default: + return ActionResult.Success; + } } } }