IND$FILE receiving

This commit is contained in:
2026-04-21 15:34:11 -04:00
parent ac88b527fa
commit bda8ea9530
15 changed files with 280 additions and 1 deletions
-1
View File
@@ -1 +0,0 @@
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,6 +6,8 @@ import org.lib3270j.listener.ScreenUpdateListener;
import org.pubvm.j3270.ui.ConnectDialog; import org.pubvm.j3270.ui.ConnectDialog;
import org.pubvm.j3270.ui.StatusBar; import org.pubvm.j3270.ui.StatusBar;
import org.pubvm.j3270.ui.TerminalPanel; import org.pubvm.j3270.ui.TerminalPanel;
import org.pubvm.j3270.ft.FileTransfer;
import org.pubvm.j3270.ft.FileTransferDialog;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
@@ -31,6 +33,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private String lastHost = ""; private String lastHost = "";
private int lastPort = 23; private int lastPort = 23;
private FileTransfer fileTransfer;
public J3270App() { public J3270App() {
super("j3270 — Java TN3270 Terminal Emulator"); super("j3270 — Java TN3270 Terminal Emulator");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
@@ -125,6 +129,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
client.reset(); client.reset();
terminalPanel.repaint(); terminalPanel.repaint();
})); }));
actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
menuBar.add(actionsMenu); menuBar.add(actionsMenu);
// Help menu // Help menu
@@ -154,6 +160,69 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
return item; return item;
} }
private void showFileTransferDialog() {
if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
"Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
if (fileTransfer == null) {
fileTransfer = new FileTransfer(client, new FileTransfer.FileTransferCallback() {
private ProgressMonitor monitor;
@Override
public void onTransferStarted() {
monitor = new ProgressMonitor(J3270App.this, "Starting IND$FILE transfer...",
"Waiting for host...", 0, 100);
monitor.setMillisToDecideToPopup(0);
monitor.setMillisToPopup(0);
// Start a timer to check for manual cancellation
Timer cancelCheckTimer = new Timer(500, e -> {
if (monitor != null && monitor.isCanceled() && fileTransfer != null) {
fileTransfer.cancel();
((Timer)e.getSource()).stop();
}
});
cancelCheckTimer.start();
}
@Override
public void onTransferRunning() {
if (monitor != null) {
monitor.setNote("Transferring data...");
}
}
@Override
public void onBytesTransferred(long bytes) {
if (monitor != null) {
monitor.setNote(bytes + " bytes transferred");
// We don't always know max size in IND$FILE, so we just pulse/update note
}
}
@Override
public void onTransferComplete(String message) {
if (monitor != null) monitor.close();
monitor = null;
JOptionPane.showMessageDialog(J3270App.this, message, "Transfer Complete", JOptionPane.INFORMATION_MESSAGE);
}
@Override
public void onTransferAborted(String error) {
if (monitor != null) monitor.close();
monitor = null;
JOptionPane.showMessageDialog(J3270App.this, "Transfer Failed: " + error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
}
});
}
FileTransferDialog dialog = new FileTransferDialog(this, fileTransfer);
dialog.setVisible(true);
}
// ========== Connection management ========== // ========== Connection management ==========
private void showConnectDialog() { private void showConnectDialog() {
@@ -221,6 +290,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private void disconnect() { private void disconnect() {
if (client != null) { if (client != null) {
if (fileTransfer != null) {
fileTransfer.cancel();
fileTransfer = null;
}
client.disconnect(); client.disconnect();
client = null; client = null;
terminalPanel.setClient(null); terminalPanel.setClient(null);
@@ -280,6 +353,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (client != null) { if (client != null) {
client.getInputProcessor().setKeyboardLocked(false); client.getInputProcessor().setKeyboardLocked(false);
} }
if (fileTransfer != null) {
fileTransfer.onScreenUpdated();
}
terminalPanel.repaint(); terminalPanel.repaint();
statusBar.updateStatus(); statusBar.updateStatus();
}); });
@@ -115,6 +115,9 @@ public class Telnet3270Client {
/** Get the input processor for keyboard operations. */ /** Get the input processor for keyboard operations. */
public InputProcessor getInputProcessor() { return inputProcessor; } public InputProcessor getInputProcessor() { return inputProcessor; }
/** Get the data stream processor. */
public DataStreamProcessor getDataStreamProcessor() { return dsProcessor; }
/** Get the connection config. */ /** Get the connection config. */
public ConnectionConfig getConfig() { return config; } public ConnectionConfig getConfig() { return config; }
@@ -130,6 +133,11 @@ public class Telnet3270Client {
} }
} }
/** Emulate input of a string, pressing Enter on newlines. */
public void emulateInput(String s) {
inputProcessor.emulateInput(s);
}
/** Send Enter key. */ /** Send Enter key. */
public void sendEnter() { public void sendEnter() {
inputProcessor.sendAid(org.lib3270j.protocol.DS3270Constants.AID_ENTER); inputProcessor.sendAid(org.lib3270j.protocol.DS3270Constants.AID_ENTER);
@@ -31,6 +31,9 @@ public class DataStreamProcessor {
// Callback for sending data back to host // Callback for sending data back to host
private OutputSender outputSender; private OutputSender outputSender;
// File Transfer DFT handler
private org.lib3270j.ft.FTDft ftDft;
/** Functional interface for sending output back through the telnet stack. */ /** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface @FunctionalInterface
public interface OutputSender { public interface OutputSender {
@@ -49,6 +52,10 @@ public class DataStreamProcessor {
this.outputSender = sender; this.outputSender = sender;
} }
public void setFTDft(org.lib3270j.ft.FTDft ftDft) {
this.ftDft = ftDft;
}
public void addScreenUpdateListener(ScreenUpdateListener l) { public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l); screenListeners.add(l);
} }
@@ -698,6 +705,13 @@ public class DataStreamProcessor {
processRecord(data, pos + 4, fieldLen - 4, false); processRecord(data, pos + 4, fieldLen - 4, false);
} }
break; break;
case SF_TRANSFER_DATA:
if (ftDft != null) {
ftDft.processStructuredField(data, pos, fieldLen);
} else {
log.fine("SF_TRANSFER_DATA received but ftDft is null");
}
break;
default: default:
log.fine("Unknown SF id: " + String.format("0x%02x", sfId)); log.fine("Unknown SF id: " + String.format("0x%02x", sfId));
break; break;
@@ -31,6 +31,7 @@ public class QueryReplyBuilder {
QR_COLOR, // 0x86 QR_COLOR, // 0x86
QR_HIGHLIGHTING, // 0x87 QR_HIGHLIGHTING, // 0x87
QR_REPLY_MODES, // 0x88 QR_REPLY_MODES, // 0x88
QR_DDM, // 0x95 - Distributed Data Management (file transfer)
QR_IMP_PART, // 0xa6 QR_IMP_PART, // 0xa6
}; };
@@ -68,6 +69,10 @@ public class QueryReplyBuilder {
// Reply Modes // Reply Modes
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
// Distributed Data Management (DFT File Transfer)
// Hardcoded limit of 4096 is standard; the client can configure it during transfer.
appendQueryReply(out, QR_DDM, buildDdm(4096));
// Implicit Partition // Implicit Partition
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
@@ -189,6 +194,19 @@ public class QueryReplyBuilder {
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR }; return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
} }
private byte[] buildDdm(int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(8);
out.write(0x00); // reserved
out.write(0x00); // reserved
out.write((bufferSize >> 8) & 0xFF); // inbound length limit (INLIM)
out.write(bufferSize & 0xFF);
out.write((bufferSize >> 8) & 0xFF); // outbound length limit (OUTLIM)
out.write(bufferSize & 0xFF);
out.write(0x01); // NSS = 01
out.write(0x01); // DDMSS = 01
return out.toByteArray();
}
private byte[] buildImplicitPartition(int maxCols, int maxRows) { private byte[] buildImplicitPartition(int maxCols, int maxRows) {
ByteArrayOutputStream out = new ByteArrayOutputStream(22); ByteArrayOutputStream out = new ByteArrayOutputStream(22);
// Implicit partition sizes, 2 self-defining parameters // Implicit partition sizes, 2 self-defining parameters
@@ -319,4 +319,113 @@ public class InputProcessor {
keyboardLocked = false; keyboardLocked = false;
insertMode = false; insertMode = false;
} }
// ========== File Transfer Support ==========
/**
* Send an AID code, bypassing the keyboard lock.
* Used internally by the file transfer protocol to send Enter/PF keys
* during a transfer when the keyboard is normally locked.
*/
public void sendAidForFT(int aidCode) {
boolean wasLocked = keyboardLocked;
keyboardLocked = false;
sendAid(aidCode);
keyboardLocked = wasLocked;
}
/**
* Type a string into the current cursor position and press Enter.
* Used by IND$FILE to inject the transfer command.
* Equivalent to emulate_input() in x3270.
*/
public void emulateInput(String text) {
// Type each character
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (ch == '\n') {
// Newline means press Enter
sendAid(AID_ENTER);
return;
}
typeCharacter(ch);
}
}
/**
* Erase the current field and return its capacity.
* Used by IND$FILE to prepare the field for the transfer command.
* Equivalent to kybd_prime() in x3270.
*
* Returns:
* - The number of characters that can fit in the field, or
* - -1 if the keyboard is locked
* - -2 if not in 3270 mode
* - -3 if no input field found
*/
public int kybdPrime() {
if (keyboardLocked) return -1;
if (!screen.isFormatted()) {
// Unformatted screen use entire buffer
int size = screen.getRows() * screen.getCols();
screen.setCursorAddress(0);
for (int i = 0; i < size; i++) {
screen.getCell(i).ec = 0;
screen.getCell(i).ucs4 = 0;
}
screen.markAllChanged();
return size;
}
// Find the field at cursor and erase it
int curAddr = screen.getCursorAddress();
byte faVal = screen.getFieldAttributeAt(curAddr);
if (faIsProtected(faVal & 0xFF)) {
// Try to find first unprotected field
curAddr = screen.findNextUnprotected(curAddr);
faVal = screen.getFieldAttributeAt(curAddr);
if (faIsProtected(faVal & 0xFF)) {
return -3; // No unprotected field
}
}
// Move to start of field
int faAddr = screen.findFieldAttribute(curAddr);
int fieldStart = screen.incrementAddress(faAddr);
screen.setCursorAddress(fieldStart);
// Count field length and erase
int fieldLen = 0;
int pos = fieldStart;
int size = screen.getRows() * screen.getCols();
while (!screen.getCell(pos).isFieldAttribute()) {
screen.getCell(pos).ec = 0;
screen.getCell(pos).ucs4 = 0;
fieldLen++;
pos = screen.incrementAddress(pos);
if (pos == fieldStart) break;
}
// Set MDT
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
screen.markAllChanged();
return fieldLen;
}
/**
* Send a structured field response directly.
* Used by DFT mode file transfer.
*/
public void sendStructuredFieldData(byte[] data) {
fsm.send3270Data(data);
}
/** Get a reference to the TelnetFSM for direct SF operations. */
public TelnetFSM getTelnetFSM() {
return fsm;
}
} }
@@ -326,4 +326,59 @@ public class ScreenBuffer {
public ExtendedAttribute getDefaultFieldAttribute() { public ExtendedAttribute getDefaultFieldAttribute() {
return defaultFA; return defaultFA;
} }
// ========== Low-level cell access for file transfer (CUT mode) ==========
/**
* Write an EBCDIC byte to a cell at the given buffer address.
* Used by CUT mode file transfer to place data into the screen buffer.
*/
public void setCell(int baddr, int ebcdicByte) {
if (baddr < 0 || baddr >= maxRows * maxCols) return;
buffer[baddr].ec = (byte) (ebcdicByte & 0xFF);
buffer[baddr].ucs4 = 0; // will be set on display refresh
screenChanged = true;
}
/**
* Write an EBCDIC byte with extended attribute flags (cs=0).
* Equivalent to ctlr_add() in x3270.
*/
public void setCellWithCS(int baddr, int ebcdicByte, int cs) {
if (baddr < 0 || baddr >= maxRows * maxCols) return;
buffer[baddr].ec = (byte) (ebcdicByte & 0xFF);
buffer[baddr].cs = (byte) (cs & 0xFF);
buffer[baddr].ucs4 = 0;
screenChanged = true;
}
/**
* Modify the field attribute byte at a given buffer address.
* Used by CUT mode to hide data fields during transfer.
*/
public void setCellFA(int baddr, byte fa) {
if (baddr < 0 || baddr >= maxRows * maxCols) return;
buffer[baddr].fa = fa;
buffer[baddr].ic = 1; // mark as field attribute
formatted = true;
screenChanged = true;
}
/**
* Get just the EBCDIC code byte from a cell.
* Convenience method for file transfer data extraction.
*/
public int getCellEC(int baddr) {
if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
return buffer[baddr].ec & 0xFF;
}
/**
* Get the field attribute byte at a cell (the fa byte, not the ic field).
* For CUT mode checking of field attribute modifications.
*/
public byte getCellFAByte(int baddr) {
if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
return buffer[baddr].fa;
}
} }