This commit is contained in:
Timothy Jaeryang Baek
2025-01-11 13:18:46 -08:00
parent 4e99cf651e
commit 9f09e26247
8 changed files with 531 additions and 32 deletions

View File

@@ -10,6 +10,8 @@ import {
import path from "path";
import started from "electron-squirrel-startup";
import { installPackage } from "./utils";
// Restrict app to a single instance
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
@@ -68,6 +70,10 @@ if (!gotTheLock) {
);
}
if (!app.isPackaged) {
mainWindow.webContents.openDevTools();
}
// Create a system tray icon
const image = nativeImage.createFromPath(
path.join(__dirname, "assets/tray.png")
@@ -102,7 +108,12 @@ if (!gotTheLock) {
});
};
ipcMain.on("load-webui", (event, arg) => {
ipcMain.handle("install-package", async (event) => {
console.log("Installing package...");
installPackage();
});
ipcMain.handle("load-webui", async (event, arg) => {
console.log(arg); // prints "ping"
mainWindow.loadURL("http://localhost:8080");
@@ -118,9 +129,14 @@ if (!gotTheLock) {
});
});
app.on("before-quit", () => {
app.isQuiting = true; // Ensure quit flag is set
});
// Quit when all windows are closed, except on macOS
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.isQuitting = true;
app.quit();
}
});

View File

