mirror of
https://github.com/open-webui/desktop.git
synced 2026-07-16 02:28:22 -04:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ef21b395c | |||
| b83f790e75 | |||
| 67c83f328d | |||
| 2a644345d4 | |||
| 3f8fe6cfe1 | |||
| f2db94ed13 | |||
| db2e170291 | |||
| 32224983cf | |||
| cf011d4642 | |||
| 96388c45c6 | |||
| db1b59a813 | |||
| 6d4ffea851 | |||
| 238cce6dc8 | |||
| b255f22fe7 | |||
| d205c176ef | |||
| 2a223e62b6 | |||
| eb2ebd2c1d | |||
| a6a944c847 | |||
| 03fea4a7cc | |||
| 6b159246d1 |
@@ -3,7 +3,7 @@ name: Build and Release Electron App (electron-builder)
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@@ -13,8 +13,8 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# - os: ubuntu-latest
|
||||
# arch: x64
|
||||
- os: ubuntu-latest
|
||||
arch: x64
|
||||
# - os: ubuntu-latest
|
||||
# arch: arm64
|
||||
- os: windows-latest
|
||||
@@ -97,22 +97,27 @@ jobs:
|
||||
echo "dist directory not found"
|
||||
fi
|
||||
|
||||
- name: Find and Rename Linux Package
|
||||
- name: Find and Rename Linux Packages
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
run: |
|
||||
if [ -d "dist" ]; then
|
||||
package_file=$(find dist -maxdepth 1 -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" -o -name "*.tar.gz" | head -1)
|
||||
if [ -n "$package_file" ]; then
|
||||
extension="${package_file##*.}"
|
||||
if [[ "$package_file" == *.tar.gz ]]; then
|
||||
shopt -s nullglob
|
||||
for package_file in dist/*.deb dist/*.rpm dist/*.AppImage dist/*.tar.gz; do
|
||||
[ -e "$package_file" ] || continue
|
||||
filename=$(basename "$package_file")
|
||||
extension="${filename##*.}"
|
||||
if [[ "$filename" == *.tar.gz ]]; then
|
||||
extension="tar.gz"
|
||||
fi
|
||||
cp "$package_file" "${{ matrix.os }}-${{ matrix.arch }}.$extension"
|
||||
echo "Copied package to: ${{ matrix.os }}-${{ matrix.arch }}.$extension"
|
||||
else
|
||||
echo "No Linux package found in dist"
|
||||
ls -la dist/ || echo "dist directory not found or empty"
|
||||
fi
|
||||
# Use original base name (without extension) as suffix to avoid collisions
|
||||
base="${filename%.*}"
|
||||
if [[ "$filename" == *.tar.gz ]]; then
|
||||
base="${filename%.tar.gz}"
|
||||
fi
|
||||
dest="${{ matrix.os }}-${{ matrix.arch }}-${base}.${extension}"
|
||||
cp "$package_file" "$dest"
|
||||
echo "Copied $package_file to $dest"
|
||||
done
|
||||
else
|
||||
echo "dist directory not found"
|
||||
fi
|
||||
@@ -150,7 +155,7 @@ jobs:
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/release'
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -1,53 +1,38 @@
|
||||
# Open WebUI Desktop 🌐
|
||||
|
||||

|
||||
**Open WebUI App** is the cross-platform desktop application for [Open WebUI](https://github.com/open-webui/open-webui). It brings the _full-featured Open WebUI experience_ directly to your device, effectively transforming it into a powerful server—without the complexities of manual setup.
|
||||
|
||||
**Open WebUI App** is the upcoming cross-platform desktop application for [Open WebUI](https://github.com/open-webui/open-webui). It brings the *full-featured Open WebUI experience* directly to your device, effectively transforming it into a powerful server—without the complexities of manual setup.
|
||||
This project is currently in **alpha** and under active development. 🛠️ Expect frequent updates and potential changes as we refine the application.
|
||||
|
||||
This project is still in an **experimental phase** and under active development. 🛠️ Expect frequent updates and potential changes as we refine the application.
|
||||
## Download 📥
|
||||
Get the latest alpha release from our [releases page](https://github.com/open-webui/desktop/releases).
|
||||
|
||||
---
|
||||
**Note**: An internet connection is required for initial setup, but afterwards the application can be used completely offline.
|
||||
|
||||
## Features
|
||||
- **One-Click Installation**: Quickly and effortlessly install and set up Open WebUI with all its dependencies. This feature is fully functional and ready to make your setup a breeze.
|
||||
- **Cross-Platform Support**: Compatible with Windows, macOS, and Linux to ensure broad accessibility.
|
||||
|
||||
---
|
||||
- **Offline Capability**: After initial setup, use the application completely offline for enhanced privacy and reliability.
|
||||
|
||||
## Project Setup
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
npm install
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
$ npm run dev
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# For windows
|
||||
$ npm run build:win
|
||||
|
||||
npm run build:win
|
||||
# For macOS
|
||||
$ npm run build:mac
|
||||
|
||||
npm run build:mac
|
||||
# For Linux
|
||||
$ npm run build:linux
|
||||
npm run build:linux
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License 📜
|
||||
This project is licensed under the **Open WebUI Sustainable Use License**. For details, see [LICENSE](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
## Stay Tuned! 🌟
|
||||
|
||||
We're actively developing Open WebUI App. Follow [Open WebUI](https://github.com/open-webui/open-webui) for updates, and join the [community on Discord](https://discord.gg/5rJgQTnV4s) to stay involved.
|
||||
We're actively developing Open WebUI App. Follow [Open WebUI](https://github.com/open-webui/open-webui) for updates, and join the [community on Discord](https://discord.gg/5rJgQTnV4s) to stay involved.
|
||||
+53
-10
@@ -61,14 +61,16 @@ 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 },
|
||||
autoHideMenuBar: true,
|
||||
...(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,
|
||||
@@ -125,6 +127,13 @@ function createWindow(show = true): void {
|
||||
uninstallHandler();
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
label: "Reset",
|
||||
click: async () => {
|
||||
await resetAppHandler();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const updatedMenu = Menu.buildFromTemplate(menuTemplate);
|
||||
@@ -244,7 +253,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",
|
||||
@@ -298,7 +307,7 @@ const startServerHandler = async () => {
|
||||
// Show system notification
|
||||
const notification = new Notification({
|
||||
title: "Open WebUI",
|
||||
body: "Server is now available and opened in your browser",
|
||||
body: "Open WebUI is now available and opened in your browser",
|
||||
});
|
||||
notification.show();
|
||||
|
||||
@@ -310,7 +319,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",
|
||||
@@ -346,11 +355,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
|
||||
@@ -487,9 +523,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) => {
|
||||
@@ -532,7 +575,7 @@ if (!gotTheLock) {
|
||||
);
|
||||
await installPackage("open-webui");
|
||||
} catch (error) {
|
||||
console.error("Failed to update package:", error);
|
||||
log.error("Failed to update package:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+161
-66
@@ -231,7 +231,7 @@ export const downloadFileWithProgress = async (
|
||||
return downloadPath;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Download failed:", error);
|
||||
log.error("Download failed:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -249,7 +249,7 @@ export const getPythonDownloadPath = (): string => {
|
||||
return downloadPath;
|
||||
};
|
||||
|
||||
export const getPythonInstallationPath = (): string => {
|
||||
export const getPythonInstallationDir = (): string => {
|
||||
const installDir = path.join(app.getPath("userData"), "python");
|
||||
|
||||
if (!fs.existsSync(installDir)) {
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -311,7 +311,7 @@ const checkInternet = async () => {
|
||||
};
|
||||
|
||||
export const installPython = async (
|
||||
installationPath?: string
|
||||
installationDir?: string
|
||||
): Promise<boolean> => {
|
||||
let pythonDownloadPath = getPythonDownloadPath();
|
||||
if (!isPythonDownloaded()) {
|
||||
@@ -326,9 +326,6 @@ export const installPython = async (
|
||||
log.info(
|
||||
`Downloading Python: ${progress.toFixed(2)}% (${downloaded} of ${total} bytes)`
|
||||
);
|
||||
log.info(
|
||||
`Downloading Python: ${progress.toFixed(2)}% (${downloaded} of ${total} bytes)`
|
||||
);
|
||||
});
|
||||
}
|
||||
if (!fs.existsSync(pythonDownloadPath)) {
|
||||
@@ -336,8 +333,8 @@ export const installPython = async (
|
||||
return false;
|
||||
}
|
||||
|
||||
installationPath = installationPath || getPythonInstallationPath();
|
||||
log.info(installationPath, pythonDownloadPath);
|
||||
installationDir = installationDir || getPythonInstallationDir();
|
||||
log.info(installationDir, pythonDownloadPath);
|
||||
|
||||
try {
|
||||
const userDataPath = getUserDataPath();
|
||||
@@ -351,8 +348,8 @@ export const installPython = async (
|
||||
}
|
||||
|
||||
// Get the path to the installed Python binary
|
||||
if (isPythonInstalled(installationPath)) {
|
||||
const pythonPath = getPythonPath(installationPath);
|
||||
if (isPythonInstalled(installationDir)) {
|
||||
const pythonPath = getPythonPath(installationDir);
|
||||
|
||||
execFileSync(pythonPath, ["-m", "pip", "install", "uv"], {
|
||||
encoding: "utf-8",
|
||||
@@ -382,14 +379,14 @@ export const getPythonExecutablePath = (envPath: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getPythonPath = (installationPath?: string) => {
|
||||
export const getPythonPath = (installationDir?: string) => {
|
||||
return path.normalize(
|
||||
getPythonExecutablePath(installationPath || getPythonInstallationPath())
|
||||
getPythonExecutablePath(installationDir || getPythonInstallationDir())
|
||||
);
|
||||
};
|
||||
|
||||
export const isPythonInstalled = (installationPath?: string) => {
|
||||
const pythonPath = getPythonPath(installationPath);
|
||||
export const isPythonInstalled = (installationDir?: string) => {
|
||||
const pythonPath = getPythonPath(installationDir);
|
||||
|
||||
if (!fs.existsSync(pythonPath)) {
|
||||
log.error("Python binary not found in install path");
|
||||
@@ -416,8 +413,8 @@ export const isPythonInstalled = (installationPath?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const isUvInstalled = (installationPath?: string) => {
|
||||
const pythonPath = getPythonPath(installationPath);
|
||||
export const isUvInstalled = (installationDir?: string) => {
|
||||
const pythonPath = getPythonPath(installationDir);
|
||||
try {
|
||||
// Check if uv is installed by running the command
|
||||
const result = execFileSync(pythonPath, ["-m", "uv", "--version"], {
|
||||
@@ -441,16 +438,17 @@ export const isUvInstalled = (installationPath?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const uninstallPython = (installationPath?: string): boolean => {
|
||||
installationPath = installationPath || getPythonInstallationPath();
|
||||
export const uninstallPython = (installationDir?: string): boolean => {
|
||||
installationDir = installationDir || getPythonInstallationDir();
|
||||
|
||||
if (!fs.existsSync(installationPath)) {
|
||||
if (!fs.existsSync(installationDir)) {
|
||||
log.error("Python installation not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
fs.rmSync(installationPath, { recursive: true });
|
||||
fs.rmSync(installationDir, { recursive: true, force: true });
|
||||
log.info("Python installation removed successfully:", installationDir);
|
||||
} catch (error) {
|
||||
log.error("Failed to remove Python installation", error);
|
||||
return false;
|
||||
@@ -471,17 +469,6 @@ export const resetApp = async (): Promise<void> => {
|
||||
await uninstallPython();
|
||||
log.info("Uninstalled Python environment");
|
||||
|
||||
// remove /data folder
|
||||
const dataPath = getOpenWebUIDataPath();
|
||||
if (fs.existsSync(dataPath)) {
|
||||
try {
|
||||
fs.rmSync(dataPath, { recursive: true });
|
||||
log.info("Removed data directory:", dataPath);
|
||||
} catch (error) {
|
||||
log.error("Failed to remove data directory:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// remove config file
|
||||
const configPath = path.join(getUserDataPath(), "config.json");
|
||||
if (fs.existsSync(configPath)) {
|
||||
@@ -503,6 +490,17 @@ export const resetApp = async (): Promise<void> => {
|
||||
log.error("Failed to remove secret key file:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// remove /data folder
|
||||
const dataPath = getOpenWebUIDataPath();
|
||||
if (fs.existsSync(dataPath)) {
|
||||
try {
|
||||
fs.rmSync(dataPath, { recursive: true, force: true });
|
||||
log.info("Removed data directory:", dataPath);
|
||||
} catch (error) {
|
||||
log.error("Failed to remove data directory:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////
|
||||
@@ -670,16 +668,15 @@ export const startServer = async (
|
||||
const pythonPath = getPythonPath();
|
||||
log.info(`Using Python at: ${pythonPath}`);
|
||||
|
||||
const openWebUIPath = path.join(path.dirname(pythonPath), "open-webui");
|
||||
|
||||
let commandArgs: string[];
|
||||
commandArgs = ["-m", "uv", "run", "open-webui", "serve", "--host", host];
|
||||
|
||||
const dataDir = path.join(app.getPath("userData"), "data");
|
||||
const dataDir = getOpenWebUIDataPath();
|
||||
const secretKey = getSecretKey();
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
process.env.DATA_DIR = dataDir;
|
||||
process.env.WEBUI_SECRET_KEY = secretKey;
|
||||
|
||||
@@ -752,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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -857,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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -870,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;
|
||||
}
|
||||
};
|
||||
@@ -883,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();
|
||||
|
||||
@@ -154,7 +154,6 @@ html {
|
||||
.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.scrollbar-hidden::-webkit-scrollbar-thumb {
|
||||
visibility: hidden;
|
||||
}
|
||||
@@ -162,3 +161,40 @@ html {
|
||||
.scrollbar-hidden::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-none::-webkit-scrollbar {
|
||||
display: none; /* for Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
.scrollbar-none::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-none {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
--tw-border-opacity: 1;
|
||||
background-color: rgba(215, 215, 215, 0.8);
|
||||
border-color: rgba(255, 255, 255, var(--tw-border-opacity));
|
||||
border-radius: 9999px;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
/* Dark theme scrollbar styles */
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(67, 67, 67, 0.8); /* Darker color for dark theme */
|
||||
border-color: rgba(0, 0, 0, var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
height: 0.6rem;
|
||||
width: 0.4rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -59,8 +59,9 @@
|
||||
await window.electronAPI.resetApp();
|
||||
toast.success("App has been reset successfully.");
|
||||
|
||||
// refresh the page to apply changes
|
||||
window.location.reload();
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
toast.error("Failed to reset the app");
|
||||
}
|
||||
|
||||
@@ -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