From ebc0b5ae6fc8067e4300d788006d08d773175102 Mon Sep 17 00:00:00 2001 From: Rudi Date: Wed, 26 Aug 2026 00:04:30 +0000 Subject: [PATCH] Fix IND$FILE --- .../haus/nightmare/j3270/ft/FileTransfer.java | 15 +++ .../haus/nightmare/lib3270j/ft/FTDft.java | 92 +++++++++++++++---- .../lib3270j/input/InputProcessor.java | 4 +- test_all.sh | 1 + 4 files changed, 91 insertions(+), 21 deletions(-) diff --git a/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransfer.java b/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransfer.java index 779e906..92a128b 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransfer.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransfer.java @@ -121,6 +121,21 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener { if (state == FTState.RUNNING || state == FTState.AWAIT_ACK) { log.info("User cancelled transfer"); setState(FTState.ABORT_WAIT); // Signal handlers to abort at next chance + // Start a safety timeout in case the host does not send further packets to trigger abort + cancelTimeout(); + timeoutTimer = new Timer("FTCancelTimeout", true); + timeoutTimer.schedule(new TimerTask() { + @Override + public void run() { + SwingUtilities.invokeLater(() -> { + if (state != FTState.NONE) { + log.warning("Transfer cancel timeout — force resetting state"); + completeTransfer("Transfer cancelled by user."); + callback.onTransferAborted("Cancelled by user."); + } + }); + } + }, 3000); } else if (state != FTState.NONE) { log.info("Forcing cancel from state " + state); completeTransfer("Transfer cancelled."); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java index 7659126..8e42da7 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java @@ -106,6 +106,7 @@ public class FTDft { } dftSaveBuf = null; dftSaveBufLen = 0; + resetState(); } /** @@ -173,9 +174,9 @@ public class FTDft { nameBuf = extractName(data, sfOffset + 3 + 31, 7); } - if (OPEN_MSG.equalsIgnoreCase(nameBuf)) { + if (isMessageStream(nameBuf)) { messageFlag = true; - log.info("DFT: Open request for message stream"); + log.info("DFT: Open request for message stream (" + nameBuf + ")"); } else { messageFlag = false; listener.onDftRunning(); @@ -188,15 +189,36 @@ public class FTDft { dftOpenAck(); } + private boolean isMessageStream(String name) { + if (name == null) return false; + String u = name.toUpperCase(); + return u.contains(OPEN_MSG) || u.contains("MSG"); + } + + private boolean isDataStream(String name) { + if (name == null) return false; + String u = name.toUpperCase(); + return u.contains("FT:DATA") || u.contains("DATA"); + } + private String extractName(byte[] data, int start, int maxLen) { - StringBuilder sb = new StringBuilder(); + StringBuilder asciiSb = new StringBuilder(); + StringBuilder ebcdicSb = new StringBuilder(); for (int i = 0; i < maxLen && (start + i) < data.length; i++) { int b = data[start + i] & 0xFF; if (b == 0) break; - char ch = translator.ebcdicToUnicode(b); - sb.append(ch); + asciiSb.append((char) b); + ebcdicSb.append(translator.ebcdicToUnicode(b)); } - return sb.toString().trim(); + String ascii = asciiSb.toString().trim(); + String ebcdic = ebcdicSb.toString().trim(); + if (isMessageStream(ascii) || isDataStream(ascii)) { + return ascii; + } + if (isMessageStream(ebcdic) || isDataStream(ebcdic)) { + return ebcdic; + } + return ascii; } private void dftOpenAck() { @@ -227,6 +249,7 @@ public class FTDft { int pos = offset + 2; int end = offset + length; + boolean dataFound = false; // Look for TR_BEGIN_DATA marker while (pos < end) { int headerCode = data[pos] & 0xFF; @@ -234,10 +257,11 @@ public class FTDft { if (headerCode == TR_BEGIN_DATA) { if (pos + 3 > end) break; int dataLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF); - int actualDataLen = dataLen - 3; + int actualDataLen = (dataLen > 5) ? Math.min(dataLen - 5, end - (pos + 3)) : (end - (pos + 3)); pos += 3; if (actualDataLen > 0 && pos + actualDataLen <= end) { + dataFound = true; if (messageFlag) { // Handle message payload from host dftDataAck(); @@ -269,25 +293,46 @@ public class FTDft { } } - // Send acknowledgement for file data - dftDataAck(); + // Send acknowledgement only if file data was actually received and processed + if (dataFound) { + dftDataAck(); + } } private void handleHostMessage(byte[] data, int offset, int length) { - StringBuilder sb = new StringBuilder(); + StringBuilder asciiSb = new StringBuilder(); + StringBuilder ebcdicSb = new StringBuilder(); for (int i = 0; i < length; i++) { int b = data[offset + i] & 0xFF; if (b == 0 || b == '$') break; - char ch = translator.ebcdicToUnicode(b); - sb.append(ch); + asciiSb.append((char) b); + ebcdicSb.append(translator.ebcdicToUnicode(b)); } - String msg = sb.toString().trim(); - log.info("DFT message: " + msg); + String asciiMsg = asciiSb.toString().trim(); + String ebcdicMsg = ebcdicSb.toString().trim(); - String msgLower = msg.toLowerCase(); - if (msg.startsWith(END_TRANSFER) || msgLower.contains("complete") || msgLower.contains("transferred") || msgLower.contains("success")) { + // Determine if message is ASCII or EBCDIC + String msg = asciiMsg; + if (asciiMsg.toUpperCase().startsWith("TRANS") || asciiMsg.toLowerCase().contains("file") || asciiMsg.toLowerCase().contains("error")) { + msg = asciiMsg; + } else if (ebcdicMsg.toUpperCase().startsWith("TRANS") || ebcdicMsg.toLowerCase().contains("file") || ebcdicMsg.toLowerCase().contains("error")) { + msg = ebcdicMsg; + } else { + int asciiPrintable = 0; + for (char c : asciiMsg.toCharArray()) { + if (c >= 32 && c <= 126) asciiPrintable++; + } + if (asciiPrintable < asciiMsg.length() / 2 && !ebcdicMsg.isEmpty()) { + msg = ebcdicMsg; + } + } + + log.info("DFT host message: " + msg); + + String msgUpper = msg.toUpperCase(); + if (msgUpper.startsWith(END_TRANSFER) || msgUpper.contains("COMPLETE") || msgUpper.contains("TRANSFERRED") || msgUpper.contains("SUCCESS")) { listener.onTransferComplete(null); - } else if (listener.getCurrentState() == FTState.ABORT_SENT || msgLower.contains("error") || msgLower.contains("failed") || msgLower.contains("abort")) { + } else if (msgUpper.startsWith("TRANS") || msgUpper.contains("ERROR") || msgUpper.contains("FAILED") || msgUpper.contains("ABORT") || msgUpper.contains("NOT FOUND") || listener.getCurrentState() == FTState.ABORT_SENT) { listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg); } else { // Informational message (default success) @@ -495,7 +540,7 @@ public class FTDft { input.sendStructuredFieldData(out.toByteArray()); - if (!messageFlag) { + if (listener.getCurrentState() != FTState.NONE) { log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)"); listener.onTransferComplete(null); } @@ -504,13 +549,20 @@ public class FTDft { // ========== Data Acknowledgement ========== private void dftDataAck() { - ByteArrayOutputStream out = new ByteArrayOutputStream(6); + ByteArrayOutputStream out = new ByteArrayOutputStream(12); out.write(AID_SF); - out.write(0); out.write(5); + out.write(0); out.write(11); // SF length = 11 (0x000B) out.write(SF_TRANSFER_DATA); out.write((TR_NORMAL_REPLY >> 8) & 0xFF); out.write(TR_NORMAL_REPLY & 0xFF); + out.write((TR_RECNUM_HDR >> 8) & 0xFF); + out.write(TR_RECNUM_HDR & 0xFF); + out.write((int) ((recnum >> 24) & 0xFF)); + out.write((int) ((recnum >> 16) & 0xFF)); + out.write((int) ((recnum >> 8) & 0xFF)); + out.write((int) (recnum & 0xFF)); + recnum++; input.sendStructuredFieldData(out.toByteArray()); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java index 7d4216c..f6a78ff 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java @@ -780,7 +780,9 @@ public class InputProcessor { * Used by DFT mode file transfer. */ public void sendStructuredFieldData(byte[] data) { - fsm.send3270Data(data); + if (fsm != null) { + fsm.send3270Data(data); + } } /** Get a reference to the TelnetFSM for direct SF operations. */ diff --git a/test_all.sh b/test_all.sh index d01fb02..fad0a92 100755 --- a/test_all.sh +++ b/test_all.sh @@ -43,6 +43,7 @@ fi BUILD_DIR="$SCRIPT_DIR/build" TEST_BUILD_DIR="$BUILD_DIR/test-classes" REPORTS_DIR="$BUILD_DIR/reports/tests" +rm -rf "$TEST_BUILD_DIR" mkdir -p "$TEST_BUILD_DIR" "$REPORTS_DIR" # 3. Ensure JUnit standalone runner is present