feat: enhance platform independence and integrate ComfyUI lifecycle management

- Added cross-platform process termination in PlatformUtils.
- Improved PathResolver with robust subfolder normalization.
- Integrated ComfyUI lifecycle service (Start/Stop/Restart) using ProcessHandle.
- Updated ComfyUI Bridge with improved UI refresh and sync logic.
- Added comprehensive integration and diagnostic tests.
- Updated documentation with process control details.
This commit is contained in:
Thomas Kippster
2026-05-14 16:48:43 +02:00
parent 64dff81e01
commit 3c52a0b520
34 changed files with 2587 additions and 269 deletions
@@ -3,12 +3,17 @@ package de.tki.comfymodels.service.impl;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
@Service
public class ConfigService {
@@ -29,7 +34,275 @@ public class ConfigService {
@jakarta.annotation.PostConstruct
public void init() {
autoDiscoverPaths();
pathResolver.setComfyUIRoot(getComfyUIPath());
loadExtraModelPaths();
}
public void loadExtraModelPaths() {
pathResolver.clearExtraModelPaths();
String comfyRoot = getComfyUIPath();
if (comfyRoot == null || comfyRoot.isEmpty()) return;
Path extraPathsFile = Paths.get(comfyRoot).resolve("extra_model_paths.yaml");
if (!Files.exists(extraPathsFile)) {
// Check AppData common location as provided in user hint
String userHome = System.getProperty("user.home");
extraPathsFile = Paths.get(userHome, "AppData/Roaming/ComfyUI/extra_models_config.yaml");
}
if (Files.exists(extraPathsFile)) {
System.out.println("📄 [Config] Loading extra model paths from: " + extraPathsFile.toAbsolutePath());
try (InputStream is = new FileInputStream(extraPathsFile.toFile())) {
Yaml yaml = new Yaml();
Map<String, Object> data = yaml.load(is);
if (data != null) {
for (Map.Entry<String, Object> entry : data.entrySet()) {
if (entry.getValue() instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> section = (Map<String, Object>) entry.getValue();
String basePathStr = (String) section.get("base_path");
if (basePathStr == null) continue;
// base_path can be relative to the YAML file
Path yamlDir = extraPathsFile.getParent();
Path basePath = Paths.get(basePathStr);
if (!basePath.isAbsolute()) {
basePath = yamlDir.resolve(basePath).toAbsolutePath().normalize();
}
System.out.println("📂 [Config] Section '" + entry.getKey() + "' base_path: " + basePath);
for (Map.Entry<String, Object> config : section.entrySet()) {
if (config.getKey().equals("base_path")) continue;
String type = config.getKey();
Object value = config.getValue();
if (value instanceof String) {
String[] folders = ((String) value).split("\\R");
for (String folder : folders) {
String trimmed = folder.trim();
if (!trimmed.isEmpty()) {
Path fullPath = basePath.resolve(trimmed).toAbsolutePath().normalize();
if (!Files.exists(fullPath)) {
System.err.println(" ⚠️ [Config] Warning: Path does not exist: " + fullPath);
}
pathResolver.addExtraModelPath(type, fullPath);
System.out.println(" -> Mapping [" + type + "] to: " + fullPath);
}
}
}
}
}
}
}
} catch (Exception e) {
System.err.println("❌ [Config] Error parsing extra_model_paths.yaml: " + e.getMessage());
}
}
}
public void autoDiscoverPaths() {
String root = getComfyUIPath();
String userHome = System.getProperty("user.home");
String currentDir = System.getProperty("user.dir");
System.out.println("🔍 [Config] Starting auto-discovery. Current Root: " + root);
// 1. VALIDATE AND DISCOVER COMFYUI ROOT
boolean rootValid = !root.isEmpty() && new File(root, "main.py").exists();
if (!rootValid && !root.isEmpty()) {
System.out.println("⚠️ [Config] main.py not found in current root. Checking subdirectories...");
// Check for nested Pinokio structure: resources/ComfyUI
File nested = new File(root, "resources/ComfyUI");
if (new File(nested, "main.py").exists()) {
root = nested.getAbsolutePath();
rootValid = true;
System.out.println("✨ [Config] Found main.py in nested Pinokio path: " + root);
setComfyUIPath(root);
} else {
// Current root is definitively wrong, reset it to allow re-discovery
System.out.println("🚫 [Config] Current root is invalid. Resetting for re-discovery.");
root = "";
}
}
if (root.isEmpty()) {
// Priority 1: Environment Variables
String[] envVars = {"COMFYUI_PATH", "COMFYUI_HOME", "PINOKIO_BIN"};
for (String var : envVars) {
String val = System.getenv(var);
if (val != null && !val.isEmpty()) {
File f = new File(val);
if (new File(f, "main.py").exists()) {
root = f.getAbsolutePath();
System.out.println("✨ [Config] Found Root via Env Var " + var + ": " + root);
break;
}
}
}
// Priority 2: Check if we are running INSIDE a ComfyUI directory
if (root.isEmpty()) {
File possibleRoot = findMainPyNearby(new File(currentDir));
if (possibleRoot != null) {
root = possibleRoot.getAbsolutePath();
System.out.println("✨ [Config] Found Root via Nearby Search: " + root);
}
}
// Priority 3: Check common Pinokio/Home locations
if (root.isEmpty()) {
String[] commonPatterns = {
"C:\\AI\\ComfyUI\\resources\\ComfyUI",
"C:\\AI\\ComfyUI",
userHome + "\\AppData\\Roaming\\pinokio\\bin\\ComfyUI",
userHome + "/pinokio/bin/ComfyUI",
userHome + "/ComfyUI",
userHome + "/AI/ComfyUI"
};
for (String p : commonPatterns) {
File f = new File(p);
if (new File(f, "main.py").exists()) {
root = f.getAbsolutePath();
System.out.println("✨ [Config] Found Root via Pattern: " + root);
break;
} else {
// Try nested resources/ComfyUI for patterns too
File nested = new File(f, "resources/ComfyUI");
if (new File(nested, "main.py").exists()) {
root = nested.getAbsolutePath();
System.out.println("✨ [Config] Found Root via Nested Pattern: " + root);
break;
}
}
}
}
if (!root.isEmpty()) {
setComfyUIPath(root);
}
}
// 2. DISCOVER MODELS DIR
String currentModelsPath = settings.optString("models_path", "");
if (currentModelsPath.isEmpty() || currentModelsPath.equals(PathResolver.MODELS_DIR)) {
// Check for Pinokio data structure: sibling of ComfyUI's main installation folder
// e.g. C:\AI\ComfyUI (binary) -> C:\AI\comfyuidata\models (data)
File comfyFolder = new File(root);
while (comfyFolder != null) {
File parent = comfyFolder.getParentFile();
if (parent != null) {
File dataModels = new File(parent, "comfyuidata/models");
if (dataModels.exists() && dataModels.isDirectory()) {
System.out.println("✨ [Config] Found models via Pinokio Data Path: " + dataModels.getAbsolutePath());
setModelsPath(dataModels.getAbsolutePath());
break;
}
// Try case-insensitive variant or simplified name
File altData = new File(parent, "data/models");
if (altData.exists() && altData.isDirectory()) {
System.out.println("✨ [Config] Found models via Data Path: " + altData.getAbsolutePath());
setModelsPath(altData.getAbsolutePath());
break;
}
}
comfyFolder = comfyFolder.getParentFile();
if (comfyFolder != null && comfyFolder.getName().equalsIgnoreCase("AI")) break; // Don't go too high
}
}
// 3. DISCOVER WORKING DIR
if ((getComfyWorkingDir().isEmpty() || !rootValid) && !root.isEmpty()) {
setComfyWorkingDir(root);
System.out.println("📁 [Config] Set Working Dir: " + root);
}
// 3. DISCOVER PYTHON & CONSTRUCT COMMAND
// Always re-validate command if root changed or command is empty
String python = getPythonPath();
if (!isPythonValid(python)) {
python = discoverPython(root);
if (isPythonValid(python) && !python.equals("python") && !python.equals("python3")) {
setPythonPath(python);
}
}
if ((getComfyLaunchCommand().isEmpty() || !rootValid) && !root.isEmpty()) {
File mainPy = new File(root, "main.py");
if (mainPy.exists()) {
int port = getComfyUIUrl().contains(":8000") ? 8000 : 8188;
String cmd = String.format("\"%s\" \"%s\" --listen 127.0.0.1 --port %d --enable-manager",
python, mainPy.getAbsolutePath(), port);
setComfyLaunchCommand(cmd);
System.out.println("🚀 [Config] Generated Launch Command: " + cmd);
} else {
System.err.println("❌ [Config] Could not find main.py in " + root);
}
} else if (!getComfyLaunchCommand().isEmpty()) {
System.out.println("✅ [Config] Launch command already present and likely valid: " + getComfyLaunchCommand());
}
// 4. RELOAD EXTRA PATHS
loadExtraModelPaths();
}
private boolean isPythonValid(String python) {
if (python == null || python.isEmpty()) return false;
if (python.equalsIgnoreCase("python") || python.equalsIgnoreCase("python3")) return true;
return new File(python).exists();
}
private File findMainPyNearby(File start) {
File current = start;
while (current != null) {
if (new File(current, "main.py").exists()) return current;
if (current.getName().equalsIgnoreCase("custom_nodes")) {
File parent = current.getParentFile();
if (parent != null && new File(parent, "main.py").exists()) return parent;
}
current = current.getParentFile();
}
return null;
}
public String discoverPython(String comfyRoot) {
System.out.println("🐍 [Config] Searching for Python in/near: " + comfyRoot);
// Priority 1: Check for .venv in comfy root or parent (Pinokio style)
File[] venvLocations = {
new File(comfyRoot, ".venv"),
new File(new File(comfyRoot).getParentFile(), ".venv"),
new File(new File(comfyRoot).getParentFile().getParentFile(), ".venv"), // Try 2 levels up
new File("C:\\AI\\comfyuidata\\.venv")
};
for (File venv : venvLocations) {
if (venv != null && venv.exists()) {
System.out.println("📂 [Config] Found venv at: " + venv.getAbsolutePath());
boolean isWin = isWindows();
File bin = new File(venv, isWin ? "Scripts/python.exe" : "bin/python3");
if (bin.exists()) return bin.getAbsolutePath();
bin = new File(venv, isWin ? "Scripts/python" : "bin/python");
if (bin.exists()) return bin.getAbsolutePath();
}
}
// Priority 2: Check for python_embeded (Portable ComfyUI style)
File portablePython = new File(new File(comfyRoot).getParentFile(), "python_embeded/python.exe");
if (portablePython.exists()) {
System.out.println("📂 [Config] Found portable python at: " + portablePython.getAbsolutePath());
return portablePython.getAbsolutePath();
}
System.out.println("⚠️ [Config] No venv found, falling back to system python.");
return isWindows() ? "python" : "python3";
}
private boolean isWindows() {
return System.getProperty("os.name").toLowerCase().contains("win");
}
/**
@@ -152,9 +425,23 @@ public class ConfigService {
settings.put("comfyui_path", path);
pathResolver.setComfyUIRoot(path);
save();
loadExtraModelPaths(); // Ensure extra paths are reloaded when root changes
}
public String getComfyUIUrl() { return settings.optString("comfyui_url", "http://127.0.0.1:8188"); }
public String getPythonPath() { return settings.optString("python_path", ""); }
public void setPythonPath(String path) { settings.put("python_path", path); save(); }
public String getComfyLaunchCommand() { return settings.optString("comfy_launch_command", ""); }
public void setComfyLaunchCommand(String cmd) { settings.put("comfy_launch_command", cmd); save(); }
public String getComfyWorkingDir() { return settings.optString("comfy_working_dir", ""); }
public void setComfyWorkingDir(String dir) { settings.put("comfy_working_dir", dir); save(); }
public boolean isRestartAfterDownloadEnabled() { return settings.optBoolean("restart_after_download", false); }
public void setRestartAfterDownloadEnabled(boolean enabled) { settings.put("restart_after_download", enabled); save(); }
public String getComfyUIUrl() { return settings.optString("comfyui_url", "http://127.0.0.1:8000"); }
public void setComfyUIUrl(String url) {
if (url != null && !url.isEmpty()) {
String sanitized = url.endsWith("/") ? url.substring(0, url.length() - 1) : url;