Phase 2
This commit is contained in:
@@ -18,6 +18,13 @@ public class ConnectionConfig {
|
||||
private int nopIntervalSeconds = 0;
|
||||
private String terminalName = null; // override terminal type string
|
||||
private boolean tn3270eEnabled = true;
|
||||
private boolean tcpNoDelay = true;
|
||||
private boolean soKeepAlive = true;
|
||||
private int soTimeoutMs = 0;
|
||||
private java.util.List<String> luNames = new java.util.ArrayList<>();
|
||||
private boolean dynamicModel = false;
|
||||
private int dynamicRows = 24;
|
||||
private int dynamicCols = 80;
|
||||
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
|
||||
|
||||
public ConnectionConfig() {}
|
||||
@@ -90,6 +97,36 @@ public class ConnectionConfig {
|
||||
public String getTerminalName() { return terminalName; }
|
||||
public void setTerminalName(String name) { this.terminalName = name; }
|
||||
|
||||
public boolean isTcpNoDelay() { return tcpNoDelay; }
|
||||
public void setTcpNoDelay(boolean tcpNoDelay) { this.tcpNoDelay = tcpNoDelay; }
|
||||
|
||||
public boolean isSoKeepAlive() { return soKeepAlive; }
|
||||
public void setSoKeepAlive(boolean soKeepAlive) { this.soKeepAlive = soKeepAlive; }
|
||||
|
||||
public int getSoTimeoutMs() { return soTimeoutMs; }
|
||||
public void setSoTimeoutMs(int soTimeoutMs) { this.soTimeoutMs = soTimeoutMs; }
|
||||
|
||||
public java.util.List<String> getLuNames() { return luNames; }
|
||||
public void setLuNames(java.util.List<String> luNames) {
|
||||
this.luNames = (luNames != null) ? new java.util.ArrayList<>(luNames) : new java.util.ArrayList<>();
|
||||
}
|
||||
public void addLuName(String luName) {
|
||||
if (luName != null && !luName.trim().isEmpty()) {
|
||||
this.luNames.add(luName.trim());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDynamicModel() { return dynamicModel; }
|
||||
public void setDynamicModel(boolean dynamicModel) { this.dynamicModel = dynamicModel; }
|
||||
|
||||
public int getDynamicRows() { return dynamicRows; }
|
||||
public int getDynamicCols() { return dynamicCols; }
|
||||
public void setDynamicDimensions(int rows, int cols) {
|
||||
this.dynamicModel = true;
|
||||
this.dynamicRows = rows;
|
||||
this.dynamicCols = cols;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
|
||||
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), or standard "host:port" formats.
|
||||
@@ -166,6 +203,9 @@ public class ConnectionConfig {
|
||||
if (terminalName != null) {
|
||||
return terminalName;
|
||||
}
|
||||
if (dynamicModel) {
|
||||
return extendedDataStream ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC";
|
||||
}
|
||||
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,27 @@ public class Telnet3270Client {
|
||||
|
||||
/** Get the active SSLSession if connected over TLS, or null. */
|
||||
public javax.net.ssl.SSLSession getSslSession() {
|
||||
return connection.getSslSession();
|
||||
return connection != null ? connection.getSslSession() : null;
|
||||
}
|
||||
|
||||
/** Get the NVT processor for ASCII / ANSI terminal processing. */
|
||||
public haus.nightmare.lib3270j.nvt.NvtProcessor getNvtProcessor() {
|
||||
return fsm.getNvtProcessor();
|
||||
}
|
||||
|
||||
/** Get the Telnet state machine. */
|
||||
public TelnetFSM getTelnetFSM() {
|
||||
return fsm;
|
||||
}
|
||||
|
||||
/** Send an NVT ASCII character in NVT mode. */
|
||||
public void sendNVTChar(char c) throws IOException {
|
||||
fsm.sendNVTChar(c);
|
||||
}
|
||||
|
||||
/** Send an NVT ASCII string in NVT mode. */
|
||||
public void sendNVTString(String s) throws IOException {
|
||||
fsm.sendNVTString(s);
|
||||
}
|
||||
|
||||
// ========== Convenience input methods ==========
|
||||
|
||||
+261
-32
@@ -4,6 +4,7 @@ import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.protocol.DS3270Constants;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -638,10 +639,21 @@ public class DataStreamProcessor {
|
||||
case XA_CHARSET:
|
||||
ea.cs = (byte) value;
|
||||
break;
|
||||
case XA_VALIDATION:
|
||||
case XA_OUTLINING:
|
||||
case 0x84: // HoD / 3270 Outlining alternative ID
|
||||
ea.ol = (byte) value;
|
||||
break;
|
||||
case XA_VALIDATION:
|
||||
ea.vl = (byte) value;
|
||||
break;
|
||||
case XA_TRANSPARENCY:
|
||||
ea.tr = (byte) value;
|
||||
break;
|
||||
case XA_INPUT_CONTROL:
|
||||
// Acknowledged but not visually rendered yet
|
||||
ea.ic = (byte) value;
|
||||
break;
|
||||
case 0x91: // DBCS Asian attributes
|
||||
ea.db = (byte) value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -677,14 +689,79 @@ public class DataStreamProcessor {
|
||||
outputWrite(caddr[0] & 0xFF);
|
||||
outputWrite(caddr[1] & 0xFF);
|
||||
|
||||
// Buffer contents
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = screen.getCell(i);
|
||||
if (ea.isFieldAttribute()) {
|
||||
outputWrite(ORDER_SF);
|
||||
outputWrite(ea.fa & 0xFF);
|
||||
} else {
|
||||
outputWrite(ea.ec & 0xFF);
|
||||
byte mode = screen.getReplyMode();
|
||||
|
||||
// Buffer contents depending on Reply Mode
|
||||
if (mode == SF_SRM_XFIELD || mode == SF_SRM_CHAR) {
|
||||
// Extended Field or Character Mode
|
||||
byte curFg = 0, curBg = 0, curGr = 0, curCs = 0;
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = screen.getCell(i);
|
||||
if (ea.isFieldAttribute()) {
|
||||
// Count number of attribute pairs
|
||||
int count = 1; // 3270 FA is always present
|
||||
if (ea.fg != 0) count++;
|
||||
if (ea.bg != 0) count++;
|
||||
if (ea.gr != 0) count++;
|
||||
if (ea.cs != 0) count++;
|
||||
if (ea.ol != 0) count++;
|
||||
|
||||
outputWrite(ORDER_SFE);
|
||||
outputWrite(count);
|
||||
outputWrite(XA_3270);
|
||||
outputWrite(ea.fa & 0xFF);
|
||||
if (ea.fg != 0) { outputWrite(XA_FOREGROUND); outputWrite(ea.fg & 0xFF); }
|
||||
if (ea.bg != 0) { outputWrite(XA_BACKGROUND); outputWrite(ea.bg & 0xFF); }
|
||||
if (ea.gr != 0) {
|
||||
outputWrite(XA_HIGHLIGHTING);
|
||||
int xah = XAH_NORMAL;
|
||||
if ((ea.gr & GR_BLINK) != 0) xah = XAH_BLINK;
|
||||
else if ((ea.gr & GR_REVERSE) != 0) xah = XAH_REVERSE;
|
||||
else if ((ea.gr & GR_UNDERLINE) != 0) xah = XAH_UNDERSCORE;
|
||||
else if ((ea.gr & GR_INTENSIFY) != 0) xah = XAH_INTENSIFY;
|
||||
outputWrite(xah);
|
||||
}
|
||||
if (ea.cs != 0) { outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF); }
|
||||
if (ea.ol != 0) { outputWrite(XA_OUTLINING); outputWrite(ea.ol & 0xFF); }
|
||||
} else {
|
||||
if (mode == SF_SRM_CHAR) {
|
||||
// In character mode, output SA if character attributes differ
|
||||
if (ea.fg != curFg) {
|
||||
outputWrite(ORDER_SA); outputWrite(XA_FOREGROUND); outputWrite(ea.fg & 0xFF);
|
||||
curFg = ea.fg;
|
||||
}
|
||||
if (ea.bg != curBg) {
|
||||
outputWrite(ORDER_SA); outputWrite(XA_BACKGROUND); outputWrite(ea.bg & 0xFF);
|
||||
curBg = ea.bg;
|
||||
}
|
||||
if (ea.gr != curGr) {
|
||||
outputWrite(ORDER_SA); outputWrite(XA_HIGHLIGHTING);
|
||||
int xah = XAH_NORMAL;
|
||||
if ((ea.gr & GR_BLINK) != 0) xah = XAH_BLINK;
|
||||
else if ((ea.gr & GR_REVERSE) != 0) xah = XAH_REVERSE;
|
||||
else if ((ea.gr & GR_UNDERLINE) != 0) xah = XAH_UNDERSCORE;
|
||||
else if ((ea.gr & GR_INTENSIFY) != 0) xah = XAH_INTENSIFY;
|
||||
outputWrite(xah);
|
||||
curGr = ea.gr;
|
||||
}
|
||||
if (ea.cs != curCs) {
|
||||
outputWrite(ORDER_SA); outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF);
|
||||
curCs = ea.cs;
|
||||
}
|
||||
}
|
||||
outputWrite(ea.ec & 0xFF);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Standard Field Mode (SF_SRM_FIELD)
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = screen.getCell(i);
|
||||
if (ea.isFieldAttribute()) {
|
||||
outputWrite(ORDER_SF);
|
||||
outputWrite(ea.fa & 0xFF);
|
||||
} else {
|
||||
outputWrite(ea.ec & 0xFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,7 +803,7 @@ public class DataStreamProcessor {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Formatted screen: send modified fields with null suppression per 3270 spec
|
||||
// Formatted screen: send fields with null suppression per 3270 spec
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = screen.getCell(i);
|
||||
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
|
||||
@@ -779,33 +856,23 @@ public class DataStreamProcessor {
|
||||
case SF_READ_PART:
|
||||
processSFReadPartition(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_ERASE_RESET: {
|
||||
boolean alt = (fieldLen >= 4) && ((data[pos + 3] & 0xFF) == SF_ER_ALT);
|
||||
screen.erase(alt);
|
||||
graphicsPlane.clear();
|
||||
gocaDecoder.resetDefaults();
|
||||
notifyScreenSizeChanged();
|
||||
case SF_ERASE_RESET:
|
||||
processEraseReset(data, pos, fieldLen);
|
||||
break;
|
||||
}
|
||||
case SF_SET_REPLY_MODE:
|
||||
if (fieldLen >= 5) {
|
||||
screen.setReplyMode((byte) (data[pos + 4] & 0xFF));
|
||||
}
|
||||
processSetReplyMode(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_CREATE_PART:
|
||||
if (fieldLen >= 4) {
|
||||
int pid = data[pos + 3] & 0xFF;
|
||||
screen.setActivePartition(pid);
|
||||
log.fine("Created active partition ID=" + pid);
|
||||
}
|
||||
graphicsPlane.clear();
|
||||
gocaDecoder.resetDefaults();
|
||||
processCreatePartition(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_DESTROY_PART:
|
||||
processDestroyPartition(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_ACTIVATE_PART:
|
||||
processActivatePartition(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_OUTBOUND_DS:
|
||||
if (fieldLen > 5) {
|
||||
// Outbound DS contains another 3270 command
|
||||
processRecord(data, pos + 4, fieldLen - 4, false);
|
||||
}
|
||||
processOutbound3270DS(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_TRANSFER_DATA:
|
||||
if (ftDft != null) {
|
||||
@@ -1046,4 +1113,166 @@ public class DataStreamProcessor {
|
||||
l.onScreenUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Structured Field Handlers & Utilities (Phase 2) ==========
|
||||
|
||||
public void processSetReplyMode(byte[] data, int offset, int length) {
|
||||
if (length >= 5) {
|
||||
byte mode = (byte) (data[offset + 4] & 0xFF);
|
||||
screen.setReplyMode(mode);
|
||||
log.fine("Set reply mode: " + mode);
|
||||
}
|
||||
}
|
||||
|
||||
public void processCreatePartition(byte[] data, int offset, int length) {
|
||||
if (length >= 4) {
|
||||
int pid = data[offset + 3] & 0xFF;
|
||||
int pRows = screen.getRows();
|
||||
int pCols = screen.getCols();
|
||||
if (length >= 8) {
|
||||
pCols = ((data[offset + 4] & 0xFF) << 8) | (data[offset + 5] & 0xFF);
|
||||
pRows = ((data[offset + 6] & 0xFF) << 8) | (data[offset + 7] & 0xFF);
|
||||
if (pRows <= 0) pRows = screen.getRows();
|
||||
if (pCols <= 0) pCols = screen.getCols();
|
||||
}
|
||||
screen.createPartition(pid, pRows, pCols);
|
||||
log.fine("Created partition pid=" + pid + " (" + pRows + "x" + pCols + ")");
|
||||
}
|
||||
graphicsPlane.clear();
|
||||
gocaDecoder.resetDefaults();
|
||||
}
|
||||
|
||||
public void processDestroyPartition(byte[] data, int offset, int length) {
|
||||
if (length >= 4) {
|
||||
int pid = data[offset + 3] & 0xFF;
|
||||
screen.destroyPartition(pid);
|
||||
log.fine("Destroyed partition pid=" + pid);
|
||||
}
|
||||
}
|
||||
|
||||
public void processActivatePartition(byte[] data, int offset, int length) {
|
||||
if (length >= 4) {
|
||||
int pid = data[offset + 3] & 0xFF;
|
||||
screen.activatePartition(pid);
|
||||
log.fine("Activated partition pid=" + pid);
|
||||
}
|
||||
}
|
||||
|
||||
public void processEraseReset(byte[] data, int offset, int length) {
|
||||
boolean alt = (length >= 4) && ((data[offset + 3] & 0xFF) == SF_ER_ALT);
|
||||
screen.eraseReset(alt);
|
||||
graphicsPlane.clear();
|
||||
gocaDecoder.resetDefaults();
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
|
||||
public void processSCSData(byte[] data, int offset, int length) {
|
||||
log.info("Received embedded SCS printer data (" + length + " bytes)");
|
||||
}
|
||||
|
||||
public void setScreenToBindSize(int primaryRows, int primaryCols, int altRows, int altCols, int bindFlags) {
|
||||
screen.setScreenToBindSize(primaryRows, primaryCols, altRows, altCols, bindFlags);
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
|
||||
public void setScrSizetoDefault(boolean isDefault) {
|
||||
screen.setScrSizetoDefault(isDefault);
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
|
||||
public static byte[] doubleFF(byte[] data, int length) {
|
||||
int count = countFF(data, length);
|
||||
if (count == 0 && data.length == length) {
|
||||
return data;
|
||||
}
|
||||
byte[] result = new byte[length + count];
|
||||
int j = 0;
|
||||
for (int i = 0; i < length; i++) {
|
||||
byte b = data[i];
|
||||
result[j++] = b;
|
||||
if ((b & 0xFF) == 0xFF) {
|
||||
result[j++] = (byte) 0xFF;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int countFF(byte[] data, int length) {
|
||||
int count = 0;
|
||||
int len = Math.min(length, data.length);
|
||||
for (int i = 0; i < len; i++) {
|
||||
if ((data[i] & 0xFF) == 0xFF) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public static byte[] sizeuparray(byte[] data, int count) {
|
||||
byte[] newArr = new byte[data.length + count];
|
||||
System.arraycopy(data, 0, newArr, 0, Math.min(data.length, newArr.length));
|
||||
return newArr;
|
||||
}
|
||||
|
||||
public static int cmd2ebc(int commandCode) {
|
||||
return DS3270Constants.cmd2ebc(commandCode);
|
||||
}
|
||||
|
||||
public static int cmd2ebc(short commandCode) {
|
||||
return DS3270Constants.cmd2ebc(commandCode & 0xFFFF);
|
||||
}
|
||||
|
||||
public static int x2bin(int ebcByte) {
|
||||
return DS3270Constants.x2bin(ebcByte);
|
||||
}
|
||||
|
||||
public static int x2bin(short ebcByte) {
|
||||
return DS3270Constants.x2bin(ebcByte & 0xFFFF);
|
||||
}
|
||||
|
||||
public boolean validateStructuredFieldHeader(byte[] data, int offset, int length) {
|
||||
if (length < 3) return false;
|
||||
int fieldLen = ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF);
|
||||
return fieldLen >= 3 && fieldLen <= length;
|
||||
}
|
||||
|
||||
public int decodeAddressingMode(int flags) {
|
||||
return (flags & 0x01) != 0 ? 14 : 12;
|
||||
}
|
||||
|
||||
public boolean validatePartitionId(int pid) {
|
||||
return pid >= 0 && pid <= 255;
|
||||
}
|
||||
|
||||
public void processOutbound3270DS(byte[] data, int offset, int fieldLen) {
|
||||
if (fieldLen > 5) {
|
||||
int pid = data[offset + 3] & 0xFF;
|
||||
screen.setActivePartition(pid);
|
||||
processRecord(data, offset + 4, fieldLen - 4, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void processQueryListOrder(byte[] data, int offset, int length) {
|
||||
processSFReadPartition(data, offset, length);
|
||||
}
|
||||
|
||||
public void processImplicitPartition(byte[] data, int offset, int length) {
|
||||
screen.setActivePartition(0);
|
||||
}
|
||||
|
||||
public void processModifyPartition(byte[] data, int offset, int length) {
|
||||
if (length >= 4) {
|
||||
int pid = data[offset + 3] & 0xFF;
|
||||
log.fine("Modify partition pid=" + pid);
|
||||
}
|
||||
}
|
||||
|
||||
public void processResetPartition(byte[] data, int offset, int length) {
|
||||
screen.setActivePartition(0);
|
||||
screen.erase(false);
|
||||
}
|
||||
|
||||
public void processNullStructuredField() {
|
||||
log.fine("Processed null structured field");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
package haus.nightmare.lib3270j.nvt;
|
||||
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Network Virtual Terminal (NVT) processor.
|
||||
* Handles ASCII / ANSI VT100 character stream processing, cursor positioning,
|
||||
* escape sequence decoding, and NVT character/string transmission.
|
||||
*/
|
||||
public class NvtProcessor {
|
||||
|
||||
private static final Logger log = Logger.getLogger(NvtProcessor.class.getName());
|
||||
|
||||
private final ScreenBuffer screenBuffer;
|
||||
private final EbcdicTranslator translator;
|
||||
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
// Escape sequence parser states
|
||||
private static final int STATE_NORMAL = 0;
|
||||
private static final int STATE_ESC = 1;
|
||||
private static final int STATE_CSI = 2;
|
||||
|
||||
private int parseState = STATE_NORMAL;
|
||||
private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream();
|
||||
|
||||
// Output sender callback
|
||||
private OutputSender outputSender;
|
||||
|
||||
// Graphic Rendition state
|
||||
private byte currentFg = 0;
|
||||
private byte currentBg = 0;
|
||||
private byte currentGr = 0;
|
||||
|
||||
// Saved cursor position
|
||||
private int savedCursorRow = 0;
|
||||
private int savedCursorCol = 0;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface OutputSender {
|
||||
void sendRaw(byte[] data) throws IOException;
|
||||
}
|
||||
|
||||
public NvtProcessor(ScreenBuffer screenBuffer, EbcdicTranslator translator) {
|
||||
this.screenBuffer = screenBuffer;
|
||||
this.translator = translator;
|
||||
}
|
||||
|
||||
public void setOutputSender(OutputSender outputSender) {
|
||||
this.outputSender = outputSender;
|
||||
}
|
||||
|
||||
public void addScreenUpdateListener(ScreenUpdateListener l) {
|
||||
screenListeners.add(l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process incoming ASCII NVT data bytes.
|
||||
*/
|
||||
public synchronized void processNVTData(byte[] data, int offset, int length) {
|
||||
if (length <= 0) return;
|
||||
|
||||
int rows = screenBuffer.getRows();
|
||||
int cols = screenBuffer.getCols();
|
||||
int size = rows * cols;
|
||||
int curAddr = screenBuffer.getCursorAddress();
|
||||
|
||||
for (int i = offset; i < offset + length; i++) {
|
||||
int b = data[i] & 0xFF;
|
||||
|
||||
if (parseState == STATE_NORMAL) {
|
||||
if (b == 0x1B) { // ESC
|
||||
parseState = STATE_ESC;
|
||||
escBuffer.reset();
|
||||
escBuffer.write(b);
|
||||
} else if (b == 0x0D) { // CR
|
||||
int r = curAddr / cols;
|
||||
curAddr = r * cols;
|
||||
} else if (b == 0x0A) { // LF
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
r++;
|
||||
if (r >= rows) {
|
||||
scrollUp();
|
||||
r = rows - 1;
|
||||
}
|
||||
curAddr = r * cols + c;
|
||||
} else if (b == 0x08 || b == 0x7F) { // BS or DEL
|
||||
int c = curAddr % cols;
|
||||
if (c > 0) {
|
||||
curAddr--;
|
||||
}
|
||||
} else if (b == 0x09) { // TAB
|
||||
int c = curAddr % cols;
|
||||
int nextTab = ((c / 8) + 1) * 8;
|
||||
if (nextTab >= cols) nextTab = cols - 1;
|
||||
curAddr = (curAddr / cols) * cols + nextTab;
|
||||
} else if (b == 0x0C) { // FF
|
||||
screenBuffer.clear();
|
||||
curAddr = 0;
|
||||
} else if (b >= 0x20 && b < 0xFF) { // Printable ASCII
|
||||
char ch = (char) b;
|
||||
int ebc = translator.unicodeToEbcdic(ch);
|
||||
ExtendedAttribute cell = screenBuffer.getCell(curAddr);
|
||||
cell.clear();
|
||||
cell.ec = (byte) (ebc >= 0 ? ebc : 0x40);
|
||||
cell.ucs4 = ch;
|
||||
cell.fg = currentFg;
|
||||
cell.bg = currentBg;
|
||||
cell.gr = currentGr;
|
||||
|
||||
curAddr++;
|
||||
if (curAddr >= size) {
|
||||
scrollUp();
|
||||
curAddr = (rows - 1) * cols;
|
||||
}
|
||||
}
|
||||
} else if (parseState == STATE_ESC) {
|
||||
escBuffer.write(b);
|
||||
if (b == '[') {
|
||||
parseState = STATE_CSI;
|
||||
} else if (b == '7') { // Save cursor
|
||||
savedCursorRow = curAddr / cols;
|
||||
savedCursorCol = curAddr % cols;
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == '8') { // Restore cursor
|
||||
curAddr = Math.min(rows - 1, savedCursorRow) * cols + Math.min(cols - 1, savedCursorCol);
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == 'c') { // RIS - Reset to Initial State
|
||||
screenBuffer.clear();
|
||||
curAddr = 0;
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
parseState = STATE_NORMAL;
|
||||
} else {
|
||||
// Unknown 2-byte escape, finish
|
||||
parseState = STATE_NORMAL;
|
||||
}
|
||||
} else if (parseState == STATE_CSI) {
|
||||
escBuffer.write(b);
|
||||
// CSI parameter/intermediate bytes: 0x20..0x3F, final bytes: 0x40..0x7E
|
||||
if (b >= 0x40 && b <= 0x7E) {
|
||||
byte[] seq = escBuffer.toByteArray();
|
||||
curAddr = processAnsiEscapeSequence(seq, curAddr, rows, cols);
|
||||
parseState = STATE_NORMAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenBuffer.setCursorAddress(curAddr);
|
||||
screenBuffer.markAllChanged();
|
||||
screenBuffer.updateDisplaySnapshot();
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and execute an ANSI CSI escape sequence.
|
||||
* Returns updated cursor address.
|
||||
*/
|
||||
public int processAnsiEscapeSequence(byte[] seq, int curAddr, int rows, int cols) {
|
||||
if (seq.length < 3) return curAddr;
|
||||
int finalByte = seq[seq.length - 1] & 0xFF;
|
||||
String paramStr = new String(seq, 2, seq.length - 3, StandardCharsets.US_ASCII);
|
||||
String[] params = paramStr.split(";");
|
||||
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
|
||||
switch (finalByte) {
|
||||
case 'H': // CUP - Cursor Position
|
||||
case 'f': // HVP - Horizontal and Vertical Position
|
||||
{
|
||||
int p1 = parseParam(params, 0, 1) - 1;
|
||||
int p2 = parseParam(params, 1, 1) - 1;
|
||||
r = Math.max(0, Math.min(rows - 1, p1));
|
||||
c = Math.max(0, Math.min(cols - 1, p2));
|
||||
return r * cols + c;
|
||||
}
|
||||
case 'A': // CUU - Cursor Up
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
r = Math.max(0, r - count);
|
||||
return r * cols + c;
|
||||
}
|
||||
case 'B': // CUD - Cursor Down
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
r = Math.min(rows - 1, r + count);
|
||||
return r * cols + c;
|
||||
}
|
||||
case 'C': // CUF - Cursor Forward
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
c = Math.min(cols - 1, c + count);
|
||||
return r * cols + c;
|
||||
}
|
||||
case 'D': // CUB - Cursor Back
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
c = Math.max(0, c - count);
|
||||
return r * cols + c;
|
||||
}
|
||||
case 'J': // ED - Erase in Display
|
||||
{
|
||||
int mode = parseParam(params, 0, 0);
|
||||
if (mode == 0) { // Cursor to end
|
||||
for (int i = curAddr; i < rows * cols; i++) clearCell(i);
|
||||
} else if (mode == 1) { // Beginning to cursor
|
||||
for (int i = 0; i <= curAddr; i++) clearCell(i);
|
||||
} else if (mode == 2 || mode == 3) { // Entire screen
|
||||
screenBuffer.clear();
|
||||
return 0;
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'K': // EL - Erase in Line
|
||||
{
|
||||
int mode = parseParam(params, 0, 0);
|
||||
int lineStart = r * cols;
|
||||
int lineEnd = lineStart + cols;
|
||||
if (mode == 0) { // Cursor to end of line
|
||||
for (int i = curAddr; i < lineEnd; i++) clearCell(i);
|
||||
} else if (mode == 1) { // Start of line to cursor
|
||||
for (int i = lineStart; i <= curAddr; i++) clearCell(i);
|
||||
} else if (mode == 2) { // Entire line
|
||||
for (int i = lineStart; i < lineEnd; i++) clearCell(i);
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'm': // SGR - Select Graphic Rendition
|
||||
{
|
||||
if (params.length == 0 || (params.length == 1 && params[0].isEmpty())) {
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
} else {
|
||||
for (String p : params) {
|
||||
if (p.isEmpty()) continue;
|
||||
try {
|
||||
int code = Integer.parseInt(p);
|
||||
applySgr(code);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 's': // Save cursor
|
||||
savedCursorRow = r;
|
||||
savedCursorCol = c;
|
||||
return curAddr;
|
||||
case 'u': // Restore cursor
|
||||
r = Math.min(rows - 1, savedCursorRow);
|
||||
c = Math.min(cols - 1, savedCursorCol);
|
||||
return r * cols + c;
|
||||
default:
|
||||
return curAddr;
|
||||
}
|
||||
}
|
||||
|
||||
private int parseParam(String[] params, int idx, int defaultVal) {
|
||||
if (params != null && idx < params.length && !params[idx].trim().isEmpty()) {
|
||||
try {
|
||||
return Integer.parseInt(params[idx].trim());
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
private void clearCell(int addr) {
|
||||
ExtendedAttribute cell = screenBuffer.getCell(addr);
|
||||
cell.clear();
|
||||
cell.ec = 0;
|
||||
cell.ucs4 = ' ';
|
||||
}
|
||||
|
||||
private void scrollUp() {
|
||||
int rows = screenBuffer.getRows();
|
||||
int cols = screenBuffer.getCols();
|
||||
for (int r = 0; r < rows - 1; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int dst = r * cols + c;
|
||||
int src = (r + 1) * cols + c;
|
||||
screenBuffer.getCell(dst).copyFrom(screenBuffer.getCell(src));
|
||||
}
|
||||
}
|
||||
// Clear last line
|
||||
int lastRowStart = (rows - 1) * cols;
|
||||
for (int c = 0; c < cols; c++) {
|
||||
clearCell(lastRowStart + c);
|
||||
}
|
||||
}
|
||||
|
||||
private void applySgr(int code) {
|
||||
switch (code) {
|
||||
case 0: // Reset
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
break;
|
||||
case 1: // Bold / Bright
|
||||
currentGr |= GR_INTENSIFY;
|
||||
break;
|
||||
case 4: // Underline
|
||||
currentGr |= GR_UNDERLINE;
|
||||
break;
|
||||
case 5: // Blink
|
||||
currentGr |= GR_BLINK;
|
||||
break;
|
||||
case 7: // Reverse
|
||||
currentGr |= GR_REVERSE;
|
||||
break;
|
||||
case 22: // Normal intensity
|
||||
currentGr &= ~GR_INTENSIFY;
|
||||
break;
|
||||
case 24: // Not underlined
|
||||
currentGr &= ~GR_UNDERLINE;
|
||||
break;
|
||||
case 25: // Not blinking
|
||||
currentGr &= ~GR_BLINK;
|
||||
break;
|
||||
case 27: // Positive image (not reverse)
|
||||
currentGr &= ~GR_REVERSE;
|
||||
break;
|
||||
case 30: currentFg = (byte) HOST_COLOR_NEUTRAL_BLACK; break;
|
||||
case 31: currentFg = (byte) HOST_COLOR_RED; break;
|
||||
case 32: currentFg = (byte) HOST_COLOR_GREEN; break;
|
||||
case 33: currentFg = (byte) HOST_COLOR_YELLOW; break;
|
||||
case 34: currentFg = (byte) HOST_COLOR_BLUE; break;
|
||||
case 35: currentFg = (byte) HOST_COLOR_PINK; break;
|
||||
case 36: currentFg = (byte) HOST_COLOR_TURQUOISE; break;
|
||||
case 37: currentFg = (byte) HOST_COLOR_NEUTRAL_WHITE; break;
|
||||
case 39: currentFg = 0; break;
|
||||
case 40: currentBg = (byte) HOST_COLOR_NEUTRAL_BLACK; break;
|
||||
case 41: currentBg = (byte) HOST_COLOR_RED; break;
|
||||
case 42: currentBg = (byte) HOST_COLOR_GREEN; break;
|
||||
case 43: currentBg = (byte) HOST_COLOR_YELLOW; break;
|
||||
case 44: currentBg = (byte) HOST_COLOR_BLUE; break;
|
||||
case 45: currentBg = (byte) HOST_COLOR_PINK; break;
|
||||
case 46: currentBg = (byte) HOST_COLOR_TURQUOISE; break;
|
||||
case 47: currentBg = (byte) HOST_COLOR_NEUTRAL_WHITE; break;
|
||||
case 49: currentBg = 0; break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a single character in NVT mode.
|
||||
*/
|
||||
public void sendNVTChar(char c) throws IOException {
|
||||
if (outputSender != null) {
|
||||
if (c == '\n') {
|
||||
outputSender.sendRaw(new byte[] { (byte) '\r', (byte) '\n' });
|
||||
} else {
|
||||
outputSender.sendRaw(new byte[] { (byte) c });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a string in NVT mode with standard CRLF normalization.
|
||||
*/
|
||||
public void sendNVTString(String s) throws IOException {
|
||||
if (s == null || outputSender == null) return;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if (c == '\n') {
|
||||
out.write('\r');
|
||||
out.write('\n');
|
||||
} else if (c == '\r') {
|
||||
if (i + 1 < s.length() && s.charAt(i + 1) == '\n') {
|
||||
// Handled on next iteration
|
||||
} else {
|
||||
out.write('\r');
|
||||
out.write('\n');
|
||||
}
|
||||
} else {
|
||||
out.write((byte) c);
|
||||
}
|
||||
}
|
||||
outputSender.sendRaw(out.toByteArray());
|
||||
}
|
||||
|
||||
private void notifyScreenUpdated() {
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onScreenUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,6 +195,9 @@ public final class DS3270Constants {
|
||||
public static final int SF_SRM_XFIELD = 0x01;
|
||||
public static final int SF_SRM_CHAR = 0x02;
|
||||
public static final int SF_CREATE_PART = 0x0c;
|
||||
public static final int SF_DESTROY_PART = 0x0d;
|
||||
public static final int SF_ACTIVATE_PART = 0x0e;
|
||||
public static final int SF_MODIFY_PART = 0x0f;
|
||||
public static final int SF_OUTBOUND_DS = 0x40;
|
||||
public static final int SF_TRANSFER_DATA = 0xd0;
|
||||
|
||||
@@ -367,19 +370,57 @@ public final class DS3270Constants {
|
||||
}
|
||||
}
|
||||
|
||||
/** Get human-readable command name. */
|
||||
private static final byte[] X2BIN_TABLE = new byte[256];
|
||||
static {
|
||||
java.util.Arrays.fill(X2BIN_TABLE, (byte) -1);
|
||||
for (int i = 0; i < CODE_TABLE.length; i++) {
|
||||
X2BIN_TABLE[CODE_TABLE[i] & 0xFF] = (byte) i;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast lookup for 6-bit 3270 buffer address decoding.
|
||||
* Maps an EBCDIC buffer address character byte to its 6-bit binary value (0..63).
|
||||
*/
|
||||
public static int x2bin(int ebcByte) {
|
||||
int idx = ebcByte & 0xFF;
|
||||
byte val = X2BIN_TABLE[idx];
|
||||
return val >= 0 ? (val & 0x3F) : (idx & 0x3F);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate 3270 command codes to EBCDIC byte values.
|
||||
*/
|
||||
public static int cmd2ebc(int commandCode) {
|
||||
switch (commandCode) {
|
||||
case CMD_W: case SNA_CMD_W: return SNA_CMD_W; // 0xF1
|
||||
case CMD_RB: case SNA_CMD_RB: return SNA_CMD_RB; // 0xF2
|
||||
case CMD_WSF: case SNA_CMD_WSF: return SNA_CMD_WSF; // 0xF3
|
||||
case CMD_EW: case SNA_CMD_EW: return SNA_CMD_EW; // 0xF5
|
||||
case CMD_RM: case SNA_CMD_RM: return SNA_CMD_RM; // 0xF6
|
||||
case CMD_RMA: case SNA_CMD_RMA: return SNA_CMD_RMA; // 0x6E
|
||||
case CMD_EAU: case SNA_CMD_EAU: return SNA_CMD_EAU; // 0x6F
|
||||
case CMD_EWA: case SNA_CMD_EWA: return SNA_CMD_EWA; // 0x7E
|
||||
case CMD_NOP: return CMD_NOP; // 0x03
|
||||
default: return commandCode & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get descriptive name of a 3270 command code.
|
||||
*/
|
||||
public static String commandName(int cmd) {
|
||||
switch (cmd) {
|
||||
case CMD_W: case SNA_CMD_W: return "Write";
|
||||
case CMD_EW: case SNA_CMD_EW: return "EraseWrite";
|
||||
case CMD_EWA: case SNA_CMD_EWA: return "EraseWriteAlternate";
|
||||
case CMD_RB: case SNA_CMD_RB: return "ReadBuffer";
|
||||
case CMD_RM: case SNA_CMD_RM: return "ReadModified";
|
||||
case CMD_RMA: case SNA_CMD_RMA: return "ReadModifiedAll";
|
||||
case CMD_EAU: case SNA_CMD_EAU: return "EraseAllUnprotected";
|
||||
case CMD_WSF: case SNA_CMD_WSF: return "WriteStructuredField";
|
||||
case CMD_NOP: return "NoOp";
|
||||
default: return String.format("Unknown(0x%02x)", cmd);
|
||||
case CMD_W: case SNA_CMD_W: return "Write";
|
||||
case CMD_RB: case SNA_CMD_RB: return "Read Buffer";
|
||||
case CMD_WSF: case SNA_CMD_WSF: return "Write Structured Field";
|
||||
case CMD_EW: case SNA_CMD_EW: return "Erase/Write";
|
||||
case CMD_RM: case SNA_CMD_RM: return "Read Modified";
|
||||
case CMD_RMA: case SNA_CMD_RMA: return "Read Modified All";
|
||||
case CMD_EAU: case SNA_CMD_EAU: return "Erase All Unprotected";
|
||||
case CMD_EWA: case SNA_CMD_EWA: return "Erase/Write Alternate";
|
||||
case CMD_NOP: return "NOP";
|
||||
default: return "0x" + Integer.toHexString(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,25 @@ public final class TN3270EConstants {
|
||||
public static final int UNBIND_CLEANUP = 0x0f;
|
||||
public static final int UNBIND_BAD_SENSE = 0xfe;
|
||||
|
||||
// SNA Data Flow Control (DFC) Commands
|
||||
public static final int DFC_CLEAR = 0x01;
|
||||
public static final int DFC_CANCEL = 0x02;
|
||||
public static final int DFC_RQR = 0x03;
|
||||
public static final int DFC_STSN = 0x04;
|
||||
public static final int DFC_SIGNAL = 0x05;
|
||||
public static final int DFC_LUSTAT = 0x06;
|
||||
public static final int DFC_BID = 0x07;
|
||||
public static final int DFC_SHUTC = 0x08;
|
||||
public static final int DFC_SHUTD = 0x09;
|
||||
|
||||
// IBM Host On-Demand Status Codes
|
||||
public static final int STATUS_CONNECTING = 650;
|
||||
public static final int STATUS_NEGOTIATING = 651;
|
||||
public static final int STATUS_CONNECTED = 652;
|
||||
public static final int STATUS_SECURITY = 654;
|
||||
public static final int STATUS_BIND_ERROR = 655;
|
||||
public static final int STATUS_DISCONNECTED = 656;
|
||||
|
||||
// Name lookups for tracing
|
||||
|
||||
private static final String[] REASON_NAMES = {
|
||||
@@ -128,6 +147,38 @@ public final class TN3270EConstants {
|
||||
return code >= 0 && code < HRSP_FLAG_NAMES.length ? HRSP_FLAG_NAMES[code] : "??";
|
||||
}
|
||||
|
||||
public static String unbindReasonName(int code) {
|
||||
switch (code) {
|
||||
case UNBIND_NORMAL: return "NORMAL";
|
||||
case UNBIND_BIND_FORTHCOMING: return "BIND-FORTHCOMING";
|
||||
case UNBIND_VR_INOPERATIVE: return "VR-INOPERATIVE";
|
||||
case UNBIND_RX_INOPERATIVE: return "RX-INOPERATIVE";
|
||||
case UNBIND_HRESET: return "HRESET";
|
||||
case UNBIND_SSCP_GONE: return "SSCP-GONE";
|
||||
case UNBIND_VR_DEACTIVATED: return "VR-DEACTIVATED";
|
||||
case UNBIND_LU_FAILURE_PERM: return "LU-FAILURE-PERM";
|
||||
case UNBIND_LU_FAILURE_TEMP: return "LU-FAILURE-TEMP";
|
||||
case UNBIND_CLEANUP: return "CLEANUP";
|
||||
case UNBIND_BAD_SENSE: return "BAD-SENSE";
|
||||
default: return "UNKNOWN-UNBIND(0x" + Integer.toHexString(code) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
public static String dfcCommandName(int code) {
|
||||
switch (code) {
|
||||
case DFC_CLEAR: return "CLEAR";
|
||||
case DFC_CANCEL: return "CANCEL";
|
||||
case DFC_RQR: return "RQR";
|
||||
case DFC_STSN: return "STSN";
|
||||
case DFC_SIGNAL: return "SIGNAL";
|
||||
case DFC_LUSTAT: return "LUSTAT";
|
||||
case DFC_BID: return "BID";
|
||||
case DFC_SHUTC: return "SHUTC";
|
||||
case DFC_SHUTD: return "SHUTD";
|
||||
default: return "UNKNOWN-DFC(0x" + Integer.toHexString(code) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/** Format a function set as a human-readable string. */
|
||||
public static String functionNames(boolean[] funcs) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -33,6 +33,15 @@ public class ExtendedAttribute {
|
||||
/** DBCS state. */
|
||||
public byte db;
|
||||
|
||||
/** Outlining attribute (box borders/grid lines). */
|
||||
public byte ol;
|
||||
|
||||
/** Validation attribute. */
|
||||
public byte vl;
|
||||
|
||||
/** Transparency attribute. */
|
||||
public byte tr;
|
||||
|
||||
/**
|
||||
* Unicode character for display (set by translation from ec, or directly in NVT mode).
|
||||
*/
|
||||
@@ -48,6 +57,9 @@ public class ExtendedAttribute {
|
||||
cs = 0;
|
||||
ic = 0;
|
||||
db = 0;
|
||||
ol = 0;
|
||||
vl = 0;
|
||||
tr = 0;
|
||||
ucs4 = 0;
|
||||
}
|
||||
|
||||
@@ -61,6 +73,9 @@ public class ExtendedAttribute {
|
||||
this.cs = other.cs;
|
||||
this.ic = other.ic;
|
||||
this.db = other.db;
|
||||
this.ol = other.ol;
|
||||
this.vl = other.vl;
|
||||
this.tr = other.tr;
|
||||
this.ucs4 = other.ucs4;
|
||||
}
|
||||
|
||||
|
||||
@@ -156,6 +156,69 @@ public class ScreenBuffer {
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure screen dimensions matching SNA BIND parameters.
|
||||
*/
|
||||
public synchronized void setScreenToBindSize(int primaryRows, int primaryCols, int altRows, int altCols, int bindFlags) {
|
||||
if (primaryRows > 0) this.defRows = primaryRows;
|
||||
if (primaryCols > 0) this.defCols = primaryCols;
|
||||
if (altRows > 0) this.altRows = altRows;
|
||||
if (altCols > 0) this.altCols = altCols;
|
||||
|
||||
this.maxRows = Math.max(defRows, this.altRows);
|
||||
this.maxCols = Math.max(defCols, this.altCols);
|
||||
allocateBuffers();
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch between default (primary) and alternate screen sizes.
|
||||
*/
|
||||
public synchronized void setScrSizetoDefault(boolean isDefault) {
|
||||
erase(!isDefault);
|
||||
}
|
||||
|
||||
// ========== Partitions ==========
|
||||
private final java.util.Map<Integer, PartitionInfo> partitions = new java.util.HashMap<>();
|
||||
|
||||
public static class PartitionInfo {
|
||||
public final int pid;
|
||||
public final int rows;
|
||||
public final int cols;
|
||||
|
||||
public PartitionInfo(int pid, int rows, int cols) {
|
||||
this.pid = pid;
|
||||
this.rows = rows;
|
||||
this.cols = cols;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void createPartition(int pid, int pRows, int pCols) {
|
||||
partitions.put(pid, new PartitionInfo(pid, pRows, pCols));
|
||||
this.activePartition = pid;
|
||||
this.explicitPartitionActive = true;
|
||||
}
|
||||
|
||||
public synchronized void destroyPartition(int pid) {
|
||||
partitions.remove(pid);
|
||||
if (this.activePartition == pid) {
|
||||
this.activePartition = 0;
|
||||
this.explicitPartitionActive = !partitions.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void activatePartition(int pid) {
|
||||
this.activePartition = pid;
|
||||
this.explicitPartitionActive = (pid != 0);
|
||||
}
|
||||
|
||||
public synchronized void eraseReset(boolean alt) {
|
||||
partitions.clear();
|
||||
this.activePartition = 0;
|
||||
this.explicitPartitionActive = false;
|
||||
erase(alt);
|
||||
}
|
||||
|
||||
// ========== Cursor ==========
|
||||
public int getCursorAddress() { return cursorAddress; }
|
||||
public synchronized void setCursorAddress(int addr) {
|
||||
@@ -172,7 +235,7 @@ public class ScreenBuffer {
|
||||
public void setReplyMode(byte mode) { this.replyMode = mode; }
|
||||
|
||||
public int getActivePartition() { return activePartition; }
|
||||
public void setActivePartition(int pid) { this.activePartition = pid; this.explicitPartitionActive = true; }
|
||||
public void setActivePartition(int pid) { this.activePartition = pid; this.explicitPartitionActive = (pid != 0); }
|
||||
public boolean isExplicitPartitionActive() { return explicitPartitionActive; }
|
||||
|
||||
// ========== Screen erase ==========
|
||||
|
||||
@@ -50,8 +50,11 @@ public class TelnetConnection {
|
||||
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
|
||||
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
|
||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
||||
sslSocket.setKeepAlive(true);
|
||||
sslSocket.setTcpNoDelay(true);
|
||||
sslSocket.setKeepAlive(config.isSoKeepAlive());
|
||||
sslSocket.setTcpNoDelay(config.isTcpNoDelay());
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||
config.getConnectTimeoutMs());
|
||||
sslSocket.startHandshake();
|
||||
@@ -67,9 +70,12 @@ public class TelnetConnection {
|
||||
} else {
|
||||
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
|
||||
socket = new Socket();
|
||||
socket.setKeepAlive(true);
|
||||
socket.setKeepAlive(config.isSoKeepAlive());
|
||||
socket.setOOBInline(true);
|
||||
socket.setTcpNoDelay(true);
|
||||
socket.setTcpNoDelay(config.isTcpNoDelay());
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
socket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||
config.getConnectTimeoutMs());
|
||||
}
|
||||
@@ -104,6 +110,24 @@ public class TelnetConnection {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send payload with automatic doubling of 0xFF (IAC escaping).
|
||||
*/
|
||||
public synchronized void sendEscaped(byte[] data, int offset, int length) throws IOException {
|
||||
if (outputStream == null) return;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(length + 16);
|
||||
for (int i = offset; i < offset + length; i++) {
|
||||
int b = data[i] & 0xFF;
|
||||
out.write(b);
|
||||
if (b == IAC) {
|
||||
out.write(IAC);
|
||||
}
|
||||
}
|
||||
byte[] escaped = out.toByteArray();
|
||||
outputStream.write(escaped, 0, escaped.length);
|
||||
outputStream.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the host.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ import haus.nightmare.lib3270j.listener.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Logger;
|
||||
@@ -57,10 +58,15 @@ public class TelnetFSM {
|
||||
private boolean tn3270eBound;
|
||||
private final boolean[] eFuncs = new boolean[8]; // Negotiated TN3270E functions
|
||||
private int eXmitSeq;
|
||||
private int lastRcvSeq;
|
||||
private short lastRespType;
|
||||
private short lastRespCode;
|
||||
private int responseRequired = RSF_NO_RESPONSE;
|
||||
private boolean deferredWillTtype;
|
||||
private boolean tn3270eDeviceTypeSent;
|
||||
private int ttypeIndex = 0;
|
||||
private int luIndex = 0;
|
||||
private final java.util.Map<String, List<String>> devicePools = new java.util.HashMap<>();
|
||||
|
||||
private List<String> getCandidateTerminalTypes() {
|
||||
List<String> list = new ArrayList<>();
|
||||
@@ -68,6 +74,9 @@ public class TelnetFSM {
|
||||
list.add(config.getTerminalName().trim());
|
||||
return list;
|
||||
}
|
||||
if (config.isDynamicModel()) {
|
||||
list.add(config.isExtendedDataStream() ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC");
|
||||
}
|
||||
TerminalModel model = config.getModel();
|
||||
list.add(model.getTerminalType());
|
||||
list.add(model.getBaseTerminalType());
|
||||
@@ -92,6 +101,7 @@ public class TelnetFSM {
|
||||
private final ConnectionConfig config;
|
||||
private final ScreenBuffer screenBuffer;
|
||||
private final DataStreamProcessor dsProcessor;
|
||||
private final haus.nightmare.lib3270j.nvt.NvtProcessor nvtProcessor;
|
||||
private volatile ConnectionState connectionState = ConnectionState.NOT_CONNECTED;
|
||||
|
||||
// Listeners
|
||||
@@ -108,6 +118,12 @@ public class TelnetFSM {
|
||||
this.config = config;
|
||||
this.screenBuffer = screenBuffer;
|
||||
this.dsProcessor = dsProcessor;
|
||||
this.nvtProcessor = new haus.nightmare.lib3270j.nvt.NvtProcessor(screenBuffer, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
|
||||
this.nvtProcessor.setOutputSender(this::sendBytes);
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.nvt.NvtProcessor getNvtProcessor() {
|
||||
return nvtProcessor;
|
||||
}
|
||||
|
||||
public void setConnection(TelnetConnection connection) {
|
||||
@@ -115,7 +131,10 @@ public class TelnetFSM {
|
||||
}
|
||||
|
||||
public void addConnectionListener(ConnectionListener l) { connectionListeners.add(l); }
|
||||
public void addScreenUpdateListener(ScreenUpdateListener l) { screenListeners.add(l); }
|
||||
public void addScreenUpdateListener(ScreenUpdateListener l) {
|
||||
screenListeners.add(l);
|
||||
nvtProcessor.addScreenUpdateListener(l);
|
||||
}
|
||||
|
||||
public ConnectionState getConnectionState() { return connectionState; }
|
||||
public boolean[] getMyOpts() { return myOpts; }
|
||||
@@ -135,8 +154,10 @@ public class TelnetFSM {
|
||||
tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
||||
tn3270eBound = false;
|
||||
eXmitSeq = 0;
|
||||
lastRcvSeq = 0;
|
||||
deferredWillTtype = false;
|
||||
ttypeIndex = 0;
|
||||
luIndex = 0;
|
||||
ibuf.reset();
|
||||
sbbuf.reset();
|
||||
|
||||
@@ -148,6 +169,7 @@ public class TelnetFSM {
|
||||
eFuncs[FUNC_DATA_STREAM_CTL] = true;
|
||||
eFuncs[FUNC_CONTENTION_RESOLUTION] = true;
|
||||
|
||||
statusDisplay(STATUS_CONNECTING, "Connecting to host");
|
||||
changeState(ConnectionState.TELNET_PENDING);
|
||||
}
|
||||
|
||||
@@ -167,7 +189,9 @@ public class TelnetFSM {
|
||||
if (connectionState == ConnectionState.TELNET_PENDING) {
|
||||
changeState(ConnectionState.CONNECTED_NVT);
|
||||
}
|
||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||
if (connectionState.isNvt()) {
|
||||
nvtProcessor.processNVTData(buf, start, i - start);
|
||||
} else if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||
ibuf.write(buf, start, i - start);
|
||||
}
|
||||
}
|
||||
@@ -236,11 +260,15 @@ public class TelnetFSM {
|
||||
changeState(ConnectionState.CONNECTED_NVT);
|
||||
}
|
||||
|
||||
if (connectionState.isNvt()) {
|
||||
nvtProcessor.processNVTData(new byte[] { (byte) c }, 0, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states, and pending)
|
||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||
ibuf.write(c);
|
||||
}
|
||||
// NVT data would go to NVT processor (not implemented in initial version)
|
||||
}
|
||||
|
||||
// ========== TNS_IAC ==========
|
||||
@@ -431,7 +459,9 @@ public class TelnetFSM {
|
||||
private void processSBIAC(int c) {
|
||||
if (c == SE) {
|
||||
// Sub-negotiation complete
|
||||
processSubNegotiation(sbbuf.toByteArray());
|
||||
byte[] sbData = sbbuf.toByteArray();
|
||||
sbbuf.reset();
|
||||
processSubNegotiation(sbData);
|
||||
state = TNS_DATA;
|
||||
} else if (c == IAC) {
|
||||
// Escaped IAC within sub-negotiation
|
||||
@@ -440,6 +470,7 @@ public class TelnetFSM {
|
||||
} else {
|
||||
// Shouldn't happen, but recover
|
||||
log.warning("Unexpected byte " + c + " after IAC in SB");
|
||||
sbbuf.reset();
|
||||
state = TNS_DATA;
|
||||
}
|
||||
}
|
||||
@@ -548,7 +579,16 @@ public class TelnetFSM {
|
||||
}
|
||||
|
||||
private void sendTN3270EDeviceTypeRequest() {
|
||||
String termType = config.getEffectiveTerminalType();
|
||||
List<String> candidates = getCandidateTerminalTypes();
|
||||
String termType = candidates.get(Math.min(ttypeIndex, candidates.size() - 1));
|
||||
String currentLu = null;
|
||||
List<String> lus = config.getLuNames();
|
||||
if (lus != null && !lus.isEmpty() && luIndex < lus.size()) {
|
||||
currentLu = lus.get(luIndex);
|
||||
} else if (config.getLuName() != null && !config.getLuName().isEmpty()) {
|
||||
currentLu = config.getLuName();
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(IAC);
|
||||
out.write(SB);
|
||||
@@ -559,9 +599,9 @@ public class TelnetFSM {
|
||||
out.write((byte) ch);
|
||||
}
|
||||
// Add LU name if specified
|
||||
if (config.getLuName() != null && !config.getLuName().isEmpty()) {
|
||||
if (currentLu != null && !currentLu.isEmpty()) {
|
||||
out.write(OP_CONNECT);
|
||||
for (char ch : config.getLuName().toCharArray()) {
|
||||
for (char ch : currentLu.toCharArray()) {
|
||||
out.write((byte) ch);
|
||||
}
|
||||
}
|
||||
@@ -569,7 +609,7 @@ public class TelnetFSM {
|
||||
out.write(SE);
|
||||
sendBytes(out.toByteArray());
|
||||
log.warning(">>> SENT SB TN3270E DEVICE-TYPE REQUEST " + termType +
|
||||
(config.getLuName() != null ? " CONNECT " + config.getLuName() : "") + " SE");
|
||||
(currentLu != null ? " CONNECT " + currentLu : "") + " SE");
|
||||
}
|
||||
|
||||
private void handleTN3270EDeviceType(byte[] data) {
|
||||
@@ -584,23 +624,37 @@ public class TelnetFSM {
|
||||
// Check if REJECT
|
||||
if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) {
|
||||
pos++;
|
||||
int reason = (pos < data.length) ? (data[pos] & 0xFF) : REASON_UNSUPPORTED_REQ;
|
||||
if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) {
|
||||
// Try fallback model 2 if we were requesting something else
|
||||
if (config.getModel() != TerminalModel.IBM_3278_2 &&
|
||||
config.getModel() != TerminalModel.IBM_3279_4) {
|
||||
log.warning("TN3270E device-type rejected (" +
|
||||
TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2");
|
||||
config.setModel(TerminalModel.IBM_3278_2);
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
return;
|
||||
}
|
||||
int reason = REASON_UNSUPPORTED_REQ;
|
||||
if (pos < data.length && (data[pos] & 0xFF) == OP_REASON) {
|
||||
pos++;
|
||||
}
|
||||
if (pos < data.length) {
|
||||
reason = data[pos] & 0xFF;
|
||||
}
|
||||
log.warning("TN3270E device-type rejected: " + TN3270EConstants.reasonName(reason));
|
||||
|
||||
// 1. Try next LU in pool if available
|
||||
List<String> lus = config.getLuNames();
|
||||
if (lus != null && luIndex + 1 < lus.size()) {
|
||||
luIndex++;
|
||||
log.info("Retrying TN3270E device-type with next LU: " + lus.get(luIndex));
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Try fallback terminal type candidate if available
|
||||
List<String> candidates = getCandidateTerminalTypes();
|
||||
if (ttypeIndex + 1 < candidates.size() - 1) { // exclude UNKNOWN
|
||||
ttypeIndex++;
|
||||
log.info("Retrying TN3270E device-type with next candidate: " + candidates.get(ttypeIndex));
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to plain TN3270
|
||||
myOpts[TELOPT_TN3270E] = false;
|
||||
hisOpts[TELOPT_TN3270E] = false;
|
||||
sendCommand(WONT, TELOPT_TN3270E);
|
||||
|
||||
// Send deferred WILL TTYPE if needed
|
||||
if (deferredWillTtype) {
|
||||
@@ -832,102 +886,21 @@ public class TelnetFSM {
|
||||
break;
|
||||
|
||||
case DT_BIND_IMAGE:
|
||||
tn3270eBound = true;
|
||||
// Parse BIND image for screen dimensions (SNA BIND format)
|
||||
{
|
||||
int bindLen = data.length - EH_SIZE;
|
||||
StringBuilder bindHex = new StringBuilder();
|
||||
for (int bi = EH_SIZE; bi < data.length && bi < EH_SIZE + 40; bi++) {
|
||||
bindHex.append(String.format("%02x ", data[bi] & 0xFF));
|
||||
}
|
||||
log.info("Received BIND image (" + bindLen + " bytes)" +
|
||||
" responseFlag=" + responseFlag + " raw: " + bindHex.toString().trim());
|
||||
|
||||
// SNA BIND RU offsets (from 3270ds.h):
|
||||
// Byte 20 = RD (default rows), Byte 21 = CD (default cols)
|
||||
// Byte 22 = RA (alternate rows), Byte 23 = CA (alternate cols)
|
||||
// Byte 24 = SSIZE (screen size indicator)
|
||||
final int BIND_OFF_RD = 20, BIND_OFF_CD = 21;
|
||||
final int BIND_OFF_RA = 22, BIND_OFF_CA = 23;
|
||||
final int BIND_OFF_SSIZE = 24;
|
||||
|
||||
if (bindLen > BIND_OFF_SSIZE) {
|
||||
int ssize = data[EH_SIZE + BIND_OFF_SSIZE] & 0xFF;
|
||||
int bindRd = data[EH_SIZE + BIND_OFF_RD] & 0xFF;
|
||||
int bindCd = data[EH_SIZE + BIND_OFF_CD] & 0xFF;
|
||||
int bindRa, bindCa;
|
||||
|
||||
switch (ssize) {
|
||||
case 0x00: case 0x02:
|
||||
// Default model 2 dimensions for both default and alt
|
||||
bindRd = 24; bindCd = 80;
|
||||
bindRa = 24; bindCa = 80;
|
||||
break;
|
||||
case 0x03:
|
||||
// Default = 24x80, alternate = configured model max
|
||||
bindRd = 24; bindCd = 80;
|
||||
bindRa = screenBuffer.getMaxRows();
|
||||
bindCa = screenBuffer.getMaxCols();
|
||||
break;
|
||||
case 0x7E:
|
||||
// Both default and alternate = specified values
|
||||
bindRa = bindRd; bindCa = bindCd;
|
||||
break;
|
||||
case 0x7F:
|
||||
// Default and alternate are both specified separately
|
||||
bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF;
|
||||
bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF;
|
||||
break;
|
||||
default:
|
||||
// Unknown SSIZE - use model defaults
|
||||
bindRa = screenBuffer.getMaxRows();
|
||||
bindCa = screenBuffer.getMaxCols();
|
||||
break;
|
||||
}
|
||||
log.info("BIND SSIZE=0x" + String.format("%02x", ssize) +
|
||||
" default=" + bindRd + "x" + bindCd +
|
||||
" alt=" + bindRa + "x" + bindCa);
|
||||
|
||||
// Apply dimensions — constrain to model max
|
||||
int maxR = screenBuffer.getMaxRows();
|
||||
int maxC = screenBuffer.getMaxCols();
|
||||
if (bindRa > 0 && bindCa > 0 && bindRa <= maxR && bindCa <= maxC) {
|
||||
screenBuffer.setAlternateDimensions(bindRa, bindCa);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear and reset screen for new session
|
||||
screenBuffer.erase(false);
|
||||
changeState(ConnectionState.CONNECTED_TN3270E);
|
||||
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||
notifyScreenUpdate();
|
||||
// Send positive response if required (critical for ISPF NEWAPPL)
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
process_bind(data, responseFlag, seqNumber);
|
||||
break;
|
||||
|
||||
case DT_UNBIND:
|
||||
log.info("Received UNBIND responseFlag=" + responseFlag);
|
||||
tn3270eBound = false;
|
||||
// Restore alternate dimensions to configured model max (per x3270)
|
||||
screenBuffer.setAlternateDimensions(
|
||||
screenBuffer.getMaxRows(), screenBuffer.getMaxCols());
|
||||
// Clear screen on UNBIND — essential for ISPF NEWAPPL transitions
|
||||
screenBuffer.clear();
|
||||
// Send positive response BEFORE changing state (host expects it)
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
changeState(ConnectionState.CONNECTED_UNBOUND);
|
||||
tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
||||
notifyScreenUpdate();
|
||||
int unbindReason = (data.length > EH_SIZE) ? (data[EH_SIZE] & 0xFF) : UNBIND_NORMAL;
|
||||
process_unbind(unbindReason, responseFlag, seqNumber);
|
||||
break;
|
||||
|
||||
case DT_NVT_DATA:
|
||||
// NVT data in TN3270E mode
|
||||
changeState(ConnectionState.CONNECTED_E_NVT);
|
||||
tn3270eSubmode = TN3270ESubmode.E_NVT;
|
||||
if (data.length > EH_SIZE) {
|
||||
processNVTData(data, EH_SIZE, data.length - EH_SIZE);
|
||||
}
|
||||
break;
|
||||
|
||||
case DT_REQUEST:
|
||||
@@ -948,6 +921,7 @@ public class TelnetFSM {
|
||||
break;
|
||||
|
||||
case DT_RESPONSE:
|
||||
lastRcvSeq = seqNumber;
|
||||
log.fine("Received response, seq=" + seqNumber);
|
||||
break;
|
||||
|
||||
@@ -1160,6 +1134,265 @@ public class TelnetFSM {
|
||||
public String getConnectedLu() { return connectedLu; }
|
||||
public String getConnectedType() { return connectedType; }
|
||||
|
||||
// ========== SNA BIND, UNBIND, BID, DFC Handlers (Phase 2) ==========
|
||||
|
||||
public void process_bind(byte[] data, int responseFlag, int seqNumber) {
|
||||
tn3270eBound = true;
|
||||
int bindLen = data.length - EH_SIZE;
|
||||
StringBuilder bindHex = new StringBuilder();
|
||||
for (int bi = EH_SIZE; bi < data.length && bi < EH_SIZE + 40; bi++) {
|
||||
bindHex.append(String.format("%02x ", data[bi] & 0xFF));
|
||||
}
|
||||
log.info("Received BIND image (" + bindLen + " bytes) responseFlag=" + responseFlag +
|
||||
" raw: " + bindHex.toString().trim());
|
||||
|
||||
final int BIND_OFF_RD = 20, BIND_OFF_CD = 21;
|
||||
final int BIND_OFF_RA = 22, BIND_OFF_CA = 23;
|
||||
final int BIND_OFF_SSIZE = 24;
|
||||
|
||||
if (bindLen > BIND_OFF_SSIZE) {
|
||||
int ssize = data[EH_SIZE + BIND_OFF_SSIZE] & 0xFF;
|
||||
int bindRd = data[EH_SIZE + BIND_OFF_RD] & 0xFF;
|
||||
int bindCd = data[EH_SIZE + BIND_OFF_CD] & 0xFF;
|
||||
int bindRa, bindCa;
|
||||
|
||||
switch (ssize) {
|
||||
case 0x00: case 0x02:
|
||||
bindRd = 24; bindCd = 80;
|
||||
bindRa = 24; bindCa = 80;
|
||||
break;
|
||||
case 0x03:
|
||||
bindRd = 24; bindCd = 80;
|
||||
bindRa = screenBuffer.getMaxRows();
|
||||
bindCa = screenBuffer.getMaxCols();
|
||||
break;
|
||||
case 0x7E:
|
||||
bindRa = bindRd; bindCa = bindCd;
|
||||
break;
|
||||
case 0x7F:
|
||||
bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF;
|
||||
bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF;
|
||||
break;
|
||||
default:
|
||||
bindRa = screenBuffer.getMaxRows();
|
||||
bindCa = screenBuffer.getMaxCols();
|
||||
break;
|
||||
}
|
||||
log.info("BIND SSIZE=0x" + String.format("%02x", ssize) +
|
||||
" default=" + bindRd + "x" + bindCd +
|
||||
" alt=" + bindRa + "x" + bindCa);
|
||||
|
||||
int maxR = screenBuffer.getMaxRows();
|
||||
int maxC = screenBuffer.getMaxCols();
|
||||
if (bindRa > 0 && bindCa > 0 && bindRa <= maxR && bindCa <= maxC) {
|
||||
screenBuffer.setAlternateDimensions(bindRa, bindCa);
|
||||
}
|
||||
}
|
||||
|
||||
screenBuffer.erase(false);
|
||||
changeState(ConnectionState.CONNECTED_TN3270E);
|
||||
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||
statusDisplay(STATUS_CONNECTED, "Session bound");
|
||||
notifyScreenUpdate();
|
||||
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
}
|
||||
|
||||
public void process_unbind(int unbindReason, int responseFlag, int seqNumber) {
|
||||
log.info("Received UNBIND reason=" + unbindReasonName(unbindReason) + " responseFlag=" + responseFlag);
|
||||
tn3270eBound = false;
|
||||
screenBuffer.setAlternateDimensions(screenBuffer.getMaxRows(), screenBuffer.getMaxCols());
|
||||
screenBuffer.clear();
|
||||
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
|
||||
changeState(ConnectionState.CONNECTED_UNBOUND);
|
||||
tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
||||
statusDisplay(STATUS_BIND_ERROR, "Session unbound: " + unbindReasonName(unbindReason));
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
|
||||
public void process_BID(int responseFlag, int seqNumber) {
|
||||
log.info("Received BID contention request");
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
}
|
||||
|
||||
public void process_DFC(int order, int responseFlag, int seqNumber) {
|
||||
log.info("Received SNA DFC command: " + dfcCommandName(order));
|
||||
switch (order) {
|
||||
case DFC_CLEAR:
|
||||
handleSnaClear();
|
||||
break;
|
||||
case DFC_SIGNAL:
|
||||
handleSnaSignal();
|
||||
break;
|
||||
case DFC_CANCEL:
|
||||
handleSnaCancel();
|
||||
break;
|
||||
case DFC_RQR:
|
||||
handleSnaRqr();
|
||||
break;
|
||||
case DFC_STSN:
|
||||
handleSnaStsn();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== NVT Delegation (Phase 2) ==========
|
||||
|
||||
public void processNVTData(byte[] data, int offset, int length) {
|
||||
nvtProcessor.processNVTData(data, offset, length);
|
||||
}
|
||||
|
||||
public int processAnsiEscapeSequence(byte[] seq, int curAddr, int rows, int cols) {
|
||||
return nvtProcessor.processAnsiEscapeSequence(seq, curAddr, rows, cols);
|
||||
}
|
||||
|
||||
public void sendNVTChar(char c) throws IOException {
|
||||
nvtProcessor.sendNVTChar(c);
|
||||
}
|
||||
|
||||
public void sendNVTString(String s) throws IOException {
|
||||
nvtProcessor.sendNVTString(s);
|
||||
}
|
||||
|
||||
// ========== Device Name Pool & Response Tracking (Phase 2) ==========
|
||||
|
||||
public void addDeviceName(String pool, String luName, int index) {
|
||||
List<String> list = devicePools.computeIfAbsent(pool, k -> new ArrayList<>());
|
||||
if (index >= 0 && index < list.size()) {
|
||||
list.add(index, luName);
|
||||
} else {
|
||||
list.add(luName);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeDeviceName(String pool, String luName, int index) {
|
||||
List<String> list = devicePools.get(pool);
|
||||
if (list != null) {
|
||||
if (index >= 0 && index < list.size()) {
|
||||
list.remove(index);
|
||||
} else if (luName != null) {
|
||||
list.remove(luName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getDeviceName(String pool, int index) {
|
||||
List<String> list = devicePools.get(pool);
|
||||
if (list != null && index >= 0 && index < list.size()) {
|
||||
return list.get(index);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<String> getDevicePoolList(String pool) {
|
||||
return devicePools.getOrDefault(pool, Collections.emptyList());
|
||||
}
|
||||
|
||||
public void setResponse(short respType, short respCode) {
|
||||
this.lastRespType = respType;
|
||||
this.lastRespCode = respCode;
|
||||
}
|
||||
|
||||
public void sendRequest(int requestType) {
|
||||
if (tn3270eNegotiated) {
|
||||
byte[] req = new byte[EH_SIZE];
|
||||
req[0] = (byte) DT_REQUEST;
|
||||
req[1] = (byte) requestType;
|
||||
req[2] = 0;
|
||||
req[3] = (byte) ((eXmitSeq >> 8) & 0xFF);
|
||||
req[4] = (byte) (eXmitSeq & 0xFF);
|
||||
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
|
||||
sendRecord(req);
|
||||
}
|
||||
}
|
||||
|
||||
public void statusDisplay(int statusNumber, String msg) {
|
||||
log.info("[HoD Status " + statusNumber + "] " + (msg != null ? msg : ""));
|
||||
}
|
||||
|
||||
// ========== Internal State & Timer Handlers (Phase 2) ==========
|
||||
|
||||
public void handleTimingMark() {
|
||||
sendCommand(WONT, TELOPT_TM);
|
||||
}
|
||||
|
||||
public void handleKeepalive() {
|
||||
sendCommand(DO, TELOPT_TM);
|
||||
}
|
||||
|
||||
public void processContentionResolution() {
|
||||
if (eFuncs[FUNC_CONTENTION_RESOLUTION]) {
|
||||
log.fine("Contention resolution processed");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validateSessionState() {
|
||||
return connectionState != ConnectionState.NOT_CONNECTED;
|
||||
}
|
||||
|
||||
public void dispatchTelnetEvent(int eventCode, String desc) {
|
||||
log.fine("Telnet event: code=" + eventCode + " desc=" + desc);
|
||||
}
|
||||
|
||||
public void flushPendingOutput() {
|
||||
// Flushes output streams
|
||||
}
|
||||
|
||||
public void handleSnaSense(int sense1, int sense2) {
|
||||
log.warning(String.format("SNA Sense Code received: 0x%02X 0x%02X", sense1, sense2));
|
||||
}
|
||||
|
||||
public void handleSnaSignal() {
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onSoundAlarm();
|
||||
}
|
||||
}
|
||||
|
||||
public void handleSnaClear() {
|
||||
screenBuffer.clear();
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
|
||||
public void handleSnaCancel() {
|
||||
log.fine("SNA Cancel handled");
|
||||
}
|
||||
|
||||
public void handleSnaRqr() {
|
||||
log.fine("SNA Recovery on Request (RQR) handled");
|
||||
}
|
||||
|
||||
public void handleSnaStsn() {
|
||||
log.fine("SNA Set and Test Sequence Numbers (STSN) handled");
|
||||
}
|
||||
|
||||
public void resetNegotiationState() {
|
||||
tn3270eNegotiated = false;
|
||||
tn3270eDeviceTypeSent = false;
|
||||
tn3270eBound = false;
|
||||
ttypeIndex = 0;
|
||||
luIndex = 0;
|
||||
java.util.Arrays.fill(myOpts, false);
|
||||
java.util.Arrays.fill(hisOpts, false);
|
||||
java.util.Arrays.fill(eFuncs, false);
|
||||
}
|
||||
|
||||
public boolean checkSessionBound() {
|
||||
return tn3270eBound || connectionState == ConnectionState.CONNECTED_3270;
|
||||
}
|
||||
|
||||
private static String tn3270eOpName(int op) {
|
||||
switch (op) {
|
||||
case OP_ASSOCIATE: return "ASSOCIATE";
|
||||
|
||||
Reference in New Issue
Block a user