Skip to content
Closed
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f966f31
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 13, 2025
fdc4931
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 13, 2025
49eca96
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 13, 2025
24f1b13
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 13, 2025
e245d2b
Merge branch 'issue-12844' of https://github.com/UdayHE/jabref into i…
UdayHE Apr 13, 2025
c430d80
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 13, 2025
d7b7586
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 13, 2025
a991627
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 13, 2025
318f01d
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 13, 2025
e85245f
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 15, 2025
69867cf
Merge branch 'main' into issue-12844
UdayHE Apr 15, 2025
b01d4a7
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 15, 2025
4abd5f1
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 15, 2025
ee83db3
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 15, 2025
482199b
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 15, 2025
9265211
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 15, 2025
0e51bba
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 15, 2025
6929226
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 15, 2025
9447181
Merge branch 'main' into issue-12844
UdayHE Apr 15, 2025
fe72794
fix: ensure embedded PostgreSQL is cleaned up on crash or forced term…
UdayHE Apr 15, 2025
ad960ec
fixes #12844
UdayHE Apr 15, 2025
5029657
fixes #12844
UdayHE Apr 15, 2025
27970d6
fixes #12844
UdayHE Apr 15, 2025
39f6df6
Merge remote-tracking branch 'upstream/main' into issue-12844
UdayHE Apr 16, 2025
2945b3b
Fixes https://github.com/JabRef/jabref/issues/12844
UdayHE Apr 16, 2025
7c590b2
Merge branch 'main' into issue-12844
UdayHE Apr 16, 2025
a9a192b
Merge remote-tracking branch 'origin' into issue-12844
UdayHE May 17, 2025
c4d90fc
Merge remote-tracking branch 'origin' into issue-12844
UdayHE May 19, 2025
fa8eab9
Fix for issue-12844:
UdayHE May 19, 2025
5d252e5
Merge branch 'main' into issue-12844
UdayHE May 19, 2025
9b5150d
Modified CHANGELOG.md.
UdayHE May 19, 2025
9cec567
Merge branch 'main' into issue-12844
UdayHE May 19, 2025
94bc898
Merge branch 'main' into issue-12844
UdayHE May 19, 2025
5bc6477
Merge branch 'main' into issue-12844
UdayHE May 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We fixed an issue where downloading PDFs from URLs to empty entries resulted in meaningless filenames like "-.pdf". [#12917](https://github.com/JabRef/jabref/issues/12917)
- We fixed an issue where pasting a PDF URL into the main table caused an import error instead of creating a new entry. [#12911](https://github.com/JabRef/jabref/pull/12911)
- We fixed an issue where libraries would sometimes be hidden when closing tabs with the Welcome tab open. [#12894](https://github.com/JabRef/jabref/issues/12894)
- We fixed an issue where postgres processes remain running after JabRef crashes or is forcefully terminated. [#12927](https://github.com/JabRef/jabref/pull/12927)
- We fixed an issue with deleting entries in large libraries that caused it to take a long time. [#8976](https://github.com/JabRef/jabref/issues/8976)

### Removed
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/org/jabref/Launcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.jabref.gui.util.DefaultFileUpdateMonitor;
import org.jabref.logic.UiCommand;
import org.jabref.logic.preferences.CliPreferences;
import org.jabref.logic.search.PostgreProcessCleaner;
import org.jabref.logic.search.PostgreServer;
import org.jabref.logic.util.HeadlessExecutorService;
import org.jabref.migrations.PreferencesMigrations;
Expand All @@ -26,6 +27,9 @@ public class Launcher {
public static void main(String[] args) {
JabKit.initLogging(args);

// Clean up old Postgres instance if needed
PostgreProcessCleaner.getInstance().checkAndCleanupOldInstances();

// Initialize preferences
final JabRefGuiPreferences preferences = JabRefGuiPreferences.getInstance();
Injector.setModelOrService(CliPreferences.class, preferences);
Expand All @@ -42,6 +46,9 @@ public static void main(String[] args) {
PostgreServer postgreServer = new PostgreServer();
Injector.setModelOrService(PostgreServer.class, postgreServer);

// Register shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(postgreServer::shutdown));

JabRefGUI.setup(uiCommands, preferences, fileUpdateMonitor);
JabRefGUI.launch(JabRefGUI.class, args);
}
Expand Down
169 changes: 169 additions & 0 deletions src/main/java/org/jabref/logic/search/PostgreProcessCleaner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package org.jabref.logic.search;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

import org.jabref.logic.os.OS;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PostgreProcessCleaner {
private static final Logger LOGGER = LoggerFactory.getLogger(PostgreProcessCleaner.class);
private static final PostgreProcessCleaner INSTANCE = new PostgreProcessCleaner();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final Path TEMP_DIR = Path.of(System.getProperty("java.io.tmpdir"));
private static final String FILE_PREFIX = "jabref-postgres-info-";
private static final String FILE_SUFFIX = ".json";
private static final int POSTGRES_SHUTDOWN_WAIT_MILLIS = 1500;

private PostgreProcessCleaner() {
}

public static PostgreProcessCleaner getInstance() {
return INSTANCE;
}

public void checkAndCleanupOldInstances() {
try (Stream<Path> files = Files.list(TEMP_DIR)) {
files.filter(path -> path.getFileName().toString().startsWith(FILE_PREFIX))
.filter(path -> path.getFileName().toString().endsWith(FILE_SUFFIX))
.forEach(this::cleanupIfDeadProcess);
} catch (IOException e) {
LOGGER.warn("Failed to list temp directory for Postgres metadata cleanup: {}", e.getMessage(), e);
}
}

private void cleanupIfDeadProcess(Path metadataPath) {
try {
Map<String, Object> metadata = new HashMap<>(OBJECT_MAPPER.readValue(Files.readAllBytes(metadataPath), HashMap.class));
long javaPid = ((Number) metadata.get("javaPid")).longValue();
if (isJavaProcessAlive(javaPid)) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the database is still running, is it necessary to clean up the data to ensure it's in the same state as a freshly started database?

}
int postgresPort = ((Number) metadata.get("postgresPort")).intValue();
destroyPostgresProcess(postgresPort);
Files.deleteIfExists(metadataPath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting question—what happens if you're running multiple JabRef instances? How can you distinguish between them?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally JabRef has only a single instance open, it is a setting in the preferences
grafik

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the question was for the other case.

Maybe, we should just start to disallow multiple instances?

} catch (IOException e) {
LOGGER.warn("Failed to read or parse metadata file '{}': {}", metadataPath, e.getMessage(), e);
} catch (InterruptedException e) {
LOGGER.warn("Cleanup was interrupted for '{}': {}", metadataPath, e.getMessage(), e);
Thread.currentThread().interrupt();
} catch (Exception e) {
LOGGER.warn("Unexpected error during cleanup of '{}': {}", metadataPath, e.getMessage(), e);
}
}

private void destroyPostgresProcess(int port) throws InterruptedException {
if (isPortOpen(port)) {
long pid = getPidUsingPort(port);
if (pid != -1) {
LOGGER.info("Old Postgres instance found on port {} (PID {}). Killing it.", port, pid);
destroyProcessByPID(pid);
} else {
LOGGER.warn("Could not determine PID using port {}. Skipping kill step.", port);
}
}
}

private void destroyProcessByPID(long pid) throws InterruptedException {
Optional<ProcessHandle> aliveProcess = ProcessHandle.of(pid).filter(ProcessHandle::isAlive);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method destroyProcessByPID should use ifPresent on Optional instead of checking isPresent and then calling get.

if (aliveProcess.isPresent()) {
aliveProcess.get().destroy();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code should use ifPresent to handle the Optional instead of checking isPresent and then calling get.

Thread.sleep(PostgreProcessCleaner.POSTGRES_SHUTDOWN_WAIT_MILLIS);
}
}

private boolean isJavaProcessAlive(long javaPid) {
Optional<ProcessHandle> handle = ProcessHandle.of(javaPid);
return handle.map(ProcessHandle::isAlive).orElse(false);
}

private boolean isPortOpen(int port) {
try (Socket _ = new Socket("localhost", port)) {
return true;
} catch (IOException ex) {
return false;
}
}

private long getPidUsingPort(int port) {
try {
Process process = createPortLookupProcess(port);
if (process == null) {
return -1;
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
return extractPidFromOutput(reader);
}
} catch (Exception e) {
LOGGER.warn("Failed to get PID for port {}: {}", port, e.getMessage(), e);
}
return -1;
}

private Process createPortLookupProcess(int port) throws IOException {
if (OS.LINUX || OS.OS_X) {
return new ProcessBuilder("lsof", "-i", "tcp:" + port, "-sTCP:LISTEN", "-Pn")
.redirectErrorStream(true).start();
} else if (OS.WINDOWS) {
return executeWindowsCommand(port);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor question: Is lsof included by default on macOS and Linux? For example, if someone is running Ubuntu with a standard configuration—without adding any extra packages—the solution should ideally work out of the box.

Additionally, there could be security or access rights issues. For instance, on Windows, a domain admin might block users from executing cmd.exe.

At the very least, the documentation should specify the conditions under which the solution is expected to run successfully.

return null;
}

private Process executeWindowsCommand(int port) throws IOException {
String systemRoot = System.getenv("SystemRoot");
if (systemRoot != null && !systemRoot.isBlank()) {
String netStatPath = systemRoot + "\\System32\\netstat.exe";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use Java paths?

String findStrPath = systemRoot + "\\System32\\findstr.exe";
String command = netStatPath + " -ano | " + findStrPath + " :" + port;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. is there an API, where you specify this for running a process:

  1. Command name
  2. List of strings that represent arguments

I think this is more stable than just passing a string

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does | do here? It's like a pipe from Unix? Does it work on Windows, or it has other meaning?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findstr should be programmed in Java

Code reads like AI generated...


tasklist ist the proposal by "the internet" - https://www.rgagnon.com/javadetails/java-0593.html

Copy link
Member

@Siedlerchr Siedlerchr Apr 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With java 9 + we can use Process Handles https://stackoverflow.com/a/45068036

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findstr should be programmed in Java

Code reads like AI generated...

tasklist ist the proposal by "the internet" - https://www.rgagnon.com/javadetails/java-0593.html

Not AI, I have referred these for executing commands:
https://coderanch.com/t/688902/java/Executing-netstat-command-process-builder
https://mkyong.com/java/java-processbuilder-examples/

Also I will check the commands we need to program using java, We can make use of command design pattern, so that if any other command needs to be implemented, we can easily extend that as well

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for ProcessHandles

return new ProcessBuilder("cmd.exe", "/c", command)
.redirectErrorStream(true).start();
}
return null;
}

private long extractPidFromOutput(BufferedReader reader) throws IOException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you call some external command to get PID, right?

I wonder, is there a nicer package for Java? Or some alternative in packages that we have.. (If there is no such thing, then I think it's okay to use commands)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried google, did not find something immediatly.

String line;
while ((line = reader.readLine()) != null) {
if (OS.LINUX || OS.OS_X) {
Long pid = parsePidFromLine(line);
if (pid != null) {
return pid;
}
} else if (OS.WINDOWS) {
Long pid = parseWindowsPidFromLine(line);
if (pid != null) {
return pid;
}
}
}
return -1;
}

private Long parsePidFromLine(String line) {
String[] parts = line.trim().split("\\s+");
if (parts.length > 1 && parts[1].matches("\\d+")) {
return Long.parseLong(parts[1]);
}
return null;
}

private Long parseWindowsPidFromLine(String line) {
String[] parts = line.trim().split("\\s+");
if (parts.length >= 5 && parts[parts.length - 1].matches("\\d+")) {
return Long.parseLong(parts[parts.length - 1]);
}
return null;
}
}
2 changes: 2 additions & 0 deletions src/main/java/org/jabref/logic/search/PostgreServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

public class PostgreServer {
private static final Logger LOGGER = LoggerFactory.getLogger(PostgreServer.class);

private final EmbeddedPostgres embeddedPostgres;
private final DataSource dataSource;

Expand All @@ -26,6 +27,7 @@ public PostgreServer() {
.setOutputRedirector(ProcessBuilder.Redirect.DISCARD)
.start();
LOGGER.info("Postgres server started, connection port: {}", embeddedPostgres.getPort());
PostgresMetadataWriter.write(embeddedPostgres.getPort());
} catch (IOException e) {
LOGGER.error("Could not start Postgres server", e);
this.embeddedPostgres = null;
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/org/jabref/logic/search/PostgresMetadataWriter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.jabref.logic.search;

import java.io.IOException;
import java.nio.file.Path;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PostgresMetadataWriter {
private static final Logger LOGGER = LoggerFactory.getLogger(PostgresMetadataWriter.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

private PostgresMetadataWriter() {
}

public static Path getMetadataFilePath() {
return Path.of(System.getProperty("java.io.tmpdir"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the org.jabref.logic.util.Directories class should be used here to obtain the directory in a standardized way.

"jabref-postgres-info-" + ProcessHandle.current().pid() + ".json");
}

public static void write(int port) {
try {
Path path = getMetadataFilePath();
OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), createMetadata(port));
LOGGER.info("Postgres metadata file path: {}", path);
} catch (IOException e) {
LOGGER.warn("Failed to write Postgres metadata file.", e);
}
}

private static Map<String, Object> createMetadata(int port) {
Map<String, Object> meta = new HashMap<>();
meta.put("javaPid", ProcessHandle.current().pid());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string literal key "javaPid" should be declared as a private static final constant to improve maintainability and readability, following best practices for handling literals.

meta.put("postgresPort", port);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string literal key "postgresPort" should be declared as a private static final constant to improve maintainability and readability, following best practices for handling literals.

meta.put("startedBy", "jabref");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some sense, trag-bot is right.

Is it possible to, maybe, make a serializable class?

meta.put("startedAt", DateTimeFormatter.ISO_INSTANT.format(Instant.now()));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string literal key "startedAt" should be declared as a private static final constant to improve maintainability and readability, following best practices for handling literals.

return meta;
}
}