Compare commits

...

7 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek cf011d4642 refac 2025-07-29 15:21:28 +04:00
Timothy Jaeryang Baek 96388c45c6 refac 2025-07-29 15:04:15 +04:00
Timothy Jaeryang Baek db1b59a813 refac 2025-07-29 14:41:33 +04:00
Timothy Jaeryang Baek 6d4ffea851 refac 2025-07-29 14:33:52 +04:00
Timothy Jaeryang Baek 238cce6dc8 refac 2025-07-29 14:31:03 +04:00
Timothy Jaeryang Baek b255f22fe7 refac 2025-07-29 14:30:09 +04:00
Timothy Jaeryang Baek d205c176ef refac 2025-07-29 14:19:48 +04:00
7 changed files with 197 additions and 48 deletions
+45 -11
View File
@@ -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
View File
@@ -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;
}
};
+10
View File
@@ -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(
+2 -1
View File
@@ -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."
+2
View File
@@ -1,4 +1,6 @@
import { writable } from "svelte/store";
export const appInfo = writable(null);
export const info = writable(null);
export const config = writable(null);