@@ -15,12 +15,16 @@ window.addEventListener("DOMContentLoaded", () => {
});
contextBridge.exposeInMainWorld("electronAPI", {
sendPing: () => {
sendPing: async () => {
console.log("Sending PING to main process...");
ipcRenderer.send("send-ping"); // Send the ping back to the main process
await ipcRenderer.invoke("send-ping"); // Send the ping back to the main process
},
loadWebUI: (arg) => {
ipcRenderer.send("load-webui", arg);
installPackage: async () => {
await ipcRenderer.invoke("install-package");
},
loadWebUI: async (arg) => {
await ipcRenderer.invoke("load-webui", arg);
},
});

View File

@@ -19,7 +19,7 @@
onclick={() => {
console.log("clicked");
if (window?.electronAPI) {
window.electronAPI.loadWebUI();
window.electronAPI.installPackage();
}
}}>Install Open WebUI</button
>

0
src/utils/build.ts Normal file
View File

View File

@@ -7,6 +7,8 @@ import {
ExecFileOptions,
execFileSync,
execSync,
spawn,
ChildProcess,
} from "child_process";
import * as tar from "tar";
@@ -22,9 +24,6 @@ import { app } from "electron";
export function getAppPath(): string {
let appDir = app.getAppPath();
if (!app.isPackaged) {
appDir = path.dirname(appDir);
}
return appDir;
}
@@ -57,7 +56,7 @@ export function getBundledPythonTarPath(): string {
return path.join(appPath, "resources", "python.tar.gz");
}
export function getBundledPythonInstallPath(): string {
export function getBundledPythonInstallationPath(): string {
const installDir = path.join(app.getPath("userData"), "python");
if (!fs.existsSync(installDir)) {
@@ -85,7 +84,7 @@ export function getPythonPath(envPath: string, isConda?: boolean) {
}
export function getBundledPythonPath() {
return getPythonPath(getBundledPythonInstallPath());
return getPythonPath(getBundledPythonInstallationPath());
}
export function isBundledPythonInstalled() {
@@ -128,37 +127,210 @@ export function createAdHocSignCommand(envPath: string): string {
)} && cd -`;
}
export async function installBundledPython(installPath?: string) {
const platform = process.platform;
const isWin = platform === "win32";
installPath = installPath || getBundledPythonInstallPath();
export async function installOpenWebUI(installationPath: string) {
console.log(installationPath);
let unpackCommand =
process.platform === "win32"
? `${installationPath}\\Scripts\\activate.bat && pip install open-webui -U`
: `source "${installationPath}/bin/activate" && pip install open-webui -U`;
// only unsign when installing from bundled installer
// if (platform === "darwin") {
// unpackCommand = `${createAdHocSignCommand(installationPath)}\n${unpackCommand}`;
// }
console.log(unpackCommand);
const commandProcess = exec(unpackCommand, {
shell: process.platform === "win32" ? "cmd.exe" : "/bin/bash",
});
// once the environment is activated, print the python version
commandProcess.stdout?.on("data", (data) => {
console.log(data);
});
commandProcess.stderr?.on("data", (data) => {
console.error(data);
});
commandProcess.on("exit", (code) => {
console.log(`Child exited with code ${code}`);
});
}
export async function installBundledPython(installationPath?: string) {
installationPath = installationPath || getBundledPythonInstallationPath();
const pythonTarPath = getBundledPythonTarPath();
console.log(installationPath, pythonTarPath);
if (!fs.existsSync(pythonTarPath)) {
log.error("Python tarball not found");
return;
}
try {
fs.mkdirSync(installPath, { recursive: true });
fs.mkdirSync(installationPath, { recursive: true });
await tar.x({
cwd: installPath,
cwd: installationPath,
file: pythonTarPath,
});
} catch (error) {
log.error(error);
}
let unpackCommand = isWin
? `${installPath}\\Scripts\\activate.bat && conda-unpack`
: `source "${installPath}/bin/activate" && conda-unpack`;
// Get the path to the installed Python binary
const bundledPythonPath = getBundledPythonPath();
// only unsign when installing from bundled installer
if (platform === "darwin") {
unpackCommand = `${createAdHocSignCommand(installPath)}\n${unpackCommand}`;
if (!fs.existsSync(bundledPythonPath)) {
log.error("Python binary not found in install path");
return;
}
const commandProcess = exec(unpackCommand, {
shell: isWin ? "cmd.exe" : "/bin/bash",
});
try {
// Execute the Python binary to print the version
const pythonVersion = execFileSync(bundledPythonPath, ["--version"], {
encoding: "utf-8",
});
console.log("Installed Python Version:", pythonVersion.trim());
} catch (error) {
log.error("Failed to execute Python binary", error);
}
}
export async function installPackage(installationPath?: string) {
installationPath = installationPath || getBundledPythonInstallationPath();
if (!isBundledPythonInstalled()) {
try {
await installBundledPython(installationPath);
} catch (error) {
log.error("Failed to install bundled Python", error);
return Promise.reject("Failed to install bundled Python");
}
}
try {
await installOpenWebUI(installationPath);
} catch (error) {
log.error("Failed to install open-webui", error);
return Promise.reject("Failed to install open-webui");
}
}
/**
* Validates that Python is installed and the `open-webui` package is present
* within the specified virtual environment.
*
* @param installationPath - The path to the virtual environment installation
* @returns Promise<void> - Resolves if all prerequisites are valid; rejects otherwise
*/
export async function validateInstallation(
installationPath: string
): Promise<void> {
const pythonPath = getPythonPath(installationPath);
// Check if Python binary exists
if (!fs.existsSync(pythonPath)) {
return Promise.reject(
`Python binary not found in environment: ${pythonPath}`
);
}
try {
// Check if `open-webui` is installed
const checkCommand =
process.platform === "win32"
? `${installationPath}\\Scripts\\activate.bat && pip show open-webui`
: `source "${installationPath}/bin/activate" && pip show open-webui`;
execSync(checkCommand, { stdio: "ignore", shell: true });
} catch (error) {
return Promise.reject(
`The 'open-webui' package is not installed in the virtual environment at ${installationPath}. Install it first.`
);
}
// All validation passed
return Promise.resolve();
}
// Map to track running processes by installation path
const activeProcesses: Map<string, ChildProcess> = new Map();
/**
* Starts the Open-WebUI server.
*
* @param installationPath - The path to the virtual environment installation
* @param port - The port on which the server will run
*/
export async function startOpenWebUIServer(
installationPath: string,
port: number
): Promise<void> {
try {
await validateInstallation(installationPath);
} catch (validationError) {
console.error(validationError);
return Promise.reject(validationError); // Abort if validation fails
}
// Construct the command based on the platform
let startCommand =
process.platform === "win32"
? `${installationPath}\\Scripts\\activate.bat && open-webui serve`
: `source "${installationPath}/bin/activate" && open-webui serve`;
if (port) {
startCommand += ` --port ${port}`;
}
// Spawn the process
console.log("Starting Open-WebUI server...");
const childProcess = spawn(startCommand, [], { shell: true });
// Log process output
childProcess.stdout?.on("data", (data) => {
console.log(`[Open-WebUI]: ${data.toString().trim()}`);
});
childProcess.stderr?.on("data", (data) => {
console.error(`[Open-WebUI Error]: ${data.toString().trim()}`);
});
childProcess.on("exit", (exitCode) => {
console.log(`Open-WebUI server exited with code ${exitCode}`);
});
// Keep track of the process for later termination
activeProcesses.set(installationPath, childProcess);
}
/**
* Stops the running Open-WebUI server.
*
* @param installationPath - The path to the virtual environment installation
*/
export async function stopOpenWebUIServer(
installationPath: string
): Promise<void> {
const processToStop = activeProcesses.get(installationPath);
if (!processToStop) {
console.error(
"No active server found for the specified installation path."
);
return;
}
console.log("Stopping Open-WebUI server...");
// Terminate the process
processToStop.kill();
// Remove from the active processes map
activeProcesses.delete(installationPath);
console.log("Open-WebUI server stopped successfully.");
}