Skip to content

Commit 490947a

Browse files
authored
[GEODE-10523] 2.0 RELEASE BLOCKER : gfsh issues after Spring Shell 3 migration (#7958)
* GEODE-10523: Fix NullPointerException in gfsh startup - Add terminal initialization before promptLoop() - Implement history file migration from JLine 2 to JLine 3 format - Fix banner display to stdout in non-headless mode After migrating from Spring Shell 1.x to 3.x, terminal and lineReader were not being initialized, causing NPE when gfsh tried to read input. Also fixed incompatible history file format and missing banner output. * Restore original printAsInfo behavior - Revert printAsInfo() to use logger.info() in non-headless mode (matching pre-Jakarta migration behavior from commit 30cd678^) - Move printBannerAndWelcome() after terminal initialization - This ensures banner output is consistent with original behavior
1 parent 80cf202 commit 490947a

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

geode-gfsh/src/main/java/org/apache/geode/management/internal/cli/shell/Gfsh.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.io.IOException;
2424
import java.io.PrintStream;
2525
import java.net.URL;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
2628
import java.nio.file.Paths;
2729
import java.text.MessageFormat;
2830
import java.util.ArrayList;
@@ -38,6 +40,7 @@
3840
import org.jline.reader.LineReader;
3941
import org.jline.reader.LineReaderBuilder;
4042
import org.jline.terminal.Terminal;
43+
import org.jline.terminal.TerminalBuilder;
4144

4245
import org.apache.geode.annotations.internal.MakeNotStatic;
4346
import org.apache.geode.annotations.internal.MutableForTesting;
@@ -1094,6 +1097,9 @@ private void write(String message, boolean isError) {
10941097
}
10951098

10961099
protected LineReader createConsoleReader() {
1100+
// Check and migrate old history file format before creating LineReader
1101+
migrateHistoryFileIfNeeded();
1102+
10971103
// Create GfshCompleter with our parser to enable TAB completion
10981104
GfshCompleter completer = new GfshCompleter(this.parser);
10991105

@@ -1110,6 +1116,50 @@ protected LineReader createConsoleReader() {
11101116
return lineReader;
11111117
}
11121118

1119+
/**
1120+
* Checks if the history file exists and is in old JLine 2 format.
1121+
* If so, backs it up and creates a new empty history file.
1122+
*/
1123+
private void migrateHistoryFileIfNeeded() {
1124+
try {
1125+
Path historyPath = Paths.get(getHistoryFileName());
1126+
if (!Files.exists(historyPath)) {
1127+
return;
1128+
}
1129+
1130+
// Check if file contains old format markers (lines starting with // or complex format)
1131+
java.util.List<String> lines = Files.readAllLines(historyPath);
1132+
boolean hasOldFormat = false;
1133+
for (String line : lines) {
1134+
String trimmed = line.trim();
1135+
// Old format had // comments and complex history format
1136+
if (trimmed.startsWith("//") || trimmed.startsWith("#")) {
1137+
hasOldFormat = true;
1138+
break;
1139+
}
1140+
// JLine 3 format should be simple: just command lines or :time:command format
1141+
// If we see anything complex, assume old format
1142+
if (trimmed.contains(":") && !trimmed.matches("^\\d+:\\d+:.*")) {
1143+
// Might be old format - be conservative and migrate
1144+
hasOldFormat = true;
1145+
break;
1146+
}
1147+
}
1148+
1149+
if (hasOldFormat) {
1150+
// Backup old history file
1151+
Path backupPath = historyPath.getParent()
1152+
.resolve(historyPath.getFileName().toString() + ".old");
1153+
Files.move(historyPath, backupPath,
1154+
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
1155+
gfshFileLogger.info("Migrated old history file format. Backup saved to: " + backupPath);
1156+
}
1157+
} catch (IOException e) {
1158+
// Ignore - history migration is not critical
1159+
gfshFileLogger.warning("Could not migrate history file", e);
1160+
}
1161+
}
1162+
11131163
protected void logCommandToOutput(String processedLine) {
11141164
String originalString = expandedPropCommandsMap.get(processedLine);
11151165
if (originalString != null) {
@@ -1589,10 +1639,27 @@ protected String expandProperties(final String input) {
15891639
@Override
15901640
public void run() {
15911641
try {
1642+
// Initialize terminal and line reader before starting prompt loop
1643+
if (!isHeadlessMode) {
1644+
initializeTerminal();
1645+
createConsoleReader();
1646+
}
15921647
printBannerAndWelcome();
15931648
promptLoop();
15941649
} catch (Exception e) {
15951650
gfshFileLogger.severe("Error in shell main loop", e);
15961651
}
15971652
}
1653+
1654+
/**
1655+
* Initializes the JLine 3 Terminal for interactive shell use.
1656+
* This must be called before creating the LineReader.
1657+
*/
1658+
private void initializeTerminal() throws IOException {
1659+
if (terminal == null) {
1660+
terminal = TerminalBuilder.builder()
1661+
.system(true)
1662+
.build();
1663+
}
1664+
}
15981665
}

geode-gfsh/src/main/java/org/apache/geode/management/internal/cli/shell/jline/GfshHistory.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
* Overrides JLine History to add History without newline characters.
2929
* Updated for JLine 3.x: extends DefaultHistory instead of MemoryHistory
3030
*
31+
* <p>
32+
* This implementation handles both old JLine 2 format history files (with timestamp comments)
33+
* and new JLine 3 format. When loading an old format file, it will be converted to the new format.
34+
*
3135
* @since GemFire 7.0
3236
*/
3337
public class GfshHistory extends DefaultHistory {
@@ -60,6 +64,68 @@ public void setHistoryFilePath(Path path) {
6064
}
6165
}
6266

67+
/**
68+
* Override attach to handle migration from old JLine 2 history format.
69+
* If loading fails due to format issues, we'll backup the old file and start fresh.
70+
*/
71+
@Override
72+
public void attach(org.jline.reader.LineReader reader) {
73+
try {
74+
super.attach(reader);
75+
} catch (Exception e) {
76+
// Check if it's a history file format issue
77+
Throwable cause = e;
78+
while (cause != null) {
79+
if (cause instanceof IllegalArgumentException
80+
&& cause.getMessage() != null
81+
&& cause.getMessage().contains("Bad history file syntax")) {
82+
// Backup old history file and start fresh
83+
migrateOldHistoryFile();
84+
// Try again with clean file
85+
try {
86+
super.attach(reader);
87+
} catch (Exception ex) {
88+
// If still fails, just continue without history
89+
}
90+
return;
91+
}
92+
cause = cause.getCause();
93+
}
94+
// Re-throw if not a history format issue
95+
if (e instanceof RuntimeException) {
96+
throw (RuntimeException) e;
97+
}
98+
throw new RuntimeException(e);
99+
}
100+
}
101+
102+
/**
103+
* Migrates old JLine 2 format history file to JLine 3 format.
104+
* Backs up the old file and creates a new one with only the valid history entries.
105+
*/
106+
private void migrateOldHistoryFile() {
107+
if (historyFilePath == null || !Files.exists(historyFilePath)) {
108+
return;
109+
}
110+
111+
try {
112+
// Backup old history file
113+
Path backupPath = historyFilePath.getParent()
114+
.resolve(historyFilePath.getFileName().toString() + ".old");
115+
Files.move(historyFilePath, backupPath,
116+
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
117+
118+
// Create new history file with timestamp
119+
try (BufferedWriter writer = Files.newBufferedWriter(historyFilePath,
120+
StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
121+
writer.write("# Migrated from old format on " + new java.util.Date());
122+
writer.newLine();
123+
}
124+
} catch (IOException e) {
125+
// Ignore - just start with empty history
126+
}
127+
}
128+
63129
public void addToHistory(String buffer) {
64130
if (isAutoFlush()) {
65131
String redacted = ArgumentRedactor.redact(buffer.trim());

0 commit comments

Comments
 (0)