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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user