Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend Move SCP response and abort DcmSend associations on C-STORE I/O errors mid transfer #710

Merged
merged 4 commits into from
Mar 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,24 @@
*/
package pt.ua.dicoogle.server.queryretrieve;

import java.io.IOException;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;

import org.dcm4che2.data.Tag;
import org.dcm4che2.data.VR;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.xml.transform.TransformerConfigurationException;
import org.dcm4che2.data.DicomElement;
import org.dcm4che2.data.DicomObject;
import org.dcm4che2.net.Association;
import org.dcm4che2.net.ConfigurationException;
import org.dcm4che2.net.DicomServiceException;
import org.dcm4che2.net.DimseRSP;
import org.dcm4che2.net.Status;
Expand All @@ -41,6 +45,9 @@
import pt.ua.dicoogle.DicomLog.LogLine;
import pt.ua.dicoogle.DicomLog.LogXML;
import pt.ua.dicoogle.core.settings.ServerSettingsManager;
import pt.ua.dicoogle.plugins.PluginController;
import pt.ua.dicoogle.sdk.StorageInputStream;
import pt.ua.dicoogle.sdk.StorageInterface;
import pt.ua.dicoogle.sdk.datastructs.MoveDestination;
import pt.ua.dicoogle.sdk.settings.server.ServerSettings;
import pt.ua.dicoogle.server.DicomNetwork;
Expand Down Expand Up @@ -68,8 +75,6 @@ public CMoveServiceSCP(String sopClass, Executor executor) {

protected DimseRSP doCMove(Association as, int pcid, DicomObject cmd, DicomObject data, DicomObject rsp)
throws DicomServiceException {
DimseRSP replay = null;

/**
* Verify Permited AETs
*/
Expand Down Expand Up @@ -112,7 +117,7 @@ protected DimseRSP doCMove(Association as, int pcid, DicomObject cmd, DicomObjec

/** Verify if it have the field destination */
if (destination == null) {
throw new DicomServiceException(cmd, Status.UnrecognizedOperation, "Missing Move Destination");
throw new DicomServiceException(cmd, 0xC000 | Status.UnrecognizedOperation, "Missing Move Destination");
}

String SOPUID = new String(data.get(Integer.parseInt("0020000D", 16)).getBytes());
Expand Down Expand Up @@ -211,18 +216,42 @@ protected DimseRSP doCMove(Association as, int pcid, DicomObject cmd, DicomObjec
}
try {
logger.debug("Destination: {}", destination);
new CallDCMSend(files, portAddr, hostDest, destination, CMoveID);
SendReport report = callDCMSend(files, portAddr, hostDest, destination, CMoveID);

// fill in properties about sub-operations
rsp.putInt(Tag.NumberOfCompletedSuboperations, VR.US, report.filesSent);
int remaining = report.filesToSend - report.filesSent - report.filesFailed;
if (remaining > 0) {
rsp.putInt(Tag.NumberOfRemainingSuboperations, VR.US, remaining);
}
int totalErrors = report.filesFailed + report.filesSkipped;
if (totalErrors > 0) {
rsp.putInt(Tag.NumberOfFailedSuboperations, VR.US, totalErrors);
}
if (report.errorID == 0) {
// report warning if there is at least one file failure
int code = totalErrors > 0 ? 0xB000 : Status.Success;
rsp.putInt(Tag.Status, VR.US, code);
} else {
rsp.putInt(Tag.Status, VR.US, 0xC000 | Status.ProcessingFailure);
rsp.putInt(Tag.ErrorID, VR.US, ERROR_ID_FILE_TRANSMISSION);
rsp.putString(Tag.ErrorComment, VR.LO, "DICOM file transmission failed");
}

} catch (Exception ex) {
logger.error("Error Sending files to Storage Server! ", ex);
logger.error("Failed to send files to DICOM node {}", destination, ex);
rsp.putInt(Tag.Status, VR.US, 0xC000 | Status.ProcessingFailure);
rsp.putInt(Tag.ErrorID, VR.US, ERROR_ID_GENERAL_FAILURE);
rsp.putString(Tag.ErrorComment, VR.LO, ex.getMessage());
}
}


replay = new MoveRSP(data, rsp); // Third Party Move
return replay;

return new MoveRSP(data, rsp);
}

private static final int ERROR_ID_FILE_TRANSMISSION = 1;
private static final int ERROR_ID_GENERAL_FAILURE = 10;

/**
* @return the service
*/
Expand All @@ -236,4 +265,65 @@ public DicomNetwork getService() {
public void setService(DicomNetwork service) {
this.service = service;
}

private static class SendReport {
final int errorID;
final int filesSent;
final int filesToSend;
final int filesFailed;
final int filesSkipped;

public SendReport(int errorID, int filesSent, int filesToSend, int filesFailed, int filesSkipped) {
this.errorID = errorID;
this.filesSent = filesSent;
this.filesToSend = filesToSend;
this.filesFailed = filesFailed;
this.filesSkipped = filesSkipped;
}
}

private static SendReport callDCMSend(List<URI> files, int port, String hostname, String AETitle, String cmoveID)
throws IOException, ConfigurationException, InterruptedException {

DicoogleDcmSend dcmsnd = new DicoogleDcmSend();

dcmsnd.setRemoteHost(hostname);
dcmsnd.setRemotePort(port);

for (URI uri : files) {
StorageInterface plugin = PluginController.getInstance().getStorageForSchema(uri);
logger.debug("uri: {}", uri);

if (plugin != null) {
logger.debug("Retrieving {}", uri);
Iterable<StorageInputStream> it = plugin.at(uri);

for (StorageInputStream file : it) {
dcmsnd.addFile(file);
logger.debug("Added file to DcmSend: {}", uri);
}
}

}
dcmsnd.setCalledAET(AETitle);

dcmsnd.configureTransferCapability();

dcmsnd.setMoveOriginatorMessageID(cmoveID);
dcmsnd.start();
dcmsnd.open();
int errorID;
try {
dcmsnd.send();
errorID = 0;
} catch (IOException ex) {
// assume that there was a fatal error in the transmission
errorID = ERROR_ID_FILE_TRANSMISSION;
} finally {
dcmsnd.close();
}

return new SendReport(errorID, dcmsnd.getNumberOfFilesSent(), dcmsnd.getNumberOfFilesToSend(),
dcmsnd.getNumberOfFilesFailed(), dcmsnd.getNumberOfFilesSkipped());
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,17 @@ public class DicoogleDcmSend extends StorageCommitmentService {

private int transcoderBufferSize = 1024;

/** Number of files sent successfully */
private int filesSent = 0;

/** Number of files skipped upon addition due to storage problems of the DICOM file
*/
private int filesSkipped = 0;

/** Number of files which failed to be sent due to presentation context negotation issues
*/
private int filesFailed = 0;

private long totalSize = 0L;

private boolean fileref = false;
Expand Down Expand Up @@ -344,6 +353,14 @@ public final int getNumberOfFilesSent() {
return filesSent;
}

public final int getNumberOfFilesSkipped() {
return filesSkipped;
}

public final int getNumberOfFilesFailed() {
return filesFailed;
}

public final long getTotalSizeSent() {
return totalSize;
}
Expand All @@ -360,6 +377,7 @@ public synchronized void addFile(StorageInputStream item) {
inStream = info.getInputStream();
} catch (IOException e) {
LOGGER.error("Failed to fetch file {} - skipped.", item.getURI(), e);
this.filesSkipped += 1;
return;
}
DicomObject dcmObj = new BasicDicomObject();
Expand All @@ -372,6 +390,7 @@ public synchronized void addFile(StorageInputStream item) {
info.fmiEndPos = in.getEndOfFileMetaInfoPosition();
} catch (IOException e) {
LOGGER.warn("Failed to parse file {} - skipped.", item.getURI(), e);
this.filesSkipped += 1;
return;
} finally {
CloseUtils.safeClose(in);
Expand All @@ -380,11 +399,13 @@ public synchronized void addFile(StorageInputStream item) {
info.cuid = dcmObj.getString(Tag.SOPClassUID);
if (info.cuid == null) {
LOGGER.warn("Missing SOP Class UID in {} - skipped.", item.getURI());
this.filesSkipped += 1;
return;
}
info.iuid = dcmObj.getString(Tag.SOPInstanceUID);
if (info.iuid == null) {
LOGGER.warn("Missing SOP Instance UID in {} - skipped.", item.getURI());
this.filesSkipped += 1;
return;
}

Expand Down Expand Up @@ -454,7 +475,7 @@ public void openToStgcmtAE() throws IOException, ConfigurationException, Interru
assoc = ae.connect(remoteStgcmtAE, executor);
}

public void send() {
public void send() throws IOException {
for (int i = 0, n = files.size(); i < n; ++i) {
FileInfo info = files.get(i);
TransferCapability tc = assoc.getTransferCapabilityAsSCU(info.cuid);
Expand Down Expand Up @@ -491,24 +512,31 @@ public void onDimseRSP(Association as, DicomObject cmd, DicomObject data) {
assoc.cstore(info.cuid, info.iuid, priority, new DataWriter(info), tsuid, rspHandler);
}



// assoc.cstore(info.cuid, info.iuid, priority, assoc.getCallingAET(), priority, new DataWriter(info), tsuid, rspHandler);

} catch (NoPresentationContextException e) {
LOGGER.warn("Cannot send {}: {}", info.item.getURI(), e.getMessage());
filesFailed += 1;
} catch (IOException e) {
LOGGER.error("Failed to send {}", info.item.getURI(), e);
LOGGER.error("Fatal I/O error while sending {}", info.item.getURI(), e);
// since this exception can be thrown mid-transfer,
// the sending process cannot be recovered.
// Try to abort the association and propagate exception
try {
assoc.abort();
} catch (Exception ex) {
// ignore
LOGGER.warn("Association could not be aborted: {}", ex.getMessage());
}
throw e;
} catch (InterruptedException e) {
// should not happen
e.printStackTrace();
throw new RuntimeException(e);
}
}
try {
assoc.waitForDimseRSP();
} catch (InterruptedException e) {
// should not happen
e.printStackTrace();
throw new RuntimeException(e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@
import org.dcm4che2.net.DimseRSP;
import org.dcm4che2.net.Status;


import pt.ua.dicoogle.server.SearchDicomResult;

/**
/** Custom C-MOVE response for {@linkplain CMoveServiceSCP}
*
* @author Luís A. Bastião Silva <[email protected]>
*/
Expand All @@ -52,30 +51,16 @@ public MoveRSP(DicomObject keys, DicomObject rsp) {
this.keys = keys;
this.current = rsp;


// DebugManager.getInstance().debug("--> Creating MoveRSP");



/** Debug - show keys, rsp, index */
if (keys != null) {
// DebugManager.getInstance().debug("keys object: ");
// DebugManager.getInstance().debug(keys.toString());
if (!this.rsp.contains(Tag.Status)) {
this.rsp.putInt(Tag.Status, VR.US, Status.Success);
}
if (rsp != null) {
// DebugManager.getInstance().debug("Rsp object");
// DebugManager.getInstance().debug(rsp.toString());
}

this.rsp.putInt(Tag.Status, VR.US, Status.Success);

}

@Override
public boolean next() throws IOException, InterruptedException {

/** Sucess */
this.rsp.putInt(Tag.Status, VR.US, Status.Success);
if (this.current == null) {
return false;
}
/** Clean pointers */
this.current = null;
return true;
Expand Down
Loading