Add headless server and setup script

This commit is contained in:
2026-09-08 11:38:38 -04:00
parent f8fc0e4b22
commit 2553990b16
14 changed files with 2256 additions and 19 deletions
+1
View File
@@ -9,3 +9,4 @@ build/*
*.jar
build
.gradle
*.txt
+33 -4
View File
@@ -132,14 +132,43 @@ java -jar j3270.jar -d mainframe.example.com
## 🛠️ Building from Source
### Standalone Build (No Gradle Required)
The project provides self-contained, portable build scripts requiring only a JDK:
### Quick Setup & Build for End Users
Self-contained setup scripts automatically locate an installed Java 11+ JDK (searching `JAVA_HOME`, system `PATH`, macOS `java_home`, Homebrew, Linux JVM paths, Windows Registry, standard installation directories, and package managers), compile `lib3270j` and `j3270`, and package the standalone executable JAR (`build/j3270.jar`):
**Linux / macOS (POSIX Shell):**
```bash
# Setup and build standalone executable JAR:
./setup.sh
# Build and immediately launch:
./setup.sh --run
# Clean build and connect to host:
./setup.sh --clean --run -- mainframe.example.com 23 4
```
**Windows (PowerShell):**
```powershell
# Setup and build standalone executable JAR:
.\setup.ps1
# Build and immediately launch:
.\setup.ps1 -Run
# Clean build and connect to host:
.\setup.ps1 -Clean -Run -- mainframe.example.com 23 4
```
*(Also available as `.\setup.ps`)*
### Standalone Build (Advanced / CI)
The project also provides low-level scripts and optional Gradle support:
```bash
# Build standalone executable JAR (build/j3270.jar):
# Direct compile and package without diagnostics:
sh ./build_all.sh
# Run all 539 automated unit tests across 88 test containers (~7s):
# Run all 539+ automated unit tests (~7s):
sh ./test_all.sh
```
+2
View File
@@ -17,6 +17,7 @@ subprojects {
test {
useJUnitPlatform()
jvmArgs '-Djava.awt.headless=true'
testLogging {
events "passed", "skipped", "failed"
}
@@ -24,5 +25,6 @@ subprojects {
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
}
+21 -5
View File
@@ -14,16 +14,32 @@ if [ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/javac" ] && [ -x "$JAVA_HOME/bin/
JAVAC_BIN="$JAVA_HOME/bin/javac"
JAR_BIN="$JAVA_HOME/bin/jar"
elif command -v javac >/dev/null 2>&1 && command -v jar >/dev/null 2>&1; then
if javac -version >/dev/null 2>&1; then
JAVAC_BIN="$(command -v javac)"
JAR_BIN="$(command -v jar)"
else
# Check common SDKMAN / macOS / Linux JDK locations
for h in "$HOME/.sdkman/candidates/java/current" \
fi
fi
if [ -z "$JAVAC_BIN" ] || [ -z "$JAR_BIN" ]; then
if [ -x /usr/libexec/java_home ]; then
MAC_JAVA_HOME="$(/usr/libexec/java_home 2>/dev/null || true)"
if [ -n "$MAC_JAVA_HOME" ] && [ -x "$MAC_JAVA_HOME/bin/javac" ] && [ -x "$MAC_JAVA_HOME/bin/jar" ]; then
export JAVA_HOME="$MAC_JAVA_HOME"
JAVAC_BIN="$JAVA_HOME/bin/javac"
JAR_BIN="$JAVA_HOME/bin/jar"
fi
fi
fi
if [ -z "$JAVAC_BIN" ] || [ -z "$JAR_BIN" ]; then
# Check standard system JDK installation locations
for h in /usr/lib/jvm/default-java \
/usr/lib/jvm/java-21-openjdk* \
/usr/lib/jvm/java-17-openjdk* \
/usr/lib/jvm/java-11-openjdk* \
/usr/lib/jvm/default-java \
/Library/Java/JavaVirtualMachines/*/Contents/Home; do
/Library/Java/JavaVirtualMachines/*/Contents/Home \
/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home \
/opt/homebrew/opt/openjdk; do
if [ -d "$h" ] && [ -x "$h/bin/javac" ] && [ -x "$h/bin/jar" ]; then
export JAVA_HOME="$h"
JAVAC_BIN="$JAVA_HOME/bin/javac"
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+248 -1
View File
@@ -1,2 +1,249 @@
#!/bin/sh
exec "$(dirname "$0")/build_all.sh" "$@"
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+92
View File
@@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -112,4 +112,48 @@ public class LiveHostHeadlessTest {
client.disconnect();
}
}
@Test
@DisplayName("Logon to live MVS host with guest account 'headless'")
public void testLiveMvsGuestLogin() throws Exception {
boolean reachable = false;
try (Socket probe = new Socket()) {
probe.connect(new InetSocketAddress(HOST, PORT), 3000);
reachable = true;
} catch (IOException e) {
logger.warning("Live host " + HOST + ":" + PORT + " unreachable: " + e.getMessage());
}
Assumptions.assumeTrue(reachable, "Skipping live test: " + HOST + ":" + PORT + " is not reachable");
ConnectionConfig config = new ConnectionConfig(HOST, PORT, TerminalModel.IBM_3279_2);
config.setConnectTimeoutMs(5000);
config.setSoTimeoutMs(10000);
Telnet3270Client client = new Telnet3270Client(config);
try {
boolean connected = client.connect(10000);
Assumptions.assumeTrue(connected, "Host connection established");
// Wait for initial screen buffer to populate
Thread.sleep(1500);
ScreenBuffer sb = client.getScreenBuffer();
String initialText = sb != null ? sb.getText().trim() : "";
logger.info("Initial screen before logon: " +
initialText.substring(0, Math.min(150, initialText.length())).replaceAll("\\s+", " "));
// Send guest account 'headless'
client.sendKeys("headless[enter]");
Thread.sleep(2000);
String responseText = sb != null ? sb.getText().trim() : "";
logger.info("Screen after typing 'headless': " +
responseText.substring(0, Math.min(150, responseText.length())).replaceAll("\\s+", " "));
assertNotNull(responseText, "Screen response should not be null");
assertFalse(responseText.isEmpty(), "Screen response should not be empty");
} finally {
client.disconnect();
}
}
}
@@ -0,0 +1,570 @@
package haus.nightmare.lib3270j.testutil;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import haus.nightmare.lib3270j.protocol.TN3270EConstants;
import haus.nightmare.lib3270j.protocol.TelnetConstants;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static haus.nightmare.lib3270j.protocol.TN3270EConstants.*;
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
/**
* Lightweight, in-memory mock TN3270E server running on loopback (127.0.0.1).
*
* Implements RFC 2355 Telnet negotiation, Device-Type subnegotiation,
* BIND image dispatch, formatted 3270 data stream framing (Write / Erase-Write),
* Contention Resolution (SDI / KRI), Keep-Alive tracking, and disconnect simulation.
*/
public class Mock3270Server implements AutoCloseable {
private static final Logger log = Logger.getLogger(Mock3270Server.class.getName());
private final ServerSocket serverSocket;
private final int port;
private final EbcdicTranslator translator;
private final ExecutorService executor = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r, "Mock3270Server-Worker");
t.setDaemon(true);
return t;
});
private volatile boolean running = true;
private volatile Socket currentSocket;
private volatile OutputStream clientOut;
private volatile InputStream clientIn;
private boolean negotiateBindImage = true;
private boolean negotiateContentionResolution = true;
private boolean autoSendBind = true;
private String assignedDeviceType = "IBM-3279-2-E";
private String assignedLu = "MOCKLU01";
private int primaryRows = 24;
private int primaryCols = 80;
private int altRows = 24;
private int altCols = 80;
private int sequenceNumber = 0;
private final List<byte[]> receivedRecords = new CopyOnWriteArrayList<>();
private final AtomicInteger receivedNops = new AtomicInteger(0);
private final AtomicInteger receivedTimingMarks = new AtomicInteger(0);
private volatile CountDownLatch connectionLatch = new CountDownLatch(1);
private volatile CountDownLatch handshakeLatch = new CountDownLatch(1);
private volatile CountDownLatch dataReceivedLatch = new CountDownLatch(1);
public Mock3270Server() throws IOException {
this(0);
}
public Mock3270Server(int requestedPort) throws IOException {
this.serverSocket = new ServerSocket(requestedPort, 10, InetAddress.getByName("127.0.0.1"));
this.port = serverSocket.getLocalPort();
this.translator = new EbcdicTranslator();
executor.submit(this::acceptLoop);
log.info("Mock3270Server started on 127.0.0.1:" + port);
}
public int getPort() {
return port;
}
public void setNegotiateBindImage(boolean negotiate) {
this.negotiateBindImage = negotiate;
}
public void setNegotiateContentionResolution(boolean cr) {
this.negotiateContentionResolution = cr;
}
public void setAutoSendBind(boolean autoSendBind) {
this.autoSendBind = autoSendBind;
}
public void setAssignedDeviceType(String assignedDeviceType) {
this.assignedDeviceType = assignedDeviceType;
}
public void setAssignedLu(String assignedLu) {
this.assignedLu = assignedLu;
}
public void setDimensions(int pRows, int pCols, int aRows, int aCols) {
this.primaryRows = pRows;
this.primaryCols = pCols;
this.altRows = aRows;
this.altCols = aCols;
}
public boolean waitForConnection(long timeout, TimeUnit unit) throws InterruptedException {
if (currentSocket != null && currentSocket.isConnected() && !currentSocket.isClosed()) {
return true;
}
return connectionLatch.await(timeout, unit);
}
public boolean waitForHandshake(long timeout, TimeUnit unit) throws InterruptedException {
return handshakeLatch.await(timeout, unit);
}
public boolean waitForClientData(long timeout, TimeUnit unit) throws InterruptedException {
if (!receivedRecords.isEmpty()) {
return true;
}
return dataReceivedLatch.await(timeout, unit);
}
public List<byte[]> getReceivedRecords() {
return Collections.unmodifiableList(receivedRecords);
}
public byte[] getLastReceivedRecord() {
if (receivedRecords.isEmpty()) return null;
return receivedRecords.get(receivedRecords.size() - 1);
}
public int getReceivedNops() {
return receivedNops.get();
}
public int getReceivedTimingMarks() {
return receivedTimingMarks.get();
}
public void resetDataLatch() {
dataReceivedLatch = new CountDownLatch(1);
}
private void acceptLoop() {
while (running) {
try {
Socket s = serverSocket.accept();
s.setTcpNoDelay(true);
log.info("Mock3270Server: accepted client connection from " + s.getRemoteSocketAddress());
synchronized (this) {
this.currentSocket = s;
this.clientIn = new BufferedInputStream(s.getInputStream());
this.clientOut = new BufferedOutputStream(s.getOutputStream());
}
connectionLatch.countDown();
executor.submit(() -> handleClient(s));
} catch (SocketException e) {
if (!running) break;
} catch (Exception e) {
if (running) {
log.log(Level.WARNING, "Error in acceptLoop", e);
}
}
}
}
private void handleClient(Socket s) {
try {
// 1. Initial Telnet option offer
sendInitialNegotiation();
// 2. Read incoming telnet options and data loop
ByteArrayOutputStream recordBuf = new ByteArrayOutputStream();
ByteArrayOutputStream sbBuf = new ByteArrayOutputStream();
int b;
boolean inIAC = false;
int iacCommand = -1;
boolean inSB = false;
while (running && !s.isClosed() && (b = clientIn.read()) != -1) {
b &= 0xFF;
if (inSB) {
if (inIAC) {
if (b == SE) {
inSB = false;
inIAC = false;
handleSubnegotiation(sbBuf.toByteArray());
sbBuf.reset();
} else if (b == IAC) {
sbBuf.write(IAC);
inIAC = false;
} else {
inIAC = false;
}
} else if (b == IAC) {
inIAC = true;
} else {
sbBuf.write(b);
}
continue;
}
if (inIAC) {
if (iacCommand == -1) {
iacCommand = b;
switch (b) {
case IAC: // Escaped 0xFF in data stream
recordBuf.write(IAC);
inIAC = false;
iacCommand = -1;
break;
case SB:
inSB = true;
inIAC = false;
iacCommand = -1;
sbBuf.reset();
break;
case EOR:
// End of record framing
byte[] fullRecord = recordBuf.toByteArray();
recordBuf.reset();
inIAC = false;
iacCommand = -1;
onRecordReceived(fullRecord);
break;
case NOP:
receivedNops.incrementAndGet();
log.fine("Mock3270Server: RCVD IAC NOP");
inIAC = false;
iacCommand = -1;
break;
case DO:
case DONT:
case WILL:
case WONT:
// Option follows next byte
break;
default:
inIAC = false;
iacCommand = -1;
break;
}
} else {
// 3-byte command: IAC <DO/DONT/WILL/WONT> <OPT>
handleOptionCommand(iacCommand, b);
inIAC = false;
iacCommand = -1;
}
} else if (b == IAC) {
inIAC = true;
iacCommand = -1;
} else {
recordBuf.write(b);
}
}
} catch (SocketException e) {
log.fine("Client socket closed: " + e.getMessage());
} catch (Exception e) {
if (running) {
log.log(Level.WARNING, "Error in handleClient", e);
}
} finally {
cleanupClientSocket(s);
}
}
private synchronized void sendInitialNegotiation() throws IOException {
if (clientOut == null) return;
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Server offers DO TN3270E, DO TRANSMIT-BINARY, WILL TRANSMIT-BINARY, DO EOR, WILL EOR
out.write(new byte[] {
(byte) IAC, (byte) DO, (byte) TELOPT_TN3270E,
(byte) IAC, (byte) DO, (byte) TELOPT_BINARY,
(byte) IAC, (byte) WILL, (byte) TELOPT_BINARY,
(byte) IAC, (byte) DO, (byte) TELOPT_EOR,
(byte) IAC, (byte) WILL, (byte) TELOPT_EOR
});
clientOut.write(out.toByteArray());
clientOut.flush();
log.info("Mock3270Server: sent initial negotiation");
}
private void handleOptionCommand(int cmd, int opt) throws IOException {
log.fine("Mock3270Server: RCVD IAC " + cmd + " opt=" + opt);
if (cmd == WILL && opt == TELOPT_TN3270E) {
// Client accepted TN3270E -> Send SB TN3270E SEND DEVICE-TYPE IAC SE
sendDeviceTypeSend();
} else if (cmd == DO && opt == TELOPT_TM) {
// Client requested Timing Mark keep-alive
receivedTimingMarks.incrementAndGet();
log.fine("Mock3270Server: RCVD IAC DO TIMING-MARK");
synchronized (this) {
if (clientOut != null) {
clientOut.write(new byte[] { (byte) IAC, (byte) WILL, (byte) TELOPT_TM });
clientOut.flush();
}
}
}
}
private synchronized void sendDeviceTypeSend() throws IOException {
if (clientOut == null) return;
byte[] devTypeSend = new byte[] {
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
(byte) OP_SEND, (byte) OP_DEVICE_TYPE,
(byte) IAC, (byte) SE
};
clientOut.write(devTypeSend);
clientOut.flush();
log.info("Mock3270Server: sent SB TN3270E SEND DEVICE-TYPE SE");
}
private void handleSubnegotiation(byte[] sb) throws IOException {
if (sb.length < 2 || (sb[0] & 0xFF) != TELOPT_TN3270E) {
return;
}
int op = sb[1] & 0xFF;
if (op == OP_DEVICE_TYPE) {
// Client sent DEVICE-TYPE REQUEST <type> [CONNECT <lu>]
int pos = 2;
if (pos < sb.length && (sb[pos] & 0xFF) == OP_REQUEST) pos++;
StringBuilder reqType = new StringBuilder();
while (pos < sb.length && (sb[pos] & 0xFF) != OP_CONNECT) {
reqType.append((char) (sb[pos] & 0xFF));
pos++;
}
log.info("Mock3270Server: client requested device-type: " + reqType);
// Respond with SB TN3270E DEVICE-TYPE IS <assignedType> CONNECT <assignedLu> IAC SE
sendDeviceTypeIs(assignedDeviceType, assignedLu);
} else if (op == OP_FUNCTIONS) {
// Client sent FUNCTIONS REQUEST <functions...>
List<Integer> funcs = new ArrayList<>();
for (int i = 3; i < sb.length; i++) {
funcs.add(sb[i] & 0xFF);
}
log.info("Mock3270Server: client requested functions: " + funcs);
// Respond with FUNCTIONS IS
sendFunctionsIs();
if (autoSendBind && negotiateBindImage) {
sendBindImage();
}
handshakeLatch.countDown();
}
}
private synchronized void sendDeviceTypeIs(String type, String lu) throws IOException {
if (clientOut == null) return;
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(new byte[] { (byte) IAC, (byte) SB, (byte) TELOPT_TN3270E, (byte) OP_DEVICE_TYPE, (byte) OP_IS });
out.write(type.getBytes(java.nio.charset.StandardCharsets.US_ASCII));
if (lu != null && !lu.isEmpty()) {
out.write(OP_CONNECT);
out.write(lu.getBytes(java.nio.charset.StandardCharsets.US_ASCII));
}
out.write(new byte[] { (byte) IAC, (byte) SE });
clientOut.write(out.toByteArray());
clientOut.flush();
log.info("Mock3270Server: sent SB TN3270E DEVICE-TYPE IS " + type + " CONNECT " + lu + " SE");
}
private synchronized void sendFunctionsIs() throws IOException {
if (clientOut == null) return;
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(new byte[] { (byte) IAC, (byte) SB, (byte) TELOPT_TN3270E, (byte) OP_FUNCTIONS, (byte) OP_IS });
if (negotiateBindImage) {
out.write(FUNC_BIND_IMAGE);
}
out.write(FUNC_RESPONSES);
if (negotiateContentionResolution) {
out.write(FUNC_CONTENTION_RESOLUTION);
}
out.write(FUNC_SYSREQ);
out.write(new byte[] { (byte) IAC, (byte) SE });
clientOut.write(out.toByteArray());
clientOut.flush();
log.info("Mock3270Server: sent SB TN3270E FUNCTIONS IS SE");
}
public synchronized void sendBindImage() throws IOException {
if (clientOut == null) return;
// BIND payload: 5-byte header + standard 26-byte BIND image
byte[] bindPayload = new byte[26];
bindPayload[20] = (byte) primaryRows;
bindPayload[21] = (byte) primaryCols;
bindPayload[22] = (byte) altRows;
bindPayload[23] = (byte) altCols;
bindPayload[24] = (byte) ((altRows == primaryRows && altCols == primaryCols) ? 0x02 : 0x7F);
sendRecord(DT_BIND_IMAGE, 0, RSF_NO_RESPONSE, bindPayload);
log.info("Mock3270Server: sent BIND image (" + primaryRows + "x" + primaryCols + " / " + altRows + "x" + altCols + ")");
}
public synchronized void sendUnbind(int reason) throws IOException {
sendRecord(DT_UNBIND, 0, RSF_NO_RESPONSE, new byte[] { (byte) reason });
log.info("Mock3270Server: sent UNBIND reason=" + reason);
}
public synchronized void sendRecord(int dataType, int requestFlag, int responseFlag, byte[] payload) throws IOException {
if (clientOut == null) throw new IOException("No client connected");
sequenceNumber = (sequenceNumber + 1) & 0xFFFF;
ByteArrayOutputStream out = new ByteArrayOutputStream(payload.length + 16);
// 5-byte TN3270E header
out.write(dataType);
out.write(requestFlag);
out.write(responseFlag);
out.write((sequenceNumber >> 8) & 0xFF);
out.write(sequenceNumber & 0xFF);
// Escape payload IAC (0xFF -> 0xFF 0xFF)
for (byte b : payload) {
int val = b & 0xFF;
out.write(val);
if (val == IAC) {
out.write(IAC);
}
}
// End of Record framing
out.write(IAC);
out.write(EOR);
clientOut.write(out.toByteArray());
clientOut.flush();
}
public synchronized void sendEraseWrite(int wcc, byte[] data, boolean sdi, boolean kri) throws IOException {
ByteArrayOutputStream payload = new ByteArrayOutputStream(data.length + 2);
payload.write(CMD_ERASE_WRITE); // 0xF5
payload.write(wcc);
payload.write(data);
int rqf = (sdi ? RQF_SEND_DATA : 0) | (kri ? RQF_KEYBOARD_RESTORE : 0);
sendRecord(DT_3270_DATA, rqf, RSF_NO_RESPONSE, payload.toByteArray());
}
public synchronized void sendWrite(int wcc, byte[] data, boolean sdi, boolean kri) throws IOException {
ByteArrayOutputStream payload = new ByteArrayOutputStream(data.length + 2);
payload.write(CMD_WRITE); // 0xF1
payload.write(wcc);
payload.write(data);
int rqf = (sdi ? RQF_SEND_DATA : 0) | (kri ? RQF_KEYBOARD_RESTORE : 0);
sendRecord(DT_3270_DATA, rqf, RSF_NO_RESPONSE, payload.toByteArray());
}
public synchronized void sendFormattedScreen(String text, int row, int col, boolean unlockKeyboard) throws IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
int addr = row * primaryCols + col;
byte[] sba = DS3270Constants.encodeAddress(addr, primaryRows, primaryCols);
// SBA to target position
data.write(ORDER_SBA);
data.write(sba[0] & 0xFF);
data.write(sba[1] & 0xFF);
// Start Field (unprotected: 0x40 or 0x41)
data.write(ORDER_SF);
data.write(0x40);
// Write EBCDIC text
for (char c : text.toCharArray()) {
data.write(translator.unicodeToEbcdicSafe(c));
}
// Insert Cursor
data.write(ORDER_IC);
// WCC: 0xC3 (sound alarm + unlock keyboard + reset MDT) or 0xC0 (locked)
int wcc = unlockKeyboard ? 0xC3 : 0xC0;
sendEraseWrite(wcc, data.toByteArray(), true, unlockKeyboard);
}
public synchronized void sendFragmented(byte[] rawData, int chunkSize, long delayMs) throws IOException, InterruptedException {
if (clientOut == null) throw new IOException("No client connected");
int offset = 0;
while (offset < rawData.length) {
int len = Math.min(chunkSize, rawData.length - offset);
clientOut.write(rawData, offset, len);
clientOut.flush();
offset += len;
if (delayMs > 0 && offset < rawData.length) {
Thread.sleep(delayMs);
}
}
}
public synchronized void sendRawBytes(byte[] raw) throws IOException {
if (clientOut == null) throw new IOException("No client connected");
clientOut.write(raw);
clientOut.flush();
}
public synchronized void disconnectClient() {
if (currentSocket != null) {
try {
log.info("Mock3270Server: intentionally disconnecting client");
currentSocket.close();
} catch (Exception ignored) {}
cleanupClientSocket(currentSocket);
}
}
private void onRecordReceived(byte[] record) {
log.info("Mock3270Server: received record length=" + record.length);
receivedRecords.add(record);
dataReceivedLatch.countDown();
}
public int getLastReceivedAID() {
byte[] last = getLastReceivedRecord();
if (last == null || last.length <= EH_SIZE) return -1;
return last[EH_SIZE] & 0xFF;
}
public int getLastReceivedCursorAddress() {
byte[] last = getLastReceivedRecord();
if (last == null || last.length < EH_SIZE + 3) return -1;
int b1 = last[EH_SIZE + 1] & 0xFF;
int b2 = last[EH_SIZE + 2] & 0xFF;
return DS3270Constants.decodeAddress(b1, b2);
}
public String getLastReceivedFieldText() {
byte[] last = getLastReceivedRecord();
if (last == null || last.length <= EH_SIZE + 3) return "";
StringBuilder sb = new StringBuilder();
// Skip header + AID (1) + cursor address (2)
int i = EH_SIZE + 3;
while (i < last.length) {
int b = last[i] & 0xFF;
if (b == ORDER_SBA) {
i += 3;
continue;
}
sb.append(translator.ebcdicToUnicode(b));
i++;
}
return sb.toString();
}
private synchronized void cleanupClientSocket(Socket s) {
if (currentSocket == s) {
currentSocket = null;
clientIn = null;
clientOut = null;
}
try {
s.close();
} catch (Exception ignored) {}
}
@Override
public void close() {
running = false;
try {
serverSocket.close();
} catch (Exception ignored) {}
disconnectClient();
executor.shutdownNow();
log.info("Mock3270Server stopped");
}
}
@@ -0,0 +1,293 @@
package haus.nightmare.lib3270j.testutil;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import haus.nightmare.lib3270j.ecl.ECLPS;
import haus.nightmare.lib3270j.ecl.ECLScreenDesc;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static haus.nightmare.lib3270j.protocol.TN3270EConstants.*;
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
import static org.junit.jupiter.api.Assertions.*;
@Timeout(value = 30, unit = TimeUnit.SECONDS)
public class Mock3270ServerIntegrationTest {
private Mock3270Server server;
private Telnet3270Client client;
@BeforeEach
public void setup() throws IOException {
server = new Mock3270Server();
}
@AfterEach
public void teardown() {
if (client != null) {
client.disconnect();
client = null;
}
if (server != null) {
server.close();
server = null;
}
}
private void waitForState(Telnet3270Client cl, ConnectionState targetState, long timeoutMs) throws Exception {
long deadline = System.currentTimeMillis() + timeoutMs;
while (System.currentTimeMillis() < deadline) {
if (cl.getConnectionState() == targetState) {
return;
}
Thread.sleep(25);
}
assertEquals(targetState, cl.getConnectionState());
}
@Test
public void testSuccessfulConnectionAndHandshake() throws Exception {
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
config.setConnectTimeoutMs(5000);
client = new Telnet3270Client(config);
boolean connected = client.connect(8000);
assertTrue(connected, "Client should establish connected session");
boolean handshakeDone = server.waitForHandshake(5, TimeUnit.SECONDS);
assertTrue(handshakeDone, "Mock server should complete TN3270E handshake");
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
assertEquals("MOCKLU01", client.getTelnetFSM().getConnectedLu());
}
@Test
public void testSendScreenAndVerifyPresentationSpace() throws Exception {
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
client = new Telnet3270Client(config);
assertTrue(client.connect(8000));
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS));
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
// Server sends formatted greeting screen at row 2, col 10
String banner = "WELCOME TO TN3270E SERVER";
server.sendFormattedScreen(banner, 2, 10, true);
// Wait up to 3s for screen update
ECLPS ps = client.getPS();
boolean matched = false;
long deadline = System.currentTimeMillis() + 3000;
while (System.currentTimeMillis() < deadline) {
String text = ps.getString(2, 11, banner.length());
if (banner.equals(text)) {
matched = true;
break;
}
Thread.sleep(20);
}
assertTrue(matched, "Presentation space should contain banner string");
assertFalse(client.getInputProcessor().isKeyboardLocked(), "Keyboard should be unlocked after WCC restore");
}
@Test
public void testClientUserInputAndAidTransmission() throws Exception {
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
client = new Telnet3270Client(config);
assertTrue(client.connect(8000));
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS));
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
// Send screen with unprotected field at row 4, col 10
server.sendFormattedScreen("ENTER DATA: ", 4, 10, true);
Thread.sleep(100);
// Client positions cursor inside unprotected field (col 23) and enters text
client.getPS().setCursorPos(4, 23);
client.sendKeys("TESTDATA[enter]");
// Verify mock server receives AID and field data
assertTrue(server.waitForClientData(5, TimeUnit.SECONDS), "Server must receive client AID submission");
assertEquals(0x7D, server.getLastReceivedAID(), "AID should be ENTER (0x7D)");
assertTrue(server.getLastReceivedFieldText().contains("TESTDATA"),
"Field text should contain submitted characters 'TESTDATA'");
}
@Test
public void testContentionResolutionAndKeyboardLock() throws Exception {
server.setNegotiateContentionResolution(true);
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
client = new Telnet3270Client(config);
client.getTelnetFSM().setNegotiateContentionResolution(true);
assertTrue(client.connect(8000));
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS));
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
// 1. Lock keyboard explicitly (as happens when user presses AID)
client.getInputProcessor().setKeyboardLocked(true);
assertTrue(client.getInputProcessor().isKeyboardLocked());
// Send write with WCC locked (0xC0) and sdi = false
byte[] dummyData = new byte[] { (byte) ORDER_SBA, 0x40, 0x40, (byte) ORDER_SF, 0x60 };
server.sendWrite(0xC0, dummyData, false, false);
Thread.sleep(100);
assertTrue(client.getInputProcessor().isKeyboardLocked(),
"Keyboard must remain locked when SDI is not signaled under contention resolution");
// 2. Unlock via Contention Resolution: send write with SDI flag = true (RQF_SEND_DATA)
server.sendWrite(0xC0, dummyData, true, false);
Thread.sleep(100);
assertFalse(client.getInputProcessor().isKeyboardLocked(),
"Keyboard must unlock upon receiving SDI flag on EOR");
}
@Test
public void testAutoSysUnlockOnEor() throws Exception {
server.setNegotiateContentionResolution(false);
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
config.setAutoSysUnlock(true);
client = new Telnet3270Client(config);
client.getTelnetFSM().setNegotiateContentionResolution(false);
assertTrue(client.connect(8000));
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS));
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
// Lock keyboard initially
client.getInputProcessor().setKeyboardLocked(true);
client.getOIA().setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
assertTrue(client.getInputProcessor().isKeyboardLocked());
// Host sends a write record without WCC restore, but autoSysUnlock is active on EOR
byte[] dummyData = new byte[] { (byte) ORDER_SBA, 0x40, 0x40, (byte) ORDER_SF, 0x60 };
server.sendWrite(0xC0, dummyData, false, false);
Thread.sleep(100);
assertFalse(client.getInputProcessor().isKeyboardLocked(),
"autoSysUnlock must unlock keyboard on EOR when not in Read command");
assertFalse(client.getOIA().isXSystem(), "OIA X SYSTEM indicator must be cleared");
}
@Test
public void testAutomatedWaitMethods() throws Exception {
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
client = new Telnet3270Client(config);
assertTrue(client.connect(8000));
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS));
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
ECLScreenDesc desc = new ECLScreenDesc();
desc.addString("LOGIN_READY");
CountDownLatch waitLatch = new CountDownLatch(1);
AtomicBoolean waitResult = new AtomicBoolean(false);
// Run waitForScreen in background thread
CompletableFuture.runAsync(() -> {
boolean res = client.getPS().waitForScreen(desc, 5000);
waitResult.set(res);
waitLatch.countDown();
});
// Delay 80ms then send matching screen from mock server
Thread.sleep(80);
server.sendFormattedScreen("LOGIN_READY", 1, 9, true);
boolean awakened = waitLatch.await(2000, TimeUnit.MILLISECONDS);
assertTrue(awakened, "waitForScreen must wake reactively upon screen update");
assertTrue(waitResult.get(), "waitForScreen must return true when screen matches descriptor");
}
@Test
public void testKeepAliveHeartbeatEngine() throws Exception {
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
config.setKeepAliveEnabled(true);
config.setKeepAliveIntervalSeconds(1);
config.setKeepAliveType("NOP");
client = new Telnet3270Client(config);
assertTrue(client.connect(8000));
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS));
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
// Idle for 2.5 seconds to allow keepalive heartbeat to fire
Thread.sleep(2500);
assertTrue(server.getReceivedNops() >= 1,
"Server must have received at least 1 IAC NOP heartbeat, received: " + server.getReceivedNops());
}
@Test
public void testAutoReconnectOnSocketDisconnect() throws Exception {
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
config.setAutoReconnect(true);
config.setReconnectMaxRetries(3);
client = new Telnet3270Client(config);
assertTrue(client.connect(8000));
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS));
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
// Disconnect active client connection abruptly
server.disconnectClient();
// Wait for client to detect disconnect and reconnect automatically
waitForState(client, ConnectionState.CONNECTED_TN3270E, 8000);
assertEquals(ConnectionState.CONNECTED_TN3270E, client.getConnectionState());
}
@Test
public void testPacketFragmentation() throws Exception {
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
client = new Telnet3270Client(config);
assertTrue(client.connect(8000));
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS));
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
// Build raw 3270 Erase/Write packet with 5-byte header + command + SBA + SF + data + IAC EOR
ByteArrayOutputStream buf = new ByteArrayOutputStream();
// 5-byte header
buf.write(new byte[] { (byte) DT_3270_DATA, 0x00, 0x00, 0x00, 0x01 });
buf.write(CMD_ERASE_WRITE); // 0xF5
buf.write(0xC3); // WCC
buf.write(ORDER_SBA);
buf.write(0x40); buf.write(0x40); // row 0, col 0
buf.write(ORDER_SF);
buf.write(0x40); // unprotected
String text = "FRAGMENTED_TEST";
for (char c : text.toCharArray()) {
buf.write(client.getScreenBuffer().getTranslator().unicodeToEbcdicSafe(c));
}
buf.write((byte) IAC);
buf.write((byte) EOR);
// Send packet in 2-byte fragments with 5ms delays
server.sendFragmented(buf.toByteArray(), 2, 5);
// Verify presentation space renders the entire reconstructed string
boolean matched = false;
long deadline = System.currentTimeMillis() + 3000;
while (System.currentTimeMillis() < deadline) {
String read = client.getPS().getString(0, 1, text.length());
if (text.equals(read)) {
matched = true;
break;
}
Thread.sleep(20);
}
assertTrue(matched, "Fragmented stream must be reassembled and displayed correctly");
}
}
+31
View File
@@ -0,0 +1,31 @@
<#
.SYNOPSIS
j3270 Terminal Emulator - Setup and Build Script (PowerShell Entrypoint)
Forwards execution directly to setup.ps1 with all supplied parameters.
.DESCRIPTION
See setup.ps1 for full implementation, parameter definitions, and documentation.
#>
[CmdletBinding()]
param (
[switch]$Run,
[switch]$Test,
[switch]$Clean,
[switch]$Help,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$RunArgs
)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
if (-not $scriptDir) {
$scriptDir = Get-Location
}
$ps1Script = Join-Path $scriptDir "setup.ps1"
if (Test-Path $ps1Script) {
& $ps1Script @PSBoundParameters @RunArgs
} else {
Write-Error "setup.ps1 not found in $scriptDir"
exit 1
}
+468
View File
@@ -0,0 +1,468 @@
<#
.SYNOPSIS
j3270 Terminal Emulator - Setup and Build Script (PowerShell)
Detects Java 11+ JDK and builds j3270 standalone executable JAR.
.DESCRIPTION
Searches for a compatible Java Development Kit (JDK 11+ / LTS 11, 17, 21),
verifies compiler (javac) and archiver (jar) toolchains, compiles the lib3270j
protocol core and j3270 Swing desktop application, and packages the standalone
executable JAR file (build/j3270.jar).
.PARAMETER Run
Launch j3270 immediately after successful build.
.PARAMETER Test
Run unit test suite after successful build.
.PARAMETER Clean
Remove previous build artifacts before compiling.
.PARAMETER Help
Display usage help message.
.EXAMPLE
.\setup.ps1
Builds the standalone j3270.jar.
.EXAMPLE
.\setup.ps1 -Clean -Run
Cleans previous build, builds j3270, and immediately launches the application.
.EXAMPLE
.\setup.ps1 -Run -- mainframe.example.com 23 4
Builds and connects to specified host, port, and terminal model.
#>
[CmdletBinding()]
param (
[switch]$Run,
[switch]$Test,
[switch]$Clean,
[switch]$Help,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$RunArgs
)
$ErrorActionPreference = "Stop"
# Resolve script root directory
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
if (-not $ScriptDir) {
$ScriptDir = Get-Location
}
Set-Location $ScriptDir
# Helper functions for colored terminal output
function Write-LogInfo($msg) { Write-Host "[INFO] $msg" -ForegroundColor Cyan }
function Write-LogSuccess($msg) { Write-Host "[OK] $msg" -ForegroundColor Green }
function Write-LogWarn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow }
function Write-LogError($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red }
if ($Help) {
Write-Host @"
j3270 Terminal Emulator - Setup & Build Script (PowerShell)
Usage:
.\setup.ps1 [-Run] [-Test] [-Clean] [-Help] [-- <run-arguments>]
Options:
-Run Build and immediately launch j3270
-Test Run automated unit tests after build
-Clean Remove previous build artifacts before compiling
-Help Show this help message
Examples:
.\setup.ps1
.\setup.ps1 -Clean
.\setup.ps1 -Run
.\setup.ps1 -Run -- mainframe.example.com 23 4
.\setup.ps1 -Test
"@
exit 0
}
Write-Host "`n=== j3270 Setup & Build (PowerShell) ===`n" -ForegroundColor White
# Determine platform
$isWindows = if (Test-Path variable:IsWindows) { $IsWindows } else { $env:OS -eq "Windows_NT" }
$exeSuffix = if ($isWindows) { ".exe" } else { "" }
# ------------------------------------------------------------------------------
# 1. Helper: Parse Major Java Version
# ------------------------------------------------------------------------------
function Get-JavaMajorVersion([string]$verStr) {
if (-not $verStr) { return 0 }
if ($verStr -match 'javac\s+([0-9][0-9._]*)' -or
$verStr -match 'version\s+"?([0-9][0-9._]*)"?' -or
$verStr -match '([0-9]+(\.[0-9]+)*)') {
$token = $matches[1]
if ($token -match '^1\.(\d+)') {
return [int]$matches[1]
}
if ($token -match '^(\d+)') {
return [int]$matches[1]
}
}
return 0
}
# ------------------------------------------------------------------------------
# 2. Helper: Test JDK Candidate Directory
# ------------------------------------------------------------------------------
function Test-JdkCandidate([string]$candHome) {
if (-not $candHome -or -not (Test-Path $candHome)) {
return $null
}
$javacPath = Join-Path $candHome "bin\javac$exeSuffix"
$jarPath = Join-Path $candHome "bin\jar$exeSuffix"
$javaPath = Join-Path $candHome "bin\java$exeSuffix"
if (-not (Test-Path $javacPath) -or -not (Test-Path $jarPath) -or -not (Test-Path $javaPath)) {
return $null
}
try {
$pInfo = New-Object System.Diagnostics.ProcessStartInfo
$pInfo.FileName = $javacPath
$pInfo.Arguments = "-version"
$pInfo.RedirectStandardOutput = $true
$pInfo.RedirectStandardError = $true
$pInfo.UseShellExecute = $false
$pInfo.CreateNoWindow = $true
$proc = [System.Diagnostics.Process]::Start($pInfo)
$stdout = $proc.StandardOutput.ReadToEnd()
$stderr = $proc.StandardError.ReadToEnd()
$proc.WaitForExit()
if ($proc.ExitCode -eq 0) {
$verOutput = ("$stdout $stderr").Trim()
$majorVer = Get-JavaMajorVersion $verOutput
if ($majorVer -ge 11) {
return [PSCustomObject]@{
Home = $candHome
Javac = $javacPath
Jar = $jarPath
Java = $javaPath
Version = $verOutput
Major = $majorVer
}
}
}
} catch {
# Process execution failed or access denied
}
return $null
}
# ------------------------------------------------------------------------------
# 3. Discover JDK 11+
# ------------------------------------------------------------------------------
Write-LogInfo "Searching for a compatible Java Development Kit (JDK 11+)..."
$detectedJdk = $null
# Strategy A: Check JAVA_HOME environment variable
if ($env:JAVA_HOME) {
$detectedJdk = Test-JdkCandidate $env:JAVA_HOME
if ($detectedJdk) {
Write-LogInfo "Found valid JDK in `$env:JAVA_HOME: $($detectedJdk.Home)"
} else {
Write-LogWarn "`$env:JAVA_HOME is set ($($env:JAVA_HOME)) but is not a valid JDK 11+."
}
}
# Strategy B: Check current system PATH
if (-not $detectedJdk) {
$pathJavac = Get-Command "javac$exeSuffix" -ErrorAction SilentlyContinue
if ($pathJavac) {
try {
$realPath = $pathJavac.Source
$candBin = Split-Path -Parent $realPath
$candHome = Split-Path -Parent $candBin
$detectedJdk = Test-JdkCandidate $candHome
if ($detectedJdk) {
Write-LogInfo "Found valid JDK on PATH: $($detectedJdk.Home)"
}
} catch {}
}
}
# Strategy C: Check Windows Registry (if running on Windows)
if (-not $detectedJdk -and $isWindows) {
$regPaths = @(
"HKLM:\SOFTWARE\JavaSoft\JDK",
"HKLM:\SOFTWARE\JavaSoft\Java Development Kit",
"HKLM:\SOFTWARE\Eclipse Adoptium\JDK",
"HKLM:\SOFTWARE\AdoptOpenJDK\JDK",
"HKLM:\SOFTWARE\Microsoft\JDK",
"HKLM:\SOFTWARE\BellSoft\Liberica JDK",
"HKLM:\SOFTWARE\Zulu\zulu-jdk",
"HKLM:\SOFTWARE\WOW6432Node\JavaSoft\JDK",
"HKLM:\SOFTWARE\WOW6432Node\JavaSoft\Java Development Kit",
"HKLM:\SOFTWARE\WOW6432Node\Eclipse Adoptium\JDK",
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\JDK"
)
foreach ($regBase in $regPaths) {
if (Test-Path $regBase) {
$subKeys = Get-ChildItem -Path $regBase -ErrorAction SilentlyContinue
foreach ($key in $subKeys) {
$item = Get-ItemProperty -Path $key.PSPath -ErrorAction SilentlyContinue
if ($item.JavaHome) {
$detectedJdk = Test-JdkCandidate $item.JavaHome
if ($detectedJdk) { break }
}
if ($item.Path) {
$detectedJdk = Test-JdkCandidate $item.Path
if ($detectedJdk) { break }
}
# Check hotspot subkey used by Adoptium/Microsoft MSI installers
$hotspotKey = Join-Path $key.PSPath "hotspot\MSI"
if (Test-Path $hotspotKey) {
$hs = Get-ItemProperty -Path $hotspotKey -ErrorAction SilentlyContinue
if ($hs.Path) {
$detectedJdk = Test-JdkCandidate $hs.Path
if ($detectedJdk) { break }
}
}
}
if ($detectedJdk) {
Write-LogInfo "Found valid JDK via Windows Registry: $($detectedJdk.Home)"
break
}
}
}
}
# Strategy D: Check Standard Filesystem Locations
if (-not $detectedJdk) {
$fsCandidates = @()
if ($isWindows) {
$searchRoots = @(
"$env:ProgramFiles\Eclipse Adoptium\*",
"$env:ProgramFiles\Java\*",
"$env:ProgramFiles\Microsoft\*",
"$env:ProgramFiles\Amazon Corretto\*",
"$env:ProgramFiles\Zulu\*",
"$env:ProgramFiles\BellSoft\*",
"$env:ProgramFiles\Semeru\*",
"$env:ProgramFiles\RedHat\*",
"${env:ProgramFiles(x86)}\Java\*",
"$env:LOCALAPPDATA\Programs\Eclipse Adoptium\*",
"$env:LOCALAPPDATA\Programs\Common\Oracle\Java\*",
"$env:USERPROFILE\.jdks\*",
"$env:USERPROFILE\.sdkman\candidates\java\*",
"$env:USERPROFILE\scoop\apps\openjdk\current",
"$env:USERPROFILE\scoop\apps\oraclejdk\current",
"$env:USERPROFILE\scoop\apps\temurin\current",
"$env:USERPROFILE\scoop\apps\zulu\current",
"C:\tools\jdk*",
"C:\Java\*"
)
foreach ($pattern in $searchRoots) {
Resolve-Path -Path $pattern -ErrorAction SilentlyContinue | ForEach-Object {
$fsCandidates += $_.Path
}
}
} else {
# POSIX / macOS search roots for pwsh
$searchRoots = @(
"/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home",
"/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home",
"/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home",
"/opt/homebrew/opt/openjdk@11/libexec/openjdk.jdk/Contents/Home",
"/opt/homebrew/opt/openjdk*",
"/usr/local/opt/openjdk/libexec/openjdk.jdk/Contents/Home",
"/Library/Java/JavaVirtualMachines/*/Contents/Home",
"/usr/lib/jvm/default-java",
"/usr/lib/jvm/java-21-openjdk*",
"/usr/lib/jvm/java-17-openjdk*",
"/usr/lib/jvm/java-11-openjdk*",
"/usr/lib/jvm/*",
"$env:HOME/.sdkman/candidates/java/current",
"$env:HOME/.sdkman/candidates/java/*",
"$env:HOME/.asdf/installs/java/*"
)
foreach ($pattern in $searchRoots) {
Resolve-Path -Path $pattern -ErrorAction SilentlyContinue | ForEach-Object {
$fsCandidates += $_.Path
}
}
}
foreach ($cand in $fsCandidates) {
$detectedJdk = Test-JdkCandidate $cand
if ($detectedJdk) {
Write-LogInfo "Found valid JDK at: $($detectedJdk.Home)"
break
}
}
}
# If no compatible JDK is found, report error with helpful instructions
if (-not $detectedJdk) {
Write-LogError "No compatible Java Development Kit (JDK 11+) was found on your system."
Write-Host @"
j3270 requires a Java Development Kit (JDK) version 11 or higher (LTS 11, 17, or 21).
Note: A JRE (runtime only) is not sufficient; the compiler (javac) and archiver (jar) are required.
To install JDK 17 on Windows:
Using Windows Package Manager (winget):
winget install EclipseAdoptium.Temurin.17.JDK
Using Chocolatey:
choco install openjdk17
Using Scoop:
scoop install openjdk17
Or download Eclipse Temurin (Adoptium MSI installer):
https://adoptium.net
After installing, restart PowerShell and re-run .\setup.ps1
"@ -ForegroundColor Yellow
exit 1
}
$env:JAVA_HOME = $detectedJdk.Home
Write-LogSuccess "JDK verified: $($detectedJdk.Version)"
Write-LogInfo "Java compiler: $($detectedJdk.Javac)"
Write-LogInfo "JAR packager: $($detectedJdk.Jar)"
Write-LogInfo "Java runtime: $($detectedJdk.Java)"
Write-LogInfo "JAVA_HOME: $($detectedJdk.Home)"
Write-Host ""
# ------------------------------------------------------------------------------
# 4. Preparation & Clean
# ------------------------------------------------------------------------------
$BuildDir = Join-Path $ScriptDir "build"
$LibBuildDir = Join-Path $BuildDir "lib3270j"
$AppBuildDir = Join-Path $BuildDir "j3270"
if ($Clean) {
Write-LogInfo "Cleaning build directory ($BuildDir)..."
Remove-Item -Path $LibBuildDir, $AppBuildDir -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path (Join-Path $BuildDir "MANIFEST.MF") -Force -ErrorAction SilentlyContinue
Remove-Item -Path (Join-Path $BuildDir "j3270.jar") -Force -ErrorAction SilentlyContinue
}
$null = New-Item -ItemType Directory -Path $LibBuildDir -Force
$null = New-Item -ItemType Directory -Path $AppBuildDir -Force
# ------------------------------------------------------------------------------
# 5. Compile Core Protocol Engine (lib3270j)
# ------------------------------------------------------------------------------
Write-LogInfo "Compiling lib3270j core engine..."
$libSrcDir = Join-Path $ScriptDir "lib3270j\src\main\java"
$libSources = Get-ChildItem -Path $libSrcDir -Filter *.java -Recurse | Select-Object -ExpandProperty FullName
if (-not $libSources -or $libSources.Count -eq 0) {
Write-LogError "No source files found in $libSrcDir"
exit 1
}
$libSourcesFile = Join-Path $BuildDir "lib_sources.txt"
$libSources | ForEach-Object { "`"$_`"" } | Set-Content -Path $libSourcesFile -Encoding UTF8
& $detectedJdk.Javac -d $LibBuildDir "@$libSourcesFile"
if ($LASTEXITCODE -ne 0) {
Write-LogError "Compilation of lib3270j failed with exit code $LASTEXITCODE."
exit $LASTEXITCODE
}
Remove-Item -Path $libSourcesFile -Force -ErrorAction SilentlyContinue
Write-LogSuccess "lib3270j compiled successfully."
# ------------------------------------------------------------------------------
# 6. Compile Desktop Application (j3270)
# ------------------------------------------------------------------------------
Write-LogInfo "Compiling j3270 terminal emulator..."
$appSrcDir = Join-Path $ScriptDir "j3270\src\main\java"
$appSources = Get-ChildItem -Path $appSrcDir -Filter *.java -Recurse | Select-Object -ExpandProperty FullName
if (-not $appSources -or $appSources.Count -eq 0) {
Write-LogError "No source files found in $appSrcDir"
exit 1
}
$appSourcesFile = Join-Path $BuildDir "app_sources.txt"
$appSources | ForEach-Object { "`"$_`"" } | Set-Content -Path $appSourcesFile -Encoding UTF8
& $detectedJdk.Javac -cp $LibBuildDir -d $AppBuildDir "@$appSourcesFile"
if ($LASTEXITCODE -ne 0) {
Write-LogError "Compilation of j3270 failed with exit code $LASTEXITCODE."
exit $LASTEXITCODE
}
Remove-Item -Path $appSourcesFile -Force -ErrorAction SilentlyContinue
Write-LogSuccess "j3270 application compiled successfully."
# Copy any resources if present
$appResDir = Join-Path $ScriptDir "j3270\src\main\resources"
if (Test-Path $appResDir) {
Copy-Item -Path (Join-Path $appResDir "*") -Destination $AppBuildDir -Recurse -Force -ErrorAction SilentlyContinue
}
$libResDir = Join-Path $ScriptDir "lib3270j\src\main\resources"
if (Test-Path $libResDir) {
Copy-Item -Path (Join-Path $libResDir "*") -Destination $LibBuildDir -Recurse -Force -ErrorAction SilentlyContinue
}
# ------------------------------------------------------------------------------
# 7. Package Standalone Executable JAR
# ------------------------------------------------------------------------------
Write-LogInfo "Packaging standalone executable JAR..."
$manifestFile = Join-Path $BuildDir "MANIFEST.MF"
$manifestText = "Manifest-Version: 1.0`r`nMain-Class: haus.nightmare.j3270.J3270App`r`nImplementation-Title: j3270`r`nImplementation-Version: 0.1.0`r`nCreated-By: j3270 setup.ps1`r`n`r`n"
[System.IO.File]::WriteAllText($manifestFile, $manifestText)
$jarFile = Join-Path $BuildDir "j3270.jar"
& $detectedJdk.Jar cvfm $jarFile $manifestFile -C $LibBuildDir . -C $AppBuildDir . | Out-Null
if (-not (Test-Path $jarFile)) {
Write-LogError "Failed to create executable JAR at $jarFile"
exit 1
}
$jarItem = Get-Item $jarFile
$jarSizeKB = [math]::Round($jarItem.Length / 1KB, 1)
Write-Host "`n=== Build Successful ===`n" -ForegroundColor Green
Write-LogSuccess "Executable JAR created: $jarFile (${jarSizeKB} KB)"
Write-Host @"
To run j3270:
java -jar "$jarFile"
# Or with arguments:
java -jar "$jarFile" mainframe.example.com 23 4
"@ -ForegroundColor White
# ------------------------------------------------------------------------------
# 8. Run Tests (if requested)
# ------------------------------------------------------------------------------
if ($Test) {
Write-Host "`n=== Running Unit Tests ===`n" -ForegroundColor White
$testScript = Join-Path $ScriptDir "test_all.sh"
if (Test-Path $testScript) {
& sh $testScript
} else {
Write-LogWarn "No test runner script found at $testScript"
}
}
# ------------------------------------------------------------------------------
# 9. Launch Application (if requested)
# ------------------------------------------------------------------------------
if ($Run) {
Write-LogInfo "Launching j3270..."
if ($isWindows) {
Start-Process -FilePath $detectedJdk.Java -ArgumentList (@("-jar", "`"$jarFile`"") + $RunArgs)
} else {
& $detectedJdk.Java -jar $jarFile @RunArgs
}
}
Executable
+428
View File
@@ -0,0 +1,428 @@
#!/bin/sh
# ==============================================================================
# j3270 Setup Script (POSIX Shell)
# Detects Java 11+ JDK and builds j3270 standalone executable JAR.
#
# Usage:
# ./setup.sh [options] [-- run-arguments]
#
# Options:
# -r, --run Launch j3270 immediately after successful build
# -t, --test Run test suite after build
# -c, --clean Clean build directory before compiling
# -h, --help Show this help message
# ==============================================================================
set -e
# Resolve directory of this script
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
# ANSI color codes (if output is to an interactive terminal)
if [ -t 1 ]; then
BOLD="\033[1m"
GREEN="\033[1;32m"
YELLOW="\033[1;33m"
CYAN="\033[1;36m"
RED="\033[1;31m"
RESET="\033[0m"
else
BOLD=""
GREEN=""
YELLOW=""
CYAN=""
RED=""
RESET=""
fi
log_info() { printf "%b[INFO]%b %s\n" "$CYAN" "$RESET" "$*"; }
log_success() { printf "%b[OK]%b %s\n" "$GREEN" "$RESET" "$*"; }
log_warn() { printf "%b[WARN]%b %s\n" "$YELLOW" "$RESET" "$*"; }
log_error() { printf "%b[ERROR]%b %s\n" "$RED" "$RESET" "$*"; }
show_help() {
cat << EOF
j3270 Terminal Emulator - Setup and Build Script
Usage:
./setup.sh [options] [-- run-arguments]
Options:
-r, --run Build and immediately launch j3270
-t, --test Run automated unit tests after build
-c, --clean Remove previous build artifacts before building
-h, --help Show this help message
Examples:
./setup.sh
./setup.sh --clean
./setup.sh --run
./setup.sh --run -- mainframe.example.com 23 4
./setup.sh --test
EOF
}
# Parse command line flags
DO_RUN=0
DO_TEST=0
DO_CLEAN=0
RUN_ARGS=""
while [ $# -gt 0 ]; do
case "$1" in
-r|--run)
DO_RUN=1
shift
;;
-t|--test)
DO_TEST=1
shift
;;
-c|--clean)
DO_CLEAN=1
shift
;;
-h|--help)
show_help
exit 0
;;
--)
shift
RUN_ARGS="$*"
break
;;
*)
RUN_ARGS="$*"
break
;;
esac
done
printf "\n%b=== j3270 Setup & Build ===%b\n\n" "$BOLD" "$RESET"
# ------------------------------------------------------------------------------
# 1. Java Version & JDK Toolchain Detection
# ------------------------------------------------------------------------------
# Extracts the integer major version from a javac or java version string
# Examples: "javac 11.0.19" -> 11, "javac 17.0.2" -> 17, "javac 1.8.0_292" -> 8
parse_major_version() {
raw_str="$1"
ver_token=$(echo "$raw_str" | sed -n \
-e 's/.*javac *\([0-9][0-9._]*\).*/\1/p' \
-e 's/.*version *"\([0-9][0-9._]*\)".*/\1/p' \
-e 's/.*version *\([0-9][0-9._]*\).*/\1/p' | head -n 1)
if [ -n "$ver_token" ]; then
echo "$ver_token" | sed -e 's/^1\.//' -e 's/[^0-9].*//'
fi
}
# Checks if a given directory contains a valid JDK >= 11
# Sets JAVAC_BIN, JAR_BIN, JAVA_BIN, JAVA_VERSION if valid
test_jdk_candidate() {
cand_home="$1"
cand_javac=""
cand_jar=""
cand_java=""
if [ -n "$cand_home" ] && [ -d "$cand_home" ]; then
if [ -x "$cand_home/bin/javac" ]; then cand_javac="$cand_home/bin/javac"; fi
if [ -x "$cand_home/bin/jar" ]; then cand_jar="$cand_home/bin/jar"; fi
if [ -x "$cand_home/bin/java" ]; then cand_java="$cand_home/bin/java"; fi
fi
if [ -z "$cand_javac" ] || [ -z "$cand_jar" ] || [ -z "$cand_java" ]; then
return 1
fi
# Verify that javac actually executes successfully
if ! "$cand_javac" -version >/dev/null 2>&1; then
return 1
fi
raw_ver=$("$cand_javac" -version 2>&1)
major_num=$(parse_major_version "$raw_ver")
if [ -n "$major_num" ] && [ "$major_num" -ge 11 ] 2>/dev/null; then
JAVAC_BIN="$cand_javac"
JAR_BIN="$cand_jar"
JAVA_BIN="$cand_java"
JAVA_VERSION="$raw_ver"
DETECTED_JAVA_HOME="$cand_home"
return 0
fi
return 1
}
JAVAC_BIN=""
JAR_BIN=""
JAVA_BIN=""
JAVA_VERSION=""
DETECTED_JAVA_HOME=""
log_info "Searching for a compatible Java Development Kit (JDK 11+)..."
# Strategy A: Check user-specified JAVA_HOME
if [ -n "$JAVA_HOME" ]; then
if test_jdk_candidate "$JAVA_HOME"; then
log_info "Found valid JDK in \$JAVA_HOME"
fi
fi
# Strategy B: Check current PATH commands (handling potential macOS /usr/bin stubs)
if [ -z "$JAVAC_BIN" ] && command -v javac >/dev/null 2>&1 && command -v jar >/dev/null 2>&1 && command -v java >/dev/null 2>&1; then
if javac -version >/dev/null 2>&1; then
raw_ver=$(javac -version 2>&1)
major_num=$(parse_major_version "$raw_ver")
if [ -n "$major_num" ] && [ "$major_num" -ge 11 ] 2>/dev/null; then
JAVAC_BIN="$(command -v javac)"
JAR_BIN="$(command -v jar)"
JAVA_BIN="$(command -v java)"
JAVA_VERSION="$raw_ver"
log_info "Found valid JDK on system PATH"
fi
fi
fi
# Strategy C: macOS java_home utility
if [ -z "$JAVAC_BIN" ] && [ -x /usr/libexec/java_home ]; then
for v in 11 17 21 24 ""; do
if [ -n "$v" ]; then
mhome="$(/usr/libexec/java_home -v "$v+" 2>/dev/null || true)"
else
mhome="$(/usr/libexec/java_home 2>/dev/null || true)"
fi
if [ -n "$mhome" ] && test_jdk_candidate "$mhome"; then
log_info "Found macOS JDK via /usr/libexec/java_home: $mhome"
break
fi
done
fi
# Strategy D: Check standard package manager & OS installation directories
if [ -z "$JAVAC_BIN" ]; then
candidate_paths="
/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home
/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
/opt/homebrew/opt/openjdk@11/libexec/openjdk.jdk/Contents/Home
/opt/homebrew/opt/openjdk
/opt/homebrew/opt/openjdk@21
/opt/homebrew/opt/openjdk@17
/opt/homebrew/opt/openjdk@11
/usr/local/opt/openjdk/libexec/openjdk.jdk/Contents/Home
/usr/local/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
/usr/local/opt/openjdk@11/libexec/openjdk.jdk/Contents/Home
/usr/local/opt/openjdk
/Library/Java/JavaVirtualMachines/*/Contents/Home
$HOME/Library/Java/JavaVirtualMachines/*/Contents/Home
/usr/lib/jvm/default-java
/usr/lib/jvm/java-21-openjdk*
/usr/lib/jvm/java-17-openjdk*
/usr/lib/jvm/java-11-openjdk*
/usr/lib/jvm/temurin-21*
/usr/lib/jvm/temurin-17*
/usr/lib/jvm/temurin-11*
/usr/lib/jvm/semeru-21*
/usr/lib/jvm/semeru-17*
/usr/lib/jvm/semeru-11*
/usr/lib/jvm/zulu-21*
/usr/lib/jvm/zulu-17*
/usr/lib/jvm/zulu-11*
/usr/lib/jvm/*
/usr/java/*
/usr/local/openjdk21
/usr/local/openjdk17
/usr/local/openjdk11
/usr/local/openjdk*
$HOME/.sdkman/candidates/java/current
$HOME/.sdkman/candidates/java/*
$HOME/.asdf/installs/java/*
$HOME/.jenv/versions/*
"
for cand in $candidate_paths; do
if [ -d "$cand" ] && test_jdk_candidate "$cand"; then
log_info "Found compatible JDK at: $cand"
break
fi
done
fi
# If no compatible JDK is found, provide OS-tailored installation help
if [ -z "$JAVAC_BIN" ] || [ -z "$JAR_BIN" ] || [ -z "$JAVA_BIN" ]; then
log_error "No compatible Java Development Kit (JDK 11+) was found on your system."
printf "\n"
printf "j3270 requires a Java Development Kit (JDK) version 11 or higher (LTS 11, 17, or 21).\n"
printf "Note: A JRE (runtime only) is not sufficient; the compiler (javac) and archiver (jar) are required.\n\n"
printf "%bTo install JDK 11+ on your system:%b\n\n" "$BOLD" "$RESET"
UNAME_S="$(uname -s 2>/dev/null || echo "Unknown")"
case "$UNAME_S" in
Darwin)
printf " • Using Homebrew:\n"
printf " brew install openjdk@17\n"
printf " sudo ln -sfn /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-17.jdk\n\n"
printf " • Or download Eclipse Temurin (Adoptium PKG installer):\n"
printf " https://adoptium.net\n\n"
;;
Linux)
if command -v apt-get >/dev/null 2>&1; then
printf " • Debian / Ubuntu / Linux Mint:\n"
printf " sudo apt update && sudo apt install -y openjdk-17-jdk\n\n"
elif command -v dnf >/dev/null 2>&1; then
printf " • Fedora / RHEL / CentOS / Rocky / AlmaLinux:\n"
printf " sudo dnf install -y java-17-openjdk-devel\n\n"
elif command -v pacman >/dev/null 2>&1; then
printf " • Arch Linux / Manjaro:\n"
printf " sudo pacman -S jdk17-openjdk\n\n"
elif command -v apk >/dev/null 2>&1; then
printf " • Alpine Linux:\n"
printf " sudo apk add openjdk17\n\n"
elif command -v zypper >/dev/null 2>&1; then
printf " • openSUSE:\n"
printf " sudo zypper install -y java-17-openjdk-devel\n\n"
else
printf " • Install JDK 17 via your system package manager or SDKMAN:\n"
printf " curl -s \"https://get.sdkman.io\" | bash\n"
printf " sdk install java 17.0.10-tem\n\n"
fi
;;
FreeBSD|OpenBSD|NetBSD)
printf " • BSD package manager:\n"
printf " pkg install openjdk17\n\n"
;;
*)
printf " • Download Eclipse Temurin JDK:\n"
printf " https://adoptium.net\n\n"
;;
esac
printf "After installing, re-run ./setup.sh\n\n"
exit 1
fi
if [ -n "$DETECTED_JAVA_HOME" ]; then
export JAVA_HOME="$DETECTED_JAVA_HOME"
fi
log_success "JDK verified: $JAVA_VERSION"
log_info "Java compiler: $JAVAC_BIN"
log_info "JAR packager: $JAR_BIN"
log_info "Java runtime: $JAVA_BIN"
if [ -n "$JAVA_HOME" ]; then
log_info "JAVA_HOME: $JAVA_HOME"
fi
printf "\n"
# ------------------------------------------------------------------------------
# 2. Preparation & Clean
# ------------------------------------------------------------------------------
BUILD_DIR="$SCRIPT_DIR/build"
if [ "$DO_CLEAN" -eq 1 ]; then
log_info "Cleaning build directory ($BUILD_DIR)..."
rm -rf "$BUILD_DIR/lib3270j" "$BUILD_DIR/j3270" "$BUILD_DIR/MANIFEST.MF" "$BUILD_DIR/j3270.jar"
fi
mkdir -p "$BUILD_DIR/lib3270j" "$BUILD_DIR/j3270"
# ------------------------------------------------------------------------------
# 3. Compile Core Protocol Engine (lib3270j)
# ------------------------------------------------------------------------------
log_info "Compiling lib3270j core engine..."
find "$SCRIPT_DIR/lib3270j/src/main/java" -name "*.java" | sed 's/^/"/;s/$/"/' > "$BUILD_DIR/lib_sources.txt"
if [ ! -s "$BUILD_DIR/lib_sources.txt" ]; then
log_error "No source files found in lib3270j/src/main/java"
exit 1
fi
"$JAVAC_BIN" -d "$BUILD_DIR/lib3270j" @"$BUILD_DIR/lib_sources.txt"
rm -f "$BUILD_DIR/lib_sources.txt"
log_success "lib3270j compiled successfully."
# ------------------------------------------------------------------------------
# 4. Compile Desktop Application (j3270)
# ------------------------------------------------------------------------------
log_info "Compiling j3270 terminal emulator..."
find "$SCRIPT_DIR/j3270/src/main/java" -name "*.java" | sed 's/^/"/;s/$/"/' > "$BUILD_DIR/app_sources.txt"
if [ ! -s "$BUILD_DIR/app_sources.txt" ]; then
log_error "No source files found in j3270/src/main/java"
exit 1
fi
"$JAVAC_BIN" -cp "$BUILD_DIR/lib3270j" -d "$BUILD_DIR/j3270" @"$BUILD_DIR/app_sources.txt"
rm -f "$BUILD_DIR/app_sources.txt"
log_success "j3270 application compiled successfully."
# Copy any resources if present
if [ -d "$SCRIPT_DIR/j3270/src/main/resources" ]; then
cp -r "$SCRIPT_DIR/j3270/src/main/resources/"* "$BUILD_DIR/j3270/" 2>/dev/null || true
fi
if [ -d "$SCRIPT_DIR/lib3270j/src/main/resources" ]; then
cp -r "$SCRIPT_DIR/lib3270j/src/main/resources/"* "$BUILD_DIR/lib3270j/" 2>/dev/null || true
fi
# ------------------------------------------------------------------------------
# 5. Package Standalone Executable JAR
# ------------------------------------------------------------------------------
log_info "Packaging standalone executable JAR..."
cat << 'EOF' > "$BUILD_DIR/MANIFEST.MF"
Manifest-Version: 1.0
Main-Class: haus.nightmare.j3270.J3270App
Implementation-Title: j3270
Implementation-Version: 0.1.0
Created-By: j3270 setup.sh
EOF
"$JAR_BIN" cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \
-C "$BUILD_DIR/lib3270j" . \
-C "$BUILD_DIR/j3270" . > /dev/null
if [ ! -f "$BUILD_DIR/j3270.jar" ]; then
log_error "Failed to create $BUILD_DIR/j3270.jar"
exit 1
fi
JAR_SIZE=$(ls -lh "$BUILD_DIR/j3270.jar" | awk '{print $5}')
printf "\n%b=== Build Successful ===%b\n" "$BOLD" "$RESET"
log_success "Executable JAR: $BUILD_DIR/j3270.jar ($JAR_SIZE)"
printf "\n%bTo run j3270:%b\n" "$BOLD" "$RESET"
printf " ./run.sh\n"
printf " # Or directly:\n"
printf " %s -jar \"%s/build/j3270.jar\"\n\n" "$JAVA_BIN" "$SCRIPT_DIR"
# ------------------------------------------------------------------------------
# 6. Run Tests (if requested)
# ------------------------------------------------------------------------------
if [ "$DO_TEST" -eq 1 ]; then
printf "%b=== Running Unit Tests ===%b\n" "$BOLD" "$RESET"
if [ -x "$SCRIPT_DIR/test_all.sh" ]; then
sh "$SCRIPT_DIR/test_all.sh"
elif [ -x "$SCRIPT_DIR/build_all.sh" ]; then
sh "$SCRIPT_DIR/build_all.sh" test
fi
fi
# ------------------------------------------------------------------------------
# 7. Launch Application (if requested)
# ------------------------------------------------------------------------------
if [ "$DO_RUN" -eq 1 ]; then
log_info "Launching j3270..."
if [ "$(uname -s)" = "Darwin" ]; then
# On macOS, enable native application menu bar
exec "$JAVA_BIN" -Dapple.laf.useScreenMenuBar=true \
-Dapple.awt.application.name=j3270 \
-jar "$BUILD_DIR/j3270.jar" $RUN_ARGS
else
exec "$JAVA_BIN" -jar "$BUILD_DIR/j3270.jar" $RUN_ARGS
fi
fi
+20 -4
View File
@@ -17,15 +17,31 @@ if [ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/javac" ] && [ -x "$JAVA_HOME/bin/
JAVAC_BIN="$JAVA_HOME/bin/javac"
JAVA_BIN="$JAVA_HOME/bin/java"
elif command -v javac >/dev/null 2>&1 && command -v java >/dev/null 2>&1; then
if javac -version >/dev/null 2>&1; then
JAVAC_BIN="$(command -v javac)"
JAVA_BIN="$(command -v java)"
else
for h in "$HOME/.sdkman/candidates/java/current" \
fi
fi
if [ -z "$JAVAC_BIN" ] || [ -z "$JAVA_BIN" ]; then
if [ -x /usr/libexec/java_home ]; then
MAC_JAVA_HOME="$(/usr/libexec/java_home 2>/dev/null || true)"
if [ -n "$MAC_JAVA_HOME" ] && [ -x "$MAC_JAVA_HOME/bin/javac" ] && [ -x "$MAC_JAVA_HOME/bin/java" ]; then
export JAVA_HOME="$MAC_JAVA_HOME"
JAVAC_BIN="$JAVA_HOME/bin/javac"
JAVA_BIN="$JAVA_HOME/bin/java"
fi
fi
fi
if [ -z "$JAVAC_BIN" ] || [ -z "$JAVA_BIN" ]; then
for h in /usr/lib/jvm/default-java \
/usr/lib/jvm/java-21-openjdk* \
/usr/lib/jvm/java-17-openjdk* \
/usr/lib/jvm/java-11-openjdk* \
/usr/lib/jvm/default-java \
/Library/Java/JavaVirtualMachines/*/Contents/Home; do
/Library/Java/JavaVirtualMachines/*/Contents/Home \
/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home \
/opt/homebrew/opt/openjdk; do
if [ -d "$h" ] && [ -x "$h/bin/javac" ] && [ -x "$h/bin/java" ]; then
export JAVA_HOME="$h"
JAVAC_BIN="$JAVA_HOME/bin/javac"