mirror of
https://github.com/open-webui/desktop
synced 2025-06-26 18:15:59 +00:00
refac
This commit is contained in:
parent
cd1e0904cc
commit
052a981077
4
.gitignore
vendored
4
.gitignore
vendored
@ -93,3 +93,7 @@ out/
|
||||
|
||||
resources/python
|
||||
resources/python.tar.gz
|
||||
|
||||
|
||||
|
||||
.webui_secret_key
|
||||
17
src/main.ts
17
src/main.ts
@ -10,7 +10,7 @@ import {
|
||||
import path from "path";
|
||||
import started from "electron-squirrel-startup";
|
||||
|
||||
import { installPackage } from "./utils";
|
||||
import { installPackage, startServer, stopAllServers } from "./utils";
|
||||
|
||||
// Restrict app to a single instance
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
@ -108,11 +108,23 @@ if (!gotTheLock) {
|
||||
});
|
||||
};
|
||||
|
||||
ipcMain.handle("install-package", async (event) => {
|
||||
ipcMain.handle("install", async (event) => {
|
||||
console.log("Installing package...");
|
||||
installPackage();
|
||||
});
|
||||
|
||||
ipcMain.handle("server:start", async (event) => {
|
||||
console.log("Starting server...");
|
||||
|
||||
startServer();
|
||||
});
|
||||
|
||||
ipcMain.handle("server:stop", async (event) => {
|
||||
console.log("Stopping server...");
|
||||
|
||||
stopAllServers();
|
||||
});
|
||||
|
||||
ipcMain.handle("load-webui", async (event, arg) => {
|
||||
console.log(arg); // prints "ping"
|
||||
mainWindow.loadURL("http://localhost:8080");
|
||||
@ -131,6 +143,7 @@ if (!gotTheLock) {
|
||||
|
||||
app.on("before-quit", () => {
|
||||
app.isQuiting = true; // Ensure quit flag is set
|
||||
stopAllServers();
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ipcRenderer, contextBridge } from "electron";
|
||||
import { start } from "repl";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
// Listen for messages from the main process
|
||||
@ -21,10 +22,14 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
},
|
||||
|
||||
installPackage: async () => {
|
||||
await ipcRenderer.invoke("install-package");
|
||||
await ipcRenderer.invoke("install");
|
||||
},
|
||||
|
||||
loadWebUI: async (arg) => {
|
||||
await ipcRenderer.invoke("load-webui", arg);
|
||||
startServer: async () => {
|
||||
await ipcRenderer.invoke("server:start");
|
||||
},
|
||||
|
||||
stopServer: async () => {
|
||||
await ipcRenderer.invoke("server:stop");
|
||||
},
|
||||
});
|
||||
|
||||
@ -3,25 +3,45 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row w-full h-full">
|
||||
<div class="m-auto">
|
||||
<!-- <div class=" flex justify-center mb-3">
|
||||
<div class="m-auto flex flex-col">
|
||||
<div class=" flex justify-center mb-3">
|
||||
<img
|
||||
src="./assets/images/splash.png"
|
||||
class="size-16 dark:invert"
|
||||
alt="hero"
|
||||
/>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- <div class=" text-2xl text-gray-50 font-secondary">Install Open WebUI</div> -->
|
||||
|
||||
<button
|
||||
class=" text-gray-100 hover:text-white transition font-medium cursor-pointer"
|
||||
onclick={() => {
|
||||
console.log("clicked");
|
||||
console.log("install clicked");
|
||||
if (window?.electronAPI) {
|
||||
window.electronAPI.installPackage();
|
||||
}
|
||||
}}>Install Open WebUI</button
|
||||
>
|
||||
|
||||
<button
|
||||
class=" text-gray-100 hover:text-white transition font-medium cursor-pointer"
|
||||
onclick={() => {
|
||||
console.log("start clicked");
|
||||
if (window?.electronAPI) {
|
||||
window.electronAPI.startServer();
|
||||
}
|
||||
}}>Start Open WebUI</button
|
||||
>
|
||||
|
||||
<button
|
||||
class=" text-gray-100 hover:text-white transition font-medium cursor-pointer"
|
||||
onclick={() => {
|
||||
console.log("stop clicked");
|
||||
if (window?.electronAPI) {
|
||||
window.electronAPI.stopServer();
|
||||
}
|
||||
}}>Stop Open WebUI</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
spawn,
|
||||
ChildProcess,
|
||||
} from "child_process";
|
||||
import net from "net";
|
||||
|
||||
import * as tar from "tar";
|
||||
import log from "electron-log";
|
||||
@ -45,6 +46,53 @@ export function getUserDataPath(): string {
|
||||
return userDataDir;
|
||||
}
|
||||
|
||||
export function getOpenWebUIDataPath(): string {
|
||||
const openWebUIDataDir = path.join(getUserDataPath(), "data");
|
||||
|
||||
if (!fs.existsSync(openWebUIDataDir)) {
|
||||
try {
|
||||
fs.mkdirSync(openWebUIDataDir, { recursive: true });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return openWebUIDataDir;
|
||||
}
|
||||
|
||||
export async function portInUse(
|
||||
port: number,
|
||||
host: string = "127.0.0.1"
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const client = new net.Socket();
|
||||
|
||||
// Attempt to connect to the port
|
||||
client
|
||||
.setTimeout(1000) // Timeout for the connection attempt
|
||||
.once("connect", () => {
|
||||
// If connection succeeds, port is in use
|
||||
client.destroy();
|
||||
resolve(true);
|
||||
})
|
||||
.once("timeout", () => {
|
||||
// If no connection after the timeout, port is not in use
|
||||
client.destroy();
|
||||
resolve(false);
|
||||
})
|
||||
.once("error", (err: any) => {
|
||||
if (err.code === "ECONNREFUSED") {
|
||||
// Port is not in use or no listener is accepting connections
|
||||
resolve(false);
|
||||
} else {
|
||||
// Unexpected error
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.connect(port, host);
|
||||
});
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
//
|
||||
// Python Utils
|
||||
@ -131,8 +179,8 @@ 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`;
|
||||
? `${installationPath}\\Scripts\\activate.bat && uv pip install open-webui -U`
|
||||
: `source "${installationPath}/bin/activate" && uv pip install open-webui -U`;
|
||||
|
||||
// only unsign when installing from bundled installer
|
||||
// if (platform === "darwin") {
|
||||
@ -145,7 +193,6 @@ export async function installOpenWebUI(installationPath: string) {
|
||||
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);
|
||||
});
|
||||
@ -202,13 +249,20 @@ export async function installBundledPython(installationPath?: string) {
|
||||
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");
|
||||
}
|
||||
// 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 installBundledPython(installationPath);
|
||||
} catch (error) {
|
||||
log.error("Failed to install bundled Python", error);
|
||||
return Promise.reject("Failed to install bundled Python");
|
||||
}
|
||||
|
||||
try {
|
||||
@ -218,79 +272,85 @@ export async function installPackage(installationPath?: string) {
|
||||
return Promise.reject("Failed to install open-webui");
|
||||
}
|
||||
}
|
||||
////////////////////////////////////////////////
|
||||
//
|
||||
// Server Manager
|
||||
//
|
||||
////////////////////////////////////////////////
|
||||
|
||||
// Set to keep track of all spawned child processes from the app
|
||||
const childProcesses: Set<ChildProcess> = new Set();
|
||||
|
||||
/**
|
||||
* 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}`
|
||||
);
|
||||
throw new Error(`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 });
|
||||
execSync(checkCommand, { stdio: "ignore" });
|
||||
} catch (error) {
|
||||
return Promise.reject(
|
||||
`The 'open-webui' package is not installed in the virtual environment at ${installationPath}. Install it first.`
|
||||
throw new Error(
|
||||
`The 'open-webui' package is not installed in the environment at ${installationPath}.`
|
||||
);
|
||||
}
|
||||
|
||||
// All validation passed
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Map to track running processes by installation path
|
||||
const activeProcesses: Map<string, ChildProcess> = new Map();
|
||||
////////////////////////////////////////////////
|
||||
//
|
||||
// Process Management
|
||||
//
|
||||
////////////////////////////////////////////////
|
||||
|
||||
// Tracks all spawned server process PIDs
|
||||
const serverPIDs: Set<number> = new Set();
|
||||
|
||||
/**
|
||||
* Starts the Open-WebUI server.
|
||||
*
|
||||
* @param installationPath - The path to the virtual environment installation
|
||||
* @param port - The port on which the server will run
|
||||
* Spawn the Open-WebUI server process.
|
||||
*/
|
||||
export async function startOpenWebUIServer(
|
||||
installationPath: string,
|
||||
port: number
|
||||
export async function startServer(
|
||||
installationPath?: string,
|
||||
port?: number
|
||||
): Promise<void> {
|
||||
try {
|
||||
await validateInstallation(installationPath);
|
||||
} catch (validationError) {
|
||||
console.error(validationError);
|
||||
return Promise.reject(validationError); // Abort if validation fails
|
||||
}
|
||||
installationPath = path.normalize(
|
||||
installationPath || getBundledPythonInstallationPath()
|
||||
);
|
||||
|
||||
// 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`;
|
||||
? `${installationPath}\\Scripts\\activate.bat && set DATA_DIR="${path.join(
|
||||
app.getPath("userData"),
|
||||
"data"
|
||||
)}" && open-webui serve`
|
||||
: `source "${installationPath}/bin/activate" && export DATA_DIR="${path.join(
|
||||
app.getPath("userData"),
|
||||
"data"
|
||||
)}" && open-webui serve`;
|
||||
|
||||
if (port) {
|
||||
startCommand += ` --port ${port}`;
|
||||
port = port || 8080;
|
||||
while (await portInUse(port)) {
|
||||
port++;
|
||||
}
|
||||
|
||||
// Spawn the process
|
||||
console.log("Starting Open-WebUI server...");
|
||||
const childProcess = spawn(startCommand, [], { shell: true });
|
||||
startCommand += ` --port ${port}`;
|
||||
|
||||
// Log process output
|
||||
console.log("Starting Open-WebUI server...");
|
||||
const childProcess = spawn(startCommand, {
|
||||
shell: true,
|
||||
detached: true,
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
// Log any output (optional)
|
||||
childProcess.stdout?.on("data", (data) => {
|
||||
console.log(`[Open-WebUI]: ${data.toString().trim()}`);
|
||||
});
|
||||
@ -299,38 +359,56 @@ export async function startOpenWebUIServer(
|
||||
console.error(`[Open-WebUI Error]: ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
childProcess.on("exit", (exitCode) => {
|
||||
console.log(`Open-WebUI server exited with code ${exitCode}`);
|
||||
childProcess.on("exit", (code) => {
|
||||
console.log(`Open-WebUI server exited with code ${code}`);
|
||||
});
|
||||
|
||||
// Keep track of the process for later termination
|
||||
activeProcesses.set(installationPath, childProcess);
|
||||
// Track server PID
|
||||
if (childProcess.pid) {
|
||||
serverPIDs.add(childProcess.pid);
|
||||
console.log(`Server started with PID: ${childProcess.pid}`);
|
||||
} else {
|
||||
console.error("Failed to start the server: PID not found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the running Open-WebUI server.
|
||||
*
|
||||
* @param installationPath - The path to the virtual environment installation
|
||||
* Terminates all server processes.
|
||||
*/
|
||||
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;
|
||||
export async function stopAllServers(): Promise<void> {
|
||||
console.log("Stopping all servers...");
|
||||
for (const pid of serverPIDs) {
|
||||
try {
|
||||
terminateProcessTree(pid);
|
||||
serverPIDs.delete(pid); // Remove from tracking set after termination
|
||||
} catch (error) {
|
||||
console.error(`Error stopping server with PID ${pid}:`, error);
|
||||
}
|
||||
}
|
||||
console.log("All servers stopped successfully.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills a process tree by PID.
|
||||
*/
|
||||
function terminateProcessTree(pid: number): void {
|
||||
if (process.platform === "win32") {
|
||||
// Use `taskkill` on Windows to recursively kill the process and its children
|
||||
try {
|
||||
execSync(`taskkill /PID ${pid} /T /F`); // /T -> terminate child processes, /F -> force termination
|
||||
console.log(`Terminated server process tree (PID: ${pid}) on Windows.`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to terminate process tree (PID: ${pid}):`, error);
|
||||
}
|
||||
} else {
|
||||
// Use `kill` on Unix-like platforms to terminate the process group (-pid)
|
||||
try {
|
||||
process.kill(-pid, "SIGKILL"); // Negative PID (-pid) kills the process group
|
||||
console.log(
|
||||
`Terminated server process tree (PID: ${pid}) on Unix-like OS.`
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Failed to terminate process tree (PID: ${pid}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user