mirror of
https://github.com/open-webui/desktop.git
synced 2026-07-09 14:57:14 -04:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf011d4642 | |||
| 96388c45c6 | |||
| db1b59a813 | |||
| 6d4ffea851 | |||
| 238cce6dc8 | |||
| b255f22fe7 | |||
| d205c176ef |
+45
-11
@@ -61,14 +61,15 @@ function createWindow(show = true): void {
|
||||
minHeight: 400,
|
||||
icon: path.join(__dirname, "assets/icon.png"),
|
||||
show: false,
|
||||
titleBarStyle: process.platform === "win32" ? "default" : "hidden",
|
||||
trafficLightPosition: { x: 16, y: 16 },
|
||||
...(process.platform === "win32"
|
||||
? {
|
||||
frame: false,
|
||||
frame: true,
|
||||
}
|
||||
: {}),
|
||||
...(process.platform === "linux" ? { icon } : {}),
|
||||
titleBarStyle: process.platform === "win32" ? "default" : "hidden",
|
||||
trafficLightPosition: { x: 16, y: 16 },
|
||||
...(process.platform !== "darwin" ? { titleBarOverlay: true } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, "../preload/index.js"),
|
||||
sandbox: false,
|
||||
@@ -129,8 +130,7 @@ function createWindow(show = true): void {
|
||||
{
|
||||
label: "Reset",
|
||||
click: async () => {
|
||||
await stopServerHandler();
|
||||
await resetApp();
|
||||
await resetAppHandler();
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -252,7 +252,7 @@ const uninstallHandler = async () => {
|
||||
});
|
||||
notification.show();
|
||||
} catch (error) {
|
||||
console.error("Uninstallation failed:", error);
|
||||
log.error("Uninstallation failed:", error);
|
||||
// Show error notification
|
||||
const notification = new Notification({
|
||||
title: "Open WebUI",
|
||||
@@ -318,7 +318,7 @@ const startServerHandler = async () => {
|
||||
|
||||
return true; // Indicate success
|
||||
} catch (error) {
|
||||
console.error("Failed to start server:", error);
|
||||
log.error("Failed to start server:", error);
|
||||
SERVER_STATUS = "failed";
|
||||
mainWindow?.webContents.send("main:data", {
|
||||
type: "status:server",
|
||||
@@ -354,11 +354,38 @@ const stopServerHandler = async () => {
|
||||
|
||||
return true; // Indicate success
|
||||
} catch (error) {
|
||||
console.error("Failed to stop server:", error);
|
||||
log.error("Failed to stop server:", error);
|
||||
return false; // Indicate failure
|
||||
}
|
||||
};
|
||||
|
||||
const resetAppHandler = async () => {
|
||||
try {
|
||||
await stopServerHandler(); // Stop the server if running
|
||||
SERVER_STATUS = null;
|
||||
|
||||
// wait a moment to ensure all processes are stopped
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
await resetApp(); // Reset the application state
|
||||
|
||||
// Show success notification
|
||||
const notification = new Notification({
|
||||
title: "Open WebUI",
|
||||
body: "Application has been reset successfully.",
|
||||
});
|
||||
notification.show();
|
||||
} catch (error) {
|
||||
log.error("Failed to reset application:", error);
|
||||
// Show error notification
|
||||
const notification = new Notification({
|
||||
title: "Open WebUI",
|
||||
body: `Failed to reset application: ${error.message}`,
|
||||
});
|
||||
notification.show();
|
||||
}
|
||||
};
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
app.quit(); // Quit if another instance is already running
|
||||
@@ -495,9 +522,16 @@ if (!gotTheLock) {
|
||||
return SERVER_STATUS;
|
||||
});
|
||||
|
||||
ipcMain.handle("app:info", async (event) => {
|
||||
return {
|
||||
version: app.getVersion(),
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
};
|
||||
});
|
||||
|
||||
ipcMain.handle("app:reset", async (event) => {
|
||||
await stopServerHandler();
|
||||
return await resetApp();
|
||||
return await resetAppHandler();
|
||||
});
|
||||
|
||||
ipcMain.handle("get:config", async (event) => {
|
||||
@@ -540,7 +574,7 @@ if (!gotTheLock) {
|
||||
);
|
||||
await installPackage("open-webui");
|
||||
} catch (error) {
|
||||
console.error("Failed to update package:", error);
|
||||
log.error("Failed to update package:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+131
-33
@@ -231,7 +231,7 @@ export const downloadFileWithProgress = async (
|
||||
return downloadPath;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Download failed:", error);
|
||||
log.error("Download failed:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -288,7 +288,7 @@ const downloadPython = async (onProgress = null) => {
|
||||
log.info(`✅ Python downloaded successfully to: ${result}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`❌ Download failed: ${error?.message}`);
|
||||
log.error(`❌ Download failed: ${error?.message}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -749,50 +749,148 @@ export const startServer = async (
|
||||
};
|
||||
|
||||
/**
|
||||
* Terminates all server processes.
|
||||
* Terminates all server processes with maximum reliability.
|
||||
*/
|
||||
export async function stopAllServers(): Promise<void> {
|
||||
log.info("Stopping all servers...");
|
||||
for (const pid of Array.from(serverPIDs)) {
|
||||
try {
|
||||
terminateProcessTree(pid);
|
||||
serverPIDs.delete(pid); // Remove from tracking set after termination
|
||||
|
||||
const pidsToStop = Array.from(serverPIDs);
|
||||
if (pidsToStop.length === 0) {
|
||||
log.info("No servers to stop.");
|
||||
return;
|
||||
}
|
||||
|
||||
// First pass: attempt graceful termination
|
||||
for (const pid of pidsToStop) {
|
||||
await terminateProcessTree(pid, false);
|
||||
}
|
||||
|
||||
// Wait a moment for graceful shutdown
|
||||
await sleep(2000);
|
||||
|
||||
// Second pass: force kill any remaining processes
|
||||
for (const pid of pidsToStop) {
|
||||
await terminateProcessTree(pid, true);
|
||||
}
|
||||
|
||||
// Final verification and cleanup
|
||||
for (const pid of pidsToStop) {
|
||||
if (!isProcessRunning(pid)) {
|
||||
serverPIDs.delete(pid);
|
||||
serverLogs.delete(pid);
|
||||
} catch (error) {
|
||||
console.error(`Error stopping server with PID ${pid}:`, error);
|
||||
} else {
|
||||
log.warn(
|
||||
`Process ${pid} may still be running after termination attempts`
|
||||
);
|
||||
}
|
||||
}
|
||||
log.info("All servers stopped successfully.");
|
||||
|
||||
log.info(
|
||||
`Stopped ${pidsToStop.length - serverPIDs.size}/${pidsToStop.length} servers successfully.`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills a process tree by PID.
|
||||
* Kills a process tree by PID with retry logic.
|
||||
*/
|
||||
function terminateProcessTree(pid: number): void {
|
||||
if (process.platform === "win32") {
|
||||
async function terminateProcessTree(
|
||||
pid: number,
|
||||
forceKill: boolean = false
|
||||
): Promise<void> {
|
||||
const maxRetries = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
execSync(`taskkill /PID ${pid} /T /F`);
|
||||
log.info(
|
||||
`Terminated server process tree (PID: ${pid}) on Windows.`
|
||||
);
|
||||
if (process.platform === "win32") {
|
||||
await terminateWindows(pid, forceKill);
|
||||
} else {
|
||||
await terminateUnix(pid, forceKill);
|
||||
}
|
||||
|
||||
// Verify termination
|
||||
if (!isProcessRunning(pid)) {
|
||||
log.info(`Successfully terminated process tree (PID: ${pid})`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to terminate process tree (PID: ${pid}):`,
|
||||
log.warn(
|
||||
`Attempt ${attempt}/${maxRetries} failed for PID ${pid}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
process.kill(-pid, "SIGKILL");
|
||||
log.info(
|
||||
`Terminated server process tree (PID: ${pid}) on Unix-like OS.`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to terminate process tree (PID: ${pid}):`,
|
||||
error
|
||||
);
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
await sleep(1000); // Wait before retry
|
||||
}
|
||||
}
|
||||
|
||||
log.error(
|
||||
`Failed to terminate process tree (PID: ${pid}) after ${maxRetries} attempts`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate process on Windows.
|
||||
*/
|
||||
async function terminateWindows(
|
||||
pid: number,
|
||||
forceKill: boolean
|
||||
): Promise<void> {
|
||||
const commands = forceKill
|
||||
? [`taskkill /PID ${pid} /T /F`]
|
||||
: [`taskkill /PID ${pid} /T`, `taskkill /PID ${pid} /T /F`];
|
||||
|
||||
for (const cmd of commands) {
|
||||
try {
|
||||
execSync(cmd, { timeout: 5000, stdio: "ignore" });
|
||||
await sleep(500); // Brief pause between commands
|
||||
} catch (error) {
|
||||
log.error(`Failed to terminate process (PID: ${pid}):`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate process on Unix-like systems.
|
||||
*/
|
||||
async function terminateUnix(pid: number, forceKill: boolean): Promise<void> {
|
||||
const signals = forceKill ? ["SIGKILL"] : ["SIGTERM", "SIGKILL"];
|
||||
|
||||
for (const signal of signals) {
|
||||
try {
|
||||
// Kill process group (negative PID)
|
||||
process.kill(-pid, signal);
|
||||
await sleep(500);
|
||||
|
||||
// Also try individual process if group kill fails
|
||||
if (isProcessRunning(pid)) {
|
||||
process.kill(pid, signal);
|
||||
await sleep(500);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to terminate process (PID: ${pid}):`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a process is still running.
|
||||
*/
|
||||
function isProcessRunning(pid: number): boolean {
|
||||
try {
|
||||
// Sending signal 0 checks if process exists without killing it
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple sleep utility.
|
||||
*/
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -854,7 +952,7 @@ export const checkUrlAndOpen = async (
|
||||
|
||||
// Start polling in the background (don't await)
|
||||
pollUrl().catch((error) => {
|
||||
console.error("Error in URL polling:", error);
|
||||
log.error("Error in URL polling:", error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -867,7 +965,7 @@ export const getConfig = async (): Promise<Record<string, any>> => {
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
console.error("Error reading config:", error);
|
||||
log.error("Error reading config:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -880,7 +978,7 @@ export const setConfig = async (config: Record<string, any>): Promise<void> => {
|
||||
JSON.stringify(config, null, 2)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error writing config:", error);
|
||||
log.error("Error writing config:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,6 +54,16 @@ const api = {
|
||||
await ipcRenderer.invoke("open:browser", { url });
|
||||
},
|
||||
|
||||
getAppInfo: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
"Access restricted: This operation is only allowed in a local environment."
|
||||
);
|
||||
}
|
||||
|
||||
return await ipcRenderer.invoke("app:info");
|
||||
},
|
||||
|
||||
getVersion: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
import Installation from "./lib/components/Installation.svelte";
|
||||
import Loading from "./lib/components/Loading.svelte";
|
||||
|
||||
import { info, config } from "./lib/stores";
|
||||
import { info, config, appInfo } from "./lib/stores";
|
||||
|
||||
let installed = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
appInfo.set(await window?.electronAPI?.getAppInfo());
|
||||
config.set(await window?.electronAPI?.getConfig());
|
||||
|
||||
const pythonStatus = await window?.electronAPI?.getPythonStatus();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { appInfo, info } from "../stores";
|
||||
|
||||
import Launching from "./Launching.svelte";
|
||||
import logoImage from "../assets/images/splash.png";
|
||||
import General from "./Controls/General.svelte";
|
||||
import About from "./Controls/About.svelte";
|
||||
import { info } from "../stores";
|
||||
|
||||
let { installed = $bindable(false) } = $props();
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
class="flex flex-col w-full h-full relative text-gray-850 dark:text-gray-100"
|
||||
>
|
||||
<div
|
||||
class="pt-3 pb-1.5 pl-22 pr-4 w-full drag-region flex flex-row gap-3 items-center justify-between"
|
||||
class="pt-3 pb-1.5 {$appInfo?.platform === 'darwin'
|
||||
? 'pl-22 pr-4'
|
||||
: 'px-4'} w-full drag-region flex flex-row gap-3 items-center justify-between"
|
||||
>
|
||||
<div class=" font-medium">Controls</div>
|
||||
|
||||
|
||||
@@ -49,10 +49,12 @@
|
||||
(await window.electronAPI.getPythonStatus()) &&
|
||||
(await window.electronAPI.getPackageStatus())
|
||||
) {
|
||||
// Notify the user that the installation is complete
|
||||
// Start the server if it's not already running
|
||||
if (!(await window.electronAPI.getServerStatus())) {
|
||||
await window.electronAPI.startServer();
|
||||
}
|
||||
|
||||
// Notify the user that the installation is complete
|
||||
await window.electronAPI.notification(
|
||||
"Installation Complete",
|
||||
"Open WebUI is now ready to use."
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export const appInfo = writable(null);
|
||||
|
||||
export const info = writable(null);
|
||||
export const config = writable(null);
|
||||
|
||||
Reference in New Issue
Block a user