diff --git a/index.html b/index.html index d8acfda..1c00fad 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ Open WebUI - +
diff --git a/src/main.ts b/src/main.ts index 6026c62..d85bf81 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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) => { diff --git a/src/preload.ts b/src/preload.ts index 9e05e8c..cfde1b8 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -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.'); diff --git a/src/render/App.svelte b/src/render/App.svelte index 83ff5af..b365b7b 100644 --- a/src/render/App.svelte +++ b/src/render/App.svelte @@ -1,6 +1,6 @@ diff --git a/src/render/lib/components/Main.svelte b/src/render/lib/components/Main.svelte index 15a50c5..0bb3174 100644 --- a/src/render/lib/components/Main.svelte +++ b/src/render/lib/components/Main.svelte @@ -1,13 +1,16 @@ diff --git a/src/render/lib/stores.ts b/src/render/lib/stores.ts index 0db9a8c..4df373c 100644 --- a/src/render/lib/stores.ts +++ b/src/render/lib/stores.ts @@ -1,3 +1,4 @@ import { writable } from 'svelte/store'; export const installStatus = writable(null); +export const serverStatus = writable(null); diff --git a/src/utils/index.ts b/src/utils/index.ts index 12f20e5..7da07e5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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 { 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://:" 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 }