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> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Open WebUI</title> <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> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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