Skip to content

Commit

Permalink
Implement install APIs (#235)
Browse files Browse the repository at this point in the history
* Implement getSystemPaths

* nit

* Remove SHOW_SELECT_DIRECTORY

* Remove FIRST_TIME_SETUP_COMPLETE

* Validate install path

* Implement migration items

* Implement VALIDATE_COMFYUI_SOURCE

* Implement SHOW_DIRECTORY_PICKER

* Implement handle install

* Move python install path

* extract handle install

* Remove renderer content

* Remove OPEN_FORUM

* Remove GET_DEFAULT_INSTALL_LOCATION

* Re-wire dev server

* Use systeminformation to replace check-disk-space
  • Loading branch information
huchenlei authored Nov 13, 2024
1 parent c475dee commit bf9b425
Show file tree
Hide file tree
Showing 14 changed files with 190 additions and 537 deletions.
6 changes: 6 additions & 0 deletions .env_example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ COMFY_PORT=8188

# Whether to use an external server instead of starting one locally.
USE_EXTERNAL_SERVER=false

# The URL of the development server to use.
# This is for the install/server-startup screen.
# Run `npm run dev` in the frontend repo(https://github.com/Comfy-Org/ComfyUI_frontend)
# to start a development server.
DEV_SERVER_URL=http://192.168.2.20:5173
6 changes: 1 addition & 5 deletions scripts/launchdev.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,8 @@ const logLevel = 'warn'
/**
* Setup watcher for `main` package
* On file changed it totally re-launch electron app.
* @param {import('vite').ViteDevServer} watchServer Renderer watch server instance.
* Needs to set up `VITE_DEV_SERVER_URL` environment variable from {@link import('vite').ViteDevServer.resolvedUrls}
*/
function setupMainPackageWatcher({ resolvedUrls }) {
process.env.VITE_DEV_SERVER_URL = resolvedUrls.local[0];

function setupMainPackageWatcher() {
/** @type {ChildProcess | null} */
let electronApp = null;

Expand Down
36 changes: 31 additions & 5 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ export const IPC_CHANNELS = {
RESTART_APP: 'restart-app',
REINSTALL: 'reinstall',
LOG_MESSAGE: 'log-message',
SHOW_SELECT_DIRECTORY: 'show-select-directory',
SELECTED_DIRECTORY: 'selected-directory',
OPEN_DIALOG: 'open-dialog',
FIRST_TIME_SETUP_COMPLETE: 'first-time-setup-complete',
DEFAULT_INSTALL_LOCATION: 'default-install-location',
DOWNLOAD_PROGRESS: 'download-progress',
START_DOWNLOAD: 'start-download',
PAUSE_DOWNLOAD: 'pause-download',
Expand All @@ -24,8 +20,12 @@ export const IPC_CHANNELS = {
OPEN_PATH: 'open-path',
OPEN_LOGS_PATH: 'open-logs-path',
OPEN_DEV_TOOLS: 'open-dev-tools',
OPEN_FORUM: 'open-forum',
IS_FIRST_TIME_SETUP: 'is-first-time-setup',
GET_SYSTEM_PATHS: 'get-system-paths',
VALIDATE_INSTALL_PATH: 'validate-install-path',
VALIDATE_COMFYUI_SOURCE: 'validate-comfyui-source',
SHOW_DIRECTORY_PICKER: 'show-directory-picker',
INSTALL_COMFYUI: 'install-comfyui',
} as const;

export enum ProgressStatus {
Expand Down Expand Up @@ -72,3 +72,29 @@ export const ELECTRON_BRIDGE_API = 'electronAPI';

export const SENTRY_URL_ENDPOINT =
'https://942cadba58d247c9cab96f45221aa813@o4507954455314432.ingest.us.sentry.io/4508007940685824';

export interface MigrationItem {
id: string;
label: string;
description: string;
}

export const MigrationItems: MigrationItem[] = [
{
id: 'user_files',
label: 'User Files',
description: 'Settings and user-created workflows',
},
{
id: 'models',
label: 'Models',
description: 'Reference model files from existing ComfyUI installations. (No copy)',
},
// TODO: Decide whether we want to auto-migrate custom nodes, and install their dependencies.
// huchenlei: This is a very essential thing for migration experience.
// {
// id: 'custom_nodes',
// label: 'Custom Nodes',
// description: 'Reference custom node files from existing ComfyUI installations. (No copy)',
// },
] as const;
5 changes: 0 additions & 5 deletions src/handlers/appInfoHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,5 @@ export class AppInfoHandlers {
ipcMain.handle(IPC_CHANNELS.GET_ELECTRON_VERSION, () => {
return app.getVersion();
});

ipcMain.handle(IPC_CHANNELS.OPEN_FORUM, () => {
shell.openExternal('https://forum.comfy.org');
});
ipcMain.handle(IPC_CHANNELS.DEFAULT_INSTALL_LOCATION, () => app.getPath('documents'));
}
}
77 changes: 76 additions & 1 deletion src/handlers/pathHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { app, ipcMain, shell } from 'electron';
import { app, dialog, ipcMain, shell } from 'electron';
import { IPC_CHANNELS } from '../constants';
import log from 'electron-log/main';
import { getModelConfigPath } from '../config/extra_model_config';
import { getBasePath } from '../install/resourcePaths';
import type { SystemPaths } from '../preload';
import fs from 'fs';
import si from 'systeminformation';
import { ComfyConfigManager } from '../config/comfyConfigManager';

export class PathHandlers {
static readonly REQUIRED_SPACE = 10 * 1024 * 1024 * 1024; // 10GB in bytes

constructor() {}

registerHandlers() {
Expand All @@ -24,5 +30,74 @@ export class PathHandlers {
log.info(`Opening path: ${folderPath}`);
shell.openPath(folderPath);
});

ipcMain.handle(IPC_CHANNELS.GET_SYSTEM_PATHS, (): SystemPaths => {
return {
appData: app.getPath('appData'),
appPath: app.getAppPath(),
defaultInstallPath: app.getPath('documents'),
};
});

/**
* Validate the install path for the application. Check whether the path is valid
* and writable. The disk should have enough free space to install the application.
*/
ipcMain.handle(
IPC_CHANNELS.VALIDATE_INSTALL_PATH,
async (event, path: string): Promise<{ isValid: boolean; error?: string }> => {
try {
// Check if path exists
if (!fs.existsSync(path)) {
return { isValid: false, error: 'Path does not exist' };
}

// Check if path is writable
try {
fs.accessSync(path, fs.constants.W_OK);
} catch (err) {
return { isValid: false, error: 'Path is not writable' };
}

// Check available disk space (require at least 10GB free)
const disks = await si.fsSize();
const disk = disks.find((disk) => path.startsWith(disk.mount));
if (disk && disk.available < PathHandlers.REQUIRED_SPACE) {
return {
isValid: false,
error: 'Insufficient disk space. At least 10GB of free space is required.',
};
}

return { isValid: true };
} catch (error) {
log.error('Error validating install path:', error);
return {
isValid: false,
error: 'Failed to validate install path: ' + error,
};
}
}
);
/**
* Validate whether the given path is a valid ComfyUI source path.
*/
ipcMain.handle(
IPC_CHANNELS.VALIDATE_COMFYUI_SOURCE,
async (event, path: string): Promise<{ isValid: boolean; error?: string }> => {
const isValid = ComfyConfigManager.isComfyUIDirectory(path);
return {
isValid,
error: isValid ? undefined : 'Invalid ComfyUI source path',
};
}
);

ipcMain.handle(IPC_CHANNELS.SHOW_DIRECTORY_PICKER, async (): Promise<string> => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory'],
});
return result.filePaths[0];
});
}
}
Empty file added src/install/index.ts
Empty file.
16 changes: 10 additions & 6 deletions src/main-process/appWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Store from 'electron-store';
import { StoreType } from '../store';
import log from 'electron-log/main';
import { IPC_CHANNELS } from '../constants';
import { getAppResourcesPath } from '../install/resourcePaths';

