Skip to content

Commit

Permalink
starting new service
Browse files Browse the repository at this point in the history
Signed-off-by: Neil South <neil.south@answerdigital.com>
  • Loading branch information
neildsouth committed Nov 10, 2023
1 parent c59cbf7 commit 91cfa12
Show file tree
Hide file tree
Showing 17 changed files with 243 additions and 27 deletions.
8 changes: 4 additions & 4 deletions doc/dependency_decisions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -774,16 +774,16 @@
- :who: neilsouth
:why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-messaging/raw/main/LICENSE)
:versions:
- 1.0.3
- 1.0.4
- 1.0.5-rc0006
- 1.0.5
:when: 2023-10-13 18:06:21.511789690 Z
- - :approve
- Monai.Deploy.Messaging.RabbitMQ
- :who: neilsouth
:why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-messaging/raw/main/LICENSE)
:versions:
- 1.0.3
- 1.0.4
- 1.0.5-rc0006
- 1.0.5
:when: 2023-10-13 18:06:21.511789690 Z
- - :approve
- Monai.Deploy.Storage
Expand Down
20 changes: 20 additions & 0 deletions src/CLI/Services/ConfigurationOptionAccessor.cs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public interface IConfigurationOptionAccessor
/// </summary>
int DicomListeningPort { get; set; }

/// <summary>
/// Gets or sets the ExternalApp DICOM SCP listening port from appsettings.json.
/// </summary>
int ExternalAppDicomListeningPort { get; set; }

/// <summary>
/// Gets or sets the HL7 listening port from appsettings.json.
/// </summary>
Expand Down Expand Up @@ -112,6 +117,21 @@ public int DicomListeningPort
}
}

public int ExternalAppDicomListeningPort
{
get
{
return GetValueFromJsonPath<int>("InformaticsGateway.dicom.scp.externalAppPort");
}
set
{
Guard.Against.OutOfRangePort(value, nameof(ExternalAppDicomListeningPort));
var jObject = ReadConfigurationFile();
jObject["InformaticsGateway"]["dicom"]["scp"]["externalAppPort"] = value;
SaveConfigurationFile(jObject);
}
}

