diff --git a/.gitignore b/.gitignore index 01b729c..1cbe887 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.log .DS_Store *.jar build diff --git a/build/j3270/org/pubvm/j3270/J3270App.class b/build/j3270/org/pubvm/j3270/J3270App.class index e4eb9df..7251b12 100644 Binary files a/build/j3270/org/pubvm/j3270/J3270App.class and b/build/j3270/org/pubvm/j3270/J3270App.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class b/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class index 769c3cf..4268587 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class and b/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class differ diff --git a/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class b/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class index f66aa83..244f2ca 100644 Binary files a/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class and b/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class differ diff --git a/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class b/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class index 53f50e6..afd2c0f 100644 Binary files a/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class and b/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class differ diff --git a/build/lib3270j/org/lib3270j/input/InputProcessor.class b/build/lib3270j/org/lib3270j/input/InputProcessor.class index 218dd86..e0afc12 100644 Binary files a/build/lib3270j/org/lib3270j/input/InputProcessor.class and b/build/lib3270j/org/lib3270j/input/InputProcessor.class differ diff --git a/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class b/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class index bf938e2..5f4c7a0 100644 Binary files a/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class and b/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class differ diff --git a/j3270/src/main/java/org/pubvm/j3270/J3270App.java b/j3270/src/main/java/org/pubvm/j3270/J3270App.java index 829ae1f..0aae383 100644 --- a/j3270/src/main/java/org/pubvm/j3270/J3270App.java +++ b/j3270/src/main/java/org/pubvm/j3270/J3270App.java @@ -438,16 +438,37 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate } } - // Configure logging over the absolute root logger + // Configure logging for application packages Logger globalRoot = Logger.getLogger(""); for (java.util.logging.Handler h : globalRoot.getHandlers()) { globalRoot.removeHandler(h); } - globalRoot.setLevel(debug ? Level.ALL : Level.WARNING); + globalRoot.setLevel(Level.WARNING); + ConsoleHandler handler = new ConsoleHandler(); - handler.setLevel(Level.ALL); + handler.setLevel(debug ? Level.ALL : Level.INFO); handler.setFormatter(new SimpleFormatter()); - globalRoot.addHandler(handler); + + Logger appLogger = Logger.getLogger("org.pubvm.j3270"); + appLogger.setLevel(Level.ALL); + appLogger.addHandler(handler); + appLogger.setUseParentHandlers(false); + + Logger libLogger = Logger.getLogger("org.lib3270j"); + libLogger.setLevel(Level.ALL); + libLogger.addHandler(handler); + libLogger.setUseParentHandlers(false); + + try { + java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false); + fileHandler.setLevel(Level.ALL); + fileHandler.setFormatter(new SimpleFormatter()); + appLogger.addHandler(fileHandler); + libLogger.addHandler(fileHandler); + log.info("Logging protocol trace to j3270.log"); + } catch (Exception e) { + System.err.println("Could not create j3270.log: " + e.getMessage()); + } // Load INI config file if specified if (configFile != null) { diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java b/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java index 190daaf..6514486 100644 --- a/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java @@ -889,10 +889,20 @@ public class TerminalPanel extends JPanel { if (faIsHigh(currentFA & 0xFF)) bold = true; - // Handle invisible fields (zero intensity) + // Handle invisible fields (zero intensity / password fields) + // Modern UX: render '*' for typed characters so user sees length/digit count if (faIsZero(currentFA & 0xFF)) { g2.setColor(this.bgColor); g2.fillRect(x, y, cellWidth, cellHeight); + + char ch = ea.ucs4; + if (ch > 0x20 && ch != 0xFF) { + Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont; + g2.setFont(f); + g2.setColor(fgColor); + g2.drawString("*", x, y + fontAscent); + } + if (isCellSelected(row, col)) { g2.setColor(SELECTION_COLOR); g2.fillRect(x, y, cellWidth, cellHeight); diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java index 6d9795b..b70fdf2 100644 --- a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java @@ -6,6 +6,7 @@ import org.lib3270j.charset.EbcdicTranslator; import org.lib3270j.listener.ScreenUpdateListener; import static org.lib3270j.protocol.DS3270Constants.*; +import java.io.ByteArrayOutputStream; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Logger; @@ -451,8 +452,8 @@ public class DataStreamProcessor { pos += 4; } - // Fill from current position to target - while (baddr != toAddr) { + // Fill from current position to target (do-while wraps entire buffer if baddr == toAddr) + do { ExtendedAttribute ea = screen.getCell(baddr); ea.fa = 0; // Destroy previous field attribute if any ea.ec = (byte) fillChar; @@ -461,7 +462,7 @@ public class DataStreamProcessor { ea.gr = (byte) currentGr; ea.cs = fillCs; baddr = (baddr + 1) % size; - } + } while (baddr != toAddr); screen.setBufferAddress(baddr); lastWasOrder = true; break; @@ -476,7 +477,7 @@ public class DataStreamProcessor { if (toAddr >= size) toAddr = toAddr % size; - while (baddr != toAddr) { + do { ExtendedAttribute ea = screen.getCell(baddr); if (!ea.isFieldAttribute()) { int faAddr = screen.findFieldAttribute(baddr); @@ -491,7 +492,7 @@ public class DataStreamProcessor { } } baddr = (baddr + 1) % size; - } + } while (baddr != toAddr); screen.setBufferAddress(baddr); pos += 3; lastWasOrder = true; @@ -637,7 +638,13 @@ public class DataStreamProcessor { outputPos = 0; - int aid = AID_NO; // Last AID + int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0) + ? inputProcessor.getLastAid() + : AID_NO; + if (inputProcessor != null) { + inputProcessor.setLastAid(AID_NO); + } + int size = screen.getRows() * screen.getCols(); // AID byte @@ -649,31 +656,56 @@ public class DataStreamProcessor { outputWrite(caddr[1] & 0xFF); if (!screen.isFormatted()) { - // Unformatted: send everything - if (all) { - for (int i = 0; i < size; i++) { - outputWrite(screen.getCell(i).ec & 0xFF); + // Unformatted screen: send data up to last non-null + int lastNonNull = -1; + for (int i = size - 1; i >= 0; i--) { + if (screen.getCell(i).ec != 0) { + lastNonNull = i; + break; + } + } + if (lastNonNull >= 0) { + for (int i = 0; i <= lastNonNull; i++) { + int b = screen.getCell(i).ec & 0xFF; + outputWrite(b != 0 ? b : 0x40); } } } else { - // Formatted: send modified fields + // Formatted screen: send modified fields with trailing NULLs stripped for (int i = 0; i < size; i++) { ExtendedAttribute ea = screen.getCell(i); if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) { int fieldStart = (i + 1) % size; - // Send SBA for field start + + // Collect field data and find last non-null byte + ByteArrayOutputStream fieldData = new ByteArrayOutputStream(); + int pos = fieldStart; + int lastNonNull = -1; + int fieldLen = 0; + while (!screen.getCell(pos).isFieldAttribute()) { + int b = screen.getCell(pos).ec & 0xFF; + fieldData.write(b); + if (b != 0x00) { + lastNonNull = fieldLen; + } + fieldLen++; + pos = (pos + 1) % size; + if (pos == fieldStart) + break; + } + + // Always send SBA and address outputWrite(ORDER_SBA); byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols()); outputWrite(addr[0] & 0xFF); outputWrite(addr[1] & 0xFF); - // Send field contents until next FA - int pos = fieldStart; - while (pos < size && !screen.getCell(pos).isFieldAttribute()) { - outputWrite(screen.getCell(pos).ec & 0xFF); - pos = (pos + 1) % size; - if (pos == fieldStart) - break; + // Send field data (strip trailing nulls) + if (lastNonNull >= 0) { + byte[] allData = fieldData.toByteArray(); + for (int k = 0; k <= lastNonNull; k++) { + outputWrite(allData[k] & 0xFF); + } } } } diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java index df5bf52..6ae25c2 100644 --- a/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java +++ b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java @@ -265,20 +265,21 @@ public class QueryReplyBuilder { private byte[] buildImplicitPartition(int maxCols, int maxRows) { ByteArrayOutputStream out = new ByteArrayOutputStream(22); - // Implicit partition sizes, 2 self-defining parameters + // Implicit partition sizes SDP (Self-Defining Parameter) - // SDP 1: Default screen size out.write(0x00); // flags out.write(0x00); // reserved - out.write(0x0b); // SDP length + // SDP 1: Implicit partition sizes + out.write(0x00); // SDP length high byte + out.write(0x0b); // SDP length low byte (11 bytes: 2 len + 1 type + 1 res + 8 dims) out.write(0x01); // SDP type: implicit partition sizes out.write(0x00); // reserved - // Default + // Default dimensions (Model 2: 80x24) out.write((MODEL_2_COLS >> 8) & 0xFF); out.write(MODEL_2_COLS & 0xFF); out.write((MODEL_2_ROWS >> 8) & 0xFF); out.write(MODEL_2_ROWS & 0xFF); - // Alternate + // Alternate dimensions out.write((maxCols >> 8) & 0xFF); out.write(maxCols & 0xFF); out.write((maxRows >> 8) & 0xFF); diff --git a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java index 688210e..959d36d 100644 --- a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java @@ -138,9 +138,10 @@ public class InputProcessor { } // Advance cursor + int startAdvance = baddr; baddr = (baddr + 1) % size; - // Skip over field attributes - while (screen.getCell(baddr).isFieldAttribute()) { + // Skip over field attributes safely + while (screen.getCell(baddr).isFieldAttribute() && baddr != startAdvance) { baddr = (baddr + 1) % size; } screen.setCursorAddress(baddr); @@ -255,18 +256,26 @@ public class InputProcessor { } } } else { - // Unformatted screen in 3270 mode: - // Send character data from address 0 up to last non-null character - int size = screen.getRows() * screen.getCols(); - int lastNonNull = -1; - for (int i = size - 1; i >= 0; i--) { - if (screen.getCell(i).ec != 0) { - lastNonNull = i; + // Unformatted screen in 3270 mode (e.g. line-mode console): + // Send AID + cursor address + only the active input line (row containing cursor) + int cols = screen.getCols(); + int curAddr = screen.getCursorAddress(); + int row = curAddr / cols; + int rowStart = row * cols; + int rowEnd = rowStart + cols; + + // Find last non-null, non-blank character on the current line + int lastChar = rowStart - 1; + for (int i = rowEnd - 1; i >= rowStart; i--) { + int ec = screen.getCell(i).ec & 0xFF; + if (ec != 0x00 && ec != 0x40) { + lastChar = i; break; } } - if (lastNonNull >= 0) { - for (int i = 0; i <= lastNonNull; i++) { + + if (lastChar >= rowStart) { + for (int i = rowStart; i <= lastChar; i++) { int b = screen.getCell(i).ec & 0xFF; out.write(b != 0 ? b : 0x40); } @@ -325,6 +334,13 @@ public class InputProcessor { screen.setCursorAddress(addr); } + public int getLastAid() { return lastAid; } + public void setLastAid(int aid) { this.lastAid = aid; } + + public void setCursorAddress(int baddr) { + screen.setCursorAddress(baddr); + } + public void backTab() { // Find previous unprotected field int addr = screen.getCursorAddress(); diff --git a/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java index 1ce34c0..ded53a8 100644 --- a/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java +++ b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java @@ -132,10 +132,14 @@ public class TelnetConnection { if (log.isLoggable(Level.FINE)) { log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n)); } - for (int i = 0; i < n; i++) { - fsm.feedByte(buf[i] & 0xFF); + try { + for (int i = 0; i < n; i++) { + fsm.feedByte(buf[i] & 0xFF); + } + fsm.endOfNetworkData(); + } catch (Throwable t) { + log.log(Level.SEVERE, "Exception processing incoming data stream", t); } - fsm.endOfNetworkData(); } } } catch (SocketException e) { @@ -148,6 +152,11 @@ public class TelnetConnection { log.log(Level.WARNING, "Read error", e); fsm.onError("Read error: " + e.getMessage()); } + } catch (Throwable t) { + if (running) { + log.log(Level.SEVERE, "Unexpected fatal error in readLoop", t); + fsm.onError("Network loop error: " + t.getMessage()); + } } }