UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

682 lines (617 loc) 21.7 kB
/** * This is for helping users to do some tasks with minikube. * * This script only uses node stdlib modules. * * manage-minikube.js ask-restart-windows * manage-minikube.js setup-hyperV-windows-networking * manage-minikube.js ask-restart-libvirt-ubuntu24 */ const { exec, execSync, spawn, spawnSync } = require('child_process'); const { readFileSync } = require('fs'); const { createServer } = require('net'); const { platform } = process; // @ts-check // enable typescript checking of a js file const DEBUG = false; /** * Requests a system restart on Windows with elevated privileges. */ function askRestartWindows() { exec( 'powershell -Command "Start-Process shutdown -ArgumentList \'/r /t 0\' -Verb RunAs"', error => { if (error) { console.error('Failed to request restart:', error.message); } else { console.log('Restart requested with elevation.'); } } ); } // Start minikube with elevated privs. // Opens a new window. // powershell -Command "Start-Process 'minikube' -ArgumentList 'start --driver=hyperv' -Verb RunAs" // Start-Process "cmd.exe" -ArgumentList "/c minikube start --driver=hyperv > C:\temp\log.txt 2>&1" -Verb RunAs -WindowStyle Hidden // powershell -Command "Start-Process 'cmd.exe' -ArgumentList '/c minikube start --driver=hyperv > C:\temp\log.txt 2>&1' -Verb RunAs -WindowStyle Hidden" /** * For Hyper-V Windows networking setup. */ function setupHyperVWindowsNetworking() { //@todo: when we know what is needed,, console.log('tbd'); } /** * Prompts the user to restart libvirt service on Ubuntu 24 systems. * * This function verifies the current platform is Linux and specifically Ubuntu 24 * before prompting the user to restart the libvirt service. This is typically * needed after making configuration changes to libvirt. * * @returns {void} * @throws {Error} If there's an issue checking the OS information */ function askRestartLibvirtUbuntu24() { if (platform !== 'linux') { console.error('This command is only available on Linux.'); return; } try { const osRelease = readFileSync('/etc/os-release', 'utf8'); if (!osRelease.includes('Ubuntu') || !osRelease.includes('24.')) { console.error('This command is specific to Ubuntu 24.'); return; } // User input handling would go here } catch (error) { console.error('Failed to check OS or restart libvirt:', error.message); } } /** * This is designed to run a command (specifically `minikube start`) * in an **elevated** (administrator) context * while keeping the command window **hidden** in the background. * * This is necessary because: * * - `minikube` with the Hyper-V driver often requires administrative privileges. * - Running `cmd.exe` with `Start-Process -Verb RunAs` elevates the process, * but detaches it from the parent process. * - To capture the output (stdout, stderr) and exit code from the elevated process, * we use **named pipes**. * * - Creates three named pipes for stdout, stderr, and exit code. * - Launches an elevated `cmd.exe` process via PowerShell using `Start-Process`. * - Redirects output and error streams to the named pipes using standard CMD redirection (`>`, `2>`, `&`). * - Writes stdout and stderr to the Node.js process's own output streams. * - Exits the Node.js process with the same exit code as the `minikube` command. * * This approach allows for elevated execution with full output capture, * without showing a visible command window. * * Additionally the UAC screen shows cmd by Microsoft, and the user can * see the command being run with privileges. * * For example it shows the user on the UAC screen: * `cmd.exe /c minikube start --driver=hyperv > \\.\pipe\minikube-123456-stdout 2> \\.\pipe\minikube-123456-stderr & echo %ERRORLEVEL% > \\.\pipe\minikube-123456-exit` * Not: * `Electron somescript.js` */ function runPrivilegedCommand(mainCommand) { const pipeBase = `\\\\.\\pipe\\minikube-${Math.floor(Math.random() * 1000000)}`; const pipes = { stdout: `${pipeBase}-stdout`, stderr: `${pipeBase}-stderr`, exit: `${pipeBase}-exit`, }; let exitCode = null; // Create named pipe servers const createPipeServer = (name, outputStream, onClose) => { const server = createServer(stream => { stream.on('data', data => { outputStream.write(data.toString()); }); stream.on('end', () => { if (onClose) onClose(); }); }); server.listen(name, () => { if (DEBUG) { console.log(`Listening on ${name}`); } }); return server; }; // Track when all pipes are done let pipesClosed = 0; const onPipeClosed = () => { pipesClosed++; if (pipesClosed === 3) { process.exit(exitCode ?? 1); // Default to 1 if exit code wasn't received } }; // Start pipe servers createPipeServer(pipes.stdout, process.stdout, onPipeClosed); createPipeServer(pipes.stderr, process.stderr, onPipeClosed); createPipeServer( pipes.exit, { write: data => { const code = parseInt(data.toString().trim(), 10); if (!isNaN(code)) { exitCode = code; } }, }, onPipeClosed ); // PowerShell command to run cmd.exe with redirection to named pipes const psCommand = ` Start-Process 'cmd.exe' -ArgumentList '/c ${mainCommand} > ${pipes.stdout} 2> ${pipes.stderr} & echo %ERRORLEVEL% > ${pipes.exit}' -Verb RunAs -WindowStyle Hidden `; // Launch PowerShell to run the command spawn('powershell.exe', ['-Command', psCommand], { stdio: 'ignore', windowsHide: true, }); } /** * * @param {string[]} args extra arguments to pass to minikube start */ function startMinikubeHyperV(args) { if (!detectIfHyperVRunning()) { console.error('Hyper-V is not running. Starting Hyper-V service...'); runPrivilegedCommand( 'powershell -Command "Start-Service vmms"' ); } const mainCommand = 'minikube start --driver=hyperv' + (args.length > 0 ? ' ' + args.join(' ') : ''); runPrivilegedCommand(mainCommand); } function stopMinikubeHyperV(args) { if (!detectIfHyperVRunning()) { console.error('Hyper-V is not running. Starting Hyper-V service...'); runPrivilegedCommand( 'powershell -Command "Start-Service vmms"' ); } const mainCommand = 'minikube stop' + (args.length > 0 ? ' ' + args.join(' ') : ''); runPrivilegedCommand(mainCommand); } function deleteMinikubeHyperV(args) { if (!detectIfHyperVRunning()) { console.error('Hyper-V is not running. Starting Hyper-V service...'); runPrivilegedCommand( 'powershell -Command "Start-Service vmms"' ); } const mainCommand = 'minikube delete' + (args.length > 0 ? ' ' + args.join(' ') : ''); runPrivilegedCommand(mainCommand); } /** * Checks to see if services like Hyper-V are running, and other system info. * On mac/win/linux. * * {"diskFree":"720.47","dockerRunning":true,"hyperVRunning":true,"ram":"15.42"} */ function detectIfHyperVRunning() { try { const output = execSync('sc query vmms').toString(); if (output.includes('RUNNING')) { return true; } else { return false; } } catch (error) { return false; } } /** * Checks if Hyper-V is enabled on Windows by verifying if the vmms service exists. * This does not require admin privileges. * @returns {boolean} true if Hyper-V (vmms service) exists, false otherwise. */ function detectIfHyperVEnabled() { try { const output = execSync('sc query type= service state= all | findstr /I "vmms"').toString(); // If output contains "SERVICE_NAME: vmms", Hyper-V is enabled return output.toLowerCase().includes('vmms'); } catch (error) { return false; } } function info() { function detectIfDockerRunning() { try { const output = execSync('docker info', { stdio: ['ignore', 'pipe', 'pipe'] }); const stdout = output.toString(); return stdout.includes('Server Version'); } catch (error) { return false; } } function getRamWindows() { try { const output = execSync('wmic computersystem get TotalPhysicalMemory').toString(); const ramInBytes = parseInt(output.split('\n')[1].trim(), 10); // Convert to GB, 2 decimals return (ramInBytes / (1024 * 1024 * 1024)).toFixed(2); } catch (error) { // Fallback to PowerShell if wmic is not available try { const output = execSync( 'powershell -Command "(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory"' ).toString(); const ramInBytes = parseInt(output.trim(), 10); if (isNaN(ramInBytes)) return null; // Convert to GB, 2 decimals return (ramInBytes / (1024 * 1024 * 1024)).toFixed(2); } catch (psError) { console.error('Failed to get RAM with wmic:', error.message); console.error('Failed to get RAM with PowerShell:', psError.message); return null; } } } function getFreeRamWindows() { try { const output = execSync('wmic OS get FreePhysicalMemory').toString(); const freeMemInKB = parseInt(output.split('\n')[1].trim(), 10); // Convert to GB, 2 decimals return (freeMemInKB / (1024 * 1024)).toFixed(2); } catch (error) { // Fallback to PowerShell if wmic is not available try { const output = execSync( 'powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory"' ).toString(); const freeMemInKB = parseInt(output.trim(), 10); if (isNaN(freeMemInKB)) return null; // Convert to GB, 2 decimals return (freeMemInKB / (1024 * 1024)).toFixed(2); } catch (psError) { console.error('Failed to get free RAM with wmic:', error.message); console.error('Failed to get free RAM with PowerShell:', psError.message); return null; } } } function getRamMac() { try { const output = execSync('sysctl -n hw.memsize').toString(); const ramInBytes = parseInt(output.trim(), 10); // Convert to GB, 2 decimals return (ramInBytes / (1024 * 1024 * 1024)).toFixed(2); } catch (error) { console.error('Failed to get RAM:', error.message); return null; } } /** * Parses the output of the `top` command on macOS to find free RAM. * @param {string} output - The output of the `top` command. * @returns {string|null} - The amount of free RAM in GB, or null if not found. * * @example * parseFreeRamMac("PhysMem: 22G used (2516M wired, 5835M compressor), 1111M unused.") -> 1.1 * parseFreeRamMac("PhysMem: 16G used (1234M wired), 2048M unused.") -> 2.0 */ function parseFreeRamMac(output) { const match = output.match(/(\d+\.?\d*)[MG] unused/); if (!match) return null; let free = match[1]; if (output.includes('unused.')) { // Determine if it's in M or G if (output.match(/(\d+\.?\d*)G unused/)) { // Already in GB return parseFloat(free).toFixed(2); } else { // In MB, convert to GB return (parseFloat(free) / 1024).toFixed(2); } } return null; } function getFreeRamMac() { try { const output = execSync('top -l 1 -s 0 | grep PhysMem').toString(); return parseFreeRamMac(output); } catch (error) { console.error('Failed to get free RAM:', error.message); return null; } } function getRamLinux() { try { const output = execSync("free -m | awk 'NR==2{print $2}'").toString(); const ramMb = parseInt(output.trim(), 10); if (isNaN(ramMb)) return null; // Convert MB to GB, 2 decimals return (ramMb / 1024).toFixed(2); } catch (error) { console.error('Failed to get RAM:', error.message); return null; } } function getFreeRamLinux() { try { const output = execSync("free -m | awk 'NR==2{print $4}'").toString(); const freeMb = parseInt(output.trim(), 10); if (isNaN(freeMb)) return null; // Convert MB to GB, 2 decimals return (freeMb / 1024).toFixed(2); } catch (error) { console.error('Failed to get free RAM:', error.message); return null; } } function getDiskFreeLinux() { try { const output = execSync("df -k / | tail -n 1 | awk '{print $4}'").toString(); const freeKb = parseInt(output.trim(), 10); if (isNaN(freeKb)) return null; // Convert KB to GB, 2 decimals return (freeKb / (1024 * 1024)).toFixed(2); } catch (error) { console.error('Failed to get disk space:', error.message); return null; } } /** * @returns gigabytes of free disk space on macOS as a number */ function getDiskFreeMac() { try { const output = execSync("df -k / | tail -n 1 | awk '{print $4}'").toString(); const freeKb = parseInt(output.trim(), 10); if (isNaN(freeKb)) return null; // Convert KB to GB, 2 decimals return (freeKb / (1024 * 1024)).toFixed(2); } catch (error) { console.error('Failed to get disk space:', error.message); return null; } } function getDiskFreeWindows() { try { const output = execSync('wmic logicaldisk get size,freespace,caption').toString(); const lines = output.split('\n').filter(line => line.trim() !== ''); if (lines.length < 2) return null; const diskInfo = lines[1].split(/\s+/); const freeSpace = parseInt(diskInfo[1], 10); if (isNaN(freeSpace)) return null; // Return GB as number, 2 decimals return (freeSpace / (1024 * 1024 * 1024)).toFixed(2); } catch (error) { // Fallback to PowerShell if wmic is not available try { const output = execSync( 'powershell -Command "(Get-PSDrive -Name ($PWD.Drive.Name)).Free"' ).toString(); const freeSpace = parseInt(output.trim(), 10); if (isNaN(freeSpace)) return null; // Return GB as number, 2 decimals return (freeSpace / (1024 * 1024 * 1024)).toFixed(2); } catch (psError) { console.error('Failed to get disk space with wmic:', error.message); console.error('Failed to get disk space with PowerShell:', psError.message); return null; } } } function getMinikubeProfileList() { try { const output = execSync('minikube profile list --output=json').toString(); return JSON.parse(output); } catch (error) { console.error('Failed to get minikube profiles:', error.message); return {}; } } const info = {}; info.minikubeProfiles = getMinikubeProfileList(); if (platform === 'win32') { info.diskFree = getDiskFreeWindows(); info.dockerRunning = detectIfDockerRunning(); info.hyperVRunning = detectIfHyperVRunning(); info.hyperVEnabled = detectIfHyperVEnabled(); info.ram = getRamWindows(); info.freeRam = getFreeRamWindows(); } if (platform === 'darwin') { info.diskFree = getDiskFreeMac(); info.dockerRunning = detectIfDockerRunning(); info.ram = getRamMac(); info.freeRam = getFreeRamMac(); } if (platform === 'linux') { info.diskFree = getDiskFreeLinux(); info.dockerRunning = detectIfDockerRunning(); info.ram = getRamLinux(); info.freeRam = getFreeRamLinux(); } const simulateSlowComputer = true; const output = JSON.stringify(info) + '\n'; // Flush stdout reliably across platforms (some pipes/windows consoles // need an extra empty write to actually flush the stream). const flushAndExit = (code = 0) => { // helper that performs an extra empty write then exits on next tick const doEmptyWriteAndExit = () => { process.stdout.write('', () => { setImmediate(() => process.exit(code)); }); }; // Write the real output first const ok = process.stdout.write(output, 'utf8'); if (!ok) { // Wait for drain, then perform an empty write and exit process.stdout.once('drain', doEmptyWriteAndExit); } else { // Already flushed synchronously in libuv queue, but do an empty write // to coax some platforms/pipes to finish flushing. doEmptyWriteAndExit(); } }; if (simulateSlowComputer) { setTimeout(() => { flushAndExit(0); }, 1000); } else { flushAndExit(0); } } function isBrewInstalled() { const shell = process.env.SHELL.includes('zsh') ? 'zsh -l -c' : 'bash -c'; const cmd = `${shell} "brew --version"`; try { execSync(cmd, { stdio: 'ignore' }); console.log('Brew is installed.'); return true; } catch (error) { console.log('Brew is not installed.'); return false; } }; function installVfkitManually() { console.log('Brew is not installed, need to install vfkit manually...'); throw new Error('Manual installation of vfkit is not implemented yet.'); } /** * Checks if vfkit is installed on macOS. * First check with if vfkit is installed. * If not, see if brew is installed. * If brew is installed, then install vfkit with brew. * If brew is not installed, then we download vfkit and install it manually. */ function ensureVfkit() { // get the users login shell, on macos this is different to what an electron app uses // if process.env.SHELL is zsh, then we use zsh -l -c to run the command // if process.env.SHELL is bash, then we use bash -c to run the command const shell = process.env.SHELL.includes('zsh') ? 'zsh -l -c' : 'bash -c'; try { const output = execSync(`${shell} "vfkit --version"`, { encoding: 'utf8' }).toString(); if (output.includes('vfkit version')) { return true; } } catch (error) { // log the error, but continue console.error('vfkit is not installed:', error.message); } // Check if brew is installed if (isBrewInstalled()) { // If brew is installed, install vfkit with brew console.log('Installing vfkit with brew...'); try { execSync(`${shell} "brew install vfkit"`, { stdio: 'inherit' }); } catch (error) { console.error('Failed to install vfkit with brew:', error.message); return false; } console.log('vfkit installed successfully with brew.'); return true; } // If brew is not installed, we need to download and install vfkit manually try { installVfkitManually(); return true; } catch (error) { console.error('Failed to install vfkit manually:', error.message); return false; } } function startMinikubeVFKit(args) { const allArgs = ['start', '--driver=vfkit', ...args]; const shellArgs = process.env.SHELL.includes('zsh') ? ['-l', '-c'] : ['-c']; const shell = process.env.SHELL.includes('zsh') ? 'zsh' : 'bash'; if (ensureVfkit()) { console.log('Starting minikube with vfkit...'); try { // Build the minikube command with arguments for zsh -l -c const minikubeCmd = ['minikube', ...allArgs].map(arg => `'${arg.replace(/'/g, `'\\''`)}'`).join(' '); const zshArgs = [...shellArgs, minikubeCmd]; console.log("process.env:", process.env.PATH); spawnSync(shell, zshArgs, { stdio: 'inherit', env: { ...process.env, } }); } catch (error) { console.error('Failed to start minikube with vfkit:', error.message); } process.exit(0); } else { console.error('Failed to ensure vfkit is installed. Try a different driver or install vfkit manually.'); process.exit(1); } } /** * Runs minikube profile command with the given arguments. * @param {string[]} args extra arguments to pass to minikube profile */ function minikubeProfile(args) { const allArgs = ['profile', ...args]; try { spawnSync('minikube', allArgs, { stdio: 'inherit', shell: true, }); } catch (error) { console.log('Failed to run minikube profile command:', error.message); } process.exit(0); } const commands = { // 'ask-restart-windows': askRestartWindows, // 'setup-hyperV-windows-networking': setupHyperVWindowsNetworking, // 'ask-restart-libvirt-ubuntu24': askRestartLibvirtUbuntu24, 'start-minikube-hyperv': startMinikubeHyperV, 'stop-minikube-hyperv': stopMinikubeHyperV, 'delete-minikube-hyperv': deleteMinikubeHyperV, 'start-minikube-vfkit': startMinikubeVFKit, 'minikube-profile': minikubeProfile, 'is-brew-installed': isBrewInstalled, info: info, }; /** * @returns the command and the rest of the args. * * We do basic command line parsing without any libraries. * Because this script can't use third party libraries without bundling them. */ function parseCommandLineArgs() { let args = process.argv.slice(2); if (args[0] === '-headless') { args = args.slice(1); } return [args[0], args.slice(1)]; } function main() { const [command, args] = parseCommandLineArgs(); // // override console.log and console.info to avoid printing to the console // // if they start with "App starting..." or "Check for updates" // const originalLog = console.log; // console.log = function (...messages) { // if (messages.length > 0) { // const message = messages.join(' '); // if (!message.startsWith('App starting...') && !message.startsWith('Check for updates')) { // originalLog.apply(console, messages); // } // } // }; // console.info = console.log; if (!command || !commands[command]) { console.error(command ? `Unknown command: ${command}` : 'No command provided'); console.log('Available commands:'); Object.keys(commands).forEach(cmd => console.log(`- ${cmd}`)); return; } commands[command](args); } main();