Connection hardening and FileTransfer UI changes
This commit is contained in:
@@ -69,6 +69,12 @@ public class ConnectionConfig {
|
||||
private int reconnectMaxRetries = 5;
|
||||
private int tcpUserTimeoutMs = 0;
|
||||
|
||||
// Host operating system type (TSO, CMS, CICS)
|
||||
private String hostType = "TSO";
|
||||
private boolean savedConnection = false;
|
||||
private String savedProfileId = null;
|
||||
private String savedProfileName = null;
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
public ConnectionConfig(String host, int port) {
|
||||
@@ -312,6 +318,51 @@ public class ConnectionConfig {
|
||||
public int getTcpUserTimeoutMs() { return tcpUserTimeoutMs; }
|
||||
public void setTcpUserTimeoutMs(int timeoutMs) { this.tcpUserTimeoutMs = Math.max(0, timeoutMs); }
|
||||
|
||||
public String getHostType() {
|
||||
return hostType != null ? hostType : "TSO";
|
||||
}
|
||||
|
||||
public void setHostType(String hostType) {
|
||||
this.hostType = (hostType != null && !hostType.trim().isEmpty()) ? hostType.trim().toUpperCase() : "TSO";
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ft.FTConfig.HostType getFtHostType() {
|
||||
if (hostType != null) {
|
||||
try {
|
||||
return haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(hostType.trim().toUpperCase());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
|
||||
}
|
||||
|
||||
public void setFtHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType ftHostType) {
|
||||
this.hostType = (ftHostType != null) ? ftHostType.name() : "TSO";
|
||||
}
|
||||
|
||||
public boolean isSavedConnection() {
|
||||
return savedConnection;
|
||||
}
|
||||
|
||||
public void setSavedConnection(boolean savedConnection) {
|
||||
this.savedConnection = savedConnection;
|
||||
}
|
||||
|
||||
public String getSavedProfileId() {
|
||||
return savedProfileId;
|
||||
}
|
||||
|
||||
public void setSavedProfileId(String savedProfileId) {
|
||||
this.savedProfileId = savedProfileId;
|
||||
}
|
||||
|
||||
public String getSavedProfileName() {
|
||||
return savedProfileName;
|
||||
}
|
||||
|
||||
public void setSavedProfileName(String savedProfileName) {
|
||||
this.savedProfileName = savedProfileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
|
||||
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
|
||||
|
||||
@@ -11,6 +11,7 @@ import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Main API entry point for lib3270j.
|
||||
@@ -195,6 +196,13 @@ public class Telnet3270Client {
|
||||
public void disconnect() {
|
||||
cancelAutoReconnect();
|
||||
if (connection != null) {
|
||||
try {
|
||||
if (fsm != null) {
|
||||
fsm.sendTelnetDisconnect();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.FINE, "Error during Telnet disconnect negotiation", e);
|
||||
}
|
||||
connection.disconnect();
|
||||
connection = null;
|
||||
}
|
||||
|
||||
@@ -542,7 +542,12 @@ public class FTConfig {
|
||||
StringBuilder cmd = new StringBuilder();
|
||||
|
||||
// Host program selection: IND$FILE or APVUFILE
|
||||
cmd.append(getProgramName()).append(" ");
|
||||
// For TSO, prefix with "TSO " so it can be called from any ISPF command line or native prompt
|
||||
String prog = getProgramName();
|
||||
if (hostType == HostType.TSO && !prog.toUpperCase().startsWith("TSO ")) {
|
||||
cmd.append("TSO ");
|
||||
}
|
||||
cmd.append(prog).append(" ");
|
||||
cmd.append(isReceive() ? "GET " : "PUT ");
|
||||
cmd.append(hostFilename != null ? hostFilename.trim() : "");
|
||||
|
||||
|
||||
@@ -26,7 +26,16 @@ public class CMSDirectoryParser {
|
||||
|
||||
String upper = trimmed.toUpperCase();
|
||||
if (upper.startsWith("FILENAME") || upper.startsWith("DIRECTORY") || upper.startsWith("FILELIST")) continue;
|
||||
if (upper.startsWith("READY;") || upper.equals("READY") || upper.startsWith("MORE...") || upper.startsWith("HOLDING")) continue;
|
||||
if (upper.startsWith("READY;") || upper.startsWith("READY(") || upper.equals("READY")
|
||||
|| upper.startsWith("MORE...") || upper.startsWith("HOLDING")
|
||||
|| upper.startsWith("RUNNING") || upper.equals("RUNNING")
|
||||
|| upper.startsWith("DMSLST") || upper.startsWith("DMS")) continue;
|
||||
|
||||
if (upper.endsWith("MORE...") || upper.endsWith("HOLDING") || upper.endsWith("RUNNING")) {
|
||||
int idx = Math.max(upper.lastIndexOf("MORE..."), Math.max(upper.lastIndexOf("HOLDING"), upper.lastIndexOf("RUNNING")));
|
||||
trimmed = trimmed.substring(0, idx).trim();
|
||||
if (trimmed.isEmpty()) continue;
|
||||
}
|
||||
|
||||
CMSDirectoryEntry entry = parseCmsLine(trimmed);
|
||||
if (entry != null) {
|
||||
|
||||
@@ -11,12 +11,16 @@ import java.util.regex.Pattern;
|
||||
public class TSODirectoryParser {
|
||||
|
||||
private static final Pattern DSN_PATTERN = Pattern.compile("([A-Z0-9@#$]+(?:\\.[A-Z0-9@#$]+)+)");
|
||||
private static final Pattern NONVSAM_PATTERN = Pattern.compile("NONVSAM\\s+-+\\s+([A-Z0-9@#$]+(?:\\.[A-Z0-9@#$]+)+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern VOLSER_PATTERN = Pattern.compile("VOLSER-+([A-Z0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern RECFM_PATTERN = Pattern.compile("RECFM-+([A-Z]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern LRECL_PATTERN = Pattern.compile("LRECL-+([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern BLKSIZE_PATTERN = Pattern.compile("BLKSIZE-+([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern DSORG_PATTERN = Pattern.compile("DSORG-+([A-Z]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern NONVSAM_PATTERN = Pattern.compile("(?:NONVSAM|CLUSTER|GDG|ALIAS|AIX|PATH)\\s*[-:=]+\\s*([A-Z0-9@#$]+(?:\\.[A-Z0-9@#$]+)*)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern VOLSER_PATTERN = Pattern.compile("VOLSER\\s*[-:=]+\\s*([A-Z0-9*@#$&]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern RECFM_PATTERN = Pattern.compile("RECFM\\s*[-:=]+\\s*([A-Z0-9,]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern LRECL_PATTERN = Pattern.compile("(?:AVG|MAX)?LRECL\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern BLKSIZE_PATTERN = Pattern.compile("(?:CISIZE|BLKSIZE)\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern DSORG_PATTERN = Pattern.compile("DSORG\\s*[-:=]+\\s*([A-Z0-9\\-]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern EXTENTS_PATTERN = Pattern.compile("(?:VOL-)?EXTENTS\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern SPACE_PRI_PATTERN = Pattern.compile("(?:SPACE-PRI|ALLOC-PRM|ALLOC_PRM)\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern SPACE_TYPE_PATTERN = Pattern.compile("SPACE-TYPE\\s*[-:=]+\\s*([A-Z]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern TRACKS_PATTERN = Pattern.compile("(?:TRACKS|TRKS)\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* Parse raw text containing TSO dataset listings into a structured list of TSODirectoryEntry.
|
||||
@@ -30,11 +34,18 @@ public class TSODirectoryParser {
|
||||
String[] lines = splitIntoLines(text);
|
||||
|
||||
// Check if text is LISTCAT format
|
||||
if (text.toUpperCase().contains("NONVSAM") || text.toUpperCase().contains("IN-CAT")) {
|
||||
if (text.toUpperCase().contains("NONVSAM") || text.toUpperCase().contains("IN-CAT") || text.toUpperCase().contains("IDCAMS")
|
||||
|| text.toUpperCase().contains("CLUSTER") || text.toUpperCase().contains("GDG")) {
|
||||
parseListcat(lines, entries);
|
||||
if (!entries.isEmpty()) return entries;
|
||||
}
|
||||
|
||||
// Check if text is LISTDS format
|
||||
if (text.toUpperCase().contains("--RECFM-") || text.toUpperCase().contains("--VOLUMES--")) {
|
||||
parseListds(lines, entries);
|
||||
if (!entries.isEmpty()) return entries;
|
||||
}
|
||||
|
||||
// Check for ISPF DSLIST or general tabular format
|
||||
for (String line : lines) {
|
||||
String trimmed = line.trim();
|
||||
@@ -72,36 +83,176 @@ public class TSODirectoryParser {
|
||||
|
||||
private static void parseListcat(String[] lines, List<TSODirectoryEntry> entries) {
|
||||
TSODirectoryEntry current = null;
|
||||
String currentSpaceType = null;
|
||||
for (String line : lines) {
|
||||
String upper = line.toUpperCase();
|
||||
Matcher nonvsamMat = NONVSAM_PATTERN.matcher(upper);
|
||||
if (nonvsamMat.find()) {
|
||||
if (current != null && current.getName() != null) {
|
||||
if (current.getTracksAllocated() > 0 && current.getExtents() == 0) {
|
||||
current.setExtents(1);
|
||||
}
|
||||
entries.add(current);
|
||||
}
|
||||
current = new TSODirectoryEntry(nonvsamMat.group(1));
|
||||
if (upper.contains("CLUSTER") || upper.contains("AIX") || upper.contains("PATH")) {
|
||||
current.setDsorg("VSAM");
|
||||
}
|
||||
currentSpaceType = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current != null) {
|
||||
Matcher volMat = VOLSER_PATTERN.matcher(upper);
|
||||
if (volMat.find()) current.setVolume(volMat.group(1));
|
||||
if (volMat.find() && current.getVolume() == null) current.setVolume(volMat.group(1));
|
||||
|
||||
Matcher recMat = RECFM_PATTERN.matcher(upper);
|
||||
if (recMat.find()) current.setRecfm(recMat.group(1));
|
||||
if (recMat.find()) {
|
||||
String raw = recMat.group(1).toUpperCase();
|
||||
if (raw.contains("VAR") && raw.contains("BLOCK")) current.setRecfm("VB");
|
||||
else if (raw.contains("FIX") && raw.contains("BLOCK")) current.setRecfm("FB");
|
||||
else if (raw.contains("VAR")) current.setRecfm("V");
|
||||
else if (raw.contains("FIX")) current.setRecfm("F");
|
||||
else if (raw.contains("UNDEF")) current.setRecfm("U");
|
||||
else current.setRecfm(raw);
|
||||
}
|
||||
|
||||
Matcher lreclMat = LRECL_PATTERN.matcher(upper);
|
||||
if (lreclMat.find()) {
|
||||
if (lreclMat.find() && current.getLrecl() == 0) {
|
||||
try { current.setLrecl(Integer.parseInt(lreclMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Matcher blkMat = BLKSIZE_PATTERN.matcher(upper);
|
||||
if (blkMat.find()) {
|
||||
if (blkMat.find() && current.getBlksize() == 0) {
|
||||
try { current.setBlksize(Integer.parseInt(blkMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Matcher dsorgMat = DSORG_PATTERN.matcher(upper);
|
||||
if (dsorgMat.find()) current.setDsorg(dsorgMat.group(1));
|
||||
if (dsorgMat.find() && current.getDsorg() == null) current.setDsorg(dsorgMat.group(1));
|
||||
|
||||
Matcher extMat = EXTENTS_PATTERN.matcher(upper);
|
||||
if (extMat.find() && current.getExtents() == 0) {
|
||||
try { current.setExtents(Integer.parseInt(extMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Matcher typeMat = SPACE_TYPE_PATTERN.matcher(upper);
|
||||
if (typeMat.find()) {
|
||||
currentSpaceType = typeMat.group(1);
|
||||
}
|
||||
|
||||
Matcher trkMat = TRACKS_PATTERN.matcher(upper);
|
||||
if (trkMat.find() && current.getTracksAllocated() == 0) {
|
||||
try { current.setTracksAllocated(Integer.parseInt(trkMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
} else {
|
||||
Matcher priMat = SPACE_PRI_PATTERN.matcher(upper);
|
||||
if (priMat.find() && current.getTracksAllocated() == 0) {
|
||||
try {
|
||||
int pri = Integer.parseInt(priMat.group(1));
|
||||
if (currentSpaceType != null && currentSpaceType.startsWith("CYL")) {
|
||||
pri = pri * 15;
|
||||
}
|
||||
current.setTracksAllocated(pri);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (current != null && current.getName() != null) {
|
||||
if (current.getTracksAllocated() > 0 && current.getExtents() == 0) {
|
||||
current.setExtents(1);
|
||||
}
|
||||
entries.add(current);
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseListds(String[] lines, List<TSODirectoryEntry> entries) {
|
||||
TSODirectoryEntry current = null;
|
||||
boolean inRecfm = false;
|
||||
boolean inVol = false;
|
||||
|
||||
for (String line : lines) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.isEmpty()) continue;
|
||||
String upper = trimmed.toUpperCase();
|
||||
|
||||
if (upper.startsWith("--RECFM-")) {
|
||||
inRecfm = true;
|
||||
inVol = false;
|
||||
continue;
|
||||
}
|
||||
if (upper.startsWith("--VOLUMES--")) {
|
||||
inVol = true;
|
||||
inRecfm = false;
|
||||
continue;
|
||||
}
|
||||
if (upper.startsWith("--ALLOCATION") || upper.startsWith("--TRACKS-")) {
|
||||
inRecfm = false;
|
||||
inVol = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inRecfm && current != null) {
|
||||
String[] toks = trimmed.split("\\s+");
|
||||
if (toks.length >= 1 && toks[0].matches("^[A-Z]+$")) current.setRecfm(toks[0]);
|
||||
if (toks.length >= 2 && toks[1].matches("^\\d+$")) {
|
||||
try { current.setLrecl(Integer.parseInt(toks[1])); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
if (toks.length >= 3 && toks[2].matches("^\\d+$")) {
|
||||
try { current.setBlksize(Integer.parseInt(toks[2])); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
if (toks.length >= 4 && toks[3].matches("^[A-Z]+$")) current.setDsorg(toks[3]);
|
||||
inRecfm = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inVol && current != null) {
|
||||
String[] toks = trimmed.split("\\s+");
|
||||
if (toks.length >= 1 && toks[0].matches("^[A-Z0-9*@#$&]{1,6}$")) {
|
||||
current.setVolume(toks[0]);
|
||||
}
|
||||
inVol = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current != null) {
|
||||
Matcher trkMat = TRACKS_PATTERN.matcher(upper);
|
||||
if (trkMat.find() && current.getTracksAllocated() == 0) {
|
||||
try { current.setTracksAllocated(Integer.parseInt(trkMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
} else {
|
||||
Matcher priMat = SPACE_PRI_PATTERN.matcher(upper);
|
||||
if (priMat.find() && current.getTracksAllocated() == 0) {
|
||||
try { current.setTracksAllocated(Integer.parseInt(priMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
Matcher extMat = EXTENTS_PATTERN.matcher(upper);
|
||||
if (extMat.find() && current.getExtents() == 0) {
|
||||
try { current.setExtents(Integer.parseInt(extMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
// Check for tabular numbers row under --TRACKS-ALLOCATED-EXTENTS-
|
||||
String[] numToks = trimmed.split("\\s+");
|
||||
if (numToks.length >= 2 && numToks[0].matches("^\\d+$") && numToks[1].matches("^\\d+$")) {
|
||||
if (current.getTracksAllocated() == 0) {
|
||||
try { current.setTracksAllocated(Integer.parseInt(numToks[0])); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
if (current.getExtents() == 0) {
|
||||
try { current.setExtents(Integer.parseInt(numToks[1])); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String[] toks = trimmed.split("\\s+");
|
||||
if (toks.length > 0) {
|
||||
String cleanTok = toks[0].replace("'", "").replace("\"", "");
|
||||
if (DSN_PATTERN.matcher(cleanTok.toUpperCase()).matches()) {
|
||||
if (current != null && current.getName() != null) {
|
||||
entries.add(current);
|
||||
}
|
||||
current = new TSODirectoryEntry(cleanTok.toUpperCase());
|
||||
inRecfm = false;
|
||||
inVol = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (current != null && current.getName() != null) {
|
||||
@@ -127,6 +278,8 @@ public class TSODirectoryParser {
|
||||
|
||||
TSODirectoryEntry entry = new TSODirectoryEntry(tokens[dsnIdx].toUpperCase());
|
||||
boolean seenFormat = false;
|
||||
int numBeforeFormat = 0;
|
||||
int numAfterFormat = 0;
|
||||
|
||||
// Parse remaining tokens
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
@@ -136,7 +289,7 @@ public class TSODirectoryParser {
|
||||
if (tok.matches("^(PS|PO|PO-E|VSAM|DA|IS)$")) {
|
||||
entry.setDsorg(tok);
|
||||
seenFormat = true;
|
||||
} else if (tok.matches("^(F|FB|V|VB|U|VBS|FBS)$")) {
|
||||
} else if (tok.matches("^(F|FB|V|VB|U|VBS|FBS|FBA|VBA|FA|VA|FBM|VBM)$")) {
|
||||
entry.setRecfm(tok);
|
||||
seenFormat = true;
|
||||
} else if (tok.matches("^(3390|3380|TAPE|VIO)$")) {
|
||||
@@ -148,23 +301,29 @@ public class TSODirectoryParser {
|
||||
} else {
|
||||
entry.setReferencedDate(tok);
|
||||
}
|
||||
} else if (tok.matches("^[A-Z0-9]{6}$") && entry.getVolume() == null && !tok.matches("^\\d+$")) {
|
||||
} else if (tok.matches("^[A-Z0-9*@#$&]{1,6}$") && entry.getVolume() == null && !tok.matches("^\\d+$")
|
||||
&& !tok.matches("^(PAGE|TOTAL|TRACKS|TRKS|EXTENTS|VOLUME|DSORG|RECFM|DATASET|NAME|DSN)$")) {
|
||||
entry.setVolume(tok);
|
||||
} else if (tok.matches("^\\d+$")) {
|
||||
int val = Integer.parseInt(tok);
|
||||
if (!seenFormat) {
|
||||
if (entry.getTracksAllocated() == 0) {
|
||||
if (numBeforeFormat == 0) {
|
||||
entry.setTracksAllocated(val);
|
||||
} else if (entry.getPercentUsed() == 0 && val <= 100) {
|
||||
numBeforeFormat++;
|
||||
} else if (numBeforeFormat == 1 && val <= 100) {
|
||||
entry.setPercentUsed(val);
|
||||
} else if (entry.getExtents() == 0 && val <= 128) {
|
||||
numBeforeFormat++;
|
||||
} else if (numBeforeFormat <= 2 && val <= 128) {
|
||||
entry.setExtents(val);
|
||||
numBeforeFormat++;
|
||||
}
|
||||
} else {
|
||||
if (entry.getLrecl() == 0 && val <= 32760) {
|
||||
if (numAfterFormat == 0 && val <= 32760) {
|
||||
entry.setLrecl(val);
|
||||
} else if (entry.getBlksize() == 0 && val <= 32760) {
|
||||
numAfterFormat++;
|
||||
} else if (numAfterFormat == 1 && val <= 32760) {
|
||||
entry.setBlksize(val);
|
||||
numAfterFormat++;
|
||||
} else if (entry.getTracksAllocated() == 0) {
|
||||
entry.setTracksAllocated(val);
|
||||
}
|
||||
@@ -172,6 +331,10 @@ public class TSODirectoryParser {
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.getTracksAllocated() > 0 && entry.getExtents() == 0) {
|
||||
entry.setExtents(1);
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,17 +556,42 @@ public class TelnetConnection {
|
||||
running = false;
|
||||
stopKeepAlive();
|
||||
try {
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.flush();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.shutdownInput();
|
||||
socket.shutdownOutput();
|
||||
try {
|
||||
if (!socket.isOutputShutdown()) {
|
||||
socket.shutdownOutput();
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
// Allow host a brief drain window to receive EOF / FIN-ACK
|
||||
if (readerThread != null && readerThread.isAlive() && Thread.currentThread() != readerThread) {
|
||||
try {
|
||||
readerThread.join(200);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
} else if (inputStream != null) {
|
||||
try {
|
||||
socket.setSoTimeout(100);
|
||||
byte[] b = new byte[256];
|
||||
while (inputStream.read(b) != -1) {}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
socket.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.log(Level.FINE, "Error during disconnect", e);
|
||||
} finally {
|
||||
socket = null;
|
||||
inputStream = null;
|
||||
outputStream = null;
|
||||
readerThread = null;
|
||||
}
|
||||
socket = null;
|
||||
inputStream = null;
|
||||
outputStream = null;
|
||||
log.info("Disconnected");
|
||||
}
|
||||
|
||||
|
||||
@@ -1366,6 +1366,71 @@ public class TelnetFSM {
|
||||
sendRecord(resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send TN3270E UNBIND request to release LU session cleanly (RFC 2355 Section 9).
|
||||
* @param reason SNA UNBIND reason code (e.g. UNBIND_NORMAL = 0x01)
|
||||
*/
|
||||
public synchronized void sendUnbind(int reason) {
|
||||
if (!tn3270eNegotiated || !tn3270eBound) {
|
||||
return;
|
||||
}
|
||||
if (connection == null || !connection.isConnected()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
byte[] unbindData = new byte[EH_SIZE + 1];
|
||||
unbindData[0] = (byte) DT_UNBIND;
|
||||
unbindData[1] = 0; // Request flags
|
||||
unbindData[2] = (byte) RSF_NO_RESPONSE; // Response flags
|
||||
unbindData[3] = (byte) ((eXmitSeq >> 8) & 0xFF);
|
||||
unbindData[4] = (byte) (eXmitSeq & 0xFF);
|
||||
unbindData[5] = (byte) (reason & 0xFF);
|
||||
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
|
||||
log.info("Sending TN3270E UNBIND reason=" + unbindReasonName(reason));
|
||||
sendRecord(unbindData);
|
||||
tn3270eBound = false;
|
||||
} catch (Exception e) {
|
||||
log.log(Level.FINE, "Failed to send TN3270E UNBIND", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send graceful Telnet de-negotiation (UNBIND if bound, followed by WONT/DONT for active options).
|
||||
*/
|
||||
public synchronized void sendTelnetDisconnect() {
|
||||
if (connection == null || !connection.isConnected()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (tn3270eNegotiated && tn3270eBound) {
|
||||
sendUnbind(UNBIND_NORMAL);
|
||||
}
|
||||
// De-negotiate TN3270E first if active
|
||||
if (myOpts[TELOPT_TN3270E]) {
|
||||
sendCommand(WONT, TELOPT_TN3270E);
|
||||
myOpts[TELOPT_TN3270E] = false;
|
||||
}
|
||||
if (hisOpts[TELOPT_TN3270E]) {
|
||||
sendCommand(DONT, TELOPT_TN3270E);
|
||||
hisOpts[TELOPT_TN3270E] = false;
|
||||
}
|
||||
// De-negotiate all other active options
|
||||
for (int opt = 0; opt < 256; opt++) {
|
||||
if (myOpts[opt]) {
|
||||
sendCommand(WONT, opt);
|
||||
myOpts[opt] = false;
|
||||
}
|
||||
if (hisOpts[opt]) {
|
||||
sendCommand(DONT, opt);
|
||||
hisOpts[opt] = false;
|
||||
}
|
||||
}
|
||||
log.fine("Sent Telnet option de-negotiation");
|
||||
} catch (Exception e) {
|
||||
log.log(Level.FINE, "Error sending Telnet de-negotiation", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Check if we should transition to 3270 mode ==========
|
||||
|
||||
private void checkIn3270() {
|
||||
@@ -1531,6 +1596,9 @@ public class TelnetFSM {
|
||||
|
||||
private void changeState(ConnectionState newState) {
|
||||
ConnectionState old = this.connectionState;
|
||||
if (old == newState) {
|
||||
return;
|
||||
}
|
||||
this.connectionState = newState;
|
||||
log.info("State: " + old + " -> " + newState);
|
||||
for (ConnectionListener l : connectionListeners) {
|
||||
|
||||
@@ -610,55 +610,242 @@ public class Xfer3270 implements FileTransferInterface {
|
||||
if (session != null && session.getPS() != null) {
|
||||
try {
|
||||
ECLPS ps = session.getPS();
|
||||
ps.SendKeys("[clear]");
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(3000);
|
||||
}
|
||||
if (hostType == VM_CMS) {
|
||||
String filter = (hostQuery != null && !hostQuery.trim().isEmpty()) ? hostQuery.trim() : "* * *";
|
||||
ps.SendKeys("LISTFILE " + filter + " ( nohead alloc date[enter]");
|
||||
} else {
|
||||
String filter = (hostQuery != null && !hostQuery.trim().isEmpty()) ? hostQuery.trim() : "";
|
||||
if (filter.isEmpty()) {
|
||||
ps.SendKeys("LISTCAT[enter]");
|
||||
} else {
|
||||
if (!filter.startsWith("'")) filter = "'" + filter + "'";
|
||||
ps.SendKeys("LISTCAT LEVEL(" + filter + ")[enter]");
|
||||
}
|
||||
}
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(5000);
|
||||
}
|
||||
ScreenBuffer sb = ps.getScreenBuffer();
|
||||
StringBuilder sbAll = new StringBuilder();
|
||||
for (int page = 0; page < 20; page++) {
|
||||
int maxPages = 500;
|
||||
|
||||
if (hostType == VM_CMS) {
|
||||
ps.SendKeys("[clear]");
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(3000);
|
||||
}
|
||||
String filter;
|
||||
if (hostQuery == null || hostQuery.trim().isEmpty()) {
|
||||
filter = "* * A";
|
||||
} else {
|
||||
String clean = hostQuery.replace("'", "").replace("\"", "").trim();
|
||||
if (clean.equals("* * *") || clean.equals("*")) {
|
||||
filter = "* * A";
|
||||
} else if (clean.contains(".")) {
|
||||
int dot = clean.indexOf('.');
|
||||
filter = clean.substring(0, dot).trim() + " " + clean.substring(dot + 1).trim() + " A";
|
||||
} else {
|
||||
String[] parts = clean.split("\\s+");
|
||||
if (parts.length == 1) {
|
||||
filter = parts[0] + " * A";
|
||||
} else if (parts.length == 2) {
|
||||
filter = parts[0] + " " + parts[1] + " A";
|
||||
} else {
|
||||
filter = parts[0] + " " + parts[1] + " " + parts[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
ps.SendKeys("LISTFILE " + filter + " ( nohead alloc date[enter]");
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
|
||||
for (int page = 0; page < maxPages; page++) {
|
||||
long startWait = System.currentTimeMillis();
|
||||
long timeoutMs = 10000;
|
||||
String screen = "";
|
||||
boolean isMore = false;
|
||||
|
||||
while (System.currentTimeMillis() - startWait < timeoutMs) {
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(500);
|
||||
}
|
||||
int rows = ps.getRows();
|
||||
int cols = ps.getCols();
|
||||
screen = ps.copyString(0, 0, rows - 1, cols - 1);
|
||||
String upper = screen.toUpperCase();
|
||||
|
||||
if (upper.contains("MORE...") || upper.contains("HOLDING")) {
|
||||
isMore = true;
|
||||
break;
|
||||
}
|
||||
if (upper.contains("READY;") || upper.contains("READY(")) {
|
||||
break;
|
||||
}
|
||||
try { Thread.sleep(150); } catch (InterruptedException ignored) {}
|
||||
}
|
||||
|
||||
sbAll.append(screen).append("\n");
|
||||
|
||||
if (isMore) {
|
||||
ps.SendKeys("[clear]");
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(3000);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically clear the listing from the terminal presentation space
|
||||
ps.SendKeys("[clear]");
|
||||
if (session.getOIA() != null) {
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
session.getOIA().waitForInput(3000);
|
||||
}
|
||||
|
||||
} else {
|
||||
// TSO
|
||||
int rows = ps.getRows();
|
||||
int cols = ps.getCols();
|
||||
String screen = ps.copyString(0, 0, rows - 1, cols - 1);
|
||||
sbAll.append(screen).append("\n");
|
||||
String initialScreen = ps.copyString(0, 0, rows - 1, cols - 1);
|
||||
boolean isFormatted = sb != null && sb.isFormatted();
|
||||
boolean hasCmdArrow = initialScreen.contains("===>");
|
||||
boolean isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF");
|
||||
|
||||
boolean hasMore = false;
|
||||
if (hostType == VM_CMS) {
|
||||
if (screen.contains("MORE...") || screen.contains("HOLDING")) {
|
||||
hasMore = true;
|
||||
ps.SendKeys("[enter]");
|
||||
String filter = (hostQuery != null && !hostQuery.trim().isEmpty()) ? hostQuery.trim() : "";
|
||||
String tsoCmd = "TSO LISTCAT ALL";
|
||||
if (!filter.isEmpty()) {
|
||||
if (filter.startsWith("'") && filter.endsWith("'") && filter.length() > 2) {
|
||||
filter = filter.substring(1, filter.length() - 1);
|
||||
}
|
||||
} else {
|
||||
if (screen.contains("***")) {
|
||||
hasMore = true;
|
||||
ps.SendKeys("[enter]");
|
||||
int paren = filter.indexOf('(');
|
||||
if (paren > 0) filter = filter.substring(0, paren);
|
||||
if (filter.equals("*") || filter.equals("* * *") || filter.equals("*.*")) {
|
||||
tsoCmd = "TSO LISTCAT ALL";
|
||||
} else {
|
||||
String level = filter;
|
||||
if (level.endsWith(".*")) {
|
||||
level = level.substring(0, level.length() - 2);
|
||||
} else if (level.endsWith("*")) {
|
||||
level = level.substring(0, level.length() - 1);
|
||||
}
|
||||
if (level.isEmpty()) {
|
||||
tsoCmd = "TSO LISTCAT ALL";
|
||||
} else {
|
||||
tsoCmd = "TSO LISTCAT LEVEL(" + level + ") ALL";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMore) {
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
if (isIspf) {
|
||||
int arrowPos = initialScreen.indexOf("===>");
|
||||
if (arrowPos >= 0 && sb != null) {
|
||||
int inputAddr = sb.findNextUnprotected(arrowPos);
|
||||
if (inputAddr > 0) {
|
||||
sb.setCursorAddress(inputAddr);
|
||||
sb.updateDisplaySnapshot();
|
||||
} else {
|
||||
ps.SendKeys("[home]");
|
||||
}
|
||||
} else {
|
||||
ps.SendKeys("[home]");
|
||||
}
|
||||
ps.SendKeys("[erase_eof]");
|
||||
ps.SendKeys(tsoCmd + "[enter]");
|
||||
} else {
|
||||
ps.SendKeys("[clear]");
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(3000);
|
||||
}
|
||||
ps.SendKeys(tsoCmd + "[enter]");
|
||||
}
|
||||
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
|
||||
// Wait for output to start appearing
|
||||
long startCmd = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - startCmd < 4000) {
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(200);
|
||||
}
|
||||
int r = ps.getRows();
|
||||
int c = ps.getCols();
|
||||
String s = ps.copyString(0, 0, r - 1, c - 1);
|
||||
String u = s.toUpperCase();
|
||||
if (u.contains("NONVSAM") || u.contains("CLUSTER") || u.contains("GDG")
|
||||
|| u.contains("ALIAS") || u.contains("IN-CAT") || u.contains("IDCAMS")
|
||||
|| u.contains("***") || u.contains("READY") || u.contains("NOT FOUND")
|
||||
|| u.contains("NO ENTRY") || s.contains(" --- ")) {
|
||||
break;
|
||||
}
|
||||
try { Thread.sleep(80); } catch (InterruptedException ignored) {}
|
||||
}
|
||||
|
||||
String prevScreen = "";
|
||||
for (int page = 0; page < maxPages; page++) {
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(300);
|
||||
}
|
||||
int r = ps.getRows();
|
||||
int c = ps.getCols();
|
||||
String screen = ps.copyString(0, 0, r - 1, c - 1);
|
||||
String upper = screen.toUpperCase();
|
||||
|
||||
if (screen.equals(prevScreen) || screen.trim().isEmpty()) {
|
||||
break;
|
||||
}
|
||||
prevScreen = screen;
|
||||
sbAll.append(screen).append("\n");
|
||||
|
||||
if (upper.contains("***")) {
|
||||
ps.SendKeys("[enter]");
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(2000);
|
||||
}
|
||||
} else if (upper.contains("NONVSAM") || upper.contains("CLUSTER") || upper.contains("GDG")
|
||||
|| upper.contains("ALIAS") || upper.contains("IDCAMS") || screen.contains(" --- ")) {
|
||||
ps.SendKeys("[enter]");
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(1000);
|
||||
}
|
||||
|
||||
int r2 = ps.getRows();
|
||||
int c2 = ps.getCols();
|
||||
String s2 = ps.copyString(0, 0, r2 - 1, c2 - 1);
|
||||
if (s2.toUpperCase().contains("***")) {
|
||||
prevScreen = s2;
|
||||
sbAll.append(s2).append("\n");
|
||||
ps.SendKeys("[enter]");
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(2000);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
if (!s2.equals(screen) && !s2.trim().isEmpty()) {
|
||||
sbAll.append(s2).append("\n");
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Terminal clean-up
|
||||
if (isIspf) {
|
||||
ps.SendKeys("[enter]");
|
||||
try { Thread.sleep(150); } catch (InterruptedException ignored) {}
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(1500);
|
||||
}
|
||||
|
||||
int r = ps.getRows();
|
||||
int c = ps.getCols();
|
||||
String currentScr = ps.copyString(0, 0, r - 1, c - 1);
|
||||
if (currentScr.toUpperCase().contains("NONVSAM") || currentScr.toUpperCase().contains("IDCAMS")
|
||||
|| currentScr.contains(" --- ") || (sb != null && !sb.isFormatted())) {
|
||||
ps.SendKeys("[clear]");
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(2000);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
ps.SendKeys("[clear]");
|
||||
if (session.getOIA() != null) {
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
session.getOIA().waitForInput(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataToParse = sbAll.toString();
|
||||
} catch (Exception ignored) {
|
||||
// Fall back to hostQuery
|
||||
|
||||
@@ -14,7 +14,7 @@ public class FTConfigTest {
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||
|
||||
assertEquals("IND$FILE GET MY.DATASET ASCII CRLF", config.buildCommand());
|
||||
assertEquals("TSO IND$FILE GET MY.DATASET ASCII CRLF", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -25,7 +25,7 @@ public class FTConfigTest {
|
||||
config.setHostFilename("'USER.BIN.DATA'");
|
||||
config.setTransferMode(FTConfig.TransferMode.BINARY);
|
||||
|
||||
assertEquals("IND$FILE GET 'USER.BIN.DATA'", config.buildCommand());
|
||||
assertEquals("TSO IND$FILE GET 'USER.BIN.DATA'", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -40,7 +40,7 @@ public class FTConfigTest {
|
||||
config.setLrecl("80");
|
||||
config.setBlksize("3120");
|
||||
|
||||
assertEquals("IND$FILE PUT MY.DATASET ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120)", config.buildCommand());
|
||||
assertEquals("TSO IND$FILE PUT MY.DATASET ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120)", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -143,7 +143,7 @@ public class FTConfigTest {
|
||||
config.setUnits(FTConfig.AllocationUnit.TRACKS);
|
||||
|
||||
String cmd = config.buildCommand();
|
||||
assertEquals("IND$FILE PUT 'USER.PDS.DATA' ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120) SPACE(15,5,10) TRACKS", cmd);
|
||||
assertEquals("TSO IND$FILE PUT 'USER.PDS.DATA' ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120) SPACE(15,5,10) TRACKS", cmd);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -157,7 +157,7 @@ public class FTConfigTest {
|
||||
assertEquals(10, config.getSecondarySpace());
|
||||
assertEquals(15, config.getDirectoryBlocks());
|
||||
assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits());
|
||||
assertEquals("IND$FILE PUT SYS1.PARMLIB ASCII CRLF SPACE(20,10,15) TRACKS", config.buildCommand());
|
||||
assertEquals("TSO IND$FILE PUT SYS1.PARMLIB ASCII CRLF SPACE(20,10,15) TRACKS", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -171,7 +171,7 @@ public class FTConfigTest {
|
||||
assertEquals(5, config.getSecondarySpace());
|
||||
assertEquals(20, config.getDirectoryBlocks());
|
||||
assertEquals(FTConfig.AllocationUnit.CYLINDERS, config.getUnits());
|
||||
assertEquals("IND$FILE PUT SYS1.PROCLIB ASCII CRLF SPACE(10,5,20) CYLINDERS", config.buildCommand());
|
||||
assertEquals("TSO IND$FILE PUT SYS1.PROCLIB ASCII CRLF SPACE(10,5,20) CYLINDERS", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -443,7 +443,7 @@ public class FTProtocolHardeningPhase16Test {
|
||||
assertEquals("APVUFILE", optionsCfg.getProgramName());
|
||||
|
||||
String cmd = optionsCfg.buildCommand();
|
||||
assertTrue(cmd.startsWith("APVUFILE PUT 'TEST.FILE'"));
|
||||
assertTrue(cmd.startsWith("TSO APVUFILE PUT 'TEST.FILE'"));
|
||||
assertTrue(cmd.contains("NEW"));
|
||||
assertTrue(cmd.contains("NOSO"));
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ public class DirectoryParserTest {
|
||||
assertEquals(80, e0.getLrecl());
|
||||
assertEquals(3120, e0.getBlksize());
|
||||
assertEquals("PS", e0.getDsorg());
|
||||
assertEquals(15, e0.getTracksAllocated());
|
||||
assertEquals(1, e0.getExtents());
|
||||
|
||||
TSODirectoryEntry e1 = entries.get(1);
|
||||
assertEquals("TSOUSER.OTHER.DATA", e1.getName());
|
||||
@@ -151,4 +153,183 @@ public class DirectoryParserTest {
|
||||
assertEquals("USER.TEST.CNTL", entries.get(0).getName());
|
||||
assertEquals("USER.TEST.DATA", entries.get(1).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCmsMoreRunningAndReadyHandling() {
|
||||
String cmsOutput =
|
||||
"PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01 MORE...\n" +
|
||||
"MORE...\n" +
|
||||
"RUNNING\n" +
|
||||
"HOLDING\n" +
|
||||
"TEST DATA A1 F 80 100 2 2026-08-01 09:15:00\n" +
|
||||
"OTHER FILE A1 V 80 50 1 2026-08-02 10:00:00 RUNNING\n" +
|
||||
"Ready(00000); T=0.01/0.01 12:00:00\n";
|
||||
|
||||
List<CMSDirectoryEntry> entries = CMSDirectoryParser.parse(cmsOutput);
|
||||
assertEquals(3, entries.size(), "Should parse 3 CMS entries and strip/ignore MORE..., RUNNING, HOLDING, Ready(00000)");
|
||||
assertEquals("PROFILE", entries.get(0).getFilename());
|
||||
assertEquals("TEST", entries.get(1).getFilename());
|
||||
assertEquals("OTHER", entries.get(2).getFilename());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoListcatWithClusterAndGdg() {
|
||||
String listcatOutput =
|
||||
"IDCAMS SYSTEM SERVICES TIME: 12:00:00\n" +
|
||||
"NONVSAM ------- TSOUSER.NORMAL.DATA\n" +
|
||||
" VOLUMES\n" +
|
||||
" VOLSER------------TSO001\n" +
|
||||
"CLUSTER ------- TSOUSER.VSAM.KSDS\n" +
|
||||
" IN-CAT --- CATALOG.USER\n" +
|
||||
"GDG ----------- TSOUSER.GDG.BASE\n" +
|
||||
"ALIAS --------- TSOUSER.MY.ALIAS\n";
|
||||
|
||||
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listcatOutput);
|
||||
assertEquals(4, entries.size(), "Should parse NONVSAM, CLUSTER, GDG, and ALIAS");
|
||||
assertEquals("TSOUSER.NORMAL.DATA", entries.get(0).getName());
|
||||
assertEquals("TSO001", entries.get(0).getVolume());
|
||||
assertEquals("TSOUSER.VSAM.KSDS", entries.get(1).getName());
|
||||
assertEquals("VSAM", entries.get(1).getDsorg());
|
||||
assertEquals("TSOUSER.GDG.BASE", entries.get(2).getName());
|
||||
assertEquals("TSOUSER.MY.ALIAS", entries.get(3).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoListcatAllocDetails() {
|
||||
String allocOutput =
|
||||
"IDCAMS SYSTEM SERVICES TIME: 12:00:00\n" +
|
||||
"NONVSAM ------- TSOUSER.PROJECT.DATA\n" +
|
||||
" IN-CAT --- CATALOG.USER\n" +
|
||||
" VOLUMES\n" +
|
||||
" VOLSER------------TSOP01 DEVTYPE------X'3010200F'\n" +
|
||||
" ALLOCATION\n" +
|
||||
" SPACE-TYPE---------TRACK SPACE-PRI----------------15 SPACE-SEC----------------5\n" +
|
||||
" EXTENTS-----------------1\n" +
|
||||
"CLUSTER ------- TSOUSER.VSAM.TEST\n" +
|
||||
" IN-CAT --- CATALOG.USER\n" +
|
||||
" DATA ------- TSOUSER.VSAM.TEST.DATA\n" +
|
||||
" VOLUMES\n" +
|
||||
" VOLSER------------TSOP02\n" +
|
||||
" ALLOCATION\n" +
|
||||
" SPACE-TYPE------CYLINDER SPACE-PRI----------------2\n" +
|
||||
" EXTENTS-----------------2\n" +
|
||||
" ATTRIBUTES\n" +
|
||||
" AVGLRECL--------------80 MAXLRECL----------------80 CISIZE----------------4096\n";
|
||||
|
||||
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(allocOutput);
|
||||
assertEquals(2, entries.size());
|
||||
|
||||
TSODirectoryEntry nonvsam = entries.get(0);
|
||||
assertEquals("TSOUSER.PROJECT.DATA", nonvsam.getName());
|
||||
assertEquals("TSOP01", nonvsam.getVolume());
|
||||
assertEquals(15, nonvsam.getTracksAllocated());
|
||||
assertEquals(1, nonvsam.getExtents());
|
||||
|
||||
TSODirectoryEntry vsam = entries.get(1);
|
||||
assertEquals("TSOUSER.VSAM.TEST", vsam.getName());
|
||||
assertEquals("VSAM", vsam.getDsorg());
|
||||
assertEquals("TSOP02", vsam.getVolume());
|
||||
assertEquals(30, vsam.getTracksAllocated()); // 2 cylinders * 15 tracks
|
||||
assertEquals(2, vsam.getExtents());
|
||||
assertEquals(80, vsam.getLrecl());
|
||||
assertEquals(4096, vsam.getBlksize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoListdsParsing() {
|
||||
String listdsOutput =
|
||||
"TSOUSER.MY.SOURCE\n" +
|
||||
"--RECFM-LRECL-BLKSIZE-DSORG\n" +
|
||||
" FB 80 3120 PO\n" +
|
||||
"--VOLUMES--\n" +
|
||||
" SRC001\n";
|
||||
|
||||
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listdsOutput);
|
||||
assertEquals(1, entries.size());
|
||||
TSODirectoryEntry e = entries.get(0);
|
||||
assertEquals("TSOUSER.MY.SOURCE", e.getName());
|
||||
assertEquals("FB", e.getRecfm());
|
||||
assertEquals(80, e.getLrecl());
|
||||
assertEquals(3120, e.getBlksize());
|
||||
assertEquals("PO", e.getDsorg());
|
||||
assertEquals("SRC001", e.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoListdsWithStatusAllocationParsing() {
|
||||
String listdsStatusOutput =
|
||||
"SYS1.ACMDLIB\n" +
|
||||
"--RECFM-LRECL-BLKSIZE-DSORG\n" +
|
||||
" U 0 32760 PO\n" +
|
||||
"--VOLUMES--\n" +
|
||||
" TK5DLB\n" +
|
||||
"--ALLOCATION---------------\n" +
|
||||
" SPACE-TYPE TRACK\n" +
|
||||
" SPACE-PRI 30\n" +
|
||||
" SPACE-SEC 5\n" +
|
||||
"--TRACKS-ALLOCATED-EXTENTS-\n" +
|
||||
" 30 1\n";
|
||||
|
||||
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listdsStatusOutput);
|
||||
assertEquals(1, entries.size());
|
||||
TSODirectoryEntry e = entries.get(0);
|
||||
assertEquals("SYS1.ACMDLIB", e.getName());
|
||||
assertEquals("U", e.getRecfm());
|
||||
assertEquals(0, e.getLrecl());
|
||||
assertEquals(32760, e.getBlksize());
|
||||
assertEquals("PO", e.getDsorg());
|
||||
assertEquals("TK5DLB", e.getVolume());
|
||||
assertEquals(30, e.getTracksAllocated());
|
||||
assertEquals(1, e.getExtents());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoTabularDslispParsing() {
|
||||
String tabular = " USER.SAMPLE.COBOL MNT01 15 50 1 3390 PO FB 80 3120\n";
|
||||
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(tabular);
|
||||
assertEquals(1, entries.size());
|
||||
TSODirectoryEntry e = entries.get(0);
|
||||
assertEquals("USER.SAMPLE.COBOL", e.getName());
|
||||
assertEquals("MNT01", e.getVolume());
|
||||
assertEquals(15, e.getTracksAllocated());
|
||||
assertEquals(50, e.getPercentUsed());
|
||||
assertEquals(1, e.getExtents());
|
||||
assertEquals("3390", e.getDevice());
|
||||
assertEquals("PO", e.getDsorg());
|
||||
assertEquals("FB", e.getRecfm());
|
||||
assertEquals(80, e.getLrecl());
|
||||
assertEquals(3120, e.getBlksize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoListcatWithSysresAsterisksVolume() {
|
||||
String listcatOutput =
|
||||
"IDCAMS SYSTEM SERVICES TIME: 02:32:00\n" +
|
||||
"NONVSAM ------- SYS1.ACMDLIB\n" +
|
||||
" IN-CAT --- SYS1.MCAT.TK5\n" +
|
||||
" HISTORY\n" +
|
||||
" OWNER-IDENT-------(NULL) CREATION----------23.146\n" +
|
||||
" RELEASE----------------2 EXPIRATION--------00.000\n" +
|
||||
" VOLUMES\n" +
|
||||
" VOLSER------------****** DEVTYPE------X'3010200F' FSEQN------------------0\n" +
|
||||
" ASSOCIATIONS--------(NULL)\n" +
|
||||
"NONVSAM ------- SYS1.SYSTEM.CMDSBSYS.CSCDATA\n" +
|
||||
" IN-CAT --- SYS1.MCAT.TK5\n" +
|
||||
" HISTORY\n" +
|
||||
" OWNER-IDENT-------(NULL) CREATION----------23.146\n" +
|
||||
" VOLUMES\n" +
|
||||
" VOLSER------------TK5002 DEVTYPE------X'3010200F' FSEQN------------------0\n" +
|
||||
" ASSOCIATIONS--------(NULL)\n";
|
||||
|
||||
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listcatOutput);
|
||||
assertEquals(2, entries.size());
|
||||
|
||||
TSODirectoryEntry sysresEntry = entries.get(0);
|
||||
assertEquals("SYS1.ACMDLIB", sysresEntry.getName());
|
||||
assertEquals("******", sysresEntry.getVolume(), "SYSRES volume with asterisks should be preserved");
|
||||
|
||||
TSODirectoryEntry explicitEntry = entries.get(1);
|
||||
assertEquals("SYS1.SYSTEM.CMDSBSYS.CSCDATA", explicitEntry.getName());
|
||||
assertEquals("TK5002", explicitEntry.getVolume());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ public class Mock3270Server implements AutoCloseable {
|
||||
private int sequenceNumber = 0;
|
||||
|
||||
private final List<byte[]> receivedRecords = new CopyOnWriteArrayList<>();
|
||||
private final List<int[]> receivedCommands = new CopyOnWriteArrayList<>();
|
||||
private final AtomicInteger receivedNops = new AtomicInteger(0);
|
||||
private final AtomicInteger receivedTimingMarks = new AtomicInteger(0);
|
||||
|
||||
@@ -126,6 +127,10 @@ public class Mock3270Server implements AutoCloseable {
|
||||
return Collections.unmodifiableList(receivedRecords);
|
||||
}
|
||||
|
||||
public List<int[]> getReceivedCommands() {
|
||||
return Collections.unmodifiableList(receivedCommands);
|
||||
}
|
||||
|
||||
public byte[] getLastReceivedRecord() {
|
||||
if (receivedRecords.isEmpty()) return null;
|
||||
return receivedRecords.get(receivedRecords.size() - 1);
|
||||
@@ -285,6 +290,7 @@ public class Mock3270Server implements AutoCloseable {
|
||||
|
||||
private void handleOptionCommand(int cmd, int opt) throws IOException {
|
||||
log.fine("Mock3270Server: RCVD IAC " + cmd + " opt=" + opt);
|
||||
receivedCommands.add(new int[] { cmd, opt });
|
||||
if (cmd == WILL && opt == TELOPT_TN3270E) {
|
||||
// Client accepted TN3270E -> Send SB TN3270E SEND DEVICE-TYPE IAC SE
|
||||
sendDeviceTypeSend();
|
||||
|
||||
+42
@@ -290,4 +290,46 @@ public class Mock3270ServerIntegrationTest {
|
||||
|
||||
assertTrue(matched, "Fragmented stream must be reassembled and displayed correctly");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGracefulDisconnectSendsUnbindAndDeNegotiation() 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);
|
||||
assertTrue(client.getTelnetFSM().isTn3270eBound());
|
||||
|
||||
// Perform graceful disconnect
|
||||
client.disconnect();
|
||||
|
||||
// Verify client transitioned to NOT_CONNECTED
|
||||
assertEquals(ConnectionState.NOT_CONNECTED, client.getConnectionState());
|
||||
assertFalse(client.isConnected());
|
||||
|
||||
// Wait briefly for server to process queued records
|
||||
Thread.sleep(150);
|
||||
|
||||
// Verify server received TN3270E UNBIND
|
||||
boolean receivedUnbind = false;
|
||||
for (byte[] rec : server.getReceivedRecords()) {
|
||||
if (rec != null && rec.length > 5 && (rec[0] & 0xFF) == DT_UNBIND) {
|
||||
int reason = rec[5] & 0xFF;
|
||||
assertEquals(UNBIND_NORMAL, reason, "UNBIND reason should be UNBIND_NORMAL (0x01)");
|
||||
receivedUnbind = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(receivedUnbind, "Server should have received TN3270E UNBIND record");
|
||||
|
||||
// Verify server received Telnet option de-negotiation (WONT TN3270E or DONT TN3270E)
|
||||
boolean receivedWontOrDontTn3270e = false;
|
||||
for (int[] cmd : server.getReceivedCommands()) {
|
||||
if ((cmd[0] == WONT || cmd[0] == DONT) && cmd[1] == TELOPT_TN3270E) {
|
||||
receivedWontOrDontTn3270e = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(receivedWontOrDontTn3270e, "Server should have received Telnet de-negotiation for TN3270E");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user