public int Hl7ListeningPort
{
get
Expand Down
6 changes: 6 additions & 0 deletions src/Configuration/ScpConfiguration.cs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ public class ScpConfiguration
[ConfigurationKeyName("port")]
public int Port { get; set; } = 104;

/// <summary>
/// Gets or sets Port number to be used for SCP service.
/// </summary>
[ConfigurationKeyName("externalAppPort")]
public int ExternalAppPort { get; set; } = 105;

/// <summary>
/// Gets or sets maximum number of simultaneous DICOM associations for the SCP service.
/// </summary>
Expand Down
6 changes: 2 additions & 4 deletions src/InformaticsGateway/Logging/Log.100.200.ScpService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,12 @@ public static partial class Log
public static partial void FailedToUpdateAppliationEntityHandlerWithUpdatedAEChange(this ILogger logger, string aeTitle, Exception? ex = null);

// SCP Service
[LoggerMessage(EventId = 200, Level = LogLevel.Information, Message = "Initializing SCP Service at port {port}...")]
public static partial void ScpServiceLoading(this ILogger logger, int port);

[LoggerMessage(EventId = 201, Level = LogLevel.Critical, Message = "Failed to initialize SCP listener.")]
public static partial void ScpListenerInitializationFailure(this ILogger logger);

[LoggerMessage(EventId = 202, Level = LogLevel.Information, Message = "SCP listening on port: {port}.")]
public static partial void ScpListeningOnPort(this ILogger logger, int port);
[LoggerMessage(EventId = 202, Level = LogLevel.Information, Message = "{serviceName} listening on port: {port}.")]
public static partial void ScpListeningOnPort(this ILogger logger, string serviceName, int port);

[LoggerMessage(EventId = 203, Level = LogLevel.Information, Message = "C-ECHO request received.")]
public static partial void CEchoReceived(this ILogger logger);
Expand Down
1 change: 1 addition & 0 deletions src/InformaticsGateway/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ internal static IHostBuilder CreateHostBuilder(string[] args) =>
services.AddHostedService<ObjectUploadService>(/*p => p.GetService<ObjectUploadService>()*/);
services.AddHostedService<DataRetrievalService>(/*p => p.GetService<DataRetrievalService>()*/);
services.AddHostedService<ScpService>(/*p => p.GetService<ScpService>()*/);
services.AddHostedService<ExternalAppScpService>();
services.AddHostedService<ScuService>(/*p => p.GetService<ScuService>()*/);
services.AddHostedService<ExtAppScuExportService>();
services.AddHostedService<ScuExportService>(/*p => p.GetService<ScuExportService>()*/);
Expand Down
24 changes: 24 additions & 0 deletions src/InformaticsGateway/Services/Common/ScpInputTypeEnum.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2023 MONAI Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Monai.Deploy.InformaticsGateway.Services.Common
{
public enum ScpInputTypeEnum
{
WorkflowTrigger,
ExternalAppReturn
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
using Monai.Deploy.InformaticsGateway.Configuration;
using Monai.Deploy.InformaticsGateway.Database.Api.Repositories;
using Monai.Deploy.InformaticsGateway.Logging;
using Monai.Deploy.InformaticsGateway.Services.Common;
using Monai.Deploy.InformaticsGateway.Services.Storage;

namespace Monai.Deploy.InformaticsGateway.Services.Scp
Expand Down Expand Up @@ -92,7 +93,7 @@ private void OnApplicationStopping()
_unsubscriberForMonaiAeChangedNotificationService.Dispose();
}

public async Task<string> HandleCStoreRequest(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId)
public async Task<string> HandleCStoreRequest(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId, ScpInputTypeEnum type)
{
Guard.Against.Null(request, nameof(request));

Expand All @@ -108,10 +109,10 @@ public async Task<string> HandleCStoreRequest(DicomCStoreRequest request, string
throw new InsufficientStorageAvailableException($"Insufficient storage available. Available storage space: {_storageInfoProvider.AvailableFreeSpace:D}");
}

return await HandleInstance(request, calledAeTitle, callingAeTitle, associationId).ConfigureAwait(false);
return await HandleInstance(request, calledAeTitle, callingAeTitle, associationId, type).ConfigureAwait(false);
}

private async Task<string> HandleInstance(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId)
private async Task<string> HandleInstance(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId, ScpInputTypeEnum type)
{
var uids = _dicomToolkit.GetStudySeriesSopInstanceUids(request.File);

Expand Down
94 changes: 94 additions & 0 deletions src/InformaticsGateway/Services/Scp/ExternalAppScpService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2021-2023 MONAI Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using Ardalis.GuardClauses;
using FellowOakDicom.Network;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Monai.Deploy.InformaticsGateway.Api.Rest;
using Monai.Deploy.InformaticsGateway.Configuration;
using Monai.Deploy.InformaticsGateway.Logging;


namespace Monai.Deploy.InformaticsGateway.Services.Scp
{
internal class ExternalAppScpService : ScpServiceBase
{
private readonly IServiceScope _serviceScope;
private readonly ILogger<ExternalAppScpService> _logger;
private readonly ILogger<ExternalAppScpServiceInternal> _scpServiceInternalLogger;
private readonly IOptions<InformaticsGatewayConfiguration> _configuration;
private readonly IApplicationEntityManager _associationDataProvider;

public override string ServiceName => "External App DICOM SCP Service";

public ExternalAppScpService(IServiceScopeFactory serviceScopeFactory,
IApplicationEntityManager applicationEntityManager,
IHostApplicationLifetime appLifetime,
IOptions<InformaticsGatewayConfiguration> configuration) : base(serviceScopeFactory, applicationEntityManager, appLifetime, configuration)
{
Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory));
Guard.Against.Null(applicationEntityManager, nameof(applicationEntityManager));
Guard.Against.Null(appLifetime, nameof(appLifetime));
Guard.Against.Null(configuration, nameof(configuration));

_associationDataProvider = applicationEntityManager;

_serviceScope = serviceScopeFactory.CreateScope();
var logginFactory = _serviceScope.ServiceProvider.GetService<ILoggerFactory>();

_logger = logginFactory!.CreateLogger<ExternalAppScpService>();
_scpServiceInternalLogger = logginFactory!.CreateLogger<ExternalAppScpServiceInternal>();
_configuration = configuration;
}

public override void ServiceStart()
{
var ScpPort = _configuration.Value.Dicom.Scp.ExternalAppPort;
try
{
_logger.ServiceStarting(ServiceName);
Server = DicomServerFactory.Create<ExternalAppScpServiceInternal>(
NetworkManager.IPv4Any,
ScpPort,
logger: _scpServiceInternalLogger,
userState: _associationDataProvider);

Server.Options.IgnoreUnsupportedTransferSyntaxChange = true;
Server.Options.LogDimseDatasets = _configuration.Value.Dicom.Scp.LogDimseDatasets;
Server.Options.MaxClientsAllowed = _configuration.Value.Dicom.Scp.MaximumNumberOfAssociations;

if (Server.Exception != null)
{
_logger.ScpListenerInitializationFailure();
throw Server.Exception;
}

Status = ServiceStatus.Running;
_logger.ScpListeningOnPort(ServiceName, ScpPort);
}
catch (System.Exception ex)
{
Status = ServiceStatus.Cancelled;
_logger.ServiceFailedToStart(ServiceName, ex);
AppLifetime.StopApplication();
}

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2021-2023 MONAI Consortium
* Copyright 2019-2021 NVIDIA Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using System.Text;
using System.Threading.Tasks;
using FellowOakDicom.Network;
using Microsoft.Extensions.Logging;
using Monai.Deploy.InformaticsGateway.Api.Models;
using Monai.Deploy.InformaticsGateway.Common;
using Monai.Deploy.InformaticsGateway.Logging;


namespace Monai.Deploy.InformaticsGateway.Services.Scp
{
internal class ExternalAppScpServiceInternal : ScpServiceInternalBase
{

private readonly DicomAssociationInfo _associationInfo;
private readonly ILogger _logger;

public ExternalAppScpServiceInternal(INetworkStream stream, Encoding fallbackEncoding, ILogger logger, DicomServiceDependencies dicomServiceDependencies)
: base(stream, fallbackEncoding, logger, dicomServiceDependencies)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_associationInfo = new DicomAssociationInfo();
}
public override async Task<DicomCStoreResponse> OnCStoreRequestAsync(DicomCStoreRequest request)
{
try
{
_logger?.TransferSyntaxUsed(request.TransferSyntax);
var payloadId = await AssociationDataProvider!.HandleCStoreRequest(request, Association.CalledAE, Association.CallingAE, AssociationId, Common.ScpInputTypeEnum.ExternalAppReturn).ConfigureAwait(false);
_associationInfo.FileReceived(payloadId);
return new DicomCStoreResponse(request, DicomStatus.Success);
}
catch (InsufficientStorageAvailableException ex)
{
_logger?.CStoreFailedDueToLowStorageSpace(ex);
_associationInfo.Errors = $"Failed to store file due to low disk space: {ex}";
return new DicomCStoreResponse(request, DicomStatus.ResourceLimitation);
}
catch (System.IO.IOException ex) when ((ex.HResult & 0xFFFF) == Constants.ERROR_HANDLE_DISK_FULL || (ex.HResult & 0xFFFF) == Constants.ERROR_DISK_FULL)
{
_logger?.CStoreFailedWithNoSpace(ex);
_associationInfo.Errors = $"Failed to store file due to low disk space: {ex}";
return new DicomCStoreResponse(request, DicomStatus.StorageStorageOutOfResources);
}
catch (Exception ex)
{
_logger?.CStoreFailed(ex);
_associationInfo.Errors = $"Failed to store file: {ex}";
return new DicomCStoreResponse(request, DicomStatus.ProcessingFailure);
}
}
}
}
3 changes: 2 additions & 1 deletion src/InformaticsGateway/Services/Scp/IApplicationEntityManager.cs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using FellowOakDicom.Network;
using Microsoft.Extensions.Options;
using Monai.Deploy.InformaticsGateway.Configuration;
using Monai.Deploy.InformaticsGateway.Services.Common;

namespace Monai.Deploy.InformaticsGateway.Services.Scp
{
Expand All @@ -35,7 +36,7 @@ public interface IApplicationEntityManager
/// <param name="calledAeTitle">Called AE Title to be associated with the call.</param>
/// <param name="calledAeTitle">Calling AE Title to be associated with the call.</param>
/// <param name="associationId">Unique association ID.</param>
Task<string> HandleCStoreRequest(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId);
Task<string> HandleCStoreRequest(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId, ScpInputTypeEnum type = ScpInputTypeEnum.WorkflowTrigger);

/// <summary>
/// Checks if a MONAI AET is configured.
Expand Down
5 changes: 2 additions & 3 deletions src/InformaticsGateway/Services/Scp/ScpServiceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ internal abstract class ScpServiceBase : IHostedService, IDisposable, IMonaiServ
private readonly IOptions<InformaticsGatewayConfiguration> _configuration;
protected FoDicomNetwork.IDicomServer? Server;
public ServiceStatus Status { get; set; } = ServiceStatus.Unknown;
public virtual string ServiceName => "DICOM SCP Service";
public abstract string ServiceName { get; }

public ScpServiceBase(IServiceScopeFactory serviceScopeFactory,
IApplicationEntityManager applicationEntityManager,
Expand Down Expand Up @@ -98,7 +98,6 @@ public Task StopAsync(CancellationToken cancellationToken)

public void ServiceStartBase(int ScpPort)
{
_logger.ScpServiceLoading(ScpPort);

try
{
Expand All @@ -120,7 +119,7 @@ public void ServiceStartBase(int ScpPort)
}

Status = ServiceStatus.Running;
_logger.ScpListeningOnPort(ScpPort);
_logger.ScpListeningOnPort(ServiceName, ScpPort);
}
catch (System.Exception ex)
{
Expand Down
2 changes: 1 addition & 1 deletion src/InformaticsGateway/Services/Scp/ScpServiceInternal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public override async Task<DicomCStoreResponse> OnCStoreRequestAsync(DicomCStore
try
{
_logger?.TransferSyntaxUsed(request.TransferSyntax);
var payloadId = await AssociationDataProvider!.HandleCStoreRequest(request, Association.CalledAE, Association.CallingAE, AssociationId).ConfigureAwait(false);
var payloadId = await AssociationDataProvider!.HandleCStoreRequest(request, Association.CalledAE, Association.CallingAE, AssociationId, Common.ScpInputTypeEnum.WorkflowTrigger).ConfigureAwait(false);
_associationInfo.FileReceived(payloadId);
return new DicomCStoreResponse(request, DicomStatus.Success);
}
Expand Down
1 change: 0 additions & 1 deletion src/InformaticsGateway/Services/Scu/ScuService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,6 @@ public Task StartAsync(CancellationToken cancellationToken)
}, CancellationToken.None);

Status = ServiceStatus.Running;
_logger.ServiceRunning(ServiceName);
if (task.IsCompleted)
return task;
return Task.CompletedTask;
Expand Down
Loading

0 comments on commit 91cfa12

Please sign in to comment.