Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
14bf7ba4b1
|
|||
|
bdfe6eec2a
|
|||
|
c28c097e25
|
@@ -1002,8 +1002,72 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
"About j3270", JOptionPane.INFORMATION_MESSAGE);
|
"About j3270", JOptionPane.INFORMATION_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure application logging. When debug is false, disables all logging
|
||||||
|
* to console and file, preventing creation of j3270.log.
|
||||||
|
*/
|
||||||
|
public static void configureLogging(boolean debug) {
|
||||||
|
Logger globalRoot = Logger.getLogger("");
|
||||||
|
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
||||||
|
globalRoot.removeHandler(h);
|
||||||
|
try {
|
||||||
|
h.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
Level logLevel = Level.ALL;
|
||||||
|
globalRoot.setLevel(Level.ALL);
|
||||||
|
|
||||||
|
java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
|
||||||
|
(record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
|
||||||
|
|
||||||
|
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||||
|
consoleHandler.setLevel(Level.ALL);
|
||||||
|
consoleHandler.setFormatter(new SimpleFormatter());
|
||||||
|
consoleHandler.setFilter(appFilter);
|
||||||
|
globalRoot.addHandler(consoleHandler);
|
||||||
|
|
||||||
|
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
|
||||||
|
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
|
||||||
|
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
|
||||||
|
@Override
|
||||||
|
public synchronized void publish(java.util.logging.LogRecord record) {
|
||||||
|
super.publish(record);
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fileHandler.setLevel(Level.ALL);
|
||||||
|
fileHandler.setFormatter(new SimpleFormatter());
|
||||||
|
fileHandler.setFilter(appFilter);
|
||||||
|
globalRoot.addHandler(fileHandler);
|
||||||
|
log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")");
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Could not create j3270.log: " + e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
globalRoot.setLevel(Level.OFF);
|
||||||
|
for (String pkg : new String[]{"haus.nightmare", "haus.nightmare.j3270", "haus.nightmare.lib3270j", "org.pubvm"}) {
|
||||||
|
Logger l = Logger.getLogger(pkg);
|
||||||
|
l.setLevel(Level.OFF);
|
||||||
|
for (java.util.logging.Handler h : l.getHandlers()) {
|
||||||
|
l.removeHandler(h);
|
||||||
|
try {
|
||||||
|
h.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Main ==========
|
// ========== Main ==========
|
||||||
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
boolean debug = false;
|
boolean debug = false;
|
||||||
boolean cliTls = false;
|
boolean cliTls = false;
|
||||||
@@ -1045,42 +1109,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Level logLevel = debug ? Level.FINE : Level.INFO;
|
configureLogging(debug);
|
||||||
|
|
||||||
Logger globalRoot = Logger.getLogger("");
|
|
||||||
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
|
||||||
globalRoot.removeHandler(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
|
|
||||||
(record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
|
|
||||||
|
|
||||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
|
||||||
consoleHandler.setLevel(Level.ALL);
|
|
||||||
consoleHandler.setFormatter(new SimpleFormatter());
|
|
||||||
consoleHandler.setFilter(appFilter);
|
|
||||||
globalRoot.addHandler(consoleHandler);
|
|
||||||
|
|
||||||
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
|
|
||||||
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
|
|
||||||
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
|
|
||||||
|
|
||||||
try {
|
|
||||||
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
|
|
||||||
@Override
|
|
||||||
public synchronized void publish(java.util.logging.LogRecord record) {
|
|
||||||
super.publish(record);
|
|
||||||
flush();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fileHandler.setLevel(Level.ALL);
|
|
||||||
fileHandler.setFormatter(new SimpleFormatter());
|
|
||||||
fileHandler.setFilter(appFilter);
|
|
||||||
globalRoot.addHandler(fileHandler);
|
|
||||||
log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")");
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Could not create j3270.log: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configFile != null) {
|
if (configFile != null) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package haus.nightmare.j3270;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.util.logging.Handler;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class LoggingConfigurationTest {
|
||||||
|
|
||||||
|
private PrintStream originalOut;
|
||||||
|
private PrintStream originalErr;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
originalOut = System.out;
|
||||||
|
originalErr = System.err;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void tearDown() {
|
||||||
|
System.setOut(originalOut);
|
||||||
|
System.setErr(originalErr);
|
||||||
|
// Ensure all handlers are closed and logging reset
|
||||||
|
J3270App.configureLogging(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLoggingDisabledWhenDebugFalse() {
|
||||||
|
File logFile = new File("j3270.log");
|
||||||
|
if (logFile.exists()) {
|
||||||
|
logFile.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
|
||||||
|
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
|
||||||
|
System.setOut(new PrintStream(outContent));
|
||||||
|
System.setErr(new PrintStream(errContent));
|
||||||
|
|
||||||
|
J3270App.configureLogging(false);
|
||||||
|
|
||||||
|
// Root logger should have no handlers attached
|
||||||
|
Logger rootLogger = Logger.getLogger("");
|
||||||
|
assertEquals(0, rootLogger.getHandlers().length, "Root logger should have no handlers when debug is disabled");
|
||||||
|
assertEquals(Level.OFF, rootLogger.getLevel(), "Root logger level should be OFF when debug is disabled");
|
||||||
|
|
||||||
|
// Application loggers should be OFF
|
||||||
|
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare").getLevel());
|
||||||
|
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.j3270").getLevel());
|
||||||
|
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
|
||||||
|
|
||||||
|
// Emit log records at all levels
|
||||||
|
Logger appLogger = Logger.getLogger("haus.nightmare.j3270.J3270App");
|
||||||
|
appLogger.severe("Test SEVERE message");
|
||||||
|
appLogger.warning("Test WARNING message");
|
||||||
|
appLogger.info("Test INFO message");
|
||||||
|
appLogger.fine("Test FINE message");
|
||||||
|
|
||||||
|
// Verify nothing was written to stdout or stderr
|
||||||
|
assertEquals(0, outContent.size(), "Standard output should be empty when debug is disabled");
|
||||||
|
assertEquals(0, errContent.size(), "Standard error should be empty when debug is disabled");
|
||||||
|
|
||||||
|
// Verify j3270.log was NOT created
|
||||||
|
assertFalse(logFile.exists(), "j3270.log should NOT be created when debug is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLoggingEnabledWhenDebugTrue() {
|
||||||
|
J3270App.configureLogging(true);
|
||||||
|
|
||||||
|
Logger rootLogger = Logger.getLogger("");
|
||||||
|
Handler[] handlers = rootLogger.getHandlers();
|
||||||
|
assertTrue(handlers.length >= 2, "Root logger should have at least ConsoleHandler and FileHandler when debug is enabled");
|
||||||
|
|
||||||
|
assertEquals(Level.ALL, rootLogger.getLevel());
|
||||||
|
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare").getLevel());
|
||||||
|
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.j3270").getLevel());
|
||||||
|
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
|
||||||
|
|
||||||
|
File logFile = new File("j3270.log");
|
||||||
|
assertTrue(logFile.exists(), "j3270.log should be created when debug is enabled");
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
J3270App.configureLogging(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -435,6 +435,14 @@ public class ConnectionConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse [lu@]host[:port] (e.g. "00C2@mvs.host.com:1023")
|
||||||
|
String parsedLu = null;
|
||||||
|
int atIdx = s.indexOf('@');
|
||||||
|
if (atIdx > 0 && atIdx < s.length() - 1) {
|
||||||
|
parsedLu = s.substring(0, atIdx).trim();
|
||||||
|
s = s.substring(atIdx + 1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
String host = s;
|
String host = s;
|
||||||
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
|
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
|
||||||
|
|
||||||
@@ -458,6 +466,9 @@ public class ConnectionConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||||
|
if (parsedLu != null && !parsedLu.isEmpty()) {
|
||||||
|
config.setLuName(parsedLu);
|
||||||
|
}
|
||||||
config.setUseTls(tls);
|
config.setUseTls(tls);
|
||||||
config.setTn3270eEnabled(tn3270e);
|
config.setTn3270eEnabled(tn3270e);
|
||||||
config.setKeepAliveEnabled(keepAlive);
|
config.setKeepAliveEnabled(keepAlive);
|
||||||
|
|||||||
+33
-13
@@ -54,6 +54,12 @@ public class DataStreamProcessor {
|
|||||||
private boolean unlockSysPending = false;
|
private boolean unlockSysPending = false;
|
||||||
private boolean rcvdRead = false;
|
private boolean rcvdRead = false;
|
||||||
|
|
||||||
|
// Modal SA (set attribute) character attributes
|
||||||
|
private byte currentFg = 0;
|
||||||
|
private byte currentBg = 0;
|
||||||
|
private byte currentGr = 0;
|
||||||
|
private byte currentCs = 0;
|
||||||
|
|
||||||
/** 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 {
|
||||||
@@ -240,6 +246,10 @@ public class DataStreamProcessor {
|
|||||||
programSymbolManager.commitStagedSymbols();
|
programSymbolManager.commitStagedSymbols();
|
||||||
log.info(">>> EAU: erasing all unprotected fields");
|
log.info(">>> EAU: erasing all unprotected fields");
|
||||||
screen.eraseAllUnprotected();
|
screen.eraseAllUnprotected();
|
||||||
|
currentFg = 0;
|
||||||
|
currentBg = 0;
|
||||||
|
currentGr = 0;
|
||||||
|
currentCs = 0;
|
||||||
break;
|
break;
|
||||||
case CMD_WSF:
|
case CMD_WSF:
|
||||||
case SNA_CMD_WSF:
|
case SNA_CMD_WSF:
|
||||||
@@ -378,11 +388,19 @@ public class DataStreamProcessor {
|
|||||||
if (wccReset(wcc)) {
|
if (wccReset(wcc)) {
|
||||||
// Reset all character attributes to defaults
|
// Reset all character attributes to defaults
|
||||||
log.fine("WCC reset: clearing default attributes");
|
log.fine("WCC reset: clearing default attributes");
|
||||||
|
currentFg = 0;
|
||||||
|
currentBg = 0;
|
||||||
|
currentGr = 0;
|
||||||
|
currentCs = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eraseFirst) {
|
if (eraseFirst) {
|
||||||
screen.clear();
|
screen.clear();
|
||||||
log.fine("Cleared screen for Erase/Write");
|
log.fine("Cleared screen for Erase/Write");
|
||||||
|
currentFg = 0;
|
||||||
|
currentBg = 0;
|
||||||
|
currentGr = 0;
|
||||||
|
currentCs = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process orders and data starting at byte 2
|
// Process orders and data starting at byte 2
|
||||||
@@ -390,9 +408,6 @@ public class DataStreamProcessor {
|
|||||||
int end = offset + length;
|
int end = offset + length;
|
||||||
int baddr = screen.getBufferAddress();
|
int baddr = screen.getBufferAddress();
|
||||||
int size = screen.getRows() * screen.getCols();
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
|
||||||
// Current SA (set attribute) values for character-mode
|
|
||||||
byte currentFg = 0, currentBg = 0, currentGr = 0, currentCs = 0;
|
|
||||||
boolean lastWasOrder = false;
|
boolean lastWasOrder = false;
|
||||||
|
|
||||||
while (pos < end) {
|
while (pos < end) {
|
||||||
@@ -427,10 +442,6 @@ public class DataStreamProcessor {
|
|||||||
// FA position is a display position that shows as blank
|
// FA position is a display position that shows as blank
|
||||||
ea.ec = 0;
|
ea.ec = 0;
|
||||||
ea.ucs4 = ' ';
|
ea.ucs4 = ' ';
|
||||||
currentFg = 0;
|
|
||||||
currentBg = 0;
|
|
||||||
currentGr = 0;
|
|
||||||
currentCs = 0;
|
|
||||||
screen.setFormatted(true);
|
screen.setFormatted(true);
|
||||||
baddr = (baddr + 1) % size;
|
baddr = (baddr + 1) % size;
|
||||||
screen.setBufferAddress(baddr);
|
screen.setBufferAddress(baddr);
|
||||||
@@ -455,10 +466,6 @@ public class DataStreamProcessor {
|
|||||||
ea.clear();
|
ea.clear();
|
||||||
ea.ec = 0;
|
ea.ec = 0;
|
||||||
ea.ucs4 = ' ';
|
ea.ucs4 = ' ';
|
||||||
currentFg = 0;
|
|
||||||
currentBg = 0;
|
|
||||||
currentGr = 0;
|
|
||||||
currentCs = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < nPairs; i++) {
|
for (int i = 0; i < nPairs; i++) {
|
||||||
int attrType = data[pos + 2 + i * 2] & 0xFF;
|
int attrType = data[pos + 2 + i * 2] & 0xFF;
|
||||||
@@ -485,6 +492,12 @@ public class DataStreamProcessor {
|
|||||||
int attrType = data[pos + 1] & 0xFF;
|
int attrType = data[pos + 1] & 0xFF;
|
||||||
int attrValue = data[pos + 2] & 0xFF;
|
int attrValue = data[pos + 2] & 0xFF;
|
||||||
switch (attrType) {
|
switch (attrType) {
|
||||||
|
case XA_ALL:
|
||||||
|
currentFg = 0;
|
||||||
|
currentBg = 0;
|
||||||
|
currentGr = 0;
|
||||||
|
currentCs = 0;
|
||||||
|
break;
|
||||||
case XA_FOREGROUND:
|
case XA_FOREGROUND:
|
||||||
currentFg = (byte) attrValue;
|
currentFg = (byte) attrValue;
|
||||||
break;
|
break;
|
||||||
@@ -984,7 +997,13 @@ public class DataStreamProcessor {
|
|||||||
break;
|
break;
|
||||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
|
||||||
if (fieldLen > 3) {
|
if (fieldLen > 3) {
|
||||||
|
graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight());
|
||||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||||
|
int targetW = screen.getCols() * qrBuilder.getCharWidth();
|
||||||
|
int targetH = screen.getRows() * qrBuilder.getCharHeight();
|
||||||
|
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
|
||||||
|
graphicsPlane.resize(targetW, targetH);
|
||||||
|
}
|
||||||
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
|
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
|
||||||
notifyScreenUpdated();
|
notifyScreenUpdated();
|
||||||
}
|
}
|
||||||
@@ -1298,9 +1317,10 @@ public class DataStreamProcessor {
|
|||||||
int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4);
|
int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4);
|
||||||
int orderLen = Math.max(0, fieldLen - (orderOffset - offset));
|
int orderLen = Math.max(0, fieldLen - (orderOffset - offset));
|
||||||
|
|
||||||
|
graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight());
|
||||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||||
int targetW = screen.getCols() * 9;
|
int targetW = screen.getCols() * qrBuilder.getCharWidth();
|
||||||
int targetH = screen.getRows() * 16;
|
int targetH = screen.getRows() * qrBuilder.getCharHeight();
|
||||||
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
|
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
|
||||||
graphicsPlane.resize(targetW, targetH);
|
graphicsPlane.resize(targetW, targetH);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -424,8 +424,8 @@ public class QueryReplyBuilder {
|
|||||||
out.write((Yr_HOD >> 16) & 0xFF);
|
out.write((Yr_HOD >> 16) & 0xFF);
|
||||||
out.write((Yr_HOD >> 8) & 0xFF);
|
out.write((Yr_HOD >> 8) & 0xFF);
|
||||||
out.write(Yr_HOD & 0xFF);
|
out.write(Yr_HOD & 0xFF);
|
||||||
int charW = getCharWidth();
|
int charW = getCharWidth(maxRows);
|
||||||
int charH = getCharHeight();
|
int charH = getCharHeight(maxRows);
|
||||||
out.write(charW); // AW
|
out.write(charW); // AW
|
||||||
out.write(charH); // AH
|
out.write(charH); // AH
|
||||||
int buf = maxCols * maxRows;
|
int buf = maxCols * maxRows;
|
||||||
@@ -435,6 +435,11 @@ public class QueryReplyBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getCharWidth() {
|
public int getCharWidth() {
|
||||||
|
int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||||
|
return getCharWidth(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCharWidth(int rows) {
|
||||||
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
|
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
|
||||||
return 12;
|
return 12;
|
||||||
}
|
}
|
||||||
@@ -442,18 +447,11 @@ public class QueryReplyBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getCharHeight() {
|
public int getCharHeight() {
|
||||||
// ARCHITECTURAL NOTE ON 3179G GOCA VERTICAL ALIGNMENT & QUERY REPLIES:
|
int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||||
// Why hardcoding SH = 12 (0x0C) in Character Sets & Usable Area failed in past iterations:
|
return getCharHeight(rows);
|
||||||
// When SDH/AH is declared as 12 (0x0C) in Query Reply, the mainframe host GDDM engine computes
|
}
|
||||||
// total presentation space as rows * 12 (e.g. 43 * 12 = 516 units, yMax = 257).
|
|
||||||
// GDDM then places the top menu bar at Row 1 (gy = 187..200).
|
public int getCharHeight(int rows) {
|
||||||
// Meanwhile, the client emulator rendered into a 16-pitch grid (43 * 16 = 688 units, yMax = 343).
|
|
||||||
// On a 688-unit canvas, gy = 200 mapped to Row 9.2 (middle of the screen), leaving a massive void above.
|
|
||||||
// When the user clicked on the visual menu drawn at Row 9, the client emitted gy = 189 with cursor at Row 9,
|
|
||||||
// which GDDM rejected as outside its menu hit box (causing terminal alarm beeps).
|
|
||||||
//
|
|
||||||
// Solution: Declare SDH = 16 (0x10) when Vector Graphics is enabled (3179G standard), ensuring host GDDM
|
|
||||||
// and client GraphicsPlane share the exact same 16-pitch presentation space (720x688, yMax = 343).
|
|
||||||
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
|
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -395,13 +395,16 @@ public class GocaDecoder {
|
|||||||
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) {
|
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
// Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00, 93 00, 91 00)
|
// Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00)
|
||||||
if (order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_ENDSEGM ||
|
if (order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_ENDSEGM ||
|
||||||
order == GocaConstants.G_GEAR || order == GocaConstants.G_GERASE ||
|
order == GocaConstants.G_GEAR || order == GocaConstants.G_GERASE ||
|
||||||
order == GocaConstants.G_GPOP || order == GocaConstants.G_GEIMG ||
|
order == GocaConstants.G_GPOP) {
|
||||||
(inImage && order == GocaConstants.G_GEIMG_ALT)) {
|
|
||||||
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
|
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
|
||||||
}
|
}
|
||||||
|
// G_GEIMG (0x93 End Image): self-defining draw order (e.g. 93 02 00 00 or 93 00 or 93)
|
||||||
|
if (order == GocaConstants.G_GEIMG) {
|
||||||
|
return (idx + 1 < end) ? ((data[idx + 1] & 0xFF) + 2) : 1;
|
||||||
|
}
|
||||||
if (idx + 1 >= end) {
|
if (idx + 1 >= end) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -552,31 +555,45 @@ public class GocaDecoder {
|
|||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GocaConstants.G_GEIMG: // 0x93
|
case GocaConstants.G_GEIMG: { // 0x93 End Image
|
||||||
case GocaConstants.G_GEIMG_ALT: { // 0x91
|
endImage();
|
||||||
if (inImage || order == GocaConstants.G_GEIMG) {
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GBIMGC: { // 0x91: Begin Image at Current Position (G_GBIMGC)
|
||||||
|
if (inImage) {
|
||||||
endImage();
|
endImage();
|
||||||
idx += orderLen;
|
|
||||||
} else {
|
|
||||||
// 0x91: Begin Image at Current Position (G_GBIMGC)
|
|
||||||
if (payloadLen >= 4 && idx + 2 + payloadLen <= end) {
|
|
||||||
int w = readCoord(inputData, idx + 2);
|
|
||||||
int h = readCoord(inputData, idx + 4);
|
|
||||||
int bitDepth = GocaConstants.BPP_1;
|
|
||||||
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
|
||||||
if (payloadLen >= 5) {
|
|
||||||
int fmt = inputData[idx + 6] & 0xFF;
|
|
||||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
|
||||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
|
||||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
|
||||||
}
|
|
||||||
if (payloadLen >= 6) {
|
|
||||||
compression = inputData[idx + 7] & 0xFF;
|
|
||||||
}
|
|
||||||
beginImage(curX, curY, w, h, bitDepth, compression);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
}
|
}
|
||||||
|
int w = 0, h = 0;
|
||||||
|
int bitDepth = GocaConstants.BPP_1;
|
||||||
|
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||||
|
if (payloadLen >= 6 && idx + 2 + payloadLen <= end) {
|
||||||
|
w = readCoord(inputData, idx + 4);
|
||||||
|
h = readCoord(inputData, idx + 6);
|
||||||
|
if (payloadLen >= 7) {
|
||||||
|
int fmt = inputData[idx + 8] & 0xFF;
|
||||||
|
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||||
|
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||||
|
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||||
|
}
|
||||||
|
if (payloadLen >= 8) {
|
||||||
|
compression = inputData[idx + 9] & 0xFF;
|
||||||
|
}
|
||||||
|
} else if (payloadLen >= 4 && idx + 2 + payloadLen <= end) {
|
||||||
|
w = readCoord(inputData, idx + 2);
|
||||||
|
h = readCoord(inputData, idx + 4);
|
||||||
|
if (payloadLen >= 5) {
|
||||||
|
int fmt = inputData[idx + 6] & 0xFF;
|
||||||
|
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||||
|
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||||
|
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||||
|
}
|
||||||
|
if (payloadLen >= 6) {
|
||||||
|
compression = inputData[idx + 7] & 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
beginImage(curX, curY, w, h, bitDepth, compression);
|
||||||
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70)
|
case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70)
|
||||||
@@ -986,21 +1003,39 @@ public class GocaDecoder {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GocaConstants.G_GBIMG: { // Begin Image (0xD1)
|
case GocaConstants.G_GBIMG: { // Begin Image (0xD1)
|
||||||
|
if (inImage) {
|
||||||
|
endImage();
|
||||||
|
}
|
||||||
if (payloadLen >= 8 && idx + 2 + payloadLen <= end) {
|
if (payloadLen >= 8 && idx + 2 + payloadLen <= end) {
|
||||||
int x = readCoord(inputData, idx + 2);
|
int x = readCoord(inputData, idx + 2);
|
||||||
int y = readCoord(inputData, idx + 4);
|
int y = readCoord(inputData, idx + 4);
|
||||||
int w = readCoord(inputData, idx + 6);
|
int w, h;
|
||||||
int h = readCoord(inputData, idx + 8);
|
|
||||||
int bitDepth = GocaConstants.BPP_1;
|
int bitDepth = GocaConstants.BPP_1;
|
||||||
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||||
if (payloadLen >= 9) {
|
|
||||||
int fmt = inputData[idx + 10] & 0xFF;
|
|
||||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
|
||||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
|
||||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
|
||||||
}
|
|
||||||
if (payloadLen >= 10) {
|
if (payloadLen >= 10) {
|
||||||
compression = inputData[idx + 11] & 0xFF;
|
w = readCoord(inputData, idx + 8);
|
||||||
|
h = readCoord(inputData, idx + 10);
|
||||||
|
if (payloadLen >= 11) {
|
||||||
|
int fmt = inputData[idx + 12] & 0xFF;
|
||||||
|
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||||
|
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||||
|
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||||
|
}
|
||||||
|
if (payloadLen >= 12) {
|
||||||
|
compression = inputData[idx + 13] & 0xFF;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w = readCoord(inputData, idx + 6);
|
||||||
|
h = readCoord(inputData, idx + 8);
|
||||||
|
if (payloadLen >= 9) {
|
||||||
|
int fmt = inputData[idx + 10] & 0xFF;
|
||||||
|
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||||
|
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||||
|
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||||
|
}
|
||||||
|
if (payloadLen >= 10) {
|
||||||
|
compression = inputData[idx + 11] & 0xFF;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
beginImage(x, y, w, h, bitDepth, compression);
|
beginImage(x, y, w, h, bitDepth, compression);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -418,9 +418,27 @@ public class GraphicsPlane implements PixelBuffer {
|
|||||||
return transform;
|
return transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Character cell dimensions matching Query Reply presentation space
|
||||||
|
private int charWidth = 9;
|
||||||
|
private int charHeight = 16;
|
||||||
|
|
||||||
|
public synchronized void setCharDimensions(int width, int height) {
|
||||||
|
if (width > 0) this.charWidth = width;
|
||||||
|
if (height > 0) this.charHeight = height;
|
||||||
|
this.transform.setDefaultCharMetrics(this.charWidth, this.charHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCharWidth() {
|
||||||
|
return charWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCharHeight() {
|
||||||
|
return charHeight;
|
||||||
|
}
|
||||||
|
|
||||||
public int getTotalWidth() {
|
public int getTotalWidth() {
|
||||||
int cols = screenCols > 0 ? screenCols : 80;
|
int cols = screenCols > 0 ? screenCols : 80;
|
||||||
return cols * 9;
|
return cols * charWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -457,7 +475,7 @@ public class GraphicsPlane implements PixelBuffer {
|
|||||||
*/
|
*/
|
||||||
public int getTotalHeight() {
|
public int getTotalHeight() {
|
||||||
int rows = screenRows > 0 ? screenRows : 24;
|
int rows = screenRows > 0 ? screenRows : 24;
|
||||||
return rows * 16;
|
return rows * charHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getXMax() {
|
public int getXMax() {
|
||||||
|
|||||||
@@ -70,8 +70,22 @@ public class TelnetFSM {
|
|||||||
|
|
||||||
private List<String> getCandidateTerminalTypes() {
|
private List<String> getCandidateTerminalTypes() {
|
||||||
List<String> list = new ArrayList<>();
|
List<String> list = new ArrayList<>();
|
||||||
|
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().trim().isEmpty()) {
|
||||||
|
currentLu = config.getLuName().trim();
|
||||||
|
}
|
||||||
|
|
||||||
if (config.getTerminalName() != null && !config.getTerminalName().trim().isEmpty()) {
|
if (config.getTerminalName() != null && !config.getTerminalName().trim().isEmpty()) {
|
||||||
list.add(config.getTerminalName().trim());
|
String tName = config.getTerminalName().trim();
|
||||||
|
if (currentLu != null && !currentLu.isEmpty() && !tName.contains("@")) {
|
||||||
|
list.add(tName + "@" + currentLu);
|
||||||
|
list.add(tName);
|
||||||
|
} else {
|
||||||
|
list.add(tName);
|
||||||
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
if (config.isDynamicModel()) {
|
if (config.isDynamicModel()) {
|
||||||
@@ -82,24 +96,41 @@ public class TelnetFSM {
|
|||||||
list.add("IBM-3279-2");
|
list.add("IBM-3279-2");
|
||||||
list.add("IBM-3278-2");
|
list.add("IBM-3278-2");
|
||||||
list.add("UNKNOWN");
|
list.add("UNKNOWN");
|
||||||
return list;
|
} else {
|
||||||
|
TerminalModel model = config.getModel();
|
||||||
|
list.add(model.getTerminalType());
|
||||||
|
list.add(model.getBaseTerminalType());
|
||||||
|
if (model.isColor()) {
|
||||||
|
try {
|
||||||
|
TerminalModel mono = TerminalModel.forModel(model.getModelNumber(), false);
|
||||||
|
list.add(mono.getTerminalType());
|
||||||
|
list.add(mono.getBaseTerminalType());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
if (model.getModelNumber() != 2) {
|
||||||
|
list.add("IBM-3279-2-E");
|
||||||
|
list.add("IBM-3279-2");
|
||||||
|
list.add("IBM-3278-2");
|
||||||
|
}
|
||||||
|
list.add("UNKNOWN");
|
||||||
}
|
}
|
||||||
TerminalModel model = config.getModel();
|
|
||||||
list.add(model.getTerminalType());
|
if (currentLu != null && !currentLu.isEmpty()) {
|
||||||
list.add(model.getBaseTerminalType());
|
List<String> luCandidates = new ArrayList<>();
|
||||||
if (model.isColor()) {
|
for (String item : list) {
|
||||||
try {
|
if (!"UNKNOWN".equalsIgnoreCase(item) && !item.contains("@")) {
|
||||||
TerminalModel mono = TerminalModel.forModel(model.getModelNumber(), false);
|
luCandidates.add(item + "@" + currentLu);
|
||||||
list.add(mono.getTerminalType());
|
}
|
||||||
list.add(mono.getBaseTerminalType());
|
}
|
||||||
} catch (Exception ignored) {}
|
for (String item : list) {
|
||||||
|
if (!"UNKNOWN".equalsIgnoreCase(item)) {
|
||||||
|
luCandidates.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
luCandidates.add("UNKNOWN");
|
||||||
|
return luCandidates;
|
||||||
}
|
}
|
||||||
if (model.getModelNumber() != 2) {
|
|
||||||
list.add("IBM-3279-2-E");
|
|
||||||
list.add("IBM-3279-2");
|
|
||||||
list.add("IBM-3278-2");
|
|
||||||
}
|
|
||||||
list.add("UNKNOWN");
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,6 +620,9 @@ public class TelnetFSM {
|
|||||||
out.write(IAC);
|
out.write(IAC);
|
||||||
out.write(SE);
|
out.write(SE);
|
||||||
sendBytes(out.toByteArray());
|
sendBytes(out.toByteArray());
|
||||||
|
if (termType.contains("@")) {
|
||||||
|
connectedLu = termType.substring(termType.indexOf('@') + 1).trim();
|
||||||
|
}
|
||||||
log.warning(">>> SENT SB TTYPE IS " + termType + " SE");
|
log.warning(">>> SENT SB TTYPE IS " + termType + " SE");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+116
@@ -82,4 +82,120 @@ public class DataStreamProcessorTest {
|
|||||||
assertNotNull(sentData.get());
|
assertNotNull(sentData.get());
|
||||||
assertEquals((byte) AID_ENTER, sentData.get()[0], "ReadBuffer must transmit operator's stored AID");
|
assertEquals((byte) AID_ENTER, sentData.get()[0], "ReadBuffer must transmit operator's stored AID");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCharacterAttributesPersistAcrossStartField() throws java.io.IOException {
|
||||||
|
// User's exact ISPF Option 0 sequence:
|
||||||
|
// SBA(7, 1) -> SF(prot,skip) -> SA(yellow) -> ' 4 ' -> SBA(7, 7) -> SF(prot,skip) -> 'DISPLAY'
|
||||||
|
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
|
||||||
|
stream.write(CMD_EW);
|
||||||
|
stream.write(0xC3); // WCC
|
||||||
|
|
||||||
|
// SBA(7, 1) -> address 7*80 + 1 = 561
|
||||||
|
byte[] addr1 = encodeAddress(561, 24, 80);
|
||||||
|
stream.write(ORDER_SBA);
|
||||||
|
stream.write(addr1);
|
||||||
|
|
||||||
|
// SF(prot, skip)
|
||||||
|
stream.write(ORDER_SF);
|
||||||
|
stream.write(FA_PRINTABLE | FA_PROTECT | FA_NUMERIC);
|
||||||
|
|
||||||
|
// SA(yellow) -> XA_FOREGROUND, COLOR_YELLOW (0xF6)
|
||||||
|
stream.write(ORDER_SA);
|
||||||
|
stream.write(XA_FOREGROUND);
|
||||||
|
stream.write(0xF6);
|
||||||
|
|
||||||
|
// Data: ' 4 '
|
||||||
|
stream.write(translator.stringToEbcdic(" 4 "));
|
||||||
|
|
||||||
|
// SBA(7, 7) -> address 7*80 + 7 = 567
|
||||||
|
byte[] addr2 = encodeAddress(567, 24, 80);
|
||||||
|
stream.write(ORDER_SBA);
|
||||||
|
stream.write(addr2);
|
||||||
|
|
||||||
|
// SF(prot, skip)
|
||||||
|
stream.write(ORDER_SF);
|
||||||
|
stream.write(FA_PRINTABLE | FA_PROTECT | FA_NUMERIC);
|
||||||
|
|
||||||
|
// Data: 'DISPLAY'
|
||||||
|
stream.write(translator.stringToEbcdic("DISPLAY"));
|
||||||
|
|
||||||
|
byte[] record = stream.toByteArray();
|
||||||
|
processor.processRecord(record, 0, record.length, true);
|
||||||
|
|
||||||
|
// Positions 562..566 (' 4 ') must be yellow (0xF6)
|
||||||
|
for (int i = 562; i <= 566; i++) {
|
||||||
|
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " should have yellow foreground (0xF6)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positions 568..574 ('DISPLAY') across the second SF must also retain yellow (0xF6)
|
||||||
|
for (int i = 568; i <= 574; i++) {
|
||||||
|
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " ('DISPLAY') should retain yellow foreground (0xF6)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now test that SA with XA_ALL resets character attributes to default (0)
|
||||||
|
java.io.ByteArrayOutputStream resetStream = new java.io.ByteArrayOutputStream();
|
||||||
|
resetStream.write(CMD_W);
|
||||||
|
resetStream.write(0xC3);
|
||||||
|
resetStream.write(ORDER_SA);
|
||||||
|
resetStream.write(XA_ALL);
|
||||||
|
resetStream.write(0x00);
|
||||||
|
resetStream.write(translator.stringToEbcdic("TEST"));
|
||||||
|
byte[] resetRecord = resetStream.toByteArray();
|
||||||
|
processor.processRecord(resetRecord, 0, resetRecord.length, true);
|
||||||
|
|
||||||
|
for (int i = 575; i < 579; i++) {
|
||||||
|
assertEquals((byte) 0, screen.getCell(i).fg, "Cell at " + i + " should have default foreground (0) after SA(XA_ALL)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCharacterAttributesPersistAcrossStartFieldExtended() throws java.io.IOException {
|
||||||
|
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
|
||||||
|
stream.write(CMD_EW);
|
||||||
|
stream.write(0xC3);
|
||||||
|
|
||||||
|
// SA(yellow)
|
||||||
|
stream.write(ORDER_SA);
|
||||||
|
stream.write(XA_FOREGROUND);
|
||||||
|
stream.write(0xF6);
|
||||||
|
|
||||||
|
// SFE with 1 pair (3270 FA)
|
||||||
|
stream.write(ORDER_SFE);
|
||||||
|
stream.write(0x01); // 1 pair
|
||||||
|
stream.write(XA_3270);
|
||||||
|
stream.write(FA_PRINTABLE);
|
||||||
|
|
||||||
|
// Data 'HELLO'
|
||||||
|
stream.write(translator.stringToEbcdic("HELLO"));
|
||||||
|
|
||||||
|
byte[] record = stream.toByteArray();
|
||||||
|
processor.processRecord(record, 0, record.length, true);
|
||||||
|
|
||||||
|
// Check cells of HELLO (positions 1..5) have fg == 0xF6
|
||||||
|
for (int i = 1; i <= 5; i++) {
|
||||||
|
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " should retain yellow foreground across SFE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCharacterAttributesResetOnErase() throws java.io.IOException {
|
||||||
|
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
|
||||||
|
stream.write(CMD_EW);
|
||||||
|
stream.write(0xC3);
|
||||||
|
stream.write(ORDER_SA);
|
||||||
|
stream.write(XA_FOREGROUND);
|
||||||
|
stream.write(0xF6);
|
||||||
|
stream.write(translator.stringToEbcdic("A"));
|
||||||
|
processor.processRecord(stream.toByteArray(), 0, stream.size(), true);
|
||||||
|
assertEquals((byte) 0xF6, screen.getCell(0).fg);
|
||||||
|
|
||||||
|
// New EW command resets attributes
|
||||||
|
java.io.ByteArrayOutputStream ewStream = new java.io.ByteArrayOutputStream();
|
||||||
|
ewStream.write(CMD_EW);
|
||||||
|
ewStream.write(0xC3);
|
||||||
|
ewStream.write(translator.stringToEbcdic("B"));
|
||||||
|
processor.processRecord(ewStream.toByteArray(), 0, ewStream.size(), true);
|
||||||
|
assertEquals((byte) 0, screen.getCell(0).fg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,6 +285,79 @@ public class GocaDecoderTest {
|
|||||||
assertTrue(plane.hasContent(), "Expected plane to have content after image decoding");
|
assertTrue(plane.hasContent(), "Expected plane to have content after image decoding");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testImonGddmImageRendering() throws Exception {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(1040, 1118);
|
||||||
|
plane.setCharDimensions(13, 26);
|
||||||
|
plane.setScreenDimensions(80, 43);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
assertEquals(1040, plane.getTotalWidth());
|
||||||
|
assertEquals(1118, plane.getTotalHeight());
|
||||||
|
assertEquals(520, plane.getXMax());
|
||||||
|
assertEquals(558, plane.getYMax());
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
// GSCP: Set Current Position (377, 494)
|
||||||
|
out.write(GocaConstants.G_GSCP);
|
||||||
|
out.write(0x04);
|
||||||
|
out.write(0x01); out.write(0x79); // X = 377
|
||||||
|
out.write(0x01); out.write(0xEE); // Y = 494
|
||||||
|
|
||||||
|
// GSCOL: Color 1 = Blue
|
||||||
|
out.write(GocaConstants.G_GSCOL);
|
||||||
|
out.write(0x01);
|
||||||
|
|
||||||
|
// G_GBIMGC (0x91): Begin Image Current Position: len=6, flags=0, w=80, h=80
|
||||||
|
out.write(GocaConstants.G_GBIMGC);
|
||||||
|
out.write(0x06);
|
||||||
|
out.write(0x00); out.write(0x00); // flags/format
|
||||||
|
out.write(0x00); out.write(0x50); // w = 80
|
||||||
|
out.write(0x00); out.write(0x50); // h = 80
|
||||||
|
|
||||||
|
// G_GIMD (0x92): 10 bytes scanline
|
||||||
|
out.write(GocaConstants.G_GIMD);
|
||||||
|
out.write(0x0A);
|
||||||
|
out.write(new byte[] { (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF });
|
||||||
|
|
||||||
|
// G_GEIMG (0x93): End Image with 2-byte operand (02 00 00 as sent by IMON)
|
||||||
|
out.write(GocaConstants.G_GEIMG);
|
||||||
|
out.write(0x02);
|
||||||
|
out.write(0x00); out.write(0x00);
|
||||||
|
|
||||||
|
// Subsequent order to verify stream synchronization: GSCOL Color 2 = Red
|
||||||
|
out.write(GocaConstants.G_GSCOL);
|
||||||
|
out.write(0x02);
|
||||||
|
|
||||||
|
// G_GBIMGC (0x91): Second image tile
|
||||||
|
out.write(GocaConstants.G_GBIMGC);
|
||||||
|
out.write(0x06);
|
||||||
|
out.write(0x00); out.write(0x00);
|
||||||
|
out.write(0x00); out.write(0x50);
|
||||||
|
out.write(0x00); out.write(0x50);
|
||||||
|
|
||||||
|
out.write(GocaConstants.G_GIMD);
|
||||||
|
out.write(0x0A);
|
||||||
|
out.write(new byte[] { (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA });
|
||||||
|
|
||||||
|
out.write(GocaConstants.G_GEIMG);
|
||||||
|
out.write(0x02);
|
||||||
|
out.write(0x00); out.write(0x00);
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeStream(stream, 0, stream.length);
|
||||||
|
|
||||||
|
assertTrue(plane.hasContent(), "Expected plane to have content after IMON GOCA decoding");
|
||||||
|
int mappedX = plane.mapX(377);
|
||||||
|
int mappedY = plane.mapY(494);
|
||||||
|
assertEquals(897, mappedX);
|
||||||
|
assertEquals(64, mappedY);
|
||||||
|
|
||||||
|
int[] rgb = plane.getRgbBuffer();
|
||||||
|
int pixelAtImg = rgb[mappedY * 1040 + mappedX];
|
||||||
|
assertNotEquals(0, pixelAtImg, "Pixel at image location should be rendered");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDirectObjectControlSf24() {
|
public void testDirectObjectControlSf24() {
|
||||||
haus.nightmare.lib3270j.screen.ScreenBuffer sb = new haus.nightmare.lib3270j.screen.ScreenBuffer(
|
haus.nightmare.lib3270j.screen.ScreenBuffer sb = new haus.nightmare.lib3270j.screen.ScreenBuffer(
|
||||||
|
|||||||
@@ -84,6 +84,41 @@ public class TelnetFSMTest {
|
|||||||
ConnectionConfig c4 = ConnectionConfig.parseHostString("non-e:vm.ibm.com", 23, TerminalModel.IBM_3279_4);
|
ConnectionConfig c4 = ConnectionConfig.parseHostString("non-e:vm.ibm.com", 23, TerminalModel.IBM_3279_4);
|
||||||
assertFalse(c4.isTn3270eEnabled());
|
assertFalse(c4.isTn3270eEnabled());
|
||||||
assertEquals("vm.ibm.com", c4.getHost());
|
assertEquals("vm.ibm.com", c4.getHost());
|
||||||
|
|
||||||
|
ConnectionConfig c5 = ConnectionConfig.parseHostString("00C2@mvs.hugfreevikings.wtf:1023", 23, TerminalModel.IBM_3279_4);
|
||||||
|
assertEquals("mvs.hugfreevikings.wtf", c5.getHost());
|
||||||
|
assertEquals(1023, c5.getPort());
|
||||||
|
assertEquals("00C2", c5.getLuName());
|
||||||
|
|
||||||
|
ConnectionConfig c6 = ConnectionConfig.parseHostString("L:TSO01@secure.mvs.com:992", 23, TerminalModel.IBM_3279_4);
|
||||||
|
assertTrue(c6.isUseTls());
|
||||||
|
assertEquals("secure.mvs.com", c6.getHost());
|
||||||
|
assertEquals(992, c6.getPort());
|
||||||
|
assertEquals("TSO01", c6.getLuName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPlainTn3270TTypeWithLuName() {
|
||||||
|
config.setTn3270eEnabled(false);
|
||||||
|
config.setLuName("00C2");
|
||||||
|
fsm.onConnected();
|
||||||
|
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE);
|
||||||
|
// Host sends SB TTYPE SEND
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
|
||||||
|
TelnetConstants.TELQUAL_SEND,
|
||||||
|
TelnetConstants.IAC, TelnetConstants.SE);
|
||||||
|
|
||||||
|
boolean foundTtypeLu = false;
|
||||||
|
for (byte[] pkt : connection.sentData) {
|
||||||
|
String s = new String(pkt, java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||||
|
if (s.contains("IBM-3279-4-E@00C2")) {
|
||||||
|
foundTtypeLu = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(foundTtypeLu, "Plain TN3270 TTYPE IS must append @<luName> when configured");
|
||||||
|
assertEquals("00C2", fsm.getConnectedLu());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user