mirror of
https://github.com/open-webui/desktop
synced 2025-06-26 18:15:59 +00:00
refac
This commit is contained in:
parent
f33baf219f
commit
82c68c4bb0
80
src/main.ts
80
src/main.ts
@ -1,16 +1,23 @@
|
||||
import {
|
||||
app,
|
||||
protocol,
|
||||
nativeImage,
|
||||
Tray,
|
||||
Menu,
|
||||
MenuItem,
|
||||
BrowserWindow,
|
||||
globalShortcut,
|
||||
ipcMain,
|
||||
} from "electron";
|
||||
import path from "path";
|
||||
import started from "electron-squirrel-startup";
|
||||
|
||||
import { installPackage, startServer, stopAllServers } from "./utils";
|
||||
import {
|
||||
installPackage,
|
||||
removePackage,
|
||||
startServer,
|
||||
stopAllServers,
|
||||
validateInstallation,
|
||||
} from "./utils";
|
||||
|
||||
// Restrict app to a single instance
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
@ -45,11 +52,23 @@ if (!gotTheLock) {
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let tray: Tray | null = null;
|
||||
|
||||
const onReady = () => {
|
||||
const loadDefaultView = () => {
|
||||
// Load index.html or dev server URL
|
||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
mainWindow.loadFile(
|
||||
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onReady = async () => {
|
||||
console.log(process.resourcesPath);
|
||||
console.log(app.getName());
|
||||
console.log(app.getPath("userData"));
|
||||
console.log(app.getPath("appData"));
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
@ -59,22 +78,49 @@ if (!gotTheLock) {
|
||||
},
|
||||
titleBarStyle: "hidden",
|
||||
});
|
||||
|
||||
mainWindow.setIcon(path.join(__dirname, "assets/icon.png"));
|
||||
|
||||
// Load index.html or dev server URL
|
||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
mainWindow.loadFile(
|
||||
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
|
||||
);
|
||||
}
|
||||
|
||||
loadDefaultView();
|
||||
if (!app.isPackaged) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
if (validateInstallation()) {
|
||||
const serverUrl = await startServer();
|
||||
mainWindow.loadURL(serverUrl);
|
||||
}
|
||||
|
||||
globalShortcut.register("Alt+CommandOrControl+O", () => {
|
||||
mainWindow?.show();
|
||||
|
||||
if (mainWindow?.isMinimized()) mainWindow?.restore();
|
||||
mainWindow?.focus();
|
||||
});
|
||||
|
||||
const defaultMenu = Menu.getApplicationMenu();
|
||||
|
||||
console.log(defaultMenu);
|
||||
// Convert the default menu to a template we can modify
|
||||
let menuTemplate = defaultMenu ? defaultMenu.items.map((item) => item) : [];
|
||||
|
||||
// Add your own custom menu items
|
||||
menuTemplate.push({
|
||||
label: "Action",
|
||||
submenu: [
|
||||
{
|
||||
label: "Home",
|
||||
accelerator: process.platform === "darwin" ? "Cmd+H" : "Ctrl+H",
|
||||
click: () => {
|
||||
loadDefaultView();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Build the updated menu and set it as the application menu
|
||||
const updatedMenu = Menu.buildFromTemplate(menuTemplate);
|
||||
Menu.setApplicationMenu(updatedMenu);
|
||||
|
||||
// Create a system tray icon
|
||||
const image = nativeImage.createFromPath(
|
||||
path.join(__dirname, "assets/tray.png")
|
||||
@ -89,7 +135,8 @@ if (!gotTheLock) {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Quit",
|
||||
label: "Quit Open WebUI",
|
||||
accelerator: "CommandOrControl+Q",
|
||||
click: () => {
|
||||
app.isQuiting = true; // Mark as quitting
|
||||
app.quit(); // Quit the application
|
||||
@ -114,6 +161,11 @@ if (!gotTheLock) {
|
||||
installPackage();
|
||||
});
|
||||
|
||||
ipcMain.handle("remove", async (event) => {
|
||||
console.log("Resetting package...");
|
||||
removePackage();
|
||||
});
|
||||
|
||||
ipcMain.handle("server:start", async (event) => {
|
||||
console.log("Starting server...");
|
||||
|
||||
|
@ -1,5 +1,17 @@
|
||||
import { ipcRenderer, contextBridge } from "electron";
|
||||
import { start } from "repl";
|
||||
|
||||
const isLocalSource = () => {
|
||||
// Check if the execution environment is local
|
||||
const origin = window.location.origin;
|
||||
|
||||
// Allow local sources: file protocol, localhost, or 0.0.0.0
|
||||
return (
|
||||
origin.startsWith("file://") ||
|
||||
origin.includes("localhost") ||
|
||||
origin.includes("127.0.0.1") ||
|
||||
origin.includes("0.0.0.0")
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
// Listen for messages from the main process
|
||||
@ -22,14 +34,42 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
},
|
||||
|
||||
installPackage: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
"Access restricted: This operation is only allowed in a local environment."
|
||||
);
|
||||
}
|
||||
|
||||
await ipcRenderer.invoke("install");
|
||||
},
|
||||
|
||||
removePackage: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
"Access restricted: This operation is only allowed in a local environment."
|
||||
);
|
||||
}
|
||||
|
||||
await ipcRenderer.invoke("remove");
|
||||
},
|
||||
|
||||
startServer: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
"Access restricted: This operation is only allowed in a local environment."
|
||||
);
|
||||
}
|
||||
|
||||
await ipcRenderer.invoke("server:start");
|
||||
},
|
||||
|
||||
stopServer: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
"Access restricted: This operation is only allowed in a local environment."
|
||||
);
|
||||
}
|
||||
|
||||
await ipcRenderer.invoke("server:stop");
|
||||
},
|
||||
});
|
||||
|
@ -1,31 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import Spinner from "./common/Spinner.svelte";
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row w-full h-full relative">
|
||||
<div class="flex flex-row w-full h-full relative dark:text-gray-100">
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-6 bg-gray-900 bg-opacity-50 draggable"
|
||||
></div>
|
||||
<div class="m-auto flex flex-col">
|
||||
<div class="m-auto flex flex-col max-w-xs w-full text-center">
|
||||
<div class=" flex justify-center mb-3">
|
||||
<img
|
||||
src="./assets/images/splash.png"
|
||||
class="size-16 dark:invert"
|
||||
class=" size-24 dark:invert"
|
||||
alt="hero"
|
||||
/>
|
||||
</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"
|
||||
<div class=" text-gray-500 hover:text-white transition">
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<div>Loading...</div>
|
||||
<div>
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <button
|
||||
class=" hover:text-white transition font-medium cursor-pointer"
|
||||
onclick={() => {
|
||||
console.log("install clicked");
|
||||
if (window?.electronAPI) {
|
||||
window.electronAPI.installPackage();
|
||||
}
|
||||
}}>Install Open WebUI</button
|
||||
}}
|
||||
>
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<div>Install</div>
|
||||
<div>
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
</div>
|
||||
</button> -->
|
||||
|
||||
<!--
|
||||
|
||||
<button
|
||||
class=" text-gray-100 hover:text-white transition font-medium cursor-pointer"
|
||||
@ -45,7 +65,7 @@
|
||||
window.electronAPI.stopServer();
|
||||
}
|
||||
}}>Stop Open WebUI</button
|
||||
>
|
||||
> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
29
src/render/lib/components/common/Spinner.svelte
Normal file
29
src/render/lib/components/common/Spinner.svelte
Normal file
@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
export let className: string = "size-5";
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center text-center">
|
||||
<svg
|
||||
class={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
@ -272,25 +272,33 @@ export async function installPackage(installationPath?: string) {
|
||||
return Promise.reject("Failed to install open-webui");
|
||||
}
|
||||
}
|
||||
|
||||
export async function removePackage(installationPath?: string) {
|
||||
installationPath = installationPath || getBundledPythonInstallationPath();
|
||||
|
||||
// remove the python env entirely
|
||||
if (fs.existsSync(installationPath)) {
|
||||
fs.rmdirSync(installationPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
//
|
||||
// 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.
|
||||
*/
|
||||
export async function validateInstallation(
|
||||
installationPath: string
|
||||
): Promise<void> {
|
||||
installationPath?: string
|
||||
): Promise<boolean> {
|
||||
installationPath = installationPath || getBundledPythonInstallationPath();
|
||||
const pythonPath = getPythonPath(installationPath);
|
||||
if (!fs.existsSync(pythonPath)) {
|
||||
throw new Error(`Python binary not found in environment: ${pythonPath}`);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const checkCommand =
|
||||
@ -299,17 +307,11 @@ export async function validateInstallation(
|
||||
: `source "${installationPath}/bin/activate" && pip show open-webui`;
|
||||
execSync(checkCommand, { stdio: "ignore" });
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`The 'open-webui' package is not installed in the environment at ${installationPath}.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
//
|
||||
// Process Management
|
||||
//
|
||||
////////////////////////////////////////////////
|
||||
return true;
|
||||
}
|
||||
|
||||
// Tracks all spawned server process PIDs
|
||||
const serverPIDs: Set<number> = new Set();
|
||||
@ -320,11 +322,16 @@ const serverPIDs: Set<number> = new Set();
|
||||
export async function startServer(
|
||||
installationPath?: string,
|
||||
port?: number
|
||||
): Promise<void> {
|
||||
): Promise<string> {
|
||||
installationPath = path.normalize(
|
||||
installationPath || getBundledPythonInstallationPath()
|
||||
);
|
||||
|
||||
if (!validateInstallation(installationPath)) {
|
||||
console.error("Failed to validate installation");
|
||||
return;
|
||||
}
|
||||
|
||||
let startCommand =
|
||||
process.platform === "win32"
|
||||
? `${installationPath}\\Scripts\\activate.bat && set DATA_DIR="${path.join(
|
||||
@ -347,29 +354,70 @@ export async function startServer(
|
||||
const childProcess = spawn(startCommand, {
|
||||
shell: true,
|
||||
detached: true,
|
||||
stdio: "pipe",
|
||||
stdio: ["ignore", "pipe", "pipe"], // Let us capture logs via stdout/stderr
|
||||
});
|
||||
|
||||
// Log any output (optional)
|
||||
childProcess.stdout?.on("data", (data) => {
|
||||
console.log(`[Open-WebUI]: ${data.toString().trim()}`);
|
||||
});
|
||||
let serverCrashed = false;
|
||||
let detectedURL: string | null = null;
|
||||
|
||||
childProcess.stderr?.on("data", (data) => {
|
||||
console.error(`[Open-WebUI Error]: ${data.toString().trim()}`);
|
||||
});
|
||||
// Wait for log output to confirm the server has started
|
||||
async function monitorServerLogs(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const handleLog = (data: Buffer) => {
|
||||
const logLine = data.toString().trim();
|
||||
console.log(`[Open-WebUI Log]: ${logLine}`);
|
||||
|
||||
childProcess.on("exit", (code) => {
|
||||
console.log(`Open-WebUI server exited with code ${code}`);
|
||||
});
|
||||
// Look for "Uvicorn running on http://<hostname>:<port>"
|
||||
const match = logLine.match(
|
||||
/Uvicorn running on (http:\/\/[^\s]+) \(Press CTRL\+C to quit\)/
|
||||
);
|
||||
if (match) {
|
||||
detectedURL = match[1]; // e.g., "http://0.0.0.0:8081"
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
// Track server PID
|
||||
// Combine stdout and stderr streams as a unified log source
|
||||
childProcess.stdout?.on("data", handleLog);
|
||||
childProcess.stderr?.on("data", handleLog);
|
||||
|
||||
childProcess.on("close", (code) => {
|
||||
serverCrashed = true;
|
||||
if (!detectedURL) {
|
||||
reject(
|
||||
new Error(
|
||||
`Process exited unexpectedly with code ${code}. No server URL detected.`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Track the child process 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");
|
||||
throw new Error("Failed to start server: No PID available");
|
||||
}
|
||||
|
||||
// Wait until the server log confirms it's started
|
||||
try {
|
||||
await monitorServerLogs();
|
||||
} catch (error) {
|
||||
if (serverCrashed) {
|
||||
throw new Error("Server crashed unexpectedly.");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!detectedURL) {
|
||||
throw new Error("Failed to detect server URL from logs.");
|
||||
}
|
||||
|
||||
console.log(`Server is now running at ${detectedURL}`);
|
||||
return detectedURL; // Return the detected URL
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user