mirror of
https://github.com/open-webui/desktop
synced 2025-06-26 18:15:59 +00:00
refac
This commit is contained in:
parent
8d97c049e6
commit
73740d7d73
@ -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>
|
||||||
|
57
src/main.ts
57
src/main.ts
@ -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) => {
|
||||||
|
@ -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.');
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user