feat: Installer mit automatischer VirtViewer-Abhängigkeit
- Windows NSIS: lädt VirtViewer MSI herunter und installiert es während der App-Installation (via PowerShell + msiexec) - Linux .deb: Depends: virt-viewer — apt zieht es automatisch rein - Linux AppImage: Startup-Check mit pkexec-Dialog falls virt-viewer fehlt - BUILD.md aktualisiert: Installer-Verhalten dokumentiert
This commit is contained in:
+62
-10
@@ -2,7 +2,7 @@ const { app, BrowserWindow, ipcMain, dialog } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const { spawn } = require('child_process');
|
||||
const { spawn, spawnSync } = require('child_process');
|
||||
const Store = require('electron-store');
|
||||
const ProxmoxClient = require('./proxmox');
|
||||
|
||||
@@ -38,6 +38,7 @@ function createWindow(file, width, height, resizable = false) {
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow('login.html', 460, 500, false);
|
||||
checkDependencies();
|
||||
app.on('activate', () => {
|
||||
if (!mainWindow) createWindow('login.html', 460, 500, false);
|
||||
});
|
||||
@@ -47,6 +48,63 @@ app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
// ---------- Dependency check (AppImage / manual install on Linux) ----------
|
||||
|
||||
function checkDependencies() {
|
||||
// On Windows the NSIS installer handles virt-viewer — no runtime check needed.
|
||||
// On Linux (AppImage or manual install) we check here so the user knows before
|
||||
// trying to connect for the first time.
|
||||
if (process.platform !== 'linux') return;
|
||||
|
||||
const candidates = ['remote-viewer', 'virt-viewer'];
|
||||
const found = candidates.some(bin => {
|
||||
const r = spawnSync('which', [bin], { encoding: 'utf8' });
|
||||
return r.status === 0;
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
setImmediate(() => {
|
||||
const choice = dialog.showMessageBoxSync(mainWindow, {
|
||||
type: 'warning',
|
||||
title: 'Abhängigkeit fehlt',
|
||||
message: 'VirtViewer nicht gefunden',
|
||||
detail:
|
||||
'VirtViewer (remote-viewer) wird für SPICE-Verbindungen benötigt und ist auf diesem System nicht installiert.\n\n' +
|
||||
'Installieren mit:\n sudo apt install virt-viewer\n\n' +
|
||||
'Soll die App versuchen, VirtViewer jetzt automatisch zu installieren?',
|
||||
buttons: ['Jetzt installieren', 'Später (manuell)'],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
});
|
||||
|
||||
if (choice === 0) {
|
||||
const installer = spawn(
|
||||
'pkexec',
|
||||
['apt', 'install', '-y', 'virt-viewer'],
|
||||
{ detached: false, stdio: 'ignore' }
|
||||
);
|
||||
installer.on('close', code => {
|
||||
if (code === 0) {
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'info',
|
||||
title: 'VirtViewer installiert',
|
||||
message: 'VirtViewer wurde erfolgreich installiert.',
|
||||
buttons: ['OK'],
|
||||
});
|
||||
} else {
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'error',
|
||||
title: 'Installation fehlgeschlagen',
|
||||
message: `Installation schlug fehl (Code ${code}).\nBitte manuell ausführen:\n sudo apt install virt-viewer`,
|
||||
buttons: ['OK'],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- IPC: credentials ----------
|
||||
|
||||
ipcMain.handle('credentials:load', () => store.get('credentials', null));
|
||||
@@ -65,7 +123,6 @@ ipcMain.handle('proxmox:login', async (_e, { host, username, password }) => {
|
||||
try {
|
||||
client = new ProxmoxClient(host);
|
||||
await client.login(username, password);
|
||||
// Switch to VM list view
|
||||
createWindow('vms.html', 680, 520, true);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
@@ -103,7 +160,6 @@ ipcMain.handle('proxmox:connect', async (_e, { node, vmid }) => {
|
||||
function buildVVFile(params, vmid) {
|
||||
const lines = ['[virt-viewer]', `type=${params.type || 'spice'}`];
|
||||
|
||||
// Proxmox spiceproxy returns 'proxy' as the SPICE host
|
||||
const host = params.proxy || params.host || client.host.split(':')[0];
|
||||
lines.push(`host=${host}`);
|
||||
|
||||
@@ -111,7 +167,6 @@ function buildVVFile(params, vmid) {
|
||||
if (params.port) lines.push(`port=${params.port}`);
|
||||
if (params.password) lines.push(`password=${params.password}`);
|
||||
|
||||
// Write CA cert to temp file if provided
|
||||
if (params.ca) {
|
||||
const caPath = path.join(os.tmpdir(), 'proxmox-spice-ca.pem');
|
||||
fs.writeFileSync(caPath, params.ca);
|
||||
@@ -119,14 +174,13 @@ function buildVVFile(params, vmid) {
|
||||
}
|
||||
if (params['host-subject']) lines.push(`host-subject=${params['host-subject']}`);
|
||||
|
||||
// UX + USB
|
||||
lines.push('fullscreen=0');
|
||||
lines.push('title=%d — SPICE');
|
||||
lines.push('delete-this-file=1'); // remote-viewer deletes the file after reading (hides password)
|
||||
lines.push('delete-this-file=1');
|
||||
lines.push('toggle-fullscreen=shift+f11');
|
||||
lines.push('release-cursor=shift+f12');
|
||||
lines.push('secure-attention=ctrl+alt+end');
|
||||
lines.push('usb-filter=-1,-1,-1,-1,0'); // allow all USB devices
|
||||
lines.push('usb-filter=-1,-1,-1,-1,0');
|
||||
|
||||
const vvPath = path.join(os.tmpdir(), `spice-${vmid}-${Date.now()}.vv`);
|
||||
fs.writeFileSync(vvPath, lines.join('\n') + '\n', { mode: 0o600 });
|
||||
@@ -143,8 +197,6 @@ function launchRemoteViewer(vvPath) {
|
||||
]
|
||||
: ['remote-viewer', 'virt-viewer'];
|
||||
|
||||
let launched = false;
|
||||
|
||||
function tryNext(i) {
|
||||
if (i >= candidates.length) {
|
||||
dialog.showErrorBox(
|
||||
@@ -158,7 +210,7 @@ function launchRemoteViewer(vvPath) {
|
||||
}
|
||||
const child = spawn(candidates[i], [vvPath], { detached: true, stdio: 'ignore' });
|
||||
child.on('error', () => tryNext(i + 1));
|
||||
child.on('spawn', () => { launched = true; });
|
||||
child.on('spawn', () => { });
|
||||
child.unref();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user