This commit is contained in:
Timothy Jaeryang Baek 2025-01-11 22:55:04 -08:00
parent 8d97c049e6
commit 73740d7d73
7 changed files with 117 additions and 25 deletions

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<title>Open WebUI</title>
<link rel="preload" href="/assets/fonts/InstrumentSerif-Regular.ttf" as="font" crossorigin="anonymous" />
<link rel="preload" href="/assets/fonts/InstrumentSerif-Regular.ttf" as="font" />
</head>
<body>
<div id="app"></div>

View File

@ -9,7 +9,8 @@ import {
BrowserWindow,
globalShortcut,
Notification,
ipcMain
ipcMain,
ipcRenderer
} from 'electron';
import path from 'path';
import started from 'electron-squirrel-startup';
@ -17,6 +18,7 @@ import started from 'electron-squirrel-startup';
import {
installPackage,
removePackage,
logEmitter,
startServer,
stopAllServers,
validateInstallation
@ -56,6 +58,11 @@ if (!gotTheLock) {
let tray: Tray | null = null;
let SERVER_URL = null;
let SERVER_STATUS = 'stopped';
logEmitter.on('log', (message) => {
mainWindow?.webContents.send('main:log', message);
});
const loadDefaultView = () => {
// Load index.html or dev server URL
@ -66,6 +73,34 @@ if (!gotTheLock) {
}
};
const startServerHandler = async () => {
SERVER_STATUS = 'starting';
mainWindow.webContents.send('main:data', {
type: 'server:status',
data: SERVER_STATUS
});
try {
SERVER_URL = await startServer();
SERVER_STATUS = 'started';
mainWindow.webContents.send('main:data', {
type: 'server:status',
data: SERVER_STATUS
});
mainWindow.loadURL(SERVER_URL);
} catch (error) {
console.error('Failed to start server:', error);
SERVER_STATUS = 'failed';
mainWindow.webContents.send('main:data', {
type: 'server:status',
data: SERVER_STATUS
});
mainWindow.webContents.send('main:log', `Failed to start server: ${error}`);
}
};
const onReady = async () => {
console.log(process.resourcesPath);
console.log(app.getName());
@ -113,12 +148,7 @@ if (!gotTheLock) {
data: true
});
try {
SERVER_URL = await startServer();
mainWindow.loadURL(SERVER_URL);
} catch (error) {
console.error('Failed to start server:', error);
}
await startServerHandler();
} else {
mainWindow.webContents.send('main:data', {
type: 'install:status',
@ -204,16 +234,25 @@ if (!gotTheLock) {
removePackage();
});
ipcMain.handle('server:status', async (event) => {
return SERVER_STATUS;
});
ipcMain.handle('server:start', async (event) => {
console.log('Starting server...');
startServer();
await startServerHandler();
});
ipcMain.handle('server:stop', async (event) => {
console.log('Stopping server...');
stopAllServers();
await stopAllServers();
SERVER_STATUS = 'stopped';
mainWindow.webContents.send('main:data', {
type: 'server:status',
data: SERVER_STATUS
});
});
ipcMain.handle('server:url', async (event) => {

View File

@ -28,9 +28,12 @@ window.addEventListener('DOMContentLoaded', () => {
});
contextBridge.exposeInMainWorld('electronAPI', {
sendPing: async () => {
console.log('Sending PING to main process...');
await ipcRenderer.invoke('send-ping'); // Send the ping back to the main process
onLog: (callback: (message: string) => void) => {
if (!isLocalSource()) {
throw new Error('Access restricted: This operation is only allowed in a local environment.');
}
ipcRenderer.on('main:log', (_, message: string) => callback(message));
},
installPackage: async () => {
@ -53,6 +56,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
await ipcRenderer.invoke('remove');
},
getServerStatus: async () => {
if (!isLocalSource()) {
throw new Error('Access restricted: This operation is only allowed in a local environment.');
}
return await ipcRenderer.invoke('server:status');
},
startServer: async () => {
if (!isLocalSource()) {
throw new Error('Access restricted: This operation is only allowed in a local environment.');

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { installStatus } from './lib/stores';
import { installStatus, serverStatus } from './lib/stores';
import Main from './lib/components/Main.svelte';
@ -24,6 +24,12 @@
break;
case 'electron:server:status':
console.log('Server status:', event.data.data);
serverStatus.set(event.data.data);
break;
default:
console.warn('Unhandled message type:', event.data.type);
}
@ -32,6 +38,11 @@
if (window.electronAPI) {
installStatus.set(await window.electronAPI.getInstallStatus());
serverStatus.set(await window.electronAPI.getServerStatus());
window.electronAPI.onLog((log) => {
console.log('Electron log:', log);
});
}
});
</script>

View File

@ -1,13 +1,16 @@
<script lang="ts">
import { onMount } from 'svelte';
import { installStatus } from '../stores';
import { installStatus, serverStatus } from '../stores';
import Spinner from './common/Spinner.svelte';
import ArrowRightCircle from './icons/ArrowRightCircle.svelte';
let installing = false;
const continueHandler = async () => {
if (window?.electronAPI) {
window.electronAPI.installPackage();
installing = true;
}
};
</script>

View File

@ -1,3 +1,4 @@
import { writable } from 'svelte/store';
export const installStatus = writable(null);
export const serverStatus = writable(null);

View File

@ -1,6 +1,9 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import net from 'net';
import crypto from 'crypto';
import {
exec,
execFile,
@ -10,13 +13,16 @@ import {
spawn,
ChildProcess
} from 'child_process';
import net from 'net';
import { EventEmitter } from 'events';
import * as tar from 'tar';
import log from 'electron-log';
import { app } from 'electron';
// Create and export a global event emitter specifically for logs
export const logEmitter = new EventEmitter();
////////////////////////////////////////////////
//
// General Utils
@ -60,6 +66,18 @@ export function getOpenWebUIDataPath(): string {
return openWebUIDataDir;
}
export function getSecretKey(keyPath?: string, key?: string): string {
keyPath = keyPath || path.join(getOpenWebUIDataPath(), '.key');
if (fs.existsSync(keyPath)) {
return fs.readFileSync(keyPath, 'utf-8');
}
key = key || crypto.randomBytes(64).toString('hex');
fs.writeFileSync(keyPath, key);
return key;
}
export async function portInUse(port: number, host: string = '127.0.0.1'): Promise<boolean> {
return new Promise((resolve) => {
const client = new net.Socket();
@ -182,22 +200,21 @@ export async function installOpenWebUI(installationPath: string) {
// unpackCommand = `${createAdHocSignCommand(installationPath)}\n${unpackCommand}`;
// }
console.log(unpackCommand);
const commandProcess = exec(unpackCommand, {
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/bash'
});
commandProcess.stdout?.on('data', (data) => {
const onLog = (data) => {
console.log(data);
});
logEmitter.emit('log', data);
};
commandProcess.stderr?.on('data', (data) => {
console.error(data);
});
commandProcess.stdout?.on('data', onLog);
commandProcess.stderr?.on('data', onLog);
commandProcess.on('exit', (code) => {
console.log(`Child exited with code ${code}`);
logEmitter.emit('log', `Child exited with code ${code}`);
});
}
@ -209,6 +226,7 @@ export async function installBundledPython(installationPath?: string) {
console.log(installationPath, pythonTarPath);
if (!fs.existsSync(pythonTarPath)) {
log.error('Python tarball not found');
logEmitter.emit('log', 'Python tarball not found'); // Emit log
return;
}
@ -220,6 +238,7 @@ export async function installBundledPython(installationPath?: string) {
});
} catch (error) {
log.error(error);
logEmitter.emit('log', error); // Emit log
}
// Get the path to the installed Python binary
@ -227,6 +246,7 @@ export async function installBundledPython(installationPath?: string) {
if (!fs.existsSync(bundledPythonPath)) {
log.error('Python binary not found in install path');
logEmitter.emit('log', 'Python binary not found in install path'); // Emit log
return;
}
@ -236,6 +256,7 @@ export async function installBundledPython(installationPath?: string) {
encoding: 'utf-8'
});
console.log('Installed Python Version:', pythonVersion.trim());
logEmitter.emit('log', `Installed Python Version: ${pythonVersion.trim()}`); // Emit log
} catch (error) {
log.error('Failed to execute Python binary', error);
}
@ -319,6 +340,7 @@ export async function startServer(installationPath?: string, port?: number): Pro
if (!(await validateInstallation(installationPath))) {
console.error('Failed to validate installation');
logEmitter.emit('log', 'Failed to validate installation'); // Emit log
return;
}
@ -327,11 +349,11 @@ export async function startServer(installationPath?: string, port?: number): Pro
? `${installationPath}\\Scripts\\activate.bat && set DATA_DIR="${path.join(
app.getPath('userData'),
'data'
)}" && open-webui serve`
)}" && set WEBUI_SECRET_KEY=${getSecretKey()} && open-webui serve`
: `source "${installationPath}/bin/activate" && export DATA_DIR="${path.join(
app.getPath('userData'),
'data'
)}" && open-webui serve`;
)}" && export WEBUI_SECRET_KEY=${getSecretKey()} && open-webui serve`;
port = port || 8080;
while (await portInUse(port)) {
@ -341,6 +363,8 @@ export async function startServer(installationPath?: string, port?: number): Pro
startCommand += ` --port ${port}`;
console.log('Starting Open-WebUI server...');
logEmitter.emit('log', 'Starting Open-WebUI server...'); // Emit log
const childProcess = spawn(startCommand, {
shell: true,
detached: true,
@ -356,6 +380,7 @@ export async function startServer(installationPath?: string, port?: number): Pro
const handleLog = (data: Buffer) => {
const logLine = data.toString().trim();
console.log(`[Open-WebUI Log]: ${logLine}`);
logEmitter.emit('log', logLine);
// Look for "Uvicorn running on http://<hostname>:<port>"
const match = logLine.match(
@ -386,6 +411,7 @@ export async function startServer(installationPath?: string, port?: number): Pro
if (childProcess.pid) {
serverPIDs.add(childProcess.pid);
console.log(`Server started with PID: ${childProcess.pid}`);
logEmitter.emit('log', `Server started with PID: ${childProcess.pid}`); // Emit PID log
} else {
throw new Error('Failed to start server: No PID available');
}
@ -405,6 +431,7 @@ export async function startServer(installationPath?: string, port?: number): Pro
}
console.log(`Server is now running at ${detectedURL}`);
logEmitter.emit('log', `Server is now running at ${detectedURL}`); // Emit server URL log
return detectedURL; // Return the detected URL
}