Add headless server and setup script
This commit is contained in:
@@ -9,3 +9,4 @@ build/*
|
|||||||
*.jar
|
*.jar
|
||||||
build
|
build
|
||||||
.gradle
|
.gradle
|
||||||
|
*.txt
|
||||||
|
|||||||
@@ -132,14 +132,43 @@ java -jar j3270.jar -d mainframe.example.com
|
|||||||
|
|
||||||
## 🛠️ Building from Source
|
## 🛠️ Building from Source
|
||||||
|
|
||||||
### Standalone Build (No Gradle Required)
|
### Quick Setup & Build for End Users
|
||||||
The project provides self-contained, portable build scripts requiring only a JDK:
|
|
||||||
|
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
|
```bash
|
||||||
# Build standalone executable JAR (build/j3270.jar):
|
# Direct compile and package without diagnostics:
|
||||||
sh ./build_all.sh
|
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
|
sh ./test_all.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ subprojects {
|
|||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
jvmArgs '-Djava.awt.headless=true'
|
||||||
testLogging {
|
testLogging {
|
||||||
events "passed", "skipped", "failed"
|
events "passed", "skipped", "failed"
|
||||||
}
|
}
|
||||||
@@ -24,5 +25,6 @@ subprojects {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
|
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"
|
JAVAC_BIN="$JAVA_HOME/bin/javac"
|
||||||
JAR_BIN="$JAVA_HOME/bin/jar"
|
JAR_BIN="$JAVA_HOME/bin/jar"
|
||||||
elif command -v javac >/dev/null 2>&1 && command -v jar >/dev/null 2>&1; then
|
elif command -v javac >/dev/null 2>&1 && command -v jar >/dev/null 2>&1; then
|
||||||
JAVAC_BIN="$(command -v javac)"
|
if javac -version >/dev/null 2>&1; then
|
||||||
JAR_BIN="$(command -v jar)"
|
JAVAC_BIN="$(command -v javac)"
|
||||||
else
|
JAR_BIN="$(command -v jar)"
|
||||||
# Check common SDKMAN / macOS / Linux JDK locations
|
fi
|
||||||
for h in "$HOME/.sdkman/candidates/java/current" \
|
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-21-openjdk* \
|
||||||
/usr/lib/jvm/java-17-openjdk* \
|
/usr/lib/jvm/java-17-openjdk* \
|
||||||
/usr/lib/jvm/java-11-openjdk* \
|
/usr/lib/jvm/java-11-openjdk* \
|
||||||
/usr/lib/jvm/default-java \
|
/Library/Java/JavaVirtualMachines/*/Contents/Home \
|
||||||
/Library/Java/JavaVirtualMachines/*/Contents/Home; do
|
/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
|
if [ -d "$h" ] && [ -x "$h/bin/javac" ] && [ -x "$h/bin/jar" ]; then
|
||||||
export JAVA_HOME="$h"
|
export JAVA_HOME="$h"
|
||||||
JAVAC_BIN="$JAVA_HOME/bin/javac"
|
JAVAC_BIN="$JAVA_HOME/bin/javac"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -1,2 +1,249 @@
|
|||||||
#!/bin/sh
|
#!/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();
|
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"
|
JAVAC_BIN="$JAVA_HOME/bin/javac"
|
||||||
JAVA_BIN="$JAVA_HOME/bin/java"
|
JAVA_BIN="$JAVA_HOME/bin/java"
|
||||||
elif command -v javac >/dev/null 2>&1 && command -v java >/dev/null 2>&1; then
|
elif command -v javac >/dev/null 2>&1 && command -v java >/dev/null 2>&1; then
|
||||||
JAVAC_BIN="$(command -v javac)"
|
if javac -version >/dev/null 2>&1; then
|
||||||
JAVA_BIN="$(command -v java)"
|
JAVAC_BIN="$(command -v javac)"
|
||||||
else
|
JAVA_BIN="$(command -v java)"
|
||||||
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-21-openjdk* \
|
||||||
/usr/lib/jvm/java-17-openjdk* \
|
/usr/lib/jvm/java-17-openjdk* \
|
||||||
/usr/lib/jvm/java-11-openjdk* \
|
/usr/lib/jvm/java-11-openjdk* \
|
||||||
/usr/lib/jvm/default-java \
|
/Library/Java/JavaVirtualMachines/*/Contents/Home \
|
||||||
/Library/Java/JavaVirtualMachines/*/Contents/Home; do
|
/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
|
if [ -d "$h" ] && [ -x "$h/bin/javac" ] && [ -x "$h/bin/java" ]; then
|
||||||
export JAVA_HOME="$h"
|
export JAVA_HOME="$h"
|
||||||
JAVAC_BIN="$JAVA_HOME/bin/javac"
|
JAVAC_BIN="$JAVA_HOME/bin/javac"
|
||||||
|
|||||||
Reference in New Issue
Block a user