Fix bugs reported in Discord
This commit is contained in:
@@ -1002,8 +1002,72 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
"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 ==========
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
boolean debug = 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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
+24
-11
@@ -54,6 +54,12 @@ public class DataStreamProcessor {
|
||||
private boolean unlockSysPending = 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. */
|
||||
@FunctionalInterface
|
||||
public interface OutputSender {
|
||||
@@ -240,6 +246,10 @@ public class DataStreamProcessor {
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
log.info(">>> EAU: erasing all unprotected fields");
|
||||
screen.eraseAllUnprotected();
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
break;
|
||||
case CMD_WSF:
|
||||
case SNA_CMD_WSF:
|
||||
@@ -378,11 +388,19 @@ public class DataStreamProcessor {
|
||||
if (wccReset(wcc)) {
|
||||
// Reset all character attributes to defaults
|
||||
log.fine("WCC reset: clearing default attributes");
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
}
|
||||
|
||||
if (eraseFirst) {
|
||||
screen.clear();
|
||||
log.fine("Cleared screen for Erase/Write");
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
}
|
||||
|
||||
// Process orders and data starting at byte 2
|
||||
@@ -390,9 +408,6 @@ public class DataStreamProcessor {
|
||||
int end = offset + length;
|
||||
int baddr = screen.getBufferAddress();
|
||||
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;
|
||||
|
||||
while (pos < end) {
|
||||
@@ -427,10 +442,6 @@ public class DataStreamProcessor {
|
||||
// FA position is a display position that shows as blank
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = ' ';
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
screen.setFormatted(true);
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
@@ -455,10 +466,6 @@ public class DataStreamProcessor {
|
||||
ea.clear();
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = ' ';
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
|
||||
for (int i = 0; i < nPairs; i++) {
|
||||
int attrType = data[pos + 2 + i * 2] & 0xFF;
|
||||
@@ -485,6 +492,12 @@ public class DataStreamProcessor {
|
||||
int attrType = data[pos + 1] & 0xFF;
|
||||
int attrValue = data[pos + 2] & 0xFF;
|
||||
switch (attrType) {
|
||||
case XA_ALL:
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
break;
|
||||
case XA_FOREGROUND:
|
||||
currentFg = (byte) attrValue;
|
||||
break;
|
||||
|
||||
+116
@@ -82,4 +82,120 @@ public class DataStreamProcessorTest {
|
||||
assertNotNull(sentData.get());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user