irc-netcore-api/IRaCIS.Core.API/HostService/DicomSCPService.cs

321 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

using AutoMapper.Execution;
using FellowOakDicom;
using FellowOakDicom.Network;
using FellowOakDicom.Network.Client;
using IRaCIS.Core.Domain.Models;
using IRaCIS.Core.Infra.EFCore;
using IRaCIS.Core.Infrastructure.Extention;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Org.BouncyCastle.Bcpg;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace IRaCIS.Core.API.HostService
{
public class DicomSCPServiceOption
{
public bool IsSupportThirdService { get; set; }
public string ThirdSearchPacsAE { get; set; }
public string ThirdCallningAE { get; set; }
public List<string> CalledAEList { get; set; }
public string ServerPort { get; set; }
}
public class DicomSCPService : DicomService, IDicomServiceProvider, IDicomCFindProvider, IDicomCEchoProvider, IDicomCMoveProvider
{
private static readonly DicomTransferSyntax[] _acceptedTransferSyntaxes = new DicomTransferSyntax[]
{
DicomTransferSyntax.ExplicitVRLittleEndian,
DicomTransferSyntax.ExplicitVRBigEndian,
DicomTransferSyntax.ImplicitVRLittleEndian
};
private static readonly DicomTransferSyntax[] _acceptedImageTransferSyntaxes = new DicomTransferSyntax[]
{
// Lossless
DicomTransferSyntax.JPEGLSLossless,
DicomTransferSyntax.JPEG2000Lossless,
DicomTransferSyntax.JPEGProcess14SV1,
DicomTransferSyntax.JPEGProcess14,
DicomTransferSyntax.RLELossless,
// Lossy
DicomTransferSyntax.JPEGLSNearLossless,
DicomTransferSyntax.JPEG2000Lossy,
DicomTransferSyntax.JPEGProcess1,
DicomTransferSyntax.JPEGProcess2_4,
// Uncompressed
DicomTransferSyntax.ExplicitVRLittleEndian,
DicomTransferSyntax.ExplicitVRBigEndian,
DicomTransferSyntax.ImplicitVRLittleEndian
};
private IServiceProvider _serviceProvider { get; set; }
private DicomSCPServiceOption DicomSCPServiceConfig { get; set; }
public string CallingAE { get; protected set; }
public string CalledAE { get; protected set; }
public DicomSCPService(INetworkStream stream, Encoding fallbackEncoding, Microsoft.Extensions.Logging.ILogger log, DicomServiceDependencies dependencies, IServiceProvider injectServiceProvider)
: base(stream, fallbackEncoding, log, dependencies)
{
_serviceProvider = injectServiceProvider.CreateScope().ServiceProvider;
}
public void OnConnectionClosed(Exception exception)
{
throw new NotImplementedException();
}
public void OnReceiveAbort(DicomAbortSource source, DicomAbortReason reason)
{
Logger.LogError($"Received abort from {source}, reason is {reason}");
}
public Task OnReceiveAssociationReleaseRequestAsync()
{
return SendAssociationReleaseResponseAsync();
}
public Task OnReceiveAssociationRequestAsync(DicomAssociation association)
{
CallingAE = association.CallingAE;
CalledAE = association.CalledAE;
Logger.LogInformation($"Received association request from AE: {CallingAE} with IP: {association.RemoteHost} ");
DicomSCPServiceConfig = _serviceProvider.GetService<IOptionsMonitor<DicomSCPServiceOption>>().CurrentValue;
var calledAEList = DicomSCPServiceConfig.CalledAEList;
//不支持三方服务 或者CallAE不对那么拒绝连接
if (!calledAEList.Contains(CalledAE) || DicomSCPServiceConfig.IsSupportThirdService == false || CallingAE != DicomSCPServiceConfig.ThirdCallningAE)
{
Logger.LogError($"Association with {CallingAE} rejected since called aet {CalledAE} is unknown");
return SendAssociationRejectAsync(DicomRejectResult.Permanent, DicomRejectSource.ServiceUser, DicomRejectReason.CalledAENotRecognized);
}
foreach (var pc in association.PresentationContexts)
{
if (pc.AbstractSyntax == DicomUID.Verification
|| pc.AbstractSyntax == DicomUID.PatientRootQueryRetrieveInformationModelFind
|| pc.AbstractSyntax == DicomUID.PatientRootQueryRetrieveInformationModelMove
|| pc.AbstractSyntax == DicomUID.StudyRootQueryRetrieveInformationModelFind
|| pc.AbstractSyntax == DicomUID.StudyRootQueryRetrieveInformationModelMove)
{
pc.AcceptTransferSyntaxes(_acceptedTransferSyntaxes);
}
else if (pc.AbstractSyntax == DicomUID.PatientRootQueryRetrieveInformationModelGet
|| pc.AbstractSyntax == DicomUID.StudyRootQueryRetrieveInformationModelGet)
{
pc.AcceptTransferSyntaxes(_acceptedImageTransferSyntaxes);
}
else if (pc.AbstractSyntax.StorageCategory != DicomStorageCategory.None)
{
pc.AcceptTransferSyntaxes(_acceptedImageTransferSyntaxes);
}
else
{
Logger.LogWarning($"Requested abstract syntax {pc.AbstractSyntax} from {CallingAE} not supported");
pc.SetResult(DicomPresentationContextResult.RejectAbstractSyntaxNotSupported);
}
}
Logger.LogInformation($"Accepted association request from {CallingAE}");
return SendAssociationAcceptAsync(association);
}
public Task<DicomCEchoResponse> OnCEchoRequestAsync(DicomCEchoRequest request)
{
Logger.LogInformation("Received verification request from AE {0} with IP: {1}", CallingAE, Association.RemoteHost);
return Task.FromResult(new DicomCEchoResponse(request, DicomStatus.Success));
}
public async IAsyncEnumerable<DicomCFindResponse> OnCFindRequestAsync(DicomCFindRequest request)
{
Console.WriteLine("Received C-FIND request, forwarding to real PACS...");
var _dicomAERepository = _serviceProvider.GetService<IRepository<DicomAE>>();
var find = await _dicomAERepository.FirstOrDefaultAsync(t => t.PacsTypeEnum == PacsType.PacsServer && t.CalledAE == DicomSCPServiceConfig.ThirdSearchPacsAE);
var hirClient = await _dicomAERepository.FirstOrDefaultAsync(t => t.PacsTypeEnum == PacsType.HIRClient);
if (find == null || hirClient == null)
{
Logger.LogInformation("客户端和Pacs配置未查询到");
}
// 创建 channel 用于异步传递响应
var channel = Channel.CreateUnbounded<DicomCFindResponse>();
// 克隆 dataset 避免线程/状态冲突
var clonedDataset = request.Dataset?.Clone() ?? new DicomDataset();
var forward = new DicomCFindRequest(request.SOPClassUID, request.Level)
{
Dataset = clonedDataset
};
// 标记是否已收到 final 状态Success/Failure/Cancel
var finalReceived = false;
// 当远端 PACS 返回响应时,异步写入 channel
forward.OnResponseReceived += (rq, rp) =>
{
var dsCopy = rp.Dataset?.Clone();
var proxyResp = new DicomCFindResponse(request, rp.Status)
{
Dataset = dsCopy
};
channel.Writer.TryWrite(proxyResp);
if (!rp.Status.Equals(DicomStatus.Pending))
{
finalReceived = true;
}
};
// 异步发送到真实 PACS
_ = Task.Run(async () =>
{
try
{
var client = DicomClientFactory.Create(find.IP, find.Port, false, hirClient.CalledAE, find.CalledAE);
await client.AddRequestAsync(forward);
await client.SendAsync();
}
catch (Exception ex)
{
Console.WriteLine("Error forwarding C-FIND: " + ex.Message);
}
finally
{
channel.Writer.Complete();
}
});
// 异步 yield 返回给上游
await foreach (var resp in channel.Reader.ReadAllAsync())
{
yield return resp;
}
// 兜底:如果没有 final 响应,返回 Success
if (!finalReceived)
{
yield return new DicomCFindResponse(request, DicomStatus.Success);
}
}
public async IAsyncEnumerable<DicomCMoveResponse> OnCMoveRequestAsync(DicomCMoveRequest request)
{
Console.WriteLine("Received C-Move request, forwarding to real PACS...");
var _dicomAERepository = _serviceProvider.GetService<IRepository<DicomAE>>();
var _cmoveStudyRepository = _serviceProvider.GetService<IRepository<CmoveStudy>>();
var find = await _dicomAERepository.FirstOrDefaultAsync(t => t.PacsTypeEnum == PacsType.PacsServer && t.CalledAE == DicomSCPServiceConfig.ThirdSearchPacsAE);
var hirServer = await _dicomAERepository.FirstOrDefaultAsync(t => t.PacsTypeEnum == PacsType.HIRServer);
var hirClient = await _dicomAERepository.FirstOrDefaultAsync(t => t.PacsTypeEnum == PacsType.HIRClient);
if (find == null || hirClient == null || hirServer == null)
{
Logger.LogInformation("客户端和Pacs配置未查询到");
}
var studyInstanceUid = request.Dataset?.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty);
if (studyInstanceUid.IsNotNullOrEmpty())
{
await _cmoveStudyRepository.AddAsync(new CmoveStudy() { CallingAE = CallingAE, CalledAE = CalledAE, DestinationAE = request.DestinationAE, StudyInstanceUIDList = new List<string>() { studyInstanceUid }, HopitalGroupIdList = new List<Guid>() }, true);
}
var channel = Channel.CreateUnbounded<DicomCMoveResponse>();
var clonedDataset = request.Dataset?.Clone() ?? new DicomDataset();
var forward = new DicomCMoveRequest(hirServer.CalledAE, studyInstanceUid)
{
Dataset = clonedDataset
};
bool finalReceived = false;
// PACS 返回响应时写入 channel
forward.OnResponseReceived += (rq, rp) =>
{
var dsCopy = rp.Dataset?.Clone();
var proxyResp = new DicomCMoveResponse(request, rp.Status)
{
Dataset = dsCopy,
Remaining = rp.Remaining,
Completed = rp.Completed,
};
channel.Writer.TryWrite(proxyResp);
if (!rp.Status.Equals(DicomStatus.Pending))
{
finalReceived = true;
}
};
// 异步发送到真实 PACS
_ = Task.Run(async () =>
{
try
{
var client = DicomClientFactory.Create(find.IP, find.Port, false, hirClient.CalledAE, find.CalledAE);
await client.AddRequestAsync(forward);
await client.SendAsync();
}
catch (Exception ex)
{
Console.WriteLine("Error forwarding C-MOVE: " + ex.Message);
}
finally
{
channel.Writer.Complete();
}
});
// 异步 yield 回上游
await foreach (var resp in channel.Reader.ReadAllAsync())
{
yield return resp;
}
// 兜底
if (!finalReceived)
{
yield return new DicomCMoveResponse(request, DicomStatus.Success);
}
}
}
}