Add headless server and setup script
This commit is contained in:
@@ -9,3 +9,4 @@ build/*
|
||||
*.jar
|
||||
build
|
||||
.gradle
|
||||
*.txt
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
+23
-7
@@ -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
|
||||
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" \
|
||||
if javac -version >/dev/null 2>&1; then
|
||||
JAVAC_BIN="$(command -v javac)"
|
||||
JAR_BIN="$(command -v jar)"
|
||||
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
@@ -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
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
+293
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+22
-6
@@ -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
|
||||
JAVAC_BIN="$(command -v javac)"
|
||||
JAVA_BIN="$(command -v java)"
|
||||
else
|
||||
for h in "$HOME/.sdkman/candidates/java/current" \
|
||||
if javac -version >/dev/null 2>&1; then
|
||||
JAVAC_BIN="$(command -v javac)"
|
||||
JAVA_BIN="$(command -v java)"
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user