/**
* Creates a single application window that displays the renderer and encapsulates all the logic for sending messages to the renderer.
Expand Down Expand Up @@ -44,7 +45,6 @@ export class AppWindow {

this.setupWindowEvents();
this.sendQueuedEventsOnReady();
this.loadRenderer();
}

public isReady(): boolean {
Expand Down Expand Up @@ -105,13 +105,17 @@ export class AppWindow {
this.window.focus();
}

private async loadRenderer(): Promise<void> {
if (process.env.VITE_DEV_SERVER_URL) {
log.info('Loading Vite Dev Server');
await this.window.loadURL(process.env.VITE_DEV_SERVER_URL);
public async loadRenderer(urlPath: string = ''): Promise<void> {
if (process.env.DEV_SERVER_URL) {
const url = `${process.env.DEV_SERVER_URL}/${urlPath}`;

log.info(`Loading development server ${url}`);
await this.window.loadURL(url);
this.window.webContents.openDevTools();
} else {
this.window.loadFile(path.join(__dirname, `../renderer/index.html`));
const appResourcesPath = await getAppResourcesPath();
const frontendPath = path.join(appResourcesPath, 'ComfyUI', 'web_custom_versions', 'desktop_app');
this.window.loadFile(path.join(frontendPath, 'index.html'), { hash: urlPath });
}
}

Expand Down
89 changes: 44 additions & 45 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { AppWindow } from './main-process/appWindow';
import { getAppResourcesPath, getBasePath, getPythonInstallPath } from './install/resourcePaths';
import { PathHandlers } from './handlers/pathHandlers';
import { AppInfoHandlers } from './handlers/appInfoHandlers';
import { InstallOptions } from './preload';

dotenv.config();

Expand Down Expand Up @@ -164,35 +165,19 @@ if (!gotTheLock) {
ipcMain.handle(IPC_CHANNELS.IS_FIRST_TIME_SETUP, () => {
return isFirstTimeSetup();
});
await handleFirstTimeSetup();
const basePath = await getBasePath();
const pythonInstallPath = await getPythonInstallPath();
if (!basePath || !pythonInstallPath) {
log.error('ERROR: Base path not found!');
sendProgressUpdate(ProgressStatus.ERROR_INSTALL_PATH);
return;
}
downloadManager = DownloadManager.getInstance(appWindow!, getModelsDirectory(basePath));

port =
port !== -1
? port
: await findAvailablePort(8000, 9999).catch((err) => {
log.error(`ERROR: Failed to find available port: ${err}`);
throw err;
});

if (!useExternalServer) {
sendProgressUpdate(ProgressStatus.PYTHON_SETUP);
const appResourcesPath = await getAppResourcesPath();
const pythonEnvironment = new PythonEnvironment(pythonInstallPath, appResourcesPath, spawnPythonAsync);
await pythonEnvironment.setup();
const modelConfigPath = getModelConfigPath();
sendProgressUpdate(ProgressStatus.STARTING_SERVER);
await launchPythonServer(pythonEnvironment.pythonInterpreterPath, appResourcesPath, modelConfigPath, basePath);
} else {
sendProgressUpdate(ProgressStatus.READY);
loadComfyIntoMainWindow();
ipcMain.on(IPC_CHANNELS.INSTALL_COMFYUI, async (event, installOptions: InstallOptions) => {
// Non-blocking call. The renderer will navigate to /server-start and show install progress.
handleInstall(installOptions);
serverStart();
});

// Loading renderer when all handlers are registered to ensure all event listeners are set up.
const firstTimeSetup = isFirstTimeSetup();
const urlPath = firstTimeSetup ? 'welcome' : 'server-start';
await appWindow.loadRenderer(urlPath);

if (!firstTimeSetup) {
await serverStart();
}
} catch (error) {
log.error(error);
Expand Down Expand Up @@ -558,27 +543,41 @@ function isFirstTimeSetup(): boolean {
return !fs.existsSync(extraModelsConfigPath);
}

async function selectedInstallDirectory(): Promise<string> {
return new Promise((resolve, reject) => {
ipcMain.on(IPC_CHANNELS.SELECTED_DIRECTORY, (_event, value) => {
log.info('User selected to install ComfyUI in:', value);
resolve(value);
});
});
async function handleInstall(installOptions: InstallOptions) {
const actualComfyDirectory = ComfyConfigManager.setUpComfyUI(installOptions.installPath);
const modelConfigPath = getModelConfigPath();
await createModelConfigFiles(modelConfigPath, actualComfyDirectory);
}

async function handleFirstTimeSetup() {
const firstTimeSetup = isFirstTimeSetup();
log.info('First time setup:', firstTimeSetup);
if (firstTimeSetup) {
appWindow.send(IPC_CHANNELS.SHOW_SELECT_DIRECTORY, null);
const selectedDirectory = await selectedInstallDirectory();
const actualComfyDirectory = ComfyConfigManager.setUpComfyUI(selectedDirectory);
async function serverStart() {
const basePath = await getBasePath();
const pythonInstallPath = await getPythonInstallPath();
if (!basePath || !pythonInstallPath) {
log.error('ERROR: Base path not found!');
sendProgressUpdate(ProgressStatus.ERROR_INSTALL_PATH);
return;
}
downloadManager = DownloadManager.getInstance(appWindow!, getModelsDirectory(basePath));

port =
port !== -1
? port
: await findAvailablePort(8000, 9999).catch((err) => {
log.error(`ERROR: Failed to find available port: ${err}`);
throw err;
});

if (!useExternalServer) {
sendProgressUpdate(ProgressStatus.PYTHON_SETUP);
const appResourcesPath = await getAppResourcesPath();
const pythonEnvironment = new PythonEnvironment(pythonInstallPath, appResourcesPath, spawnPythonAsync);
await pythonEnvironment.setup();
const modelConfigPath = getModelConfigPath();
await createModelConfigFiles(modelConfigPath, actualComfyDirectory);
sendProgressUpdate(ProgressStatus.STARTING_SERVER);
await launchPythonServer(pythonEnvironment.pythonInterpreterPath, appResourcesPath, modelConfigPath, basePath);
} else {
appWindow.send(IPC_CHANNELS.FIRST_TIME_SETUP_COMPLETE, null);
sendProgressUpdate(ProgressStatus.READY);
loadComfyIntoMainWindow();
}
}

Expand Down
Loading

0 comments on commit bf9b425

Please sign in to comment.