Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@
<groupId>org.jdom</groupId>
<artifactId>jdom2</artifactId>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<!-- <dependency>-->
<!-- <groupId>net.revelc.code.formatter</groupId>-->
<!-- <artifactId>jsdt-core</artifactId>-->
Expand Down
41 changes: 24 additions & 17 deletions src/main/java/org/czbiohub/royerlab/CondaEnvironmentFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -337,25 +337,30 @@ public static String openDialogToFindUltrack() throws InterruptedException {
}

public static String getUltrackPath() throws InterruptedException {
String condaPath = getCurrentCondaEnv();
ArrayList<String> possibleUltrackPaths = new ArrayList<>();
possibleUltrackPaths.add("ultrack");
if (condaPath != null) {
possibleUltrackPaths.add(condaPath + File.separator + "bin" + File.separator + "ultrack");
possibleUltrackPaths.add(condaPath + File.separator + "Scripts" + File.separator + "ultrack.exe");
}
// Iterative rather than recursive: if the user repeatedly picks an env
// that doesn't have ultrack installed the recursive version would overflow
// the stack, whereas this loop just keeps asking.
while (true) {
String condaEnv = getCurrentCondaEnv();
ArrayList<String> candidates = new ArrayList<>();
candidates.add("ultrack");
if (condaEnv != null) {
candidates.add(condaEnv + File.separator + "bin" + File.separator + "ultrack");
candidates.add(condaEnv + File.separator + "Scripts" + File.separator + "ultrack.exe");
}

for (String path : possibleUltrackPaths) {
if (checkIfCanExecute(path)) {
return path;
for (String path : candidates) {
if (checkIfCanExecute(path)) {
return path;
}
}
}

condaPath = CondaEnvironmentFinder.openDialogToFindUltrack();
if (condaPath == null) {
return null;
String selected = openDialogToFindUltrack();
if (selected == null) {
return null;
}
// Loop: re-check candidates with the newly saved conda env.
}
return CondaEnvironmentFinder.getUltrackPath();
}

private void buildGUI() {
Expand All @@ -378,8 +383,10 @@ private boolean execute() {

private void updateCondaEnvironments(File condaPath) {
try {
String command = condaPath.getAbsolutePath() + " env list";
Process process = Runtime.getRuntime().exec(command);
// Use ProcessBuilder so paths containing spaces are handled correctly.
ProcessBuilder pb = new ProcessBuilder(condaPath.getAbsolutePath(), "env", "list");
pb.redirectErrorStream(true);
Process process = pb.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

String line;
Expand Down
37 changes: 28 additions & 9 deletions src/main/java/org/czbiohub/royerlab/JavaConnector.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,24 @@ public void onExecutionErrorAction() {
};
ultrackConnector.startServer();

javascriptConnector.call("setPort", ultrackConnector.getPort());
// startServer() returns early (port stays -1) when ultrackPath is null/empty.
// Proceeding with port=-1 would make the JS polling loop spin on
// 127.0.0.1:-1/config/available forever, so bail out here instead.
int port = ultrackConnector.getPort();
if (port == -1) {
return;
}
javascriptConnector.call("setPort", port);
javascriptConnector.call("startServer", "Ultrack Server Started");
}

@SuppressWarnings("unused")
public void stopUltrackServer() {
System.out.println("Stopping Ultrack Server");
ultrackConnector.stopServer();
ultrackConnector = null;
if (ultrackConnector != null) {
ultrackConnector.stopServer();
ultrackConnector = null;
}
javascriptConnector.call("successfullyStopped");
}

Expand Down Expand Up @@ -162,18 +171,28 @@ public void openBrowserWithUrl(String url) {
}

private void onMessageConsumer(String response) {
// parse json response
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(response, JsonObject.class);
String err = jsonObject.get("err_log").getAsString();
String out = jsonObject.get("std_log").getAsString();
JsonObject jsonObject;
try {
jsonObject = gson.fromJson(response, JsonObject.class);
if (jsonObject == null) {
throw new IllegalArgumentException("Received null JSON from server.");
}
} catch (Exception e) {
// The server sent plain text (e.g. a traceback) or malformed JSON.
// Log it as an error rather than crashing with JsonSyntaxException.
onError.accept(response);
Platform.runLater(() -> javascriptConnector.call("closeConnection"));
return;
}
String err = jsonObject.has("err_log") ? jsonObject.get("err_log").getAsString() : "";
String out = jsonObject.has("std_log") ? jsonObject.get("std_log").getAsString() : "";
onLog.accept(out);
onError.accept(err);
Platform.runLater(() -> javascriptConnector.call("updateJson", response));

if (jsonObject.get("status").getAsString().equals("success")) {
if (jsonObject.has("status") && jsonObject.get("status").getAsString().equals("success")) {
Platform.runLater(() -> javascriptConnector.call("finishTracking", ""));

}
}

Expand Down
81 changes: 46 additions & 35 deletions src/main/java/org/czbiohub/royerlab/MainApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/
import ij.ImageJ;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.beans.value.ChangeListener;
import javafx.concurrent.Worker;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
Expand All @@ -43,6 +43,8 @@ public class MainApp extends JFrame {
private JTextArea logAreaOut;
private JTextArea internalLogArea;
private WebEngine webEngine;
/** Held so it can be removed before a new one is added on env change. */
private ChangeListener<Worker.State> pageLoadListener;

public MainApp() {
super("Ultrack");
Expand Down Expand Up @@ -85,11 +87,13 @@ public void onExit() {

@Override
public void onUpdateCondaEnv() {
// Called from the background thread launched in AppMenu — never on JAT.
String path = null;
try {
path = CondaEnvironmentFinder.getUltrackPath();
} catch (InterruptedException e) {
throw new RuntimeException(e);
Thread.currentThread().interrupt();
return;
}
String finalPath = path;
Platform.runLater(() -> onLoadUltrackPath(finalPath));
Expand All @@ -102,34 +106,38 @@ public void onUpdateCondaEnv() {
WebView webView = new WebView();
fxPanel.setScene(new Scene(webView));
webEngine = webView.getEngine();
try {
Task<Void> task = new Task<Void>() {
@Override
protected Void call() {
String path = null;
while (path == null) {
try {
path = CondaEnvironmentFinder.getUltrackPath();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (path == null) {
JOptionPane.showMessageDialog(null, "You can't proceed without selecting the ultrack path", "Error", JOptionPane.ERROR_MESSAGE);
}
}
String finalPath = path;
Platform.runLater(() -> onLoadUltrackPath(finalPath));
return null;
}
};
task.run();

// Show the loading screen immediately so the UI is responsive.
try {
webEngine.load(String.valueOf(getClass().getResource("/web/loading.html").toURI()));
} catch (Exception e) {
e.printStackTrace();
}


// Find the ultrack path in a background thread so the JavaFX thread
// is never blocked. Blocking the JavaFX thread here causes the window
// to freeze: dialogs shown by CondaEnvironmentFinder cannot receive
// events and the loading screen never renders.
Thread findPathThread = new Thread(() -> {
String path = null;
while (path == null) {
try {
path = CondaEnvironmentFinder.getUltrackPath();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
} catch (Exception e) {
e.printStackTrace();
}
if (path == null) {
JOptionPane.showMessageDialog(null, "You can't proceed without selecting the ultrack path", "Error", JOptionPane.ERROR_MESSAGE);
}
}
String finalPath = path;
Platform.runLater(() -> onLoadUltrackPath(finalPath));
});
findPathThread.setDaemon(true);
findPathThread.start();

} catch (Exception e) {
e.printStackTrace();
Expand All @@ -152,24 +160,27 @@ public static void main(String[] args) {
}

private void onLoadUltrackPath(String ultrackPath) {
// get resource from the resources folder
URL url = getClass().getResource("/web/index.html");

// set up the listener
webEngine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
if (url == null) {
JOptionPane.showMessageDialog(null, "Resource web/index.html not found.", "Error", JOptionPane.ERROR_MESSAGE);
return;
}

// Remove the previous listener before adding a new one so that repeated
// calls (e.g. after an env change) don't accumulate stale listeners that
// re-wire javaConnector on every subsequent page load.
if (pageLoadListener != null) {
webEngine.getLoadWorker().stateProperty().removeListener(pageLoadListener);
}
pageLoadListener = (observable, oldValue, newValue) -> {
if (Worker.State.SUCCEEDED == newValue) {
// get the Javascript connector object.
javascriptConnector = (JSObject) webEngine.executeScript("getJsConnector()");
javaConnector = new JavaConnector(javascriptConnector, ultrackPath, this::onLog, this::onLogError, this::onServerLog);

// set an interface object named 'javaConnector' in the web engine's page
JSObject window = (JSObject) webEngine.executeScript("window");
window.setMember("javaConnector", javaConnector);
}
});

// now load the page
assert url != null;
};
webEngine.getLoadWorker().stateProperty().addListener(pageLoadListener);
webEngine.load(url.toString());
}

Expand Down
39 changes: 32 additions & 7 deletions src/main/java/org/czbiohub/royerlab/UltrackConnector.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.net.URISyntaxException;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

public abstract class UltrackConnector {
Expand Down Expand Up @@ -101,6 +102,12 @@ private String loadTemporaryResource(String resourceName) {
}

public void startServer() {
if (ultrackPath == null || ultrackPath.isEmpty()) {
SwingUtilities.invokeLater(() -> onExecutionError(
"ultrack path is not configured. Please select a conda environment first."));
return;
}

int randomPort;
do {
randomPort = (int) (Math.random() * 50000 + 10000);
Expand Down Expand Up @@ -175,12 +182,30 @@ public void connectToWebsocket(String url, String message, Consumer<String> onMe
}
c.connect();
CountDownLatch latch = c.latch;
try {
latch.await();
System.out.println(message);
c.send(message);
} catch (InterruptedException e) {
onExecutionError("Error connecting to the websocket: " + e.getMessage());
}
// Waiting for the latch blocks the calling thread. Since this method can be
// invoked from the JavaFX Application Thread (via JS → connectToUltrackWebsocket),
// blocking here would freeze the UI just like the startup-freeze bug. Hand the
// wait off to a daemon background thread instead.
Thread connectThread = new Thread(() -> {
try {
boolean connected = latch.await(30, TimeUnit.SECONDS);
if (!connected) {
// onError was never called (timeout), or onOpen never fired.
onExecutionError("Timed out waiting for WebSocket connection.");
return;
}
if (!c.isOpen()) {
// onError already surfaced the failure via onErrorConsumer.
return;
}
System.out.println(message);
c.send(message);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
onExecutionError("Error connecting to the websocket: " + e.getMessage());
}
});
connectThread.setDaemon(true);
connectThread.start();
}
}
Loading
Loading