334 lines
12 KiB
C#
334 lines
12 KiB
C#
using AutoMapper.Execution;
|
||
using DocumentFormat.OpenXml.Bibliography;
|
||
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)
|
||
{
|
||
if (exception != null)
|
||
{
|
||
Logger.LogError($"Closed, exception is {exception.Message}");
|
||
}
|
||
}
|
||
|
||
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
|
||
|
||
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();
|
||
}
|
||
|
||
//_ = Task.Run(async () =>
|
||
//{
|
||
|
||
//});
|
||
|
||
// 异步 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,
|
||
|
||
};
|
||
|
||
Logger.LogInformation($"Completed:{rp.Completed}");
|
||
|
||
channel.Writer.TryWrite(proxyResp);
|
||
|
||
if (!rp.Status.Equals(DicomStatus.Pending))
|
||
{
|
||
finalReceived = true;
|
||
}
|
||
};
|
||
|
||
// 异步发送到真实 PACS
|
||
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();
|
||
}
|
||
|
||
//_ = Task.Run(async () =>
|
||
//{
|
||
|
||
//});
|
||
|
||
// 异步 yield 回上游
|
||
await foreach (var resp in channel.Reader.ReadAllAsync())
|
||
{
|
||
yield return resp;
|
||
}
|
||
|
||
// 兜底
|
||
if (!finalReceived)
|
||
{
|
||
yield return new DicomCMoveResponse(request, DicomStatus.Success);
|
||
}
|
||
}
|
||
}
|
||
}
|