Fix gradle and add workflows
Build and Test j3270 / Build JAR & Run Tests (push) Failing after 2m24s

This commit is contained in:
2026-08-20 18:24:35 -04:00
parent 80166c477b
commit a1b1273022
68 changed files with 1102 additions and 153 deletions
@@ -196,6 +196,18 @@ public class Telnet3270Client {
public void tab() { inputProcessor.tab(); }
/** Back-tab to previous unprotected field. */
public void backTab() { inputProcessor.backTab(); }
/** Move cursor to next line. */
public void newline() { inputProcessor.newline(); }
/** Erase all unprotected fields. */
public void eraseInput() { inputProcessor.eraseInput(); }
/** Insert Duplicate order. */
public void dup() { inputProcessor.dup(); }
/** Insert Field Mark order. */
public void fieldMark() { inputProcessor.fieldMark(); }
/** Attention key. */
public void attn() { inputProcessor.attn(); }
/** SysReq key. */
public void sysReq() { inputProcessor.sysReq(); }
/** Reset (unlock keyboard). */
public void reset() { inputProcessor.reset(); }
}
@@ -816,8 +816,18 @@ public class DataStreamProcessor {
}
private void sendRequestedQueryReplies(byte[] codes) {
// For now, send all — refinement can come later
sendAllQueryReplies();
byte[] qr = qrBuilder.buildQueryReplies(codes, screen.getMaxCols(), screen.getMaxRows(),
screen.getMaxCols() * screen.getMaxRows());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < qr.length; i++) {
sb.append(String.format("%02x ", qr[i] & 0xFF));
if ((i + 1) % 32 == 0)
sb.append("\n ");
}
log.info("Requested query reply (" + qr.length + " bytes):\n " + sb.toString().trim());
if (outputSender != null) {
outputSender.send3270Data(qr);
}
}
// ========== Output helpers ==========
@@ -70,13 +70,69 @@ public class QueryReplyBuilder {
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
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
log.info("Built " + out.size() + " bytes of query replies");
log.info("Built " + out.size() + " bytes of all query replies");
return out.toByteArray();
}
/**
* Build specific query replies in response to a Read Partition Query List (SF_RPQ_LIST).
* For any unsupported requested query code, emits a QR_NULL (0xFF) structured field
* matching x3270 sf.c behavior.
*/
public byte[] buildQueryReplies(byte[] requestedCodes, int maxCols, int maxRows, int bufferSize) {
if (requestedCodes == null || requestedCodes.length == 0) {
return buildAllQueryReplies(maxCols, maxRows, bufferSize);
}
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
out.write(AID_SF);
for (byte codeByte : requestedCodes) {
int code = codeByte & 0xFF;
switch (code) {
case QR_SUMMARY:
appendQueryReply(out, QR_SUMMARY, buildSummary());
break;
case QR_USABLE_AREA:
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
break;
case QR_ALPHA_PART:
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
break;
case QR_CHARSETS:
appendQueryReply(out, QR_CHARSETS, buildCharsets());
break;
case QR_COLOR:
appendQueryReply(out, QR_COLOR, buildColor());
break;
case QR_HIGHLIGHTING:
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
break;
case QR_REPLY_MODES:
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
break;
case QR_DDM:
appendQueryReply(out, QR_DDM, buildDdm(4096));
break;
case QR_IMP_PART:
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
break;
case QR_RPQNAMES:
appendQueryReply(out, QR_RPQNAMES, new byte[0]);
break;
default:
// Unsupported query reply code — emit QR_NULL
appendQueryReply(out, QR_NULL, new byte[0]);
break;
}
}
log.info("Built " + out.size() + " bytes of requested query replies for " + requestedCodes.length + " codes");
return out.toByteArray();
}
@@ -225,84 +225,96 @@ public class FTConfig {
// IND$FILE GET/PUT hostfile
cmd.append("IND$FILE ");
cmd.append(isReceive() ? "GET " : "PUT ");
cmd.append(hostFilename);
cmd.append(hostFilename != null ? hostFilename.trim() : "");
// CMS/CICS options use ( prefix
if (hostType != HostType.TSO) {
cmd.append(" (");
} else {
cmd.append(" ");
}
// Collect options into a separate buffer
StringBuilder opts = new StringBuilder();
// Mode
if (isAscii()) {
cmd.append("ASCII");
opts.append("ASCII");
} else if (hostType == HostType.CICS) {
cmd.append("BINARY");
opts.append("BINARY");
}
// CR/LF handling
if (isAscii() && isCrFlag()) {
cmd.append(" CRLF");
} else if (hostType == HostType.CICS) {
cmd.append(" NOCRLF");
if (opts.length() > 0) opts.append(" ");
opts.append("CRLF");
} else if (hostType == HostType.CICS && !isAscii()) {
if (opts.length() > 0) opts.append(" ");
opts.append("NOCRLF");
}
// Append (send only)
if (isAppend() && isSend()) {
cmd.append(" APPEND");
if (opts.length() > 0) opts.append(" ");
opts.append("APPEND");
}
// TSO-specific send options
// Host-specific send options
if (isSend()) {
if (hostType == HostType.TSO) {
if (recfm != RecordFormat.DEFAULT) {
cmd.append(" RECFM(");
if (opts.length() > 0) opts.append(" ");
opts.append("RECFM(");
switch (recfm) {
case FIXED: cmd.append("F"); break;
case VARIABLE: cmd.append("V"); break;
case UNDEFINED: cmd.append("U"); break;
case FIXED: opts.append("F"); break;
case VARIABLE: opts.append("V"); break;
case UNDEFINED: opts.append("U"); break;
default: break;
}
cmd.append(")");
opts.append(")");
if (lrecl > 0) {
cmd.append(" LRECL(").append(lrecl).append(")");
opts.append(" LRECL(").append(lrecl).append(")");
}
if (blksize > 0) {
cmd.append(" BLKSIZE(").append(blksize).append(")");
opts.append(" BLKSIZE(").append(blksize).append(")");
}
}
if (units != AllocationUnit.DEFAULT) {
cmd.append(" SPACE(").append(primarySpace);
if (opts.length() > 0) opts.append(" ");
opts.append("SPACE(").append(primarySpace);
if (secondarySpace > 0) {
cmd.append(",").append(secondarySpace);
opts.append(",").append(secondarySpace);
}
cmd.append(")");
opts.append(")");
switch (units) {
case TRACKS: cmd.append(" TRACKS"); break;
case CYLINDERS: cmd.append(" CYLINDERS"); break;
case AVBLOCK: cmd.append(" AVBLOCK(").append(avblock).append(")"); break;
case TRACKS: opts.append(" TRACKS"); break;
case CYLINDERS: opts.append(" CYLINDERS"); break;
case AVBLOCK: opts.append(" AVBLOCK(").append(avblock).append(")"); break;
default: break;
}
}
} else if (hostType == HostType.CMS) {
if (recfm != RecordFormat.DEFAULT) {
cmd.append(" RECFM ");
if (opts.length() > 0) opts.append(" ");
opts.append("RECFM ");
switch (recfm) {
case FIXED: cmd.append("F"); break;
case VARIABLE: cmd.append("V"); break;
case FIXED: opts.append("F"); break;
case VARIABLE: opts.append("V"); break;
default: break;
}
if (lrecl > 0) {
cmd.append(" LRECL ").append(lrecl);
opts.append(" LRECL ").append(lrecl);
}
}
}
}
// Additional options
if (otherOptions != null) {
cmd.append(" ").append(otherOptions);
if (otherOptions != null && !otherOptions.trim().isEmpty()) {
if (opts.length() > 0) opts.append(" ");
opts.append(otherOptions.trim());
}
// Apply options with host-specific prefix only if options are present
if (opts.length() > 0) {
if (hostType != HostType.TSO) {
cmd.append(" (").append(opts.toString());
} else {
cmd.append(" ").append(opts.toString());
}
}
return cmd.toString().trim();
@@ -21,7 +21,7 @@ public class FTCut {
/** Callback for transfer events */
public interface FTCutListener {
void onTransferRunning();
void onCutRunning();
void onTransferComplete(String errorMessage);
void onTransferAborted(String errorMessage);
void onBytesTransferred(long bytes);
@@ -221,7 +221,7 @@ public class FTCut {
xlateBufIx = 0;
cutEof = false;
cutAck();
listener.onTransferRunning();
listener.onCutRunning();
break;
case SC_XFER_COMPLETE:
@@ -23,7 +23,7 @@ public class FTDft {
/** Callback for transfer events (shared interface with CUT) */
public interface FTDftListener {
void onTransferRunning();
void onDftRunning();
void onTransferComplete(String errorMessage);
void onTransferAborted(String errorMessage);
void onBytesTransferred(long bytes);
@@ -178,7 +178,7 @@ public class FTDft {
log.info("DFT: Open request for message stream");
} else {
messageFlag = false;
listener.onTransferRunning();
listener.onDftRunning();
}
dftEof = false;
@@ -4,6 +4,7 @@ import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.screen.ExtendedAttribute;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.telnet.TelnetFSM;
import org.lib3270j.protocol.TelnetConstants;
import static org.lib3270j.protocol.DS3270Constants.*;
import java.io.ByteArrayOutputStream;
@@ -362,6 +363,82 @@ public class InputProcessor {
deleteChar();
}
/** Erase All Unprotected fields (Erase Input key in 3270). */
public void eraseInput() {
if (keyboardLocked) return;
screen.eraseAllUnprotected();
screen.markAllChanged();
}
/** Move cursor to first unprotected field on next line (Newline key in 3270). */
public void newline() {
if (keyboardLocked) return;
int cur = screen.getCursorAddress();
int cols = screen.getCols();
int rows = screen.getRows();
int curRow = cur / cols;
int nextRow = (curRow + 1) % rows;
int target = nextRow * cols;
if (screen.isFormatted()) {
target = screen.findNextUnprotected(target == 0 ? (rows * cols - 1) : (target - 1));
}
screen.setCursorAddress(target);
screen.markAllChanged();
}
/** Insert Duplicate (DUP) code and advance to next field. */
public void dup() {
if (keyboardLocked) return;
int baddr = screen.getCursorAddress();
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) return;
ExtendedAttribute ea = screen.getCell(baddr);
ea.ec = (byte) FCORDER_DUP;
ea.ucs4 = '*';
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
tab();
}
/** Insert Field Mark (FM) code. */
public void fieldMark() {
if (keyboardLocked) return;
int baddr = screen.getCursorAddress();
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) return;
ExtendedAttribute ea = screen.getCell(baddr);
ea.ec = (byte) FCORDER_FM;
ea.ucs4 = ';';
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
int size = screen.getRows() * screen.getCols();
baddr = (baddr + 1) % size;
while (screen.getCell(baddr).isFieldAttribute()) {
baddr = (baddr + 1) % size;
}
screen.setCursorAddress(baddr);
screen.markAllChanged();
}
/** Attention key (sends Telnet IP). */
public void attn() {
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
byte[] ip = new byte[] { (byte) TelnetConstants.IAC, (byte) TelnetConstants.IP };
fsm.sendRecord(ip);
}
}
/** SysReq key. */
public void sysReq() {
reset();
}
/** Reset (unlock keyboard, cancel insert mode). */
public void reset() {
setKeyboardLocked(false);
@@ -305,6 +305,7 @@ public class ScreenBuffer {
private char getAplGraphic(int ec) {
switch (ec) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
@@ -316,6 +317,24 @@ public class ScreenBuffer {
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼'
// Special math and APL symbols (matching x3270 cg.c / apl.c)
case 0x8C: return '\u2264'; // Less-than or equal '≤'
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠'
case 0xAD: return '['; // Left bracket
case 0xBD: return ']'; // Right bracket
case 0x8D: return '{'; // Left brace
case 0x9D: return '}'; // Right brace
case 0xB0: return '\u00B0'; // Degree '°'
case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬'
default: return translator.ebcdicToUnicode(ec);
}
}
@@ -37,13 +37,26 @@ public class TelnetConnection {
* Connect to the host. Blocks until connection is established or fails.
*/
public void connect() throws IOException {
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
socket = new Socket();
socket.setKeepAlive(true);
socket.setOOBInline(true);
socket.setTcpNoDelay(true);
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
if (config.isUseTls()) {
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort());
javax.net.ssl.SSLSocketFactory ssf = (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault();
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
sslSocket.setKeepAlive(true);
sslSocket.setOOBInline(true);
sslSocket.setTcpNoDelay(true);
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
sslSocket.startHandshake();
socket = sslSocket;
} else {
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
socket = new Socket();
socket.setKeepAlive(true);
socket.setOOBInline(true);
socket.setTcpNoDelay(true);
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
}
inputStream = new BufferedInputStream(socket.getInputStream(), READ_BUFFER_SIZE);
outputStream = new BufferedOutputStream(socket.getOutputStream());