Skip to content

Commit f966f31

Browse files
committed
fix: ensure embedded PostgreSQL is cleaned up on crash or forced termination (Fix JabRef#12844)
* Added logic to detect and clean up stale embedded PostgreSQL instances left behind by previous JabRef sessions. * Introduced PostgresProcessCleaner to safely shut down any orphaned PostgreSQL processes using PID-based detection. * Registered a shutdown hook in Launcher to ensure embedded PostgreSQL is properly terminated during normal or abrupt shutdown.
1 parent 4e11d76 commit f966f31

File tree

4 files changed

+177
-0
lines changed

4 files changed

+177
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
104104
- We fixed an issue where valid DOI could not be imported if it had special characters like `<` or `>`. [#12434](https://github.com/JabRef/jabref/issues/12434)
105105
- We fixed an issue where the tooltip only displayed the first linked file when hovering. [#12470](https://github.com/JabRef/jabref/issues/12470)
106106
- We fixed an issue where some texts in the "Citation Information" tab and the "Preferences" dialog could not be translated. [#12883](https://github.com/JabRef/jabref/pull/12883)
107+
- We fixed an issue where postgres processes remain running after JabRef crashes or is forcefully terminated. [#12844] ()
107108

108109
### Removed
109110

src/main/java/org/jabref/Launcher.java

+7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.jabref.logic.UiCommand;
1111
import org.jabref.logic.preferences.CliPreferences;
1212
import org.jabref.logic.search.PostgreServer;
13+
import org.jabref.logic.search.PostgreProcessCleaner;
1314
import org.jabref.logic.util.HeadlessExecutorService;
1415
import org.jabref.migrations.PreferencesMigrations;
1516

@@ -26,6 +27,9 @@ public class Launcher {
2627
public static void main(String[] args) {
2728
JabKit.initLogging(args);
2829

30+
//Clean up old Postgres instance if needed
31+
PostgreProcessCleaner.getInstance().checkAndCleanupOldInstance();
32+
2933
// Initialize preferences
3034
final JabRefGuiPreferences preferences = JabRefGuiPreferences.getInstance();
3135
Injector.setModelOrService(CliPreferences.class, preferences);
@@ -42,6 +46,9 @@ public static void main(String[] args) {
4246
PostgreServer postgreServer = new PostgreServer();
4347
Injector.setModelOrService(PostgreServer.class, postgreServer);
4448

49+
//Register shutdown hook
50+
Runtime.getRuntime().addShutdownHook(new Thread(postgreServer::shutdown));
51+
4552
JabRefGUI.setup(uiCommands, preferences, fileUpdateMonitor);
4653
JabRefGUI.launch(JabRefGUI.class, args);
4754
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package org.jabref.logic.search;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
7+
import java.io.BufferedReader;
8+
import java.io.IOException;
9+
import java.io.InputStreamReader;
10+
import java.net.Socket;
11+
import java.nio.file.Files;
12+
import java.util.HashMap;
13+
import java.util.Map;
14+
import java.util.Optional;
15+
16+
import static org.jabref.logic.os.OS.getHostName;
17+
import static org.jabref.logic.search.PostgreServer.POSTGRES_METADATA_FILE;
18+
19+
public class PostgreProcessCleaner {
20+
private static final Logger LOGGER = LoggerFactory.getLogger(PostgreProcessCleaner.class);
21+
private static final PostgreProcessCleaner INSTANCE = new PostgreProcessCleaner();
22+
23+
private PostgreProcessCleaner() {}
24+
25+
public static PostgreProcessCleaner getInstance() {
26+
return INSTANCE;
27+
}
28+
29+
public void checkAndCleanupOldInstance() {
30+
if (!Files.exists(POSTGRES_METADATA_FILE))
31+
return;
32+
33+
try {
34+
Map<String, Object> metadata = new HashMap<>(new ObjectMapper()
35+
.readValue(Files.readAllBytes(POSTGRES_METADATA_FILE), HashMap.class));
36+
if(!metadata.isEmpty()) {
37+
int port = ((Number) metadata.get("postgresPort")).intValue();
38+
destroyPreviousJavaProcess(metadata);
39+
destroyPostgresProcess(port);
40+
}
41+
Files.deleteIfExists(POSTGRES_METADATA_FILE);
42+
} catch (IOException e) {
43+
LOGGER.warn("Failed to read Postgres metadata file: {}", e.getMessage());
44+
} catch (InterruptedException e) {
45+
LOGGER.warn("Thread sleep was interrupted while Postgres cleanup: {}", e.getMessage());
46+
Thread.currentThread().interrupt();
47+
} catch (Exception e) {
48+
LOGGER.warn("Failed to clean up old embedded Postgres: {}", e.getMessage());
49+
}
50+
}
51+
52+
private void destroyPreviousJavaProcess(Map<String, Object> meta) throws InterruptedException {
53+
long javaPid = ((Number) meta.get("javaPid")).longValue();
54+
destroyProcessByPID(javaPid, 1000);
55+
}
56+
57+
private void destroyPostgresProcess(int port) throws InterruptedException {
58+
if (isPortOpen(getHostName(), port)) {
59+
long pid = getPidUsingPort(port);
60+
if (pid != -1) {
61+
LOGGER.info("Old Postgres instance found on port {} (PID {}). Killing it...", port, pid);
62+
destroyProcessByPID(pid, 1500);
63+
} else {
64+
LOGGER.warn("Could not determine PID using port {}. Skipping kill step.", port);
65+
}
66+
}
67+
}
68+
69+
private void destroyProcessByPID(long pid, int millis) throws InterruptedException {
70+
Optional<ProcessHandle> handle = ProcessHandle.of(pid);
71+
if (handle.isPresent() && handle.get().isAlive()) {
72+
handle.get().destroy();
73+
Thread.sleep(millis);
74+
}
75+
}
76+
77+
private boolean isPortOpen(String host, int port) {
78+
try (Socket _ = new Socket(host, port)) {
79+
return true;
80+
} catch (IOException ex) {
81+
return false;
82+
}
83+
}
84+
85+
private long getPidUsingPort(int port) {
86+
String os = System.getProperty("os.name").toLowerCase();
87+
try {
88+
Process process = createPortLookupProcess(os, port);
89+
if (process == null) {
90+
LOGGER.warn("Unsupported OS for port-based PID lookup.");
91+
return -1;
92+
}
93+
94+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
95+
return extractPidFromOutput(os, reader);
96+
}
97+
} catch (Exception e) {
98+
LOGGER.warn("Failed to get PID for port {}: {}", port, e.getMessage());
99+
}
100+
101+
return -1;
102+
}
103+
104+
private Process createPortLookupProcess(String os, int port) throws IOException {
105+
if (os.contains("mac") || os.contains("nix") || os.contains("nux")) {
106+
return new ProcessBuilder("lsof", "-i", "tcp:" + port, "-sTCP:LISTEN", "-Pn")
107+
.redirectErrorStream(true).start();
108+
} else if (os.contains("win")) {
109+
return new ProcessBuilder("cmd.exe", "/c", "netstat -ano | findstr :" + port)
110+
.redirectErrorStream(true).start();
111+
}
112+
return null;
113+
}
114+
115+
private long extractPidFromOutput(String os, BufferedReader reader) throws IOException {
116+
String line;
117+
while ((line = reader.readLine()) != null) {
118+
if (os.contains("nix") || os.contains("nux") || os.contains("mac")) {
119+
Long pid = parseUnixPidFromLine(line);
120+
if (pid != null) return pid;
121+
} else if (os.contains("win")) {
122+
Long pid = parseWindowsPidFromLine(line);
123+
if (pid != null) return pid;
124+
}
125+
}
126+
return -1;
127+
}
128+
129+
private Long parseUnixPidFromLine(String line) {
130+
String[] parts = line.trim().split("\\s+");
131+
if (parts.length > 1 && parts[1].matches("\\d+"))
132+
return Long.parseLong(parts[1]);
133+
return null;
134+
}
135+
136+
private Long parseWindowsPidFromLine(String line) {
137+
String[] parts = line.trim().split("\\s+");
138+
if (parts.length >= 5 && parts[parts.length - 1].matches("\\d+"))
139+
return Long.parseLong(parts[parts.length - 1]);
140+
return null;
141+
}
142+
143+
}

src/main/java/org/jabref/logic/search/PostgreServer.java

+26
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,27 @@
66

77
import javax.sql.DataSource;
88

9+
import com.fasterxml.jackson.databind.ObjectMapper;
910
import org.jabref.model.search.PostgreConstants;
1011

1112
import io.zonky.test.db.postgres.embedded.EmbeddedPostgres;
1213
import org.slf4j.Logger;
1314
import org.slf4j.LoggerFactory;
1415

16+
17+
import java.nio.file.Path;
18+
import java.time.Instant;
19+
import java.time.format.DateTimeFormatter;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
1523
import static org.jabref.model.search.PostgreConstants.BIB_FIELDS_SCHEME;
1624

1725
public class PostgreServer {
1826
private static final Logger LOGGER = LoggerFactory.getLogger(PostgreServer.class);
27+
public static final Path POSTGRES_METADATA_FILE = Path.of("/tmp/jabref-postgres-info.json");
28+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
29+
1930
private final EmbeddedPostgres embeddedPostgres;
2031
private final DataSource dataSource;
2132

@@ -26,6 +37,7 @@ public PostgreServer() {
2637
.setOutputRedirector(ProcessBuilder.Redirect.DISCARD)
2738
.start();
2839
LOGGER.info("Postgres server started, connection port: {}", embeddedPostgres.getPort());
40+
writeMetadataToFile(embeddedPostgres.getPort());
2941
} catch (IOException e) {
3042
LOGGER.error("Could not start Postgres server", e);
3143
this.embeddedPostgres = null;
@@ -96,4 +108,18 @@ public void shutdown() {
96108
}
97109
}
98110
}
111+
112+
private void writeMetadataToFile(int port) {
113+
try {
114+
Map<String, Object> meta = new HashMap<>();
115+
meta.put("javaPid", ProcessHandle.current().pid());
116+
meta.put("postgresPort", port);
117+
meta.put("startedBy", "jabref");
118+
meta.put("startedAt", DateTimeFormatter.ISO_INSTANT.format(Instant.now()));
119+
OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValue(POSTGRES_METADATA_FILE.toFile(), meta);
120+
} catch (IOException e) {
121+
LOGGER.warn("Failed to write Postgres metadata file", e);
122+
}
123+
}
124+
99125
}

0 commit comments

Comments
 (0)