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:
@@ -30,6 +30,38 @@ The "Bridge" is a small extension for ComfyUI that adds a "Send to Downloader" b
|
||||
2. Rename it to `comfyui-model-downloader`.
|
||||
3. Restart ComfyUI.
|
||||
|
||||
## ⚙️ ComfyUI Process Control (Dynamic Discovery)
|
||||
|
||||
The application can manage your ComfyUI server life cycle (Start/Stop/Restart) completely **autonomously**. It dynamically determines the correct paths and commands for **Windows, Linux, and macOS**.
|
||||
|
||||
### How Discovery Works
|
||||
The app scans your system to find ComfyUI without manual input:
|
||||
1. **System Variables:** Evaluates `COMFYUI_PATH`, `COMFYUI_HOME`, or `PINOKIO_BIN`.
|
||||
2. **Context Recognition:** Detects if the downloader is running within or near a ComfyUI directory.
|
||||
3. **Environment Heuristics:** Automatically locates `.venv` or `python` environments and standard installation paths (like `%USERPROFILE%\ComfyUI` or Pinokio folders).
|
||||
4. **Automatic Port Sync:** Adjusts the launch command's port based on your configured Server URL.
|
||||
|
||||
### Reference Examples (Auto-Detected)
|
||||
While usually not required, here is what the app typically discovers on different systems:
|
||||
|
||||
#### 🪟 Windows (Pinokio)
|
||||
* **Python:** `...\comfyuidata\.venv\Scripts\python.exe`
|
||||
* **Main:** `...\ComfyUI\resources\ComfyUI\main.py`
|
||||
* **Default Port:** `8000`
|
||||
|
||||
#### 🐧 Linux (Manual Venv)
|
||||
* **Python:** `./venv/bin/python3`
|
||||
* **Main:** `./main.py`
|
||||
* **Default Port:** `8188`
|
||||
|
||||
#### 🍎 macOS (Apple Silicon)
|
||||
* **Python:** `python3` (System)
|
||||
* **Main:** `/Users/name/ComfyUI/main.py`
|
||||
|
||||
*Note: You can manually override these in **🔌 Comfy Control -> Advanced Settings** if you have a non-standard setup.*
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
Choose one of the following three ways to install and run the application:
|
||||
|
||||
+28
-22
@@ -2,42 +2,48 @@
|
||||
import server
|
||||
import folder_paths
|
||||
import os
|
||||
import asyncio
|
||||
import importlib
|
||||
from aiohttp import web
|
||||
|
||||
print("\033[95m[Model-Downloader] UI Extension Bridge active\033[0m")
|
||||
print("\033[95m[CMFD] UI Extension Bridge active\033[0m")
|
||||
|
||||
USER_MODEL_PATH = r"C:\AI\comfyuidata\models"
|
||||
if os.path.exists(USER_MODEL_PATH):
|
||||
try:
|
||||
pass
|
||||
except: pass
|
||||
|
||||
@server.PromptServer.instance.routes.post("/kippster/model-downloaded")
|
||||
async def model_downloaded(request):
|
||||
print("\033[92m🔥 [Model-Downloader] PING ERHALTEN! Bereite UI-Refresh vor...\033[0m")
|
||||
@server.PromptServer.instance.routes.post("/cmfd/refresh-models")
|
||||
async def refresh_models(request):
|
||||
data = await request.json() if request.has_body else {}
|
||||
force_reload = data.get("force_reload", False)
|
||||
|
||||
print(f"\033[92m🔥 [CMFD] Refresh signal received (Force Reload: {force_reload})\033[0m")
|
||||
|
||||
# Wait for OS
|
||||
await asyncio.sleep(1)
|
||||
|
||||
try:
|
||||
# 1. Wir löschen nur die Caches, machen aber KEINEN eigenen Scan mehr!
|
||||
# Das verhindert, dass wir einen noch unfertigen Windows-Ordner scannen und den Fehler cachen.
|
||||
# Invalidate
|
||||
if hasattr(folder_paths, "invalidate_all_cached_folders"):
|
||||
folder_paths.invalidate_all_cached_folders()
|
||||
|
||||
if hasattr(folder_paths, "filename_list_cache"):
|
||||
folder_paths.filename_list_cache.clear()
|
||||
|
||||
if hasattr(folder_paths, "cache_helper") and hasattr(folder_paths.cache_helper, "clear"):
|
||||
folder_paths.cache_helper.clear()
|
||||
|
||||
if hasattr(folder_paths, "cache") and hasattr(folder_paths.cache, "clear"):
|
||||
folder_paths.cache.clear()
|
||||
try:
|
||||
importlib.reload(folder_paths)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Object Info zurücksetzen (zwingt ComfyUI beim nächsten UI-Klick zum Neuladen)
|
||||
# Object Info Reset
|
||||
server.PromptServer.instance.object_info = None
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Model-Downloader] Fehler beim Cache-Reset: {e}")
|
||||
print(f"[CMFD] Error: {e}")
|
||||
|
||||
# 3. Signal an das Frontend senden
|
||||
server.PromptServer.instance.send_sync("kippster-refresh-ui", {"message": "ready"})
|
||||
# One-way broadcast to frontend
|
||||
server.PromptServer.instance.send_sync("cmfd-refresh-ui", {
|
||||
"status": "ok",
|
||||
"force_reload": force_reload
|
||||
})
|
||||
|
||||
return web.json_response({"status": "success"})
|
||||
return web.json_response({"status": "ok"})
|
||||
|
||||
WEB_DIRECTORY = "web"
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
|
||||
@@ -86,6 +86,11 @@
|
||||
<artifactId>flatlaf-extras</artifactId>
|
||||
<version>3.7.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>2.4</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -11,6 +11,7 @@ import de.tki.comfymodels.service.impl.GeminiAIService;
|
||||
import de.tki.comfymodels.service.impl.ModelListService;
|
||||
import de.tki.comfymodels.service.impl.ModelHashRegistry;
|
||||
import de.tki.comfymodels.ui.DotGridPanel;
|
||||
import de.tki.comfymodels.ui.ComfyWebPanel;
|
||||
import com.formdev.flatlaf.FlatDarkLaf;
|
||||
import com.formdev.flatlaf.FlatLightLaf;
|
||||
import com.formdev.flatlaf.FlatLaf;
|
||||
@@ -55,6 +56,9 @@ public class Main extends JFrame {
|
||||
private final IModelValidator modelValidator;
|
||||
private final de.tki.comfymodels.service.impl.RestBridgeService restBridge;
|
||||
private final de.tki.comfymodels.service.impl.ArchiveService archiveService;
|
||||
private final de.tki.comfymodels.service.IComfyLifecycleService lifecycleService;
|
||||
private final de.tki.comfymodels.service.impl.ComfyDiagnosticService diagnosticService;
|
||||
private de.tki.comfymodels.ui.ComfyWebPanel comfyWebPanel;
|
||||
|
||||
@Autowired
|
||||
private ConfigService configService;
|
||||
@@ -76,8 +80,10 @@ public class Main extends JFrame {
|
||||
|
||||
private JCheckBox backgroundCheck;
|
||||
private JCheckBox shutdownCheck;
|
||||
private JCheckBox restartCheck;
|
||||
private JCheckBox darkCheck;
|
||||
private JLabel activeAiModelLabel;
|
||||
private JLabel lifecycleStatusLabel;
|
||||
private JTextArea jsonInputArea;
|
||||
private DefaultTableModel tableModel;
|
||||
private JLabel statusLabel;
|
||||
@@ -89,7 +95,9 @@ public class Main extends JFrame {
|
||||
public Main(IModelAnalyzer analyzer, IDownloadManager downloadManager,
|
||||
IWorkflowService workflowService, IModelSearchService searchService,
|
||||
IModelValidator modelValidator, de.tki.comfymodels.service.impl.RestBridgeService restBridge,
|
||||
de.tki.comfymodels.service.impl.ArchiveService archiveService) {
|
||||
de.tki.comfymodels.service.impl.ArchiveService archiveService,
|
||||
de.tki.comfymodels.service.IComfyLifecycleService lifecycleService,
|
||||
de.tki.comfymodels.service.impl.ComfyDiagnosticService diagnosticService) {
|
||||
this.analyzer = analyzer;
|
||||
this.downloadManager = downloadManager;
|
||||
this.workflowService = workflowService;
|
||||
@@ -97,6 +105,8 @@ public class Main extends JFrame {
|
||||
this.modelValidator = modelValidator;
|
||||
this.restBridge = restBridge;
|
||||
this.archiveService = archiveService;
|
||||
this.lifecycleService = lifecycleService;
|
||||
this.diagnosticService = diagnosticService;
|
||||
}
|
||||
|
||||
public void launch(String[] args) {
|
||||
@@ -148,6 +158,7 @@ public class Main extends JFrame {
|
||||
if (configService.isBackgroundModeEnabled()) {
|
||||
setVisible(false);
|
||||
} else {
|
||||
lifecycleService.stop();
|
||||
downloadManager.stop();
|
||||
System.exit(0);
|
||||
}
|
||||
@@ -282,10 +293,7 @@ public class Main extends JFrame {
|
||||
MenuItem showItem = new MenuItem("Show UI");
|
||||
showItem.addActionListener(e -> setVisible(true));
|
||||
MenuItem exitItem = new MenuItem("Exit");
|
||||
exitItem.addActionListener(e -> {
|
||||
downloadManager.stop();
|
||||
System.exit(0);
|
||||
});
|
||||
exitItem.addActionListener(e -> performAppExit());
|
||||
|
||||
popup.add(showItem);
|
||||
popup.addSeparator();
|
||||
@@ -350,6 +358,7 @@ public class Main extends JFrame {
|
||||
|
||||
try {
|
||||
configService.unlock(pass);
|
||||
configService.autoDiscoverPaths(); // Ensure paths are discovered after unlock
|
||||
restBridge.setApiToken(configService.getApiToken());
|
||||
syncBridgeFiles(); // NEW: Automatically sync code & token on every unlock
|
||||
|
||||
@@ -366,9 +375,110 @@ public class Main extends JFrame {
|
||||
}
|
||||
}
|
||||
|
||||
private void showLifecycleDialog() {
|
||||
JDialog dialog = new JDialog(this, "ComfyUI Control", true);
|
||||
dialog.setLayout(new BorderLayout());
|
||||
dialog.setSize(880, 420);
|
||||
dialog.setLocationRelativeTo(this);
|
||||
|
||||
JPanel content = new JPanel(new GridBagLayout());
|
||||
content.setBorder(BorderFactory.createEmptyBorder(25, 25, 25, 25));
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 1.0;
|
||||
|
||||
JLabel titleLabel = new JLabel("ComfyUI Server Status");
|
||||
titleLabel.setFont(new Font("SansSerif", Font.BOLD, 18));
|
||||
titleLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
content.add(titleLabel, gbc);
|
||||
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(25, 0, 25, 0);
|
||||
lifecycleStatusLabel = new JLabel(lifecycleService.getStatus());
|
||||
lifecycleStatusLabel.setFont(new Font("SansSerif", Font.BOLD, 16));
|
||||
lifecycleStatusLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
content.add(lifecycleStatusLabel, gbc);
|
||||
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(10, 0, 20, 0);
|
||||
JPanel controlButtons = new JPanel(new FlowLayout(FlowLayout.CENTER, 15, 10));
|
||||
JButton startBtn = new JButton("▶ Start");
|
||||
startBtn.setPreferredSize(new Dimension(110, 40));
|
||||
startBtn.addActionListener(e -> {
|
||||
configService.autoDiscoverPaths();
|
||||
startComfyAndReload();
|
||||
});
|
||||
|
||||
JButton stopBtn = new JButton("⏹ Stop");
|
||||
stopBtn.setPreferredSize(new Dimension(110, 40));
|
||||
stopBtn.addActionListener(e -> lifecycleService.stop());
|
||||
|
||||
JButton restartBtn = new JButton("🔄 Restart");
|
||||
restartBtn.setPreferredSize(new Dimension(110, 40));
|
||||
restartBtn.addActionListener(e -> {
|
||||
configService.autoDiscoverPaths();
|
||||
new Thread(() -> {
|
||||
lifecycleService.stop();
|
||||
try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
|
||||
startComfyAndReload();
|
||||
}).start();
|
||||
});
|
||||
|
||||
JButton openBrowserBtn = new JButton("🌐 Open Interface");
|
||||
openBrowserBtn.setPreferredSize(new Dimension(150, 40));
|
||||
openBrowserBtn.addActionListener(e -> {
|
||||
try {
|
||||
Desktop.getDesktop().browse(new java.net.URI(configService.getComfyUIUrl()));
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(dialog, "Could not open browser: " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
controlButtons.add(startBtn);
|
||||
controlButtons.add(stopBtn);
|
||||
controlButtons.add(restartBtn);
|
||||
controlButtons.add(openBrowserBtn);
|
||||
content.add(controlButtons, gbc);
|
||||
|
||||
// Timer to update status
|
||||
Timer timer = new Timer(1000, e -> {
|
||||
lifecycleStatusLabel.setText(lifecycleService.getStatus());
|
||||
if (lifecycleService.isRunning()) {
|
||||
lifecycleStatusLabel.setForeground(new Color(0, 150, 0));
|
||||
startBtn.setEnabled(false);
|
||||
stopBtn.setEnabled(true);
|
||||
} else {
|
||||
lifecycleStatusLabel.setForeground(Color.RED);
|
||||
startBtn.setEnabled(true);
|
||||
stopBtn.setEnabled(false);
|
||||
}
|
||||
});
|
||||
timer.start();
|
||||
dialog.addWindowListener(new java.awt.event.WindowAdapter() {
|
||||
@Override public void windowClosing(java.awt.event.WindowEvent e) { timer.stop(); }
|
||||
});
|
||||
|
||||
gbc.gridy++;
|
||||
gbc.weighty = 1.0;
|
||||
content.add(new JPanel(), gbc);
|
||||
|
||||
JPanel bottom = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
bottom.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
|
||||
|
||||
JButton close = new JButton("Close");
|
||||
close.addActionListener(e -> dialog.dispose());
|
||||
|
||||
bottom.add(close);
|
||||
|
||||
dialog.add(content, BorderLayout.CENTER);
|
||||
dialog.add(bottom, BorderLayout.SOUTH);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
private void loadSettingsIntoUI() {
|
||||
if (backgroundCheck != null) backgroundCheck.setSelected(configService.isBackgroundModeEnabled());
|
||||
if (shutdownCheck != null) shutdownCheck.setSelected(configService.isShutdownAfterDownloadEnabled());
|
||||
if (restartCheck != null) restartCheck.setSelected(configService.isRestartAfterDownloadEnabled());
|
||||
if (darkCheck != null) darkCheck.setSelected(configService.isDarkMode());
|
||||
}
|
||||
|
||||
@@ -382,23 +492,42 @@ public class Main extends JFrame {
|
||||
private void initUI() {
|
||||
loadIcon();
|
||||
setTitle("ComfyUIModel-Downloader");
|
||||
setSize(1450, 900);
|
||||
setSize(1450, 950);
|
||||
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||
|
||||
getRootPane().putClientProperty("flatlaf.useWindowDecorations", true);
|
||||
|
||||
JPanel mainContainer = new DotGridPanel(new BorderLayout(10, 10));
|
||||
mainContainer.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
setContentPane(mainContainer);
|
||||
// --- PREPARE TABBED INTERFACE ---
|
||||
JTabbedPane mainTabs = new JTabbedPane();
|
||||
mainTabs.setFont(new Font("SansSerif", Font.BOLD, 13));
|
||||
|
||||
comfyWebPanel = new de.tki.comfymodels.ui.ComfyWebPanel(configService.getComfyUIUrl());
|
||||
|
||||
// --- NEW COMPACT TOOLBAR ---
|
||||
// --- TAB 1: Model Manager (Original UI) ---
|
||||
JPanel managerPanel = new JPanel(new BorderLayout(10, 10));
|
||||
managerPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
|
||||
// --- TOOLBAR ---
|
||||
JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5));
|
||||
toolbar.setBorder(BorderFactory.createEtchedBorder());
|
||||
|
||||
JButton settingsBtn = new JButton("⚙ Settings...");
|
||||
settingsBtn.addActionListener(e -> showSettingsMenu(settingsBtn));
|
||||
|
||||
JButton lifecycleBtn = new JButton("🔌 Comfy Control...");
|
||||
lifecycleBtn.addActionListener(e -> showLifecycleDialog());
|
||||
|
||||
JButton startServerBtn = new JButton("▶ Start ComfyUI");
|
||||
startServerBtn.setToolTipText("Starts the ComfyUI server in the background");
|
||||
startServerBtn.addActionListener(e -> {
|
||||
if (lifecycleService.isRunning()) {
|
||||
JOptionPane.showMessageDialog(this, "ComfyUI is already running.", "Info", JOptionPane.INFORMATION_MESSAGE);
|
||||
} else {
|
||||
startComfyAndReload();
|
||||
updateComfyTabVisibility(mainTabs);
|
||||
}
|
||||
});
|
||||
|
||||
JButton verifyBtn = new JButton("🔍 Fast Verify");
|
||||
verifyBtn.addActionListener(e -> verifyLocalModels(false));
|
||||
|
||||
@@ -419,10 +548,51 @@ public class Main extends JFrame {
|
||||
JButton helpBtn = new JButton("ℹ Help");
|
||||
helpBtn.addActionListener(e -> showHelpDialog());
|
||||
|
||||
JButton diagnosticBtn = new JButton("🩺 Diagnostic");
|
||||
diagnosticBtn.addActionListener(e -> {
|
||||
if (modelsToDownload == null || modelsToDownload.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "Load a workflow first to perform a diagnostic check.");
|
||||
return;
|
||||
}
|
||||
List<String> missing = diagnosticService.getMissingModels(modelsToDownload);
|
||||
if (missing.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "✅ All workflow models are visible to ComfyUI!", "Diagnostic Passed", JOptionPane.INFORMATION_MESSAGE);
|
||||
} else {
|
||||
String list = String.join("\n- ", missing);
|
||||
|
||||
List<Object> optionsList = new ArrayList<>();
|
||||
optionsList.add("🚀 Restart ComfyUI (Full Service Restart)");
|
||||
|
||||
boolean canStart = !lifecycleService.isRunning() &&
|
||||
configService.getComfyLaunchCommand() != null &&
|
||||
!configService.getComfyLaunchCommand().trim().isEmpty();
|
||||
|
||||
if (canStart) {
|
||||
optionsList.add("▶ Start ComfyUI");
|
||||
}
|
||||
optionsList.add("Ignore");
|
||||
|
||||
Object[] options = optionsList.toArray();
|
||||
int choice = JOptionPane.showOptionDialog(this,
|
||||
"❌ ComfyUI still reports these models as missing:\n- " + list + "\n\n" +
|
||||
"A restart is required for ComfyUI to detect new models.",
|
||||
"Diagnostic Failed", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]);
|
||||
|
||||
if (choice == 0) {
|
||||
performFullServiceRestart();
|
||||
} else if (canStart && choice == 1) {
|
||||
lifecycleService.start();
|
||||
JOptionPane.showMessageDialog(this, "ComfyUI is being started. Please wait a few moments for it to initialize.", "Starting Service", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
backgroundCheck = new JCheckBox("Stay in Background");
|
||||
backgroundCheck.addActionListener(e -> configService.setBackgroundModeEnabled(backgroundCheck.isSelected()));
|
||||
shutdownCheck = new JCheckBox("Shutdown after Queue");
|
||||
shutdownCheck.addActionListener(e -> configService.setShutdownAfterDownloadEnabled(shutdownCheck.isSelected()));
|
||||
restartCheck = new JCheckBox("Full Restart after Queue");
|
||||
restartCheck.addActionListener(e -> configService.setRestartAfterDownloadEnabled(restartCheck.isSelected()));
|
||||
darkCheck = new JCheckBox("Dark Mode");
|
||||
darkCheck.addActionListener(e -> {
|
||||
boolean isDark = darkCheck.isSelected();
|
||||
@@ -435,6 +605,7 @@ public class Main extends JFrame {
|
||||
});
|
||||
|
||||
toolbar.add(settingsBtn);
|
||||
toolbar.add(lifecycleBtn);
|
||||
toolbar.add(new JSeparator(JSeparator.VERTICAL));
|
||||
toolbar.add(verifyBtn);
|
||||
toolbar.add(optimizeBtn);
|
||||
@@ -442,6 +613,7 @@ public class Main extends JFrame {
|
||||
toolbar.add(new JSeparator(JSeparator.VERTICAL));
|
||||
toolbar.add(backgroundCheck);
|
||||
toolbar.add(shutdownCheck);
|
||||
toolbar.add(restartCheck);
|
||||
toolbar.add(darkCheck);
|
||||
|
||||
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||
@@ -504,13 +676,15 @@ public class Main extends JFrame {
|
||||
|
||||
splitPane.setTopComponent(jsonPanel);
|
||||
splitPane.setBottomComponent(tablePanel);
|
||||
splitPane.setDividerLocation(300);
|
||||
splitPane.setDividerLocation(350);
|
||||
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout());
|
||||
statusLabel = new JLabel("Ready");
|
||||
statusLabel.setName("statusLabel");
|
||||
|
||||
JPanel progressPanel = new JPanel(new GridLayout(2, 1));
|
||||
activeAiModelLabel = new JLabel("Active AI: Loading...");
|
||||
activeAiModelLabel.setName("activeAiModelLabel");
|
||||
activeAiModelLabel.setFont(new Font("SansSerif", Font.ITALIC, 11));
|
||||
activeAiModelLabel.setForeground(Color.GRAY);
|
||||
progressPanel.add(statusLabel);
|
||||
@@ -518,14 +692,17 @@ public class Main extends JFrame {
|
||||
|
||||
JPanel actionButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
downloadButton = new JButton("Start Queue");
|
||||
downloadButton.setName("downloadButton");
|
||||
downloadButton.setEnabled(false);
|
||||
downloadButton.addActionListener(e -> startDownloadQueue());
|
||||
pauseButton = new JButton("Pause");
|
||||
pauseButton.setName("pauseButton");
|
||||
pauseButton.addActionListener(e -> {
|
||||
downloadManager.togglePause();
|
||||
pauseButton.setText(downloadManager.isPaused() ? "Resume" : "Pause");
|
||||
});
|
||||
stopButton = new JButton("Stop");
|
||||
stopButton.setName("stopButton");
|
||||
stopButton.addActionListener(e -> downloadManager.stop());
|
||||
actionButtons.add(downloadButton);
|
||||
actionButtons.add(pauseButton);
|
||||
@@ -534,9 +711,50 @@ public class Main extends JFrame {
|
||||
bottomPanel.add(progressPanel, BorderLayout.CENTER);
|
||||
bottomPanel.add(actionButtons, BorderLayout.EAST);
|
||||
|
||||
mainContainer.add(toolbar, BorderLayout.NORTH);
|
||||
mainContainer.add(splitPane, BorderLayout.CENTER);
|
||||
mainContainer.add(bottomPanel, BorderLayout.SOUTH);
|
||||
managerPanel.add(toolbar, BorderLayout.NORTH);
|
||||
managerPanel.add(splitPane, BorderLayout.CENTER);
|
||||
managerPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(managerPanel);
|
||||
}
|
||||
|
||||
private void startComfyAndReload() {
|
||||
new Thread(() -> {
|
||||
lifecycleService.start();
|
||||
// Wait for health
|
||||
for (int i = 0; i < 30; i++) {
|
||||
if (lifecycleService.isHealthy()) {
|
||||
if (comfyWebPanel != null) {
|
||||
comfyWebPanel.reload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void updateComfyTabVisibility(JTabbedPane tabs) {
|
||||
boolean isRunning = lifecycleService.isRunning();
|
||||
boolean isHealthy = lifecycleService.isHealthy();
|
||||
|
||||
// Only show if actually running or healthy
|
||||
boolean shouldShow = isRunning || isHealthy;
|
||||
|
||||
int comfyTabIndex = tabs.indexOfComponent(comfyWebPanel);
|
||||
|
||||
if (shouldShow && comfyTabIndex == -1) {
|
||||
System.out.println("✅ [UI] Showing ComfyUI Tab (Running=" + isRunning + ", Healthy=" + isHealthy + ")");
|
||||
tabs.addTab("🎨 ComfyUI Interface", comfyWebPanel);
|
||||
comfyWebPanel.loadUrl(configService.getComfyUIUrl()); // Force reload when re-added
|
||||
tabs.revalidate();
|
||||
tabs.repaint();
|
||||
} else if (!shouldShow && comfyTabIndex != -1) {
|
||||
System.out.println("🚫 [UI] Hiding ComfyUI Tab (Running=" + isRunning + ", Healthy=" + isHealthy + ")");
|
||||
tabs.removeTabAt(comfyTabIndex);
|
||||
tabs.revalidate();
|
||||
tabs.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private void showSettingsMenu(JButton parent) {
|
||||
@@ -555,10 +773,7 @@ public class Main extends JFrame {
|
||||
helpItem.addActionListener(e -> showHelpDialog());
|
||||
|
||||
JMenuItem exitItem = new JMenuItem("❌ Exit Application");
|
||||
exitItem.addActionListener(e -> {
|
||||
downloadManager.stop();
|
||||
System.exit(0);
|
||||
});
|
||||
exitItem.addActionListener(e -> performAppExit());
|
||||
|
||||
menu.add(pathsItem);
|
||||
menu.add(apiItem);
|
||||
@@ -572,10 +787,16 @@ public class Main extends JFrame {
|
||||
menu.show(parent, 0, parent.getHeight());
|
||||
}
|
||||
|
||||
private void performAppExit() {
|
||||
lifecycleService.stop();
|
||||
downloadManager.stop();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private void showPathsDialog() {
|
||||
JDialog dialog = new JDialog(this, "Directory Settings", true);
|
||||
dialog.setLayout(new BorderLayout());
|
||||
dialog.setSize(600, 300);
|
||||
dialog.setSize(600, 480);
|
||||
dialog.setLocationRelativeTo(this);
|
||||
|
||||
JPanel panel = new JPanel(new GridBagLayout());
|
||||
@@ -585,7 +806,7 @@ public class Main extends JFrame {
|
||||
gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 1.0;
|
||||
gbc.insets = new Insets(0, 0, 5, 0);
|
||||
|
||||
panel.add(new JLabel("ComfyUI Models Path:"), gbc);
|
||||
panel.add(new JLabel("ComfyUI Models Path (Target for downloads):"), gbc);
|
||||
|
||||
gbc.gridy++;
|
||||
JPanel row1 = new JPanel(new BorderLayout(5, 0));
|
||||
@@ -604,7 +825,7 @@ public class Main extends JFrame {
|
||||
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(10, 0, 5, 0);
|
||||
panel.add(new JLabel("Archive Path:"), gbc);
|
||||
panel.add(new JLabel("Archive Path (Offload storage):"), gbc);
|
||||
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(0, 0, 5, 0);
|
||||
@@ -622,6 +843,55 @@ public class Main extends JFrame {
|
||||
row2.add(browse2, BorderLayout.EAST);
|
||||
panel.add(row2, gbc);
|
||||
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(10, 0, 5, 0);
|
||||
panel.add(new JLabel("ComfyUI Main Directory (contains main.py):"), gbc);
|
||||
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(0, 0, 5, 0);
|
||||
JPanel row3 = new JPanel(new BorderLayout(5, 0));
|
||||
JTextField field3 = new JTextField(configService.getComfyUIPath());
|
||||
JButton browse3 = new JButton("Browse...");
|
||||
row3.add(field3, BorderLayout.CENTER);
|
||||
row3.add(browse3, BorderLayout.EAST);
|
||||
panel.add(row3, gbc);
|
||||
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(10, 0, 5, 0);
|
||||
panel.add(new JLabel("Python Executable Path:"), gbc);
|
||||
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(0, 0, 5, 0);
|
||||
JPanel row4 = new JPanel(new BorderLayout(5, 0));
|
||||
JTextField field4 = new JTextField(configService.getPythonPath());
|
||||
JButton browse4 = new JButton("Browse...");
|
||||
row4.add(field4, BorderLayout.CENTER);
|
||||
row4.add(browse4, BorderLayout.EAST);
|
||||
panel.add(row4, gbc);
|
||||
|
||||
browse3.addActionListener(e -> {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
|
||||
if (chooser.showOpenDialog(dialog) == JFileChooser.APPROVE_OPTION) {
|
||||
String path = chooser.getSelectedFile().getAbsolutePath();
|
||||
field3.setText(path);
|
||||
|
||||
// Auto-fill Python Path if possible
|
||||
String discoveredPython = configService.discoverPython(path);
|
||||
if (discoveredPython != null && !discoveredPython.equals("python") && !discoveredPython.equals("python3")) {
|
||||
field4.setText(discoveredPython);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
browse4.addActionListener(e -> {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
|
||||
if (chooser.showOpenDialog(dialog) == JFileChooser.APPROVE_OPTION) {
|
||||
field4.setText(chooser.getSelectedFile().getAbsolutePath());
|
||||
}
|
||||
});
|
||||
|
||||
gbc.gridy++;
|
||||
gbc.weighty = 1.0;
|
||||
panel.add(new JPanel(), gbc);
|
||||
@@ -633,6 +903,13 @@ public class Main extends JFrame {
|
||||
save.addActionListener(e -> {
|
||||
configService.setModelsPath(field1.getText().trim());
|
||||
configService.setArchivePath(field2.getText().trim());
|
||||
configService.setComfyUIPath(field3.getText().trim());
|
||||
configService.setPythonPath(field4.getText().trim());
|
||||
|
||||
// Re-generate launch command with new paths
|
||||
configService.setComfyLaunchCommand("");
|
||||
configService.autoDiscoverPaths();
|
||||
|
||||
statusLabel.setText("Settings updated.");
|
||||
analyzeJsonContent();
|
||||
dialog.dispose();
|
||||
@@ -713,7 +990,7 @@ public class Main extends JFrame {
|
||||
for (int i = 0; i < rowCount; i++) {
|
||||
if (selected[i]) {
|
||||
String currentStatus = (String) tableModel.getValueAt(i, 7);
|
||||
if ("📦 Found in Archive".equals(currentStatus)) {
|
||||
if ("📦 Archived".equals(currentStatus)) {
|
||||
final int idx = i;
|
||||
ModelInfo info = modelsToDownload.get(idx);
|
||||
String folder = info.getSave_path() != null ? info.getSave_path() : (info.getType() != null ? info.getType() : de.tki.comfymodels.domain.ModelFolder.CHECKPOINTS.getDefaultFolderName());
|
||||
@@ -748,9 +1025,6 @@ public class Main extends JFrame {
|
||||
}
|
||||
}
|
||||
|
||||
// After all restores, notify ComfyUI
|
||||
downloadManager.notifyComfyUI();
|
||||
|
||||
// After restores, start the actual download manager for the remaining models
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// Update selected array (some might have been unselected by restore)
|
||||
@@ -769,7 +1043,8 @@ public class Main extends JFrame {
|
||||
() -> SwingUtilities.invokeLater(() -> {
|
||||
downloadButton.setEnabled(true);
|
||||
statusLabel.setText("Queue finished.");
|
||||
if (configService.isShutdownAfterDownloadEnabled()) performSystemShutdown();
|
||||
|
||||
triggerPostOperationActions();
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -1016,24 +1291,41 @@ public class Main extends JFrame {
|
||||
|
||||
String normalizedFolder = archiveService.normalizeFolder(folder);
|
||||
|
||||
// 1. Primary path check (Standard Models Path)
|
||||
Path local = "root".equals(normalizedFolder) ? Paths.get(base, info.getName()) : Paths.get(base, normalizedFolder, info.getName());
|
||||
Path archivedPath = "root".equals(normalizedFolder) ? Paths.get(archive, info.getName()) : Paths.get(archive, normalizedFolder, info.getName());
|
||||
|
||||
boolean exists = Files.exists(local) && Files.isRegularFile(local);
|
||||
boolean inArchive = Files.exists(archivedPath) && Files.isRegularFile(archivedPath);
|
||||
boolean sizeMismatch = false;
|
||||
|
||||
// Initial size check for primary location
|
||||
if (exists && info.getByteSize() > 0) {
|
||||
try {
|
||||
if (Files.size(local) != info.getByteSize()) {
|
||||
sizeMismatch = true;
|
||||
exists = false;
|
||||
}
|
||||
} catch (IOException e) { exists = false; }
|
||||
// 2. Primary archive check (Standard Archive Path)
|
||||
boolean inArchive = false;
|
||||
Path archivedPath = null;
|
||||
if (archive != null && !archive.trim().isEmpty()) {
|
||||
archivedPath = "root".equals(normalizedFolder) ? Paths.get(archive, info.getName()) : Paths.get(archive, normalizedFolder, info.getName());
|
||||
inArchive = Files.exists(archivedPath) && Files.isRegularFile(archivedPath);
|
||||
}
|
||||
|
||||
// Fallback: Recursive search in ARCHIVE if not found at primary location
|
||||
boolean sizeMismatch = false;
|
||||
|
||||
// 3. Robust existence and archive cross-check (Safety Guard)
|
||||
if (archive != null && !archive.isEmpty()) {
|
||||
try {
|
||||
Path absArchive = Paths.get(archive).toAbsolutePath().normalize();
|
||||
|
||||
// If 'local' is actually inside the archive, it's NOT a local active model
|
||||
if (exists && local.toAbsolutePath().normalize().startsWith(absArchive)) {
|
||||
exists = false;
|
||||
inArchive = true;
|
||||
}
|
||||
|
||||
// If we found it in the primary archive location, verify size if known
|
||||
if (inArchive && info.getByteSize() > 0) {
|
||||
if (Files.size(archivedPath) != info.getByteSize()) {
|
||||
inArchive = false; // Size mismatch in archive doesn't count as "found"
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// 4. Fallback: Recursive search in ARCHIVE if not found at primary archive location
|
||||
if (!inArchive && archive != null && !archive.isEmpty()) {
|
||||
java.util.Optional<Path> foundInArchive = localScanner.findModelWithPrefSize(Paths.get(archive), info.getName(), info.getByteSize());
|
||||
if (foundInArchive.isPresent()) {
|
||||
@@ -1042,26 +1334,26 @@ public class Main extends JFrame {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Recursive search in LOCAL MODELS if not found OR size mismatch at primary location
|
||||
// 5. Fallback: Recursive search in LOCAL MODELS if not found OR size mismatch at primary location
|
||||
if ((!exists || sizeMismatch) && base != null && !base.isEmpty()) {
|
||||
java.util.Optional<Path> foundLocally = localScanner.findModelWithPrefSize(Paths.get(base), info.getName(), info.getByteSize());
|
||||
java.util.Optional<Path> foundLocally = localScanner.findModelWithPrefSizeAndType(Paths.get(base), info.getName(), info.getByteSize(), type);
|
||||
if (foundLocally.isPresent()) {
|
||||
Path potentialLocal = foundLocally.get();
|
||||
try {
|
||||
long potSize = Files.size(potentialLocal);
|
||||
// Only accept if size matches OR we don't know the remote size yet
|
||||
if (info.getByteSize() <= 0 || potSize == info.getByteSize()) {
|
||||
local = potentialLocal;
|
||||
exists = true;
|
||||
sizeMismatch = false; // Found a correct one!
|
||||
sizeMismatch = false;
|
||||
|
||||
// Update normalizedFolder to the actual location found locally
|
||||
Path root = Paths.get(base);
|
||||
Path rel = root.relativize(local);
|
||||
if (rel.getParent() != null) {
|
||||
normalizedFolder = rel.getParent().toString().replace("\\", "/");
|
||||
Path root = Paths.get(base).toAbsolutePath().normalize();
|
||||
Path absPotential = potentialLocal.toAbsolutePath().normalize();
|
||||
|
||||
if (absPotential.startsWith(root)) {
|
||||
Path rel = root.relativize(absPotential);
|
||||
normalizedFolder = (rel.getParent() != null) ? rel.getParent().toString().replace("\\", "/") : "root";
|
||||
} else {
|
||||
normalizedFolder = "root";
|
||||
normalizedFolder = "extra/" + potentialLocal.getParent().getFileName();
|
||||
}
|
||||
info.setSave_path(normalizedFolder);
|
||||
}
|
||||
@@ -1069,44 +1361,12 @@ public class Main extends JFrame {
|
||||
}
|
||||
}
|
||||
|
||||
// Safety check: if it exists at 'local' but 'local' is inside the archive folder, it's NOT locally existing in the right place
|
||||
if (exists && archive != null && !archive.isEmpty() &&
|
||||
local.toAbsolutePath().toString().toLowerCase().startsWith(new File(archive).getAbsolutePath().toLowerCase())) {
|
||||
exists = false;
|
||||
inArchive = true;
|
||||
}
|
||||
|
||||
// Rigorous 'Already exists' check: must match byteSize if known (redundant but safe)
|
||||
if (exists && info.getByteSize() > 0) {
|
||||
try {
|
||||
long localSize = Files.size(local);
|
||||
if (localSize != info.getByteSize()) {
|
||||
sizeMismatch = true;
|
||||
exists = false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
exists = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate size from local or archive if it exists and currently unknown
|
||||
if ((exists || inArchive || sizeMismatch) && ("Unknown".equals(info.getSize()) || info.getByteSize() <= 0)) {
|
||||
try {
|
||||
long bytes = Files.size(exists ? local : (sizeMismatch ? local : archivedPath));
|
||||
if (bytes > 0) {
|
||||
if (info.getByteSize() <= 0) info.setByteSize(bytes);
|
||||
if ("Unknown".equals(info.getSize())) info.setSize(searchService.formatSize(bytes));
|
||||
}
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
|
||||
// 6. Final Status Determination
|
||||
String status;
|
||||
if (exists && inArchive) {
|
||||
status = "✅ Already exists (Archived copy found)";
|
||||
} else if (exists) {
|
||||
if (exists) {
|
||||
status = "✅ Already exists";
|
||||
} else if (inArchive) {
|
||||
status = "📦 Found in Archive";
|
||||
status = "📦 Archived";
|
||||
} else if (sizeMismatch) {
|
||||
status = "🔄 Size Mismatch";
|
||||
} else if (info.getUrl().equals("MISSING")) {
|
||||
@@ -1148,7 +1408,7 @@ public class Main extends JFrame {
|
||||
|
||||
// Re-verify status with new remote size info
|
||||
String currentStatus = (String) tableModel.getValueAt(idx, 7);
|
||||
if (currentStatus.contains("Already exists") || "🔄 Size Mismatch".equals(currentStatus) || "📦 Found in Archive".equals(currentStatus)) {
|
||||
if (currentStatus.contains("Already exists") || "🔄 Size Mismatch".equals(currentStatus) || "📦 Archived".equals(currentStatus)) {
|
||||
String type = info.getType() != null ? info.getType() : de.tki.comfymodels.domain.ModelFolder.CHECKPOINTS.getDefaultFolderName();
|
||||
String folder = info.getSave_path() != null ? info.getSave_path() : type;
|
||||
String base = configService.getModelsPath();
|
||||
@@ -1191,13 +1451,13 @@ public class Main extends JFrame {
|
||||
boolean shouldSelect = false;
|
||||
|
||||
if (localSizeMatch) {
|
||||
newStatus = inArchive ? "✅ Already exists (Archived copy found)" : "✅ Already exists";
|
||||
newStatus = "✅ Already exists";
|
||||
shouldSelect = false;
|
||||
} else if (localExists) {
|
||||
newStatus = "🔄 Size Mismatch";
|
||||
shouldSelect = true;
|
||||
} else if (inArchive) {
|
||||
newStatus = "📦 Found in Archive";
|
||||
newStatus = "📦 Archived";
|
||||
shouldSelect = true;
|
||||
} else {
|
||||
newStatus = "✅ Known Good";
|
||||
@@ -1267,11 +1527,12 @@ public class Main extends JFrame {
|
||||
if (exists) {
|
||||
newStatus = "✅ Already exists";
|
||||
} else if (inArchive) {
|
||||
newStatus = "📦 Found in Archive";
|
||||
newStatus = "📦 Archived";
|
||||
} else {
|
||||
newStatus = "✅ Known Good";
|
||||
}
|
||||
|
||||
|
||||
tableModel.setValueAt(newStatus, idx, 7);
|
||||
tableModel.setValueAt(!exists, idx, 0); // Keep selected for download/restore if NOT local
|
||||
}),
|
||||
@@ -1603,6 +1864,59 @@ public class Main extends JFrame {
|
||||
ArchiveWorkItem(int i, String f, String n, String s) { index = i; folder = f; name = n; size = s; }
|
||||
}
|
||||
|
||||
private void performFullServiceRestart() {
|
||||
new Thread(() -> {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("🚀 Triggering Full Service Restart...");
|
||||
if (tableModel != null) {
|
||||
statusLabel.setText("🚀 Refreshing local library & Restarting ComfyUI...");
|
||||
}
|
||||
});
|
||||
|
||||
// 1. Core logic: Stop -> Discover -> Reload Config -> Start
|
||||
configService.autoDiscoverPaths();
|
||||
lifecycleService.restart();
|
||||
|
||||
// 2. Wait for health and notify user in status label
|
||||
for (int i = 0; i < 45; i++) {
|
||||
if (lifecycleService.isHealthy()) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("🔄 Syncing interface...");
|
||||
|
||||
// 3. Reload integrated browser
|
||||
if (comfyWebPanel != null) {
|
||||
comfyWebPanel.reload();
|
||||
}
|
||||
|
||||
// 4. Trigger external browser reload via Bridge (with a small delay for reconnect)
|
||||
new Thread(() -> {
|
||||
try { Thread.sleep(3000); } catch (InterruptedException ignored) {}
|
||||
downloadManager.notifyComfyUI(true);
|
||||
}).start();
|
||||
|
||||
// 5. Refresh Model Table (Local existence checks)
|
||||
analyzeJsonContent();
|
||||
|
||||
statusLabel.setText("✅ Ready");
|
||||
});
|
||||
return;
|
||||
}
|
||||
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> statusLabel.setText("⚠️ ComfyUI restart taking longer than expected. Check Control Panel."));
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void triggerPostOperationActions() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// Standardized Full Reload (replacing all soft reloads)
|
||||
performFullServiceRestart();
|
||||
|
||||
// Shutdown (only if enabled)
|
||||
if (configService.isShutdownAfterDownloadEnabled()) performSystemShutdown();
|
||||
});
|
||||
}
|
||||
|
||||
private void showModalProgressDialog(String title, String statusPrefix, List<Integer> selectedRows,
|
||||
DefaultTableModel sourceModel, DefaultTableModel targetModel, TaskExecutor executor) {
|
||||
JDialog progressDialog = new JDialog(this, title, true);
|
||||
@@ -1668,7 +1982,7 @@ public class Main extends JFrame {
|
||||
for (Object[] rowData : rowsToMove) targetModel.addRow(rowData);
|
||||
|
||||
analyzeJsonContent();
|
||||
downloadManager.notifyComfyUI();
|
||||
triggerPostOperationActions();
|
||||
|
||||
JOptionPane.showMessageDialog(this, finalSuccess + " models successfully " + title.toLowerCase() + "ed.",
|
||||
"Operation Complete", JOptionPane.INFORMATION_MESSAGE);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
public interface IComfyLifecycleService {
|
||||
/**
|
||||
* Starts the ComfyUI process using the configured command and working directory.
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Stops the ComfyUI process forcefully.
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Restarts the ComfyUI process (Stop -> Start).
|
||||
*/
|
||||
void restart();
|
||||
|
||||
/**
|
||||
* Returns the current status of the ComfyUI process.
|
||||
*/
|
||||
String getStatus();
|
||||
|
||||
/**
|
||||
* Returns true if the ComfyUI process is currently running.
|
||||
*/
|
||||
boolean isRunning();
|
||||
|
||||
/**
|
||||
* Performs a health check by pinging the ComfyUI API.
|
||||
*/
|
||||
boolean isHealthy();
|
||||
}
|
||||
@@ -16,6 +16,11 @@ public interface IDownloadManager {
|
||||
*/
|
||||
void notifyComfyUI();
|
||||
|
||||
/**
|
||||
* Triggers a refresh signal and optionally forces a browser reload.
|
||||
*/
|
||||
void notifyComfyUI(boolean forceReload);
|
||||
|
||||
/**
|
||||
* Returns a map of current statuses for each model in the queue.
|
||||
*/
|
||||
|
||||
@@ -35,31 +35,61 @@ public class ArchiveService {
|
||||
}
|
||||
|
||||
public Map<String, List<ModelInfo>> getModelsGroupedByFolder() {
|
||||
return scanDirectory(configService.getModelsPath(), true);
|
||||
Map<String, List<ModelInfo>> allModels = new TreeMap<>();
|
||||
java.util.Set<String> seenPaths = new java.util.HashSet<>();
|
||||
|
||||
// 1. Scan Standard Models Path
|
||||
Map<String, List<ModelInfo>> standardModels = scanDirectory(configService.getModelsPath(), true, seenPaths);
|
||||
allModels.putAll(standardModels);
|
||||
|
||||
// 2. Scan Extra Model Paths
|
||||
Map<String, List<Path>> extraPaths = pathResolver.getAllExtraModelPaths();
|
||||
for (Map.Entry<String, List<Path>> entry : extraPaths.entrySet()) {
|
||||
String type = entry.getKey();
|
||||
for (Path extraPath : entry.getValue()) {
|
||||
if (Files.exists(extraPath) && Files.isDirectory(extraPath)) {
|
||||
Map<String, List<ModelInfo>> extraModels = scanDirectory(extraPath.toString(), true, seenPaths);
|
||||
// Merge extra models.
|
||||
for (Map.Entry<String, List<ModelInfo>> extraEntry : extraModels.entrySet()) {
|
||||
String folder = extraEntry.getKey();
|
||||
String targetFolder = "root".equals(folder) ? type : type + "/" + folder;
|
||||
allModels.computeIfAbsent(targetFolder, k -> new ArrayList<>()).addAll(extraEntry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allModels;
|
||||
}
|
||||
|
||||
public Map<String, List<ModelInfo>> getArchivedModelsGroupedByFolder() {
|
||||
return scanDirectory(configService.getArchivePath(), false);
|
||||
String archivePath = configService.getArchivePath();
|
||||
if (archivePath == null || archivePath.trim().isEmpty()) {
|
||||
return new TreeMap<>();
|
||||
}
|
||||
return scanDirectory(archivePath, false, new java.util.HashSet<>());
|
||||
}
|
||||
|
||||
private Map<String, List<ModelInfo>> scanDirectory(String pathStr, boolean excludeArchived) {
|
||||
private Map<String, List<ModelInfo>> scanDirectory(String pathStr, boolean excludeArchived, java.util.Set<String> seenPaths) {
|
||||
Map<String, List<ModelInfo>> grouped = new TreeMap<>();
|
||||
if (pathStr == null || pathStr.isEmpty()) return grouped;
|
||||
|
||||
Path root = Paths.get(pathStr);
|
||||
Path root = Paths.get(pathStr).toAbsolutePath().normalize();
|
||||
if (!Files.exists(root)) {
|
||||
System.err.println("Scan directory does not exist: " + pathStr);
|
||||
return grouped;
|
||||
}
|
||||
|
||||
try (Stream<Path> walk = Files.walk(root)) {
|
||||
walk.filter(p -> {
|
||||
try {
|
||||
// Ignore hidden directories and .venv / venv
|
||||
// Ignore hidden directories and .venv / venv / archive (if excluding)
|
||||
Path relativeToRoot = root.relativize(p);
|
||||
for (Path part : relativeToRoot) {
|
||||
String name = part.toString();
|
||||
if (name.startsWith(".") || name.equalsIgnoreCase("venv") || name.equalsIgnoreCase(".venv") || name.equalsIgnoreCase("__pycache__")) {
|
||||
String name = part.toString().toLowerCase();
|
||||
if (name.startsWith(".") || name.equals("venv") || name.equals(".venv") || name.equals("__pycache__")) {
|
||||
return false;
|
||||
}
|
||||
if (excludeArchived && (name.equals("archive") || name.equals("archived"))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -68,11 +98,12 @@ public class ArchiveService {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.filter(p -> Files.isRegularFile(p))
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(this::isSupportedModel)
|
||||
.filter(p -> !excludeArchived || !isAlreadyArchived(p))
|
||||
.forEach(p -> {
|
||||
if (!Files.exists(p)) return; // Double check existence
|
||||
String absPath = p.toAbsolutePath().normalize().toString().toLowerCase();
|
||||
if (!seenPaths.add(absPath)) return;
|
||||
|
||||
Path relative = root.relativize(p);
|
||||
String folder = relative.getParent() != null ? relative.getParent().toString().replace("\\", "/") : "root";
|
||||
@@ -84,6 +115,7 @@ public class ArchiveService {
|
||||
|
||||
try {
|
||||
long bytes = Files.size(p);
|
||||
info.setByteSize(bytes);
|
||||
double mb = bytes / (1024.0 * 1024.0);
|
||||
if (mb > 1024) {
|
||||
info.setSize(String.format("%.2f GB", mb / 1024.0));
|
||||
@@ -98,7 +130,6 @@ public class ArchiveService {
|
||||
});
|
||||
} catch (IOException e) {
|
||||
System.err.println("Error walking directory " + pathStr + ": " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return grouped;
|
||||
@@ -112,39 +143,76 @@ public class ArchiveService {
|
||||
private boolean isAlreadyArchived(Path p) {
|
||||
String archivePathStr = configService.getArchivePath();
|
||||
if (archivePathStr == null || archivePathStr.isEmpty()) return false;
|
||||
Path archivePath = Paths.get(archivePathStr).toAbsolutePath();
|
||||
return p.toAbsolutePath().startsWith(archivePath);
|
||||
try {
|
||||
Path archivePath = Paths.get(archivePathStr).toAbsolutePath().normalize();
|
||||
Path targetPath = p.toAbsolutePath().normalize();
|
||||
return targetPath.startsWith(archivePath);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void moveToArchiveWithProgress(String relativePath, LongConsumer progressUpdate) throws IOException {
|
||||
String modelsPath = configService.getModelsPath();
|
||||
String archivePath = configService.getArchivePath();
|
||||
|
||||
if (modelsPath == null || archivePath == null) throw new IOException("Paths not configured");
|
||||
|
||||
Path source = Paths.get(modelsPath, relativePath);
|
||||
Path target = Paths.get(archivePath, relativePath);
|
||||
|
||||
if (!Files.exists(source)) throw new IOException("Source not found: " + source);
|
||||
|
||||
Files.createDirectories(target.getParent());
|
||||
moveWithProgress(source, target, progressUpdate);
|
||||
Path p = Paths.get(relativePath);
|
||||
String filename = p.getFileName().toString();
|
||||
String folder = p.getParent() != null ? p.getParent().toString().replace("\\", "/") : "root";
|
||||
moveToArchiveWithProgress(folder, filename, progressUpdate);
|
||||
}
|
||||
|
||||
public void moveToArchive(String relativePath) throws IOException {
|
||||
moveToArchiveWithProgress(relativePath, null);
|
||||
}
|
||||
|
||||
public void moveToArchiveWithProgress(String folder, String filename, LongConsumer progressUpdate) throws IOException {
|
||||
String normFolder = normalizeFolder(folder);
|
||||
String relPath = "root".equals(normFolder) ? filename : Paths.get(normFolder, filename).toString();
|
||||
moveToArchiveWithProgress(relPath, progressUpdate);
|
||||
}
|
||||
|
||||
public void moveToArchive(String folder, String filename) throws IOException {
|
||||
moveToArchiveWithProgress(folder, filename, null);
|
||||
}
|
||||
|
||||
public void moveToArchiveWithProgress(String folder, String filename, LongConsumer progressUpdate) throws IOException {
|
||||
String archivePathStr = configService.getArchivePath();
|
||||
if (archivePathStr == null) throw new IOException("Archive path not configured");
|
||||
Path archivePath = Paths.get(archivePathStr);
|
||||
|
||||
String normFolder = normalizeFolder(folder);
|
||||
|
||||
// Find source path by checking standard and extra paths
|
||||
Path source = findActualSourcePath(normFolder, filename);
|
||||
if (source == null || !Files.exists(source)) {
|
||||
throw new IOException("Source model not found: " + filename + " in folder " + normFolder);
|
||||
}
|
||||
|
||||
// Target in archive follows the relative structure
|
||||
String relPath = "root".equals(normFolder) ? filename : Paths.get(normFolder, filename).toString();
|
||||
Path target = archivePath.resolve(relPath);
|
||||
|
||||
Files.createDirectories(target.getParent());
|
||||
moveWithProgress(source, target, progressUpdate);
|
||||
}
|
||||
|
||||
private Path findActualSourcePath(String folder, String filename) {
|
||||
String modelsPathStr = configService.getModelsPath();
|
||||
if (modelsPathStr == null) return null;
|
||||
|
||||
Path modelsPath = Paths.get(modelsPathStr);
|
||||
|
||||
// 1. Check standard path
|
||||
Path standardPath = "root".equals(folder) ? modelsPath.resolve(filename) : modelsPath.resolve(folder).resolve(filename);
|
||||
if (Files.exists(standardPath)) return standardPath;
|
||||
|
||||
// 2. Check extra paths for this type/folder
|
||||
List<Path> extraPaths = pathResolver.getModelPaths(folder);
|
||||
for (Path extraRoot : extraPaths) {
|
||||
// Since extraRoot often ALREADY points to the type folder (e.g. .../checkpoints)
|
||||
// we check both extraRoot/filename AND extraRoot/folder/filename
|
||||
Path p1 = extraRoot.resolve(filename);
|
||||
if (Files.exists(p1)) return p1;
|
||||
|
||||
Path p2 = extraRoot.resolve(folder).resolve(filename);
|
||||
if (Files.exists(p2)) return p2;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean restoreFromArchiveWithProgress(String folder, String filename, LongConsumer progressUpdate) {
|
||||
String modelsPathStr = configService.getModelsPath();
|
||||
String archivePathStr = configService.getArchivePath();
|
||||
@@ -156,20 +224,25 @@ public class ArchiveService {
|
||||
|
||||
String normFolder = normalizeFolder(folder);
|
||||
|
||||
// Primary location with case-insensitive resolution
|
||||
// Primary location in archive
|
||||
Path archivedFolder = "root".equals(normFolder) ? archivePath : pathResolver.resolveCaseInsensitiveRecursive(archivePath, normFolder);
|
||||
Path archived = archivedFolder.resolve(filename);
|
||||
|
||||
// Determine target folder: prefer extra paths if configured
|
||||
Path targetFolder = "root".equals(normFolder) ? modelsPath : modelsPath.resolve(normFolder);
|
||||
List<Path> extraPaths = pathResolver.getModelPaths(normFolder);
|
||||
if (!extraPaths.isEmpty()) {
|
||||
targetFolder = extraPaths.get(0);
|
||||
}
|
||||
|
||||
Path target = targetFolder.resolve(filename);
|
||||
|
||||
// Fallback: If not at primary, check root of archive
|
||||
// Fallback: If not at primary archive location, check root of archive or search
|
||||
if (!Files.exists(archived)) {
|
||||
Path rootArchived = archivePath.resolve(filename);
|
||||
if (Files.exists(rootArchived)) {
|
||||
archived = rootArchived;
|
||||
} else {
|
||||
// Secondary Fallback: Check if it exists ANYWHERE in the archive (shallow search)
|
||||
try (Stream<Path> walk = Files.walk(archivePath, 2)) {
|
||||
java.util.Optional<Path> found = walk.filter(Files::isRegularFile)
|
||||
.filter(p -> p.getFileName().toString().equalsIgnoreCase(filename))
|
||||
@@ -177,7 +250,7 @@ public class ArchiveService {
|
||||
if (found.isPresent()) {
|
||||
archived = found.get();
|
||||
} else {
|
||||
System.err.println("Restore failed: " + filename + " not found in archive (checked " + normFolder + " and root)");
|
||||
System.err.println("Restore failed: " + filename + " not found in archive");
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package de.tki.comfymodels.service.impl;
|
||||
|
||||
import de.tki.comfymodels.domain.ModelInfo;
|
||||
import org.json.JSONObject;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class ComfyDiagnosticService {
|
||||
|
||||
@Autowired
|
||||
private ConfigService configService;
|
||||
|
||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(2))
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Checks if a specific model is currently known to ComfyUI.
|
||||
* Queries /object_info to see if the model appears in the checkpoint/lora lists.
|
||||
*/
|
||||
public boolean isModelAvailableInComfy(String modelName, String type) {
|
||||
String url = configService.getComfyUIUrl();
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url + "/object_info"))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != 200) return false;
|
||||
|
||||
JSONObject info = new JSONObject(response.body());
|
||||
|
||||
// Typical node names for models
|
||||
String[] nodesToCheck = {"CheckpointLoaderSimple", "LoraLoader", "CheckpointLoader"};
|
||||
|
||||
for (String node : nodesToCheck) {
|
||||
if (info.has(node)) {
|
||||
JSONObject nodeInfo = info.getJSONObject(node);
|
||||
if (nodeInfo.has("input")) {
|
||||
JSONObject input = nodeInfo.getJSONObject("input");
|
||||
if (input.has("required")) {
|
||||
JSONObject required = input.getJSONObject("required");
|
||||
for (String key : required.keySet()) {
|
||||
Object val = required.get(key);
|
||||
if (val instanceof org.json.JSONArray) {
|
||||
org.json.JSONArray outerArray = (org.json.JSONArray) val;
|
||||
if (outerArray.length() > 0) {
|
||||
Object firstElement = outerArray.get(0);
|
||||
if (firstElement instanceof org.json.JSONArray) {
|
||||
org.json.JSONArray options = (org.json.JSONArray) firstElement;
|
||||
for (int i = 0; i < options.length(); i++) {
|
||||
if (options.get(i) instanceof String && options.getString(i).equals(modelName)) return true;
|
||||
}
|
||||
} else if (firstElement instanceof String) {
|
||||
// Handle cases where the array itself is the list of options
|
||||
for (int i = 0; i < outerArray.length(); i++) {
|
||||
if (outerArray.get(i) instanceof String && outerArray.getString(i).equals(modelName)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("[Diagnostic] API Check failed: " + e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk check for a list of models.
|
||||
*/
|
||||
public List<String> getMissingModels(List<ModelInfo> models) {
|
||||
List<String> missing = new ArrayList<>();
|
||||
for (ModelInfo info : models) {
|
||||
if (!isModelAvailableInComfy(info.getName(), info.getType())) {
|
||||
missing.add(info.getName());
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package de.tki.comfymodels.service.impl;
|
||||
|
||||
import de.tki.comfymodels.service.IComfyLifecycleService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Service
|
||||
public class ComfyLifecycleService implements IComfyLifecycleService {
|
||||
|
||||
@Autowired
|
||||
private ConfigService configService;
|
||||
|
||||
private Process comfyProcess;
|
||||
private final AtomicReference<String> status = new AtomicReference<>("Stopped");
|
||||
private final HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofMillis(500)).build();
|
||||
|
||||
@Override
|
||||
public synchronized void start() {
|
||||
if (isRunning()) {
|
||||
status.set("Already Running");
|
||||
return;
|
||||
}
|
||||
|
||||
String command = configService.getComfyLaunchCommand();
|
||||
String workingDir = configService.getComfyWorkingDir();
|
||||
|
||||
if (command == null || command.trim().isEmpty()) {
|
||||
status.set("Error: No launch command configured");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
status.set("Starting...");
|
||||
|
||||
java.util.List<String> commandList = new java.util.ArrayList<>();
|
||||
if (de.tki.comfymodels.util.PlatformUtils.isWindows()) {
|
||||
commandList.add("cmd");
|
||||
commandList.add("/c");
|
||||
// Wrap the entire command in extra quotes for Windows cmd /c quirk
|
||||
commandList.add("\"" + command + "\"");
|
||||
} else {
|
||||
commandList.add("sh");
|
||||
commandList.add("-c");
|
||||
commandList.add(command);
|
||||
}
|
||||
|
||||
System.out.println("🚀 [Lifecycle] Starting command: " + String.join(" ", commandList));
|
||||
if (workingDir != null) System.out.println("📁 [Lifecycle] In directory: " + workingDir);
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(commandList);
|
||||
if (workingDir != null && !workingDir.isEmpty()) {
|
||||
pb.directory(new File(workingDir));
|
||||
}
|
||||
pb.redirectErrorStream(true);
|
||||
comfyProcess = pb.start();
|
||||
|
||||
// Quietly consume output to prevent process buffer stalls
|
||||
new Thread(() -> {
|
||||
try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(comfyProcess.getInputStream()))) {
|
||||
while (reader.readLine() != null) {
|
||||
// Just consume, don't print
|
||||
}
|
||||
} catch (java.io.IOException ignored) {}
|
||||
}).start();
|
||||
status.set("Running (PID: " + comfyProcess.pid() + ")");
|
||||
|
||||
// Background thread to wait for exit
|
||||
new Thread(() -> {
|
||||
try {
|
||||
int exitCode = comfyProcess.waitFor();
|
||||
status.set("Stopped (Exit Code: " + exitCode + ")");
|
||||
comfyProcess = null;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}).start();
|
||||
|
||||
} catch (IOException e) {
|
||||
status.set("Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop() {
|
||||
status.set("Stopping...");
|
||||
|
||||
// 1. Kill internal process if managed
|
||||
if (comfyProcess != null) {
|
||||
try {
|
||||
comfyProcess.descendants().forEach(ProcessHandle::destroyForcibly);
|
||||
comfyProcess.destroyForcibly();
|
||||
comfyProcess.waitFor();
|
||||
} catch (Exception ignored) {}
|
||||
comfyProcess = null;
|
||||
}
|
||||
|
||||
// 2. Kill by port fallback (for external/orphaned instances)
|
||||
if (isHealthy()) {
|
||||
killProcessOnPort();
|
||||
}
|
||||
|
||||
status.set("Stopped");
|
||||
}
|
||||
|
||||
private void killProcessOnPort() {
|
||||
try {
|
||||
String url = configService.getComfyUIUrl();
|
||||
int port = 8000;
|
||||
try {
|
||||
port = Integer.parseInt(url.substring(url.lastIndexOf(":") + 1));
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
if (de.tki.comfymodels.util.PlatformUtils.isWindows()) {
|
||||
// Find PID on port and kill it
|
||||
Process p = Runtime.getRuntime().exec("cmd /c netstat -ano | findstr :" + port);
|
||||
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(p.getInputStream()));
|
||||
String line = reader.readLine();
|
||||
if (line != null) {
|
||||
String[] parts = line.trim().split("\\s+");
|
||||
long pid = Long.parseLong(parts[parts.length - 1]);
|
||||
ProcessHandle.of(pid).ifPresent(ProcessHandle::destroyForcibly);
|
||||
}
|
||||
} else {
|
||||
// Linux/Mac fallback
|
||||
Runtime.getRuntime().exec("sh -c fuser -k " + port + "/tcp");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to kill process on port: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restart() {
|
||||
stop();
|
||||
try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
|
||||
|
||||
// Reload extra paths in case they were changed before restart
|
||||
configService.loadExtraModelPaths();
|
||||
|
||||
start();
|
||||
|
||||
// Optional: Wait for health check in background
|
||||
new Thread(() -> {
|
||||
status.set("Restarting (Waiting for API...)");
|
||||
for (int i = 0; i < 30; i++) { // Wait up to 30 seconds
|
||||
if (isHealthy()) {
|
||||
status.set("Running & Healthy");
|
||||
return;
|
||||
}
|
||||
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
|
||||
}
|
||||
status.set("Running (Healthy Check Timeout)");
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStatus() {
|
||||
String current = status.get();
|
||||
if (current.startsWith("Stopped") || current.startsWith("Error")) {
|
||||
if (isHealthy()) {
|
||||
return "Running (External Instance)";
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return (comfyProcess != null && comfyProcess.isAlive()) || isHealthy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHealthy() {
|
||||
String url = configService.getComfyUIUrl();
|
||||
if (url == null || url.isEmpty()) return false;
|
||||
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url + "/system_stats"))
|
||||
.timeout(Duration.ofMillis(1000))
|
||||
.GET().build();
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
boolean ok = response.statusCode() == 200;
|
||||
if (!ok) System.err.println("⚠️ [Lifecycle] Health check failed for " + url + " - Status: " + response.statusCode());
|
||||
return ok;
|
||||
} catch (Exception e) {
|
||||
// Silently fail for regular polling, but keep diagnostic info ready if needed
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -122,8 +122,13 @@ public class DefaultDownloadManager implements IDownloadManager {
|
||||
// NEU: Benachrichtigt ComfyUI
|
||||
@Override
|
||||
public void notifyComfyUI() {
|
||||
notifyComfyUI(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyComfyUI(boolean forceReload) {
|
||||
String comfyUrl = configService.getComfyUIUrl();
|
||||
if (sendRefreshPing(comfyUrl)) {
|
||||
if (sendRefreshPing(comfyUrl, forceReload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -133,23 +138,25 @@ public class DefaultDownloadManager implements IDownloadManager {
|
||||
if (discoveredUrl != null) {
|
||||
System.out.println("✨ [Model-Downloader] ComfyUI automatisch gefunden unter: " + discoveredUrl);
|
||||
configService.setComfyUIUrl(discoveredUrl);
|
||||
sendRefreshPing(discoveredUrl);
|
||||
sendRefreshPing(discoveredUrl, forceReload);
|
||||
} else {
|
||||
System.err.println("❌ [Model-Downloader] ComfyUI konnte nicht automatisch gefunden werden. Bitte stelle sicher, dass es läuft.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean sendRefreshPing(String url) {
|
||||
private boolean sendRefreshPing(String url, boolean forceReload) {
|
||||
try {
|
||||
String json = "{\"force_reload\": " + forceReload + "}";
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url + "/kippster/model-downloaded"))
|
||||
.uri(URI.create(url + "/cmfd/refresh-models"))
|
||||
.timeout(java.time.Duration.ofSeconds(2))
|
||||
.POST(HttpRequest.BodyPublishers.noBody())
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(json))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() == 200) {
|
||||
System.out.println("✅ [Model-Downloader] ComfyUI (" + url + ") erfolgreich benachrichtigt.");
|
||||
System.out.println("✅ [Model-Downloader] ComfyUI (" + url + ") erfolgreich benachrichtigt (Force: " + forceReload + ").");
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
@@ -222,6 +229,10 @@ public class DefaultDownloadManager implements IDownloadManager {
|
||||
}
|
||||
|
||||
long existingFileSize = (file.exists() && !isActuallyInArchive) ? file.length() : 0;
|
||||
|
||||
Path partFile = targetFile.resolveSibling(targetFile.getFileName().toString() + ".cmfd");
|
||||
File pFile = partFile.toFile();
|
||||
long existingPartSize = pFile.exists() ? pFile.length() : 0;
|
||||
|
||||
if (waitForPauseAndCheckSelection(index, statusUpdater)) return;
|
||||
|
||||
@@ -276,13 +287,19 @@ public class DefaultDownloadManager implements IDownloadManager {
|
||||
downloadBuilder.header("Authorization", "Bearer " + hfToken);
|
||||
}
|
||||
|
||||
if (existingFileSize > 0) downloadBuilder.header("Range", "bytes=" + existingFileSize + "-");
|
||||
if (existingPartSize > 0) downloadBuilder.header("Range", "bytes=" + existingPartSize + "-");
|
||||
|
||||
HttpResponse<InputStream> response = httpClient.send(downloadBuilder.build(), HttpResponse.BodyHandlers.ofInputStream());
|
||||
int statusCode = response.statusCode();
|
||||
|
||||
if (statusCode == 416) {
|
||||
safeUpdateStatus(index, "✅ Already exists", statusUpdater);
|
||||
// Part might be complete but not moved
|
||||
if (existingPartSize == totalRemoteSize) {
|
||||
Files.move(partFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
safeUpdateStatus(index, "✅ Finished", statusUpdater);
|
||||
} else {
|
||||
safeUpdateStatus(index, "✅ Already exists", statusUpdater);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -292,19 +309,19 @@ public class DefaultDownloadManager implements IDownloadManager {
|
||||
}
|
||||
|
||||
long contentLen = response.headers().firstValueAsLong("Content-Length").orElse(0L);
|
||||
long totalBytes = (statusCode == 206) ? contentLen + existingFileSize : contentLen;
|
||||
long totalBytes = (statusCode == 206) ? contentLen + existingPartSize : contentLen;
|
||||
|
||||
if (totalBytes > 0 && totalBytes < 5000 && info.getName().endsWith(".safetensors")) {
|
||||
safeUpdateStatus(index, "❌ LFS Stub detected (Token?)", statusUpdater);
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusCode == 200) existingFileSize = 0;
|
||||
if (statusCode == 200) existingPartSize = 0;
|
||||
|
||||
try (InputStream is = response.body(); RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
|
||||
raf.seek(existingFileSize);
|
||||
try (InputStream is = response.body(); RandomAccessFile raf = new RandomAccessFile(pFile, "rw")) {
|
||||
raf.seek(existingPartSize);
|
||||
byte[] buffer = new byte[65536];
|
||||
long downloaded = existingFileSize;
|
||||
long downloaded = existingPartSize;
|
||||
int read;
|
||||
long lastUpdate = 0;
|
||||
while ((read = is.read(buffer)) != -1) {
|
||||
@@ -332,19 +349,21 @@ public class DefaultDownloadManager implements IDownloadManager {
|
||||
if (isStopped || !isSelected(index)) {
|
||||
safeUpdateStatus(index, !isSelected(index) ? "Skipped (Unchecked)" : "Stopped", statusUpdater);
|
||||
} else {
|
||||
long finalSize = file.length();
|
||||
long finalSize = pFile.length();
|
||||
boolean sizeMismatch = totalBytes > 0 && finalSize < totalBytes;
|
||||
boolean corruptedSafetensor = info.getName().endsWith(".safetensors") && (finalSize % 2 != 0);
|
||||
|
||||
if ((sizeMismatch || corruptedSafetensor) && retryCount < 1) {
|
||||
safeUpdateStatus(index, "🔄 Verification failed, redownloading...", statusUpdater);
|
||||
file.delete();
|
||||
pFile.delete();
|
||||
downloadWithResumeInternal(info, targetFile, index, statusUpdater, retryCount + 1);
|
||||
} else if (sizeMismatch) {
|
||||
safeUpdateStatus(index, "❌ Incomplete (" + formatSize(finalSize) + "/" + formatSize(totalBytes) + ")", statusUpdater);
|
||||
} else if (corruptedSafetensor) {
|
||||
safeUpdateStatus(index, "❌ Corrupted (Odd Size)", statusUpdater);
|
||||
} else {
|
||||
// Atomic move to final destination
|
||||
Files.move(partFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
safeUpdateStatus(index, "✅ Finished", statusUpdater);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,71 +24,116 @@ import java.util.stream.Stream;
|
||||
public class LocalModelScanner {
|
||||
|
||||
private final ConfigService configService;
|
||||
private final PathResolver pathResolver;
|
||||
private final Map<String, Path> modelCache = new HashMap<>();
|
||||
|
||||
@Autowired
|
||||
public LocalModelScanner(ConfigService configService) {
|
||||
public LocalModelScanner(ConfigService configService, PathResolver pathResolver) {
|
||||
this.configService = configService;
|
||||
this.pathResolver = pathResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the configured models directory for supported model files.
|
||||
*
|
||||
* @return A list of ModelInfo objects representing the found local models.
|
||||
* Scans all configured models directories (standard and extra) for supported model files.
|
||||
*/
|
||||
public List<ModelInfo> scanLocalModels() {
|
||||
List<ModelInfo> foundModels = new ArrayList<>();
|
||||
java.util.Set<String> seenPaths = new java.util.HashSet<>();
|
||||
|
||||
// 1. Scan Standard Models Path
|
||||
String modelsPathStr = configService.getModelsPath();
|
||||
if (modelsPathStr == null || modelsPathStr.isEmpty()) {
|
||||
return foundModels;
|
||||
if (modelsPathStr != null && !modelsPathStr.isEmpty()) {
|
||||
scanDirectory(Paths.get(modelsPathStr), foundModels, seenPaths, null);
|
||||
}
|
||||
|
||||
Path rootPath = Paths.get(modelsPathStr);
|
||||
if (!Files.exists(rootPath) || !Files.isDirectory(rootPath)) {
|
||||
return foundModels;
|
||||
}
|
||||
|
||||
modelCache.clear();
|
||||
try (Stream<Path> walk = Files.walk(rootPath)) {
|
||||
List<Path> files = walk
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(this::isSupportedModelFile)
|
||||
.filter(p -> !isIgnored(p, rootPath))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (Path file : files) {
|
||||
modelCache.put(file.getFileName().toString().toLowerCase(), file);
|
||||
foundModels.add(createModelInfo(file, rootPath));
|
||||
// 2. Scan Extra Model Paths (from extra_model_paths.yaml)
|
||||
Map<String, List<Path>> extraPaths = pathResolver.getAllExtraModelPaths();
|
||||
for (Map.Entry<String, List<Path>> entry : extraPaths.entrySet()) {
|
||||
String type = entry.getKey();
|
||||
for (Path extraPath : entry.getValue()) {
|
||||
if (Files.exists(extraPath) && Files.isDirectory(extraPath)) {
|
||||
scanDirectory(extraPath, foundModels, seenPaths, type);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Error scanning local models: " + e.getMessage());
|
||||
}
|
||||
|
||||
return foundModels;
|
||||
}
|
||||
|
||||
private void scanDirectory(Path rootPath, List<ModelInfo> foundModels, java.util.Set<String> seenPaths, String forcedType) {
|
||||
Path absRoot = rootPath.toAbsolutePath().normalize();
|
||||
try (Stream<Path> walk = Files.walk(absRoot)) {
|
||||
List<Path> files = walk
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(this::isSupportedModelFile)
|
||||
.filter(p -> !isIgnored(p, absRoot))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (Path file : files) {
|
||||
String absPath = file.toAbsolutePath().normalize().toString().toLowerCase();
|
||||
if (seenPaths.add(absPath)) {
|
||||
modelCache.put(file.getFileName().toString().toLowerCase(), file);
|
||||
foundModels.add(createModelInfo(file, absRoot, forcedType));
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Error scanning directory " + absRoot + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a local model by its filename, regardless of subfolder.
|
||||
* Finds a local model by its filename, searching across all configured paths.
|
||||
*/
|
||||
public Optional<Path> findModel(String filename) {
|
||||
return findModelWithPrefSize(null, filename, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a model by its filename recursively, searching all configured paths and prioritizing matches with the preferred size.
|
||||
*/
|
||||
public Optional<Path> findModelWithPrefSize(Path specificRoot, String filename, long preferredSize) {
|
||||
return findModelWithPrefSizeAndType(specificRoot, filename, preferredSize, null);
|
||||
}
|
||||
|
||||
public Optional<Path> findModelWithPrefSizeAndType(Path specificRoot, String filename, long preferredSize, String type) {
|
||||
if (filename == null) return Optional.empty();
|
||||
|
||||
// 1. Search in specific root if provided
|
||||
if (specificRoot != null) {
|
||||
Optional<Path> found = searchInDirectory(specificRoot, filename, preferredSize);
|
||||
if (found.isPresent()) return found;
|
||||
}
|
||||
|
||||
// 2. Search in Type-Specific Extra Paths (High Priority)
|
||||
if (type != null) {
|
||||
List<Path> typePaths = pathResolver.getModelPaths(type);
|
||||
for (Path p : typePaths) {
|
||||
Optional<Path> found = searchInDirectory(p, filename, preferredSize);
|
||||
if (found.isPresent()) return found;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Search in Standard Models Path
|
||||
String modelsPathStr = configService.getModelsPath();
|
||||
if (modelsPathStr == null || modelsPathStr.isEmpty()) return Optional.empty();
|
||||
return findModelInDirectory(Paths.get(modelsPathStr), filename);
|
||||
}
|
||||
if (modelsPathStr != null && !modelsPathStr.isEmpty()) {
|
||||
Optional<Path> found = searchInDirectory(Paths.get(modelsPathStr), filename, preferredSize);
|
||||
if (found.isPresent()) return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a model by its filename recursively starting from the given root directory.
|
||||
*/
|
||||
public Optional<Path> findModelInDirectory(Path root, String filename) {
|
||||
return findModelWithPrefSize(root, filename, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a model by its filename recursively, prioritizing matches with the preferred size.
|
||||
*/
|
||||
public Optional<Path> findModelWithPrefSize(Path root, String filename, long preferredSize) {
|
||||
if (filename == null || root == null || !Files.exists(root)) return Optional.empty();
|
||||
// 4. Search in ALL Extra Model Paths (Fallback)
|
||||
Map<String, List<Path>> extraPaths = pathResolver.getAllExtraModelPaths();
|
||||
for (List<Path> paths : extraPaths.values()) {
|
||||
for (Path p : paths) {
|
||||
Optional<Path> found = searchInDirectory(p, filename, preferredSize);
|
||||
if (found.isPresent()) return found;
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<Path> searchInDirectory(Path root, String filename, long preferredSize) {
|
||||
if (root == null || !Files.exists(root)) return Optional.empty();
|
||||
try (Stream<Path> walk = Files.walk(root)) {
|
||||
List<Path> matches = walk.filter(Files::isRegularFile)
|
||||
.filter(p -> !isIgnored(p, root))
|
||||
@@ -97,7 +142,6 @@ public class LocalModelScanner {
|
||||
|
||||
if (matches.isEmpty()) return Optional.empty();
|
||||
|
||||
// 1. Try to find a match with exact size if preferredSize is known
|
||||
if (preferredSize > 0) {
|
||||
for (Path p : matches) {
|
||||
try {
|
||||
@@ -105,25 +149,23 @@ public class LocalModelScanner {
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Return first match if no size match found or size unknown
|
||||
return Optional.of(matches.get(0));
|
||||
} catch (IOException e) {
|
||||
System.err.println("Error searching in " + root + ": " + e.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSupportedModelFile(Path path) {
|
||||
String name = path.getFileName().toString().toLowerCase();
|
||||
return name.endsWith(".safetensors") || name.endsWith(".ckpt") || name.endsWith(".pt");
|
||||
return name.endsWith(".safetensors") || name.endsWith(".sft") || name.endsWith(".ckpt") ||
|
||||
name.endsWith(".pth") || name.endsWith(".pt") || name.endsWith(".bin");
|
||||
}
|
||||
|
||||
private boolean isIgnored(Path path, Path rootPath) {
|
||||
// 1. If we are already searching inside an archive folder (e.g. E:\Archive),
|
||||
// we should not filter out anything based on the "archive" name.
|
||||
String rootName = rootPath.getFileName() != null ? rootPath.getFileName().toString().toLowerCase() : "";
|
||||
if (rootName.contains("archive")) {
|
||||
if (rootName.contains("archive") || rootName.contains("archived")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -131,31 +173,31 @@ public class LocalModelScanner {
|
||||
try {
|
||||
String archivePathStr = configService.getArchivePath();
|
||||
if (archivePathStr != null && !archivePathStr.isEmpty()) {
|
||||
Path archivePath = Paths.get(archivePathStr).toAbsolutePath();
|
||||
if (path.toAbsolutePath().startsWith(archivePath)) {
|
||||
Path archivePath = Paths.get(archivePathStr).toAbsolutePath().normalize();
|
||||
Path targetPath = path.toAbsolutePath().normalize();
|
||||
if (targetPath.startsWith(archivePath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// 3. Fallback: ignore any subfolder named "archive" or ".venv"
|
||||
int rootCount = rootPath.getNameCount();
|
||||
for (int i = rootCount; i < path.getNameCount(); i++) {
|
||||
for (int i = 0; i < path.getNameCount(); i++) {
|
||||
String part = path.getName(i).toString().toLowerCase();
|
||||
if (part.equals("archive") || part.equals(".venv")) {
|
||||
if (part.equals("archive") || part.equals("archived") || part.equals(".venv") || part.equals("venv")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private ModelInfo createModelInfo(Path file, Path rootPath) {
|
||||
private ModelInfo createModelInfo(Path file, Path rootPath, String forcedType) {
|
||||
String fileName = file.getFileName().toString();
|
||||
Path relativePath = rootPath.relativize(file);
|
||||
|
||||
// Derive type from the first folder after root
|
||||
String type = "unknown";
|
||||
if (relativePath.getNameCount() > 1) {
|
||||
String type = (forcedType != null) ? forcedType : "unknown";
|
||||
if (forcedType == null && relativePath.getNameCount() > 1) {
|
||||
type = relativePath.getName(0).toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Component
|
||||
@@ -17,6 +21,7 @@ public class PathResolver {
|
||||
public static final String CUSTOM_NODES_DIR = "custom_nodes";
|
||||
|
||||
private String comfyUIRoot;
|
||||
private final Map<String, List<Path>> extraModelPaths = new HashMap<>();
|
||||
|
||||
public void setComfyUIRoot(String root) {
|
||||
this.comfyUIRoot = root;
|
||||
@@ -26,6 +31,59 @@ public class PathResolver {
|
||||
return comfyUIRoot;
|
||||
}
|
||||
|
||||
public void addExtraModelPath(String type, Path path) {
|
||||
extraModelPaths.computeIfAbsent(type.toLowerCase(), k -> new ArrayList<>()).add(path);
|
||||
}
|
||||
|
||||
public void clearExtraModelPaths() {
|
||||
extraModelPaths.clear();
|
||||
}
|
||||
|
||||
public List<Path> getModelPaths(String type) {
|
||||
List<Path> paths = new ArrayList<>();
|
||||
if (type == null) return paths;
|
||||
|
||||
String inputType = type.replace("\\", "/");
|
||||
String normalizedType = inputType.toLowerCase();
|
||||
|
||||
// Handle common ComfyUI type aliases/groupings
|
||||
List<String> typesToSearch = new ArrayList<>();
|
||||
typesToSearch.add(normalizedType);
|
||||
|
||||
if (normalizedType.equals("unet") || normalizedType.equals("diffusion_models")) {
|
||||
typesToSearch.add("unet");
|
||||
typesToSearch.add("diffusion_models");
|
||||
} else if (normalizedType.equals("clip") || normalizedType.equals("text_encoders")) {
|
||||
typesToSearch.add("clip");
|
||||
typesToSearch.add("text_encoders");
|
||||
}
|
||||
|
||||
for (String t : typesToSearch) {
|
||||
List<Path> extras = extraModelPaths.get(t);
|
||||
if (extras != null) paths.addAll(extras);
|
||||
}
|
||||
|
||||
// NEW: Prefix matching for subfolders
|
||||
// If type is "checkpoints/SD15" and we have an extra path for "checkpoints"
|
||||
if (paths.isEmpty() && normalizedType.contains("/")) {
|
||||
for (Map.Entry<String, List<Path>> entry : extraModelPaths.entrySet()) {
|
||||
String extraType = entry.getKey();
|
||||
if (normalizedType.startsWith(extraType + "/")) {
|
||||
String subPath = inputType.substring(extraType.length() + 1);
|
||||
for (Path root : entry.getValue()) {
|
||||
paths.add(root.resolve(subPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
public Map<String, List<Path>> getAllExtraModelPaths() {
|
||||
return new HashMap<>(extraModelPaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a path. If it's absolute, returns it.
|
||||
* If relative, resolves against ComfyUI root.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.tki.comfymodels.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* A simplified panel that provides a link to open ComfyUI in the system browser.
|
||||
* Replaces the problematic WebView for better compatibility with modern ComfyUI frontends.
|
||||
*/
|
||||
public class ComfyWebPanel extends JPanel {
|
||||
|
||||
private String currentUrl;
|
||||
|
||||
public ComfyWebPanel(String initialUrl) {
|
||||
this.currentUrl = initialUrl;
|
||||
setLayout(new GridBagLayout());
|
||||
initComponents();
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
removeAll();
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
gbc.insets = new Insets(10, 10, 10, 10);
|
||||
|
||||
JLabel iconLabel = new JLabel("🎨");
|
||||
iconLabel.setFont(new Font("SansSerif", Font.PLAIN, 64));
|
||||
add(iconLabel, gbc);
|
||||
|
||||
gbc.gridy++;
|
||||
JLabel titleLabel = new JLabel("ComfyUI Interface is Ready");
|
||||
titleLabel.setFont(new Font("SansSerif", Font.BOLD, 20));
|
||||
add(titleLabel, gbc);
|
||||
|
||||
gbc.gridy++;
|
||||
JLabel infoLabel = new JLabel("The modern ComfyUI v1 interface works best in a full browser.");
|
||||
infoLabel.setFont(new Font("SansSerif", Font.PLAIN, 14));
|
||||
infoLabel.setForeground(Color.GRAY);
|
||||
add(infoLabel, gbc);
|
||||
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(30, 10, 10, 10);
|
||||
JButton openBtn = new JButton("🌍 Open ComfyUI in Browser");
|
||||
openBtn.setFont(new Font("SansSerif", Font.BOLD, 16));
|
||||
openBtn.setPreferredSize(new Dimension(300, 50));
|
||||
openBtn.addActionListener(e -> openInBrowser());
|
||||
add(openBtn, gbc);
|
||||
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(5, 10, 10, 10);
|
||||
JLabel urlLabel = new JLabel(currentUrl);
|
||||
urlLabel.setFont(new Font("Monospaced", Font.PLAIN, 12));
|
||||
urlLabel.setForeground(new Color(100, 150, 255));
|
||||
add(urlLabel, gbc);
|
||||
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
private void openInBrowser() {
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(currentUrl));
|
||||
} catch (Exception e) {
|
||||
JOptionPane.showMessageDialog(this, "Could not open browser: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the URL and refreshes the UI.
|
||||
*/
|
||||
public void loadUrl(String url) {
|
||||
this.currentUrl = url;
|
||||
initComponents();
|
||||
}
|
||||
|
||||
/**
|
||||
* For compatibility with previous calls, though no longer strictly needed for WebView.
|
||||
*/
|
||||
public void reload() {
|
||||
initComponents();
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,20 @@ public class PlatformUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void killProcessByName(String name) {
|
||||
try {
|
||||
if (isWindows()) {
|
||||
if (!name.endsWith(".exe")) name += ".exe";
|
||||
Runtime.getRuntime().exec("taskkill /F /IM " + name + " /T");
|
||||
} else {
|
||||
// Linux/Mac
|
||||
Runtime.getRuntime().exec("pkill -f " + name);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to kill process " + name + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isSystemTraySupported() {
|
||||
try {
|
||||
return java.awt.SystemTray.isSupported();
|
||||
|
||||
@@ -2,9 +2,48 @@
|
||||
import server
|
||||
import folder_paths
|
||||
import os
|
||||
import asyncio
|
||||
import importlib
|
||||
from aiohttp import web
|
||||
|
||||
print("\033[95m[Model-Downloader] UI Extension Bridge active\033[0m")
|
||||
print("\033[95m[CMFD] UI Extension Bridge active\033[0m")
|
||||
|
||||
@server.PromptServer.instance.routes.post("/cmfd/refresh-models")
|
||||
async def refresh_models(request):
|
||||
data = await request.json() if request.has_body else {}
|
||||
force_reload = data.get("force_reload", False)
|
||||
|
||||
print(f"\033[92m🔥 [CMFD] Refresh signal received (Force Reload: {force_reload})\033[0m")
|
||||
|
||||
# Wait for OS
|
||||
await asyncio.sleep(1)
|
||||
|
||||
try:
|
||||
# Invalidate
|
||||
if hasattr(folder_paths, "invalidate_all_cached_folders"):
|
||||
folder_paths.invalidate_all_cached_folders()
|
||||
|
||||
if hasattr(folder_paths, "filename_list_cache"):
|
||||
folder_paths.filename_list_cache.clear()
|
||||
|
||||
try:
|
||||
importlib.reload(folder_paths)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Object Info Reset
|
||||
server.PromptServer.instance.object_info = None
|
||||
|
||||
except Exception as e:
|
||||
print(f"[CMFD] Error: {e}")
|
||||
|
||||
# One-way broadcast to frontend
|
||||
server.PromptServer.instance.send_sync("cmfd-refresh-ui", {
|
||||
"status": "ok",
|
||||
"force_reload": force_reload
|
||||
})
|
||||
|
||||
return web.json_response({"status": "ok"})
|
||||
|
||||
WEB_DIRECTORY = "web"
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
|
||||
@@ -21,29 +21,66 @@ const initializeExtension = async () => {
|
||||
};
|
||||
await loadConfig();
|
||||
|
||||
// Event Listener for UI Refresh
|
||||
api.addEventListener("cmfd-refresh-ui", async (event) => {
|
||||
const data = event.detail || {};
|
||||
const forceReload = data.force_reload === true;
|
||||
|
||||
console.log(`%c🔥 [CMFD] Refresh Signal Received. Force Reload: ${forceReload}`, "background: #ff0000; color: #fff; font-size: 14px;");
|
||||
|
||||
if (forceReload) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Clear Frontend Caches
|
||||
api.nodeDefs = null;
|
||||
|
||||
// 2. Trigger standard ComfyUI refresh
|
||||
if (app.refresh) {
|
||||
await app.refresh();
|
||||
}
|
||||
|
||||
// 3. Update existing combo widgets
|
||||
const nodeDefs = await api.getNodeDefs();
|
||||
if (app.graph && app.graph._nodes) {
|
||||
for (const node of app.graph._nodes) {
|
||||
const def = nodeDefs[node.type];
|
||||
if (!def || !node.widgets) continue;
|
||||
|
||||
for (const widget of node.widgets) {
|
||||
if (widget.type === "combo") {
|
||||
const inputDef = def.input?.required?.[widget.name] || def.input?.optional?.[widget.name];
|
||||
if (inputDef && Array.isArray(inputDef[0])) {
|
||||
widget.options.values = inputDef[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
console.log("[CMFD] UI Sync Complete.");
|
||||
} catch (e) {
|
||||
console.error("[CMFD] UI Refresh Failed:", e);
|
||||
}
|
||||
});
|
||||
|
||||
const addUI = () => {
|
||||
if (document.getElementById("tki-downloader-fab")) return;
|
||||
const fab = document.createElement("div");
|
||||
fab.id = "tki-downloader-fab";
|
||||
fab.innerHTML = "🚀";
|
||||
fab.title = "An Model Downloader senden";
|
||||
fab.style = "position:fixed; bottom:30px; right:30px; z-index:10000; cursor:pointer; font-size:30px; background:#ffcc00; border-radius:50%; width:60px; height:60px; display:flex; align-items:center; justify-content:center; box-shadow:0 0 20px rgba(0,0,0,0.5); border: 2px solid white; transition: transform 0.2s;";
|
||||
fab.onmouseover = () => fab.style.transform = "scale(1.1)";
|
||||
fab.onmouseout = () => fab.style.transform = "scale(1.0)";
|
||||
fab.onclick = async () => {
|
||||
if (!app?.graph) return;
|
||||
try {
|
||||
const response = await fetch("http://127.0.0.1:12345/import", {
|
||||
await fetch("http://127.0.0.1:12345/import", {
|
||||
method: "POST", mode: "cors",
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiToken}` },
|
||||
body: JSON.stringify(app.graph.serialize())
|
||||
});
|
||||
if (response.ok) {
|
||||
if (app.ui?.dialog?.show) app.ui.dialog.show("🚀 Workflow an Downloader gesendet!");
|
||||
}
|
||||
} catch (e) {
|
||||
alert("❌ Downloader App nicht erreichbar!");
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
document.body.appendChild(fab);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.tki.comfymodels;
|
||||
import de.tki.comfymodels.domain.ModelInfo;
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.LocalModelScanner;
|
||||
import de.tki.comfymodels.service.impl.PathResolver;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
@@ -26,13 +27,16 @@ class SubfolderRecognitionTest {
|
||||
|
||||
private ConfigService configService;
|
||||
|
||||
private PathResolver pathResolver;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
configService = new ConfigService(null, null) {
|
||||
pathResolver = new PathResolver();
|
||||
configService = new ConfigService(null, pathResolver) {
|
||||
@Override public String getModelsPath() { return tempModels.toString(); }
|
||||
@Override public String getArchivePath() { return "archive_mock"; }
|
||||
};
|
||||
localScanner = new LocalModelScanner(configService);
|
||||
localScanner = new LocalModelScanner(configService, pathResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -44,7 +48,7 @@ class SubfolderRecognitionTest {
|
||||
Files.createFile(modelFile);
|
||||
|
||||
// Test the generic recursive search method
|
||||
Optional<Path> found = localScanner.findModelInDirectory(tempModels, "wan2.2_test.safetensors");
|
||||
Optional<Path> found = localScanner.findModelWithPrefSize(tempModels, "wan2.2_test.safetensors", -1);
|
||||
assertTrue(found.isPresent(), "Model should be found in deep subfolder");
|
||||
assertEquals(modelFile.toAbsolutePath(), found.get().toAbsolutePath(), "Found path should match actual file path");
|
||||
|
||||
@@ -85,18 +89,18 @@ class SubfolderRecognitionTest {
|
||||
Files.write(archivedFile, new byte[1234]);
|
||||
|
||||
// Mock config to have separate models and archive paths
|
||||
ConfigService dualConfig = new ConfigService(null, null) {
|
||||
ConfigService dualConfig = new ConfigService(null, pathResolver) {
|
||||
@Override public String getModelsPath() { return tempModels.toString(); }
|
||||
@Override public String getArchivePath() { return archiveDir.toString(); }
|
||||
};
|
||||
LocalModelScanner dualScanner = new LocalModelScanner(dualConfig);
|
||||
LocalModelScanner dualScanner = new LocalModelScanner(dualConfig, pathResolver);
|
||||
|
||||
// Scan in models dir - should NOT find it
|
||||
Optional<Path> foundLocal = dualScanner.findModelInDirectory(tempModels, "exclusive_model.safetensors");
|
||||
Optional<Path> foundLocal = dualScanner.findModelWithPrefSize(tempModels, "exclusive_model.safetensors", -1);
|
||||
assertFalse(foundLocal.isPresent(), "Should not find archived model in local models scan");
|
||||
|
||||
// Scan in archive dir - SHOULD find it
|
||||
Optional<Path> foundArchive = dualScanner.findModelInDirectory(archiveDir, "exclusive_model.safetensors");
|
||||
Optional<Path> foundArchive = dualScanner.findModelWithPrefSize(archiveDir, "exclusive_model.safetensors", -1);
|
||||
assertTrue(foundArchive.isPresent(), "Should find model in archive scan");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.ComfyLifecycleService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Integration test to verify the full ComfyUI server lifecycle:
|
||||
* Auto-discovery -> Start -> Reachability Check -> Stop -> Reachability Check.
|
||||
*/
|
||||
public class ComfyLifecycleIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void testFullServerLifecycle() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
|
||||
ConfigService config = context.getBean(ConfigService.class);
|
||||
IComfyLifecycleService lifecycle = context.getBean(IComfyLifecycleService.class);
|
||||
|
||||
System.out.println("\n--- [START] Lifecycle Integration Test ---");
|
||||
|
||||
// 1. Discovery Phase
|
||||
System.out.println("Step 1: Running path discovery...");
|
||||
// Simulate real environment conditions
|
||||
config.autoDiscoverPaths();
|
||||
|
||||
String root = config.getComfyUIPath();
|
||||
String command = config.getComfyLaunchCommand();
|
||||
String models = config.getModelsPath();
|
||||
|
||||
System.out.println("Detected Root: " + root);
|
||||
System.out.println("Detected Command: " + command);
|
||||
System.out.println("Detected Models: " + models);
|
||||
|
||||
assertNotNull(root, "ComfyUI Root should not be null");
|
||||
assertFalse(root.isEmpty(), "ComfyUI Root should not be empty");
|
||||
assertTrue(new File(root, "main.py").exists(), "main.py MUST exist in discovered root: " + root);
|
||||
assertNotNull(command, "Launch command should not be null");
|
||||
assertFalse(command.isEmpty(), "Launch command should not be empty");
|
||||
assertNotNull(models, "Models path should not be null");
|
||||
assertTrue(new File(models).exists(), "Models path MUST exist: " + models);
|
||||
|
||||
// 2. Startup Phase
|
||||
System.out.println("Step 2: Starting server...");
|
||||
lifecycle.start();
|
||||
|
||||
// Wait for server to become healthy (API response)
|
||||
boolean started = false;
|
||||
for (int i = 0; i < 30; i++) {
|
||||
System.out.println("Waiting for health (Attempt " + (i + 1) + "/30)... Status: " + lifecycle.getStatus());
|
||||
if (lifecycle.isHealthy()) {
|
||||
started = true;
|
||||
break;
|
||||
}
|
||||
Thread.sleep(2000); // 2s steps for ComfyUI
|
||||
}
|
||||
|
||||
assertTrue(started, "Server should be healthy after start. Current Status: " + lifecycle.getStatus());
|
||||
assertTrue(lifecycle.isRunning(), "Server should be running according to Lifecycle Service");
|
||||
|
||||
// 3. Stop Phase
|
||||
System.out.println("Step 3: Stopping server...");
|
||||
lifecycle.stop();
|
||||
|
||||
// Wait a bit for port release
|
||||
Thread.sleep(5000);
|
||||
|
||||
assertFalse(lifecycle.isHealthy(), "Server should NO LONGER be healthy after stop");
|
||||
assertFalse(lifecycle.isRunning(), "Server should NO LONGER be running after stop");
|
||||
|
||||
System.out.println("--- [SUCCESS] Lifecycle Integration Test PASSED ---\n");
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ComponentScan("de.tki.comfymodels")
|
||||
public static class TestConfig {
|
||||
// Standard scan picks up all components
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public class ComprehensiveStatusFlowTest {
|
||||
configService.setModelsPath(modelsDir.toString());
|
||||
configService.setArchivePath(archiveDir.toString());
|
||||
|
||||
localScanner = new LocalModelScanner(configService);
|
||||
localScanner = new LocalModelScanner(configService, pathResolver);
|
||||
archiveService = new ArchiveService(configService, pathResolver);
|
||||
analyzer = new ComfyModelAnalyzer();
|
||||
|
||||
@@ -76,23 +76,23 @@ public class ComprehensiveStatusFlowTest {
|
||||
Files.write(modelPath, new byte[2000]); // Update to 2KB
|
||||
assertEquals("✅ Already exists", getStatus(results.get(0), true, false, 2000));
|
||||
|
||||
// 5. State: Found in Archive (Removed locally, moved to archive)
|
||||
// 5. State: Archived (Removed locally, moved to archive)
|
||||
Files.delete(modelPath);
|
||||
Path archivePath = archiveDir.resolve("checkpoints").resolve(modelName);
|
||||
Files.createDirectories(archivePath.getParent());
|
||||
Files.write(archivePath, new byte[2000]);
|
||||
|
||||
assertEquals("📦 Found in Archive", getStatus(results.get(0), false, true, 0));
|
||||
|
||||
// 6. State: Already exists (Archived copy found) - Exists in both
|
||||
assertEquals("📦 Archived", getStatus(results.get(0), false, true, 0));
|
||||
|
||||
// 6. State: Already exists (Size match, even if archived)
|
||||
Files.write(modelPath, new byte[2000]);
|
||||
assertEquals("✅ Already exists (Archived copy found)", getStatus(results.get(0), true, true, 2000));
|
||||
}
|
||||
assertEquals("✅ Already exists", getStatus(results.get(0), true, true, 2000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to simulate the status logic from Main.java analyzeJsonContent()
|
||||
*/
|
||||
private String getStatus(ModelInfo info, boolean existsLocally, boolean existsInArchive, long localSize) {
|
||||
/**
|
||||
* Helper to simulate the status logic from Main.java analyzeJsonContent()
|
||||
*/
|
||||
private String getStatus(ModelInfo info, boolean existsLocally, boolean existsInArchive, long localSize) {
|
||||
boolean sizeMismatch = false;
|
||||
if (existsLocally && info.getByteSize() > 0) {
|
||||
if (localSize != info.getByteSize()) {
|
||||
@@ -101,13 +101,13 @@ public class ComprehensiveStatusFlowTest {
|
||||
}
|
||||
}
|
||||
|
||||
if (existsLocally && existsInArchive) return "✅ Already exists (Archived copy found)";
|
||||
if (existsLocally) return "✅ Already exists";
|
||||
if (existsInArchive) return "📦 Found in Archive";
|
||||
if (existsInArchive) return "📦 Archived";
|
||||
if (sizeMismatch) return "🔄 Size Mismatch";
|
||||
|
||||
|
||||
if (info.getUrl().equals("MISSING")) return "Idle";
|
||||
|
||||
|
||||
return "✅ Known Good";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONArray;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DeepModelDiagnosticTest {
|
||||
|
||||
@Test
|
||||
public void inspectNodeOptions() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ConfigService configService = context.getBean(ConfigService.class);
|
||||
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
|
||||
String targetModel = "ltx2.3-transition.safetensors";
|
||||
String url = configService.getComfyUIUrl() + "/object_info";
|
||||
|
||||
System.out.println("--- DEEP API INSPECTION ---");
|
||||
System.out.println("Target: " + targetModel);
|
||||
System.out.println("Querying: " + url);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
System.out.println("❌ API Error: " + response.statusCode());
|
||||
return;
|
||||
}
|
||||
|
||||
JSONObject allNodes = new JSONObject(response.body());
|
||||
String[] nodesToInspect = {"LoraLoader", "CheckpointLoaderSimple", "CheckpointLoader", "LoraLoaderModelOnly"};
|
||||
|
||||
boolean foundInAny = false;
|
||||
for (String nodeName : nodesToInspect) {
|
||||
if (allNodes.has(nodeName)) {
|
||||
System.out.println("\nChecking Node: " + nodeName);
|
||||
JSONObject node = allNodes.getJSONObject(nodeName);
|
||||
if (node.has("input")) {
|
||||
JSONObject input = node.getJSONObject("input");
|
||||
JSONObject required = input.optJSONObject("required");
|
||||
if (required != null) {
|
||||
for (String key : required.keySet()) {
|
||||
JSONArray param = required.optJSONArray(key);
|
||||
if (param != null && param.length() > 0 && param.get(0) instanceof JSONArray) {
|
||||
JSONArray options = param.getJSONArray(0);
|
||||
System.out.println(" Parameter '" + key + "' has " + options.length() + " options.");
|
||||
|
||||
List<String> matches = new ArrayList<>();
|
||||
for (int i = 0; i < options.length(); i++) {
|
||||
String opt = options.getString(i);
|
||||
if (opt.toLowerCase().contains("ltx") || opt.equalsIgnoreCase(targetModel)) {
|
||||
matches.add(opt);
|
||||
}
|
||||
}
|
||||
|
||||
if (!matches.isEmpty()) {
|
||||
System.out.println(" 🎯 POTENTIAL MATCHES FOUND:");
|
||||
for (String m : matches) {
|
||||
System.out.println(" - " + m);
|
||||
if (m.equals(targetModel)) foundInAny = true;
|
||||
}
|
||||
} else {
|
||||
System.out.println(" (No LTX-related models in this list)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
System.out.println("\nNode not found: " + nodeName);
|
||||
}
|
||||
}
|
||||
|
||||
if (foundInAny) {
|
||||
System.out.println("\n✅ VERIFIED: The model is technically present in the API options.");
|
||||
System.out.println("If it is missing in the GUI, it might be a caching issue in the browser or a custom node conflict.");
|
||||
} else {
|
||||
System.out.println("\n❌ FAILED: The model is NOT present in any examined node parameters.");
|
||||
}
|
||||
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.domain.ModelInfo;
|
||||
import de.tki.comfymodels.service.impl.ArchiveService;
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.LocalModelScanner;
|
||||
import de.tki.comfymodels.service.impl.PathResolver;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ExtraModelPathsIntegrationTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private LocalModelScanner scanner;
|
||||
private ConfigService configService;
|
||||
private PathResolver pathResolver;
|
||||
private ArchiveService archiveService;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
pathResolver = new PathResolver();
|
||||
// Custom ConfigService to use our tempDir for everything and avoid complex setup
|
||||
configService = new ConfigService(null, pathResolver) {
|
||||
private String comfyPath = "";
|
||||
private String modelsPath = "";
|
||||
private String archivePath = "";
|
||||
|
||||
@Override public String getComfyUIPath() { return comfyPath; }
|
||||
@Override public void setComfyUIPath(String path) { this.comfyPath = path; pathResolver.setComfyUIRoot(path); }
|
||||
@Override public String getModelsPath() { return modelsPath; }
|
||||
@Override public void setModelsPath(String path) { this.modelsPath = path; }
|
||||
@Override public String getArchivePath() { return archivePath; }
|
||||
@Override public void setArchivePath(String path) { this.archivePath = path; }
|
||||
@Override public String getAppDataPath() { return tempDir.toString(); }
|
||||
@Override public void save() { /* avoid encryption issues in test */ }
|
||||
@Override public boolean isUnlocked() { return true; }
|
||||
};
|
||||
scanner = new LocalModelScanner(configService, pathResolver);
|
||||
archiveService = new ArchiveService(configService, pathResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtraModelPathsDiscovery() throws IOException {
|
||||
// 1. Setup ComfyUI Root and extra_model_paths.yaml
|
||||
Path comfyRoot = tempDir.resolve("ComfyUI");
|
||||
Files.createDirectories(comfyRoot);
|
||||
configService.setComfyUIPath(comfyRoot.toString());
|
||||
|
||||
Path extraModelsDir = tempDir.resolve("ExternalModels");
|
||||
Path checkpointsDir = extraModelsDir.resolve("my_checkpoints");
|
||||
Files.createDirectories(checkpointsDir);
|
||||
|
||||
// Create a model file in the extra path
|
||||
Path modelFile = checkpointsDir.resolve("extra_model.safetensors");
|
||||
Files.writeString(modelFile, "dummy");
|
||||
|
||||
// Create the YAML
|
||||
String yamlContent =
|
||||
"my_extra_path:\n" +
|
||||
" base_path: " + extraModelsDir.toAbsolutePath().toString().replace("\\", "/") + "\n" +
|
||||
" checkpoints: my_checkpoints\n";
|
||||
|
||||
Files.writeString(comfyRoot.resolve("extra_model_paths.yaml"), yamlContent);
|
||||
|
||||
// 2. Load the extra paths
|
||||
configService.loadExtraModelPaths();
|
||||
|
||||
// 3. Verify PathResolver knows about it
|
||||
List<Path> paths = pathResolver.getModelPaths("checkpoints");
|
||||
assertFalse(paths.isEmpty(), "Should have found extra checkpoints path");
|
||||
assertTrue(paths.stream().anyMatch(p -> p.toAbsolutePath().equals(checkpointsDir.toAbsolutePath())), "Should contain the correct checkpoints dir");
|
||||
|
||||
// 4. Run Scanner and verify discovery
|
||||
List<ModelInfo> results = scanner.scanLocalModels();
|
||||
|
||||
boolean found = results.stream().anyMatch(m -> m.getName().equals("extra_model.safetensors"));
|
||||
assertTrue(found, "Scanner should find model in extra path");
|
||||
|
||||
ModelInfo info = results.stream().filter(m -> m.getName().equals("extra_model.safetensors")).findFirst().get();
|
||||
assertEquals("checkpoints", info.getType(), "Should correctly identify as checkpoint");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArchiveRestorationToExtraPath() throws IOException {
|
||||
// 1. Setup ComfyUI Root and extra_model_paths.yaml
|
||||
Path comfyRoot = tempDir.resolve("ComfyUI");
|
||||
Files.createDirectories(comfyRoot);
|
||||
configService.setComfyUIPath(comfyRoot.toString());
|
||||
|
||||
Path extraModelsDir = tempDir.resolve("ExternalModels");
|
||||
Path checkpointsDir = extraModelsDir.resolve("my_checkpoints");
|
||||
Files.createDirectories(checkpointsDir);
|
||||
|
||||
// Create the YAML mapping checkpoints to ExternalModels/my_checkpoints
|
||||
String yamlContent =
|
||||
"my_extra_path:\n" +
|
||||
" base_path: " + extraModelsDir.toAbsolutePath().toString().replace("\\", "/") + "\n" +
|
||||
" checkpoints: my_checkpoints\n";
|
||||
Files.writeString(comfyRoot.resolve("extra_model_paths.yaml"), yamlContent);
|
||||
configService.loadExtraModelPaths();
|
||||
|
||||
// 2. Setup Archive
|
||||
Path archiveRoot = tempDir.resolve("Archive");
|
||||
Files.createDirectories(archiveRoot);
|
||||
configService.setArchivePath(archiveRoot.toString());
|
||||
|
||||
// Put a model in the archive structure
|
||||
Path archivedModel = archiveRoot.resolve("checkpoints").resolve("archived_extra.safetensors");
|
||||
Files.createDirectories(archivedModel.getParent());
|
||||
Files.writeString(archivedModel, "dummy model data");
|
||||
|
||||
// 3. Run restoration for "checkpoints" folder
|
||||
boolean success = archiveService.restoreFromArchiveWithProgress("checkpoints", "archived_extra.safetensors", null);
|
||||
|
||||
assertTrue(success, "Restoration should be successful");
|
||||
|
||||
// 4. VERIFY: Should be in checkpointsDir (extra path)
|
||||
Path expectedTarget = checkpointsDir.resolve("archived_extra.safetensors");
|
||||
assertTrue(Files.exists(expectedTarget), "Model should be restored to the extra checkpoints path: " + expectedTarget);
|
||||
assertFalse(Files.exists(archivedModel), "Source should be deleted from archive after successful move");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArchiveRestorationToNestedExtraPath() throws IOException {
|
||||
// Setup same as above but with nested folder
|
||||
Path comfyRoot = tempDir.resolve("ComfyUI");
|
||||
Files.createDirectories(comfyRoot);
|
||||
configService.setComfyUIPath(comfyRoot.toString());
|
||||
|
||||
Path extraModelsDir = tempDir.resolve("ExternalModels");
|
||||
Path checkpointsDir = extraModelsDir.resolve("checkpoints");
|
||||
Path sd15Dir = checkpointsDir.resolve("SD15");
|
||||
Files.createDirectories(sd15Dir);
|
||||
|
||||
String yamlContent =
|
||||
"my_extra_path:\n" +
|
||||
" base_path: " + extraModelsDir.toAbsolutePath().toString().replace("\\", "/") + "\n" +
|
||||
" checkpoints: checkpoints\n";
|
||||
Files.writeString(comfyRoot.resolve("extra_model_paths.yaml"), yamlContent);
|
||||
configService.loadExtraModelPaths();
|
||||
|
||||
// Setup Archive
|
||||
Path archiveRoot = tempDir.resolve("Archive");
|
||||
Files.createDirectories(archiveRoot);
|
||||
configService.setArchivePath(archiveRoot.toString());
|
||||
|
||||
// Model in archive: checkpoints/SD15/nested.safetensors
|
||||
Path archivedModel = archiveRoot.resolve("checkpoints/SD15/nested.safetensors");
|
||||
Files.createDirectories(archivedModel.getParent());
|
||||
Files.writeString(archivedModel, "nested data");
|
||||
|
||||
// Run restoration for "checkpoints/SD15"
|
||||
boolean success = archiveService.restoreFromArchiveWithProgress("checkpoints/SD15", "nested.safetensors", null);
|
||||
|
||||
assertTrue(success, "Nested restoration should be successful");
|
||||
|
||||
// VERIFY: Should be in sd15Dir (extra path + subfolder)
|
||||
Path expectedTarget = sd15Dir.resolve("nested.safetensors");
|
||||
assertTrue(Files.exists(expectedTarget), "Model should be restored to the nested extra path: " + expectedTarget);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.ComfyLifecycleService;
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
|
||||
public class FinalDampfhammerTest {
|
||||
|
||||
@Test
|
||||
public void executeRestartAndVerify() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ComfyLifecycleService lifecycleService = context.getBean(ComfyLifecycleService.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
|
||||
System.out.println("--- 🔨 EXECUTING FINAL DAMPFHAMMER RESTART ---");
|
||||
|
||||
// 1. Force Kill
|
||||
System.out.println("⏹ Terminating existing Python processes...");
|
||||
Runtime.getRuntime().exec("taskkill /F /IM python.exe /T").waitFor();
|
||||
Thread.sleep(2000);
|
||||
|
||||
// 2. Start via Service
|
||||
System.out.println("▶ Starting ComfyUI...");
|
||||
lifecycleService.start();
|
||||
|
||||
// 3. Robust Health Check (Wait up to 90 seconds)
|
||||
System.out.println("⏳ Waiting for API (http://127.0.0.1:8000)...");
|
||||
boolean ready = false;
|
||||
for (int i = 0; i < 90; i++) {
|
||||
if (lifecycleService.isHealthy()) {
|
||||
ready = true;
|
||||
break;
|
||||
}
|
||||
if (i % 10 == 0) System.out.println("Still waiting... (" + i + "s)");
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
if (ready) {
|
||||
System.out.println("✅ ComfyUI is ONLINE and HEALTHY.");
|
||||
|
||||
// 4. Final verification of the specific model
|
||||
String model = "ltx2.3-transition.safetensors";
|
||||
if (diagnosticService.isModelAvailableInComfy(model, "loras")) {
|
||||
System.out.println("🎯 TARGET ACHIEVED: '" + model + "' is recognized by ComfyUI!");
|
||||
} else {
|
||||
System.out.println("❌ Model still missing. This implies the file is not in 'models/loras' or extra_model_paths is misconfigured.");
|
||||
}
|
||||
} else {
|
||||
System.out.println("❌ ComfyUI failed to respond. Please check your console/logs.");
|
||||
}
|
||||
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
@@ -25,14 +25,15 @@ public class LocalModelScannerTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
PathResolver pathResolver = new PathResolver();
|
||||
// Use a simple stub for ConfigService to avoid Mockito issues on Java 25
|
||||
configService = new ConfigService(null, new PathResolver()) {
|
||||
configService = new ConfigService(null, pathResolver) {
|
||||
@Override
|
||||
public String getModelsPath() {
|
||||
return tempDir.toString();
|
||||
}
|
||||
};
|
||||
scanner = new LocalModelScanner(configService);
|
||||
scanner = new LocalModelScanner(configService, pathResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
|
||||
public class ModelPresenceVerificationTest {
|
||||
|
||||
@Test
|
||||
public void verifyModelPresence() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();
|
||||
|
||||
String modelName = "ltx2.3-transition.safetensors";
|
||||
String type = "loras";
|
||||
|
||||
System.out.println("--- Starting Automated Refresh Loop for: " + modelName + " ---");
|
||||
|
||||
boolean found = false;
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
System.out.println("Attempt " + i + ": Triggering Refresh...");
|
||||
|
||||
// Trigger Refresh
|
||||
try {
|
||||
HttpRequest refreshReq = HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:8000/cmfd/refresh-models"))
|
||||
.POST(HttpRequest.BodyPublishers.noBody())
|
||||
.build();
|
||||
client.send(refreshReq, HttpResponse.BodyHandlers.ofString());
|
||||
} catch (Exception e) {
|
||||
System.out.println("Refresh ping failed (Bridge maybe not installed?): " + e.getMessage());
|
||||
}
|
||||
|
||||
// Wait for processing
|
||||
Thread.sleep(3000);
|
||||
|
||||
// Check Presence
|
||||
if (diagnosticService.isModelAvailableInComfy(modelName, type)) {
|
||||
System.out.println("✅ SUCCESS: Model '" + modelName + "' is now visible in ComfyUI!");
|
||||
found = true;
|
||||
break;
|
||||
} else {
|
||||
System.out.println("❌ Model not found yet.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
System.out.println("⚠️ FAILURE: Model still not visible after 5 attempts.");
|
||||
}
|
||||
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.Main;
|
||||
import de.tki.comfymodels.domain.ModelInfo;
|
||||
import de.tki.comfymodels.service.impl.*;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModelStatusLogicIntegrationTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private Main main;
|
||||
private ConfigService configService;
|
||||
private PathResolver pathResolver;
|
||||
private ArchiveService archiveService;
|
||||
private LocalModelScanner localScanner;
|
||||
private ComfyModelAnalyzer analyzer;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
pathResolver = new PathResolver();
|
||||
configService = new ConfigService(null, pathResolver) {
|
||||
private String modelsPath = "";
|
||||
private String archivePath = "";
|
||||
@Override public String getModelsPath() { return modelsPath; }
|
||||
@Override public void setModelsPath(String path) { this.modelsPath = path; }
|
||||
@Override public String getArchivePath() { return archivePath; }
|
||||
@Override public void setArchivePath(String path) { this.archivePath = path; }
|
||||
@Override public String getAppDataPath() { return tempDir.toString(); }
|
||||
@Override public boolean isUnlocked() { return true; }
|
||||
@Override public void save() {}
|
||||
};
|
||||
|
||||
analyzer = Mockito.mock(ComfyModelAnalyzer.class);
|
||||
localScanner = new LocalModelScanner(configService, pathResolver);
|
||||
archiveService = new ArchiveService(configService, pathResolver);
|
||||
|
||||
main = new Main(
|
||||
analyzer,
|
||||
Mockito.mock(DefaultDownloadManager.class),
|
||||
Mockito.mock(WorkflowService.class),
|
||||
Mockito.mock(ModelSearchService.class),
|
||||
Mockito.mock(ModelValidator.class),
|
||||
Mockito.mock(RestBridgeService.class),
|
||||
archiveService,
|
||||
Mockito.mock(ComfyLifecycleService.class),
|
||||
Mockito.mock(ComfyDiagnosticService.class)
|
||||
);
|
||||
|
||||
// Inject dependencies via reflection since they are @Autowired in Main
|
||||
setField(main, "configService", configService);
|
||||
setField(main, "localScanner", localScanner);
|
||||
setField(main, "pathResolver", pathResolver);
|
||||
setField(main, "archiveService", archiveService);
|
||||
|
||||
// Initialize enough UI for analyzeJsonContent to work
|
||||
setField(main, "tableModel", new DefaultTableModel(new String[]{"S", "T", "N", "Sz", "Src", "P", "U", "Status"}, 0));
|
||||
JTextArea area = new JTextArea("dummy workflow");
|
||||
setField(main, "jsonInputArea", area);
|
||||
setField(main, "statusLabel", new JLabel());
|
||||
setField(main, "downloadButton", new JButton());
|
||||
}
|
||||
|
||||
private void setField(Object target, String name, Object value) throws Exception {
|
||||
Field f = target.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
f.set(target, value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStatusTransitions() throws IOException, Exception {
|
||||
Path modelsDir = tempDir.resolve("models");
|
||||
Files.createDirectories(modelsDir.resolve("checkpoints"));
|
||||
configService.setModelsPath(modelsDir.toString());
|
||||
|
||||
Path archiveDir = tempDir.resolve("archive");
|
||||
Files.createDirectories(archiveDir.resolve("checkpoints"));
|
||||
configService.setArchivePath(archiveDir.toString());
|
||||
|
||||
// Prepare 3 scenarios
|
||||
List<ModelInfo> models = new ArrayList<>();
|
||||
models.add(new ModelInfo("checkpoints", "local_only.safetensors", "COMMUNITY"));
|
||||
models.get(0).setUrl("http://example.com/1");
|
||||
|
||||
models.add(new ModelInfo("checkpoints", "archived_only.safetensors", "COMMUNITY"));
|
||||
models.get(1).setUrl("http://example.com/2");
|
||||
|
||||
models.add(new ModelInfo("checkpoints", "both_locations.safetensors", "COMMUNITY"));
|
||||
models.get(2).setUrl("http://example.com/3");
|
||||
|
||||
models.add(new ModelInfo("checkpoints", "new_model.safetensors", "COMMUNITY"));
|
||||
models.get(3).setUrl("http://example.com/4");
|
||||
|
||||
when(analyzer.analyze(anyString(), anyString())).thenReturn(models);
|
||||
|
||||
// Create the files
|
||||
Files.writeString(modelsDir.resolve("checkpoints/local_only.safetensors"), "data");
|
||||
Files.writeString(archiveDir.resolve("checkpoints/archived_only.safetensors"), "data");
|
||||
Files.writeString(modelsDir.resolve("checkpoints/both_locations.safetensors"), "data");
|
||||
Files.writeString(archiveDir.resolve("checkpoints/both_locations.safetensors"), "data");
|
||||
|
||||
// Execute analysis
|
||||
java.lang.reflect.Method method = main.getClass().getDeclaredMethod("analyzeJsonContent");
|
||||
method.setAccessible(true);
|
||||
method.invoke(main);
|
||||
|
||||
DefaultTableModel model = (DefaultTableModel) getField(main, "tableModel");
|
||||
|
||||
// Verify Statuses
|
||||
assertEquals("✅ Already exists", model.getValueAt(0, 7), "Should be local only");
|
||||
assertEquals("📦 Archived", model.getValueAt(1, 7), "Should be in archive only");
|
||||
assertEquals("✅ Already exists", model.getValueAt(2, 7), "Should prioritize local over archive (obsolete status removed)");
|
||||
assertEquals("✅ Known Good", model.getValueAt(3, 7), "Should be new/downloadable");
|
||||
}
|
||||
|
||||
private Object getField(Object target, String name) throws Exception {
|
||||
Field f = target.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class NuclearFrontendRefreshTest {
|
||||
|
||||
@Test
|
||||
public void triggerReload() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ConfigService configService = context.getBean(ConfigService.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
HttpClient client = HttpClient.newBuilder().build();
|
||||
|
||||
System.out.println("--- ☢️ TRIGGERING NUCLEAR FRONTEND REFRESH ---");
|
||||
|
||||
// 1. Sync Bridge to Disk (Ensure latest JS/Python is there)
|
||||
String comfyPath = "C:\\AI\\ComfyUI\\resources\\ComfyUI";
|
||||
Path bridgeDir = Paths.get(comfyPath, "custom_nodes", "comfyui-model-downloader");
|
||||
System.out.println("📦 Syncing Bridge to: " + bridgeDir);
|
||||
Files.createDirectories(bridgeDir.resolve("web"));
|
||||
|
||||
copyResource("/comfyui-bridge/__init__.py", bridgeDir.resolve("__init__.py"));
|
||||
copyResource("/comfyui-bridge/web/downloader.js", bridgeDir.resolve("web/downloader.js"));
|
||||
Files.writeString(bridgeDir.resolve("web/config.json"), "{\"token\": \"" + configService.getApiToken() + "\"}");
|
||||
|
||||
// 2. Send the "Nuclear" REST Call
|
||||
System.out.println("🚀 Sending REST signal with 'force_reload: true'...");
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:8000/cmfd/refresh-models"))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString("{\"force_reload\": true}"))
|
||||
.build();
|
||||
|
||||
try {
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
System.out.println("📡 Server Response: " + response.statusCode() + " " + response.body());
|
||||
} catch (Exception e) {
|
||||
System.out.println("❌ Failed to reach ComfyUI: " + e.getMessage());
|
||||
}
|
||||
|
||||
// 3. Verify Backend
|
||||
String model = "ltx2.3-transition.safetensors";
|
||||
if (diagnosticService.isModelAvailableInComfy(model, "loras")) {
|
||||
System.out.println("✅ BACKEND VERIFIED: '" + model + "' is present in the API.");
|
||||
} else {
|
||||
System.out.println("❌ BACKEND FAILED: Model still not in API.");
|
||||
}
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
private void copyResource(String name, Path target) throws Exception {
|
||||
try (InputStream is = getClass().getResourceAsStream(name)) {
|
||||
Files.copy(is, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.ComfyLifecycleService;
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import de.tki.comfymodels.Main;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
public class OneClickFixTest {
|
||||
|
||||
@Test
|
||||
public void performNuclearFix() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ConfigService configService = context.getBean(ConfigService.class);
|
||||
ComfyLifecycleService lifecycleService = context.getBean(ComfyLifecycleService.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
|
||||
System.out.println("--- ☢️ NUCLEAR ONE-CLICK FIX STARTING ---");
|
||||
|
||||
// 1. Reinstall Bridge to the correct location
|
||||
String comfyPath = "C:\\AI\\ComfyUI\\resources\\ComfyUI";
|
||||
Path bridgeDir = Paths.get(comfyPath, "custom_nodes", "comfyui-model-downloader");
|
||||
System.out.println("📦 Reinstalling Bridge to: " + bridgeDir);
|
||||
Files.createDirectories(bridgeDir);
|
||||
Files.createDirectories(bridgeDir.resolve("web"));
|
||||
|
||||
copyResource("/comfyui-bridge/__init__.py", bridgeDir.resolve("__init__.py"));
|
||||
copyResource("/comfyui-bridge/web/downloader.js", bridgeDir.resolve("web/downloader.js"));
|
||||
|
||||
String configJson = "{\"token\": \"" + configService.getApiToken() + "\"}";
|
||||
Files.writeString(bridgeDir.resolve("web/config.json"), configJson);
|
||||
System.out.println("✅ Bridge reinstalled.");
|
||||
|
||||
// 2. Kill existing process
|
||||
System.out.println("⏹ Stopping ComfyUI...");
|
||||
lifecycleService.stop();
|
||||
// Fallback kill for PID 14544 just in case
|
||||
Runtime.getRuntime().exec("taskkill /F /PID 14544").waitFor();
|
||||
System.out.println("✅ ComfyUI stopped.");
|
||||
|
||||
// 3. Start via Lifecycle Service
|
||||
System.out.println("▶ Starting ComfyUI...");
|
||||
lifecycleService.start();
|
||||
|
||||
// 4. Wait for Health
|
||||
System.out.println("⏳ Waiting for API to become healthy...");
|
||||
boolean healthy = false;
|
||||
for (int i = 0; i < 60; i++) {
|
||||
if (lifecycleService.isHealthy()) {
|
||||
healthy = true;
|
||||
break;
|
||||
}
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
if (healthy) {
|
||||
System.out.println("✅ ComfyUI is back online!");
|
||||
|
||||
// 5. Final Diagnostic
|
||||
String model = "ltx2.3-transition.safetensors";
|
||||
if (diagnosticService.isModelAvailableInComfy(model, "loras")) {
|
||||
System.out.println("🎯 TARGET VERIFIED: '" + model + "' is now fully recognized!");
|
||||
} else {
|
||||
System.out.println("❌ Diagnostic failed even after restart. Please check if the file is in 'models/loras'.");
|
||||
}
|
||||
} else {
|
||||
System.out.println("❌ ComfyUI failed to start within 60 seconds.");
|
||||
}
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
private void copyResource(String name, Path target) throws Exception {
|
||||
try (InputStream is = getClass().getResourceAsStream(name)) {
|
||||
Files.copy(is, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Rapid Diagnostic Test for Refresh Mechanisms.
|
||||
* This test uses the ComfyDiagnosticService to verify if ComfyUI actually sees
|
||||
* the models after our refresh calls.
|
||||
*/
|
||||
public class RefreshDiagnosticTest {
|
||||
|
||||
@Test
|
||||
public void testModelVisibility() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
|
||||
System.out.println("--- ComfyUI Diagnostic Scan ---");
|
||||
boolean reachable = diagnosticService.isModelAvailableInComfy("non_existent_model_xyz", "checkpoint");
|
||||
|
||||
// If the API is up, it should at least return false for a non-existent model
|
||||
// If it throws an exception or timeout, the API is down.
|
||||
System.out.println("API Reachable: " + !reachable);
|
||||
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package de.tki.comfymodels.service;
|
||||
import de.tki.comfymodels.domain.ModelInfo;
|
||||
import de.tki.comfymodels.service.impl.ArchiveService;
|
||||
import de.tki.comfymodels.service.impl.LocalModelScanner;
|
||||
import de.tki.comfymodels.service.impl.PathResolver;
|
||||
import de.tki.comfymodels.service.impl.RestBridgeService;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -31,7 +32,7 @@ class RestBridgeServiceTest {
|
||||
private final String testToken = "test-token";
|
||||
|
||||
private static class LocalModelScannerStub extends LocalModelScanner {
|
||||
public LocalModelScannerStub() { super(null); }
|
||||
public LocalModelScannerStub() { super(null, new PathResolver()); }
|
||||
@Override
|
||||
public java.util.List<ModelInfo> scanLocalModels() {
|
||||
return Collections.singletonList(new ModelInfo("checkpoints", "local_test.safetensors", "LOCAL"));
|
||||
|
||||
@@ -28,9 +28,9 @@ public class RestorationStatusTest {
|
||||
private Path tempArchiveDir;
|
||||
|
||||
// Status Constants (as used in Main.java)
|
||||
public static final String STATUS_FOUND_IN_ARCHIVE = "📦 Found in Archive";
|
||||
public static final String STATUS_ARCHIVED = "📦 Archived";
|
||||
public static final String STATUS_RESTORING = "📦 Restoring...";
|
||||
public static final String STATUS_RESTORED = "✅ Restored";
|
||||
public static final String STATUS_RESTORED = "✅ Already exists";
|
||||
public static final String STATUS_RESTORE_FAILED = "❌ Restore Failed";
|
||||
|
||||
@BeforeEach
|
||||
@@ -76,8 +76,8 @@ public class RestorationStatusTest {
|
||||
List<String> statusSequence = new ArrayList<>();
|
||||
|
||||
// Initial state (determined by UI/Logic)
|
||||
statusSequence.add(STATUS_FOUND_IN_ARCHIVE);
|
||||
assertEquals(STATUS_FOUND_IN_ARCHIVE, statusSequence.get(0));
|
||||
statusSequence.add(STATUS_ARCHIVED);
|
||||
assertEquals(STATUS_ARCHIVED, statusSequence.get(0));
|
||||
|
||||
// 3. Act: Start Restoration
|
||||
statusSequence.add(STATUS_RESTORING);
|
||||
@@ -109,7 +109,7 @@ public class RestorationStatusTest {
|
||||
|
||||
// 2. Define transitions tracker
|
||||
List<String> statusSequence = new ArrayList<>();
|
||||
statusSequence.add(STATUS_FOUND_IN_ARCHIVE);
|
||||
statusSequence.add(STATUS_ARCHIVED);
|
||||
statusSequence.add(STATUS_RESTORING);
|
||||
|
||||
// 3. Act: Attempt Restoration
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
|
||||
public class StabilityValidationTest {
|
||||
|
||||
@Test
|
||||
public void validateStability() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();
|
||||
|
||||
System.out.println("--- 🛡️ STARTING STABILITY VALIDATION ---");
|
||||
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
System.out.println("Cycle " + i + ": Triggering Refresh...");
|
||||
|
||||
HttpRequest refreshReq = HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:8000/cmfd/refresh-models"))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString("{\"force_reload\": false}"))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(refreshReq, HttpResponse.BodyHandlers.ofString());
|
||||
System.out.println("📡 Server Response: " + response.statusCode());
|
||||
|
||||
Thread.sleep(2000);
|
||||
|
||||
// Check if API is still alive
|
||||
if (diagnosticService.isModelAvailableInComfy("ltx2.3-transition.safetensors", "loras")) {
|
||||
System.out.println("✅ Cycle " + i + ": Server is healthy and model is visible.");
|
||||
} else {
|
||||
System.out.println("❌ Cycle " + i + ": Model visibility check failed.");
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("--- 🛡️ STABILITY VALIDATION COMPLETE ---");
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.ComfyLifecycleService;
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class UltimateGuiSyncTest {
|
||||
|
||||
@Test
|
||||
public void ensureGuiSyncs() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ConfigService configService = context.getBean(ConfigService.class);
|
||||
ComfyLifecycleService lifecycleService = context.getBean(ComfyLifecycleService.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
HttpClient client = HttpClient.newBuilder().build();
|
||||
|
||||
System.out.println("--- 🚀 ULTIMATE GUI SYNC SEQUENCE ---");
|
||||
|
||||
// 1. HARD REINSTALL OF BRIDGE FILES
|
||||
String comfyPath = "C:\\AI\\ComfyUI\\resources\\ComfyUI";
|
||||
Path bridgeDir = Paths.get(comfyPath, "custom_nodes", "comfyui-model-downloader");
|
||||
System.out.println("📦 Enforcing Latest Bridge in: " + bridgeDir);
|
||||
Files.createDirectories(bridgeDir.resolve("web"));
|
||||
|
||||
copyResource("/comfyui-bridge/__init__.py", bridgeDir.resolve("__init__.py"));
|
||||
copyResource("/comfyui-bridge/web/downloader.js", bridgeDir.resolve("web/downloader.js"));
|
||||
Files.writeString(bridgeDir.resolve("web/config.json"), "{\"token\": \"" + configService.getApiToken() + "\"}");
|
||||
|
||||
// 2. FORCE RESTART BACKEND (To load new Python bridge)
|
||||
System.out.println("⏹ Terminating ComfyUI for Clean State...");
|
||||
Runtime.getRuntime().exec("taskkill /F /IM python.exe /T").waitFor();
|
||||
Thread.sleep(2000);
|
||||
System.out.println("▶ Starting ComfyUI...");
|
||||
lifecycleService.start();
|
||||
|
||||
// 3. WAIT FOR BACKEND READINESS
|
||||
System.out.println("⏳ Waiting for API...");
|
||||
for (int i = 0; i < 90; i++) {
|
||||
if (lifecycleService.isHealthy()) break;
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
// 4. TRIGGER NUCLEAR FRONTEND RELOAD
|
||||
// This will force every open browser tab to run window.location.reload()
|
||||
System.out.println("☢️ Triggering Nuclear Browser Reload...");
|
||||
HttpRequest reloadReq = HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:8000/cmfd/refresh-models"))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString("{\"force_reload\": true}"))
|
||||
.build();
|
||||
|
||||
try {
|
||||
client.send(reloadReq, HttpResponse.BodyHandlers.ofString());
|
||||
System.out.println("✅ Reload signal broadcasted to all tabs.");
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// 5. FINAL VERIFICATION
|
||||
String model = "ltx2.3-transition.safetensors";
|
||||
if (diagnosticService.isModelAvailableInComfy(model, "loras")) {
|
||||
System.out.println("🎯 TARGET ACHIEVED: Backend is synchronized.");
|
||||
System.out.println("Check your browser now. All tabs should have reloaded and the model should be visible!");
|
||||
} else {
|
||||
System.out.println("❌ Critical Failure: Model still missing from API.");
|
||||
}
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
private void copyResource(String name, Path target) throws Exception {
|
||||
try (InputStream is = getClass().getResourceAsStream(name)) {
|
||||
Files.copy(is, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
-10
@@ -21,29 +21,66 @@ const initializeExtension = async () => {
|
||||
};
|
||||
await loadConfig();
|
||||
|
||||
// Event Listener for UI Refresh
|
||||
api.addEventListener("cmfd-refresh-ui", async (event) => {
|
||||
const data = event.detail || {};
|
||||
const forceReload = data.force_reload === true;
|
||||
|
||||
console.log(`%c🔥 [CMFD] Refresh Signal Received. Force Reload: ${forceReload}`, "background: #ff0000; color: #fff; font-size: 14px;");
|
||||
|
||||
if (forceReload) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Clear Frontend Caches
|
||||
api.nodeDefs = null;
|
||||
|
||||
// 2. Trigger standard ComfyUI refresh
|
||||
if (app.refresh) {
|
||||
await app.refresh();
|
||||
}
|
||||
|
||||
// 3. Update existing combo widgets
|
||||
const nodeDefs = await api.getNodeDefs();
|
||||
if (app.graph && app.graph._nodes) {
|
||||
for (const node of app.graph._nodes) {
|
||||
const def = nodeDefs[node.type];
|
||||
if (!def || !node.widgets) continue;
|
||||
|
||||
for (const widget of node.widgets) {
|
||||
if (widget.type === "combo") {
|
||||
const inputDef = def.input?.required?.[widget.name] || def.input?.optional?.[widget.name];
|
||||
if (inputDef && Array.isArray(inputDef[0])) {
|
||||
widget.options.values = inputDef[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
console.log("[CMFD] UI Sync Complete.");
|
||||
} catch (e) {
|
||||
console.error("[CMFD] UI Refresh Failed:", e);
|
||||
}
|
||||
});
|
||||
|
||||
const addUI = () => {
|
||||
if (document.getElementById("tki-downloader-fab")) return;
|
||||
const fab = document.createElement("div");
|
||||
fab.id = "tki-downloader-fab";
|
||||
fab.innerHTML = "🚀";
|
||||
fab.title = "An Model Downloader senden";
|
||||
fab.style = "position:fixed; bottom:30px; right:30px; z-index:10000; cursor:pointer; font-size:30px; background:#ffcc00; border-radius:50%; width:60px; height:60px; display:flex; align-items:center; justify-content:center; box-shadow:0 0 20px rgba(0,0,0,0.5); border: 2px solid white; transition: transform 0.2s;";
|
||||
fab.onmouseover = () => fab.style.transform = "scale(1.1)";
|
||||
fab.onmouseout = () => fab.style.transform = "scale(1.0)";
|
||||
fab.onclick = async () => {
|
||||
if (!app?.graph) return;
|
||||
try {
|
||||
const response = await fetch("http://127.0.0.1:12345/import", {
|
||||
await fetch("http://127.0.0.1:12345/import", {
|
||||
method: "POST", mode: "cors",
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiToken}` },
|
||||
body: JSON.stringify(app.graph.serialize())
|
||||
});
|
||||
if (response.ok) {
|
||||
if (app.ui?.dialog?.show) app.ui.dialog.show("🚀 Workflow an Downloader gesendet!");
|
||||
}
|
||||
} catch (e) {
|
||||
alert("❌ Downloader App nicht erreichbar!");
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
document.body.appendChild(fab);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user