From f7aedbc7d497c17353241e5417f5c6b9dadf0e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 31 Jan 2025 09:51:32 +0100 Subject: [PATCH 1/3] Add installer for windows setup.exe --- .../src/installers/index.ts | 3 + .../src/installers/windows-registry.ts | 53 +++++++++++++ .../src/installers/windows-setup.ts | 74 +++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 packages/compass-smoke-tests/src/installers/windows-registry.ts create mode 100644 packages/compass-smoke-tests/src/installers/windows-setup.ts diff --git a/packages/compass-smoke-tests/src/installers/index.ts b/packages/compass-smoke-tests/src/installers/index.ts index a3ac52ff4d2..31fc022b0c0 100644 --- a/packages/compass-smoke-tests/src/installers/index.ts +++ b/packages/compass-smoke-tests/src/installers/index.ts @@ -3,6 +3,7 @@ import { installMacDMG } from './mac-dmg'; import { installMacZIP } from './mac-zip'; import { installWindowsZIP } from './windows-zip'; import { installWindowsMSI } from './windows-msi'; +import { installWindowsSetup } from './windows-setup'; export function getInstaller(kind: PackageKind) { if (kind === 'osx_dmg') { @@ -13,6 +14,8 @@ export function getInstaller(kind: PackageKind) { return installWindowsZIP; } else if (kind === 'windows_msi') { return installWindowsMSI; + } else if (kind === 'windows_setup') { + return installWindowsSetup; } else { throw new Error(`Installer for '${kind}' is not yet implemented`); } diff --git a/packages/compass-smoke-tests/src/installers/windows-registry.ts b/packages/compass-smoke-tests/src/installers/windows-registry.ts new file mode 100644 index 00000000000..452d3185247 --- /dev/null +++ b/packages/compass-smoke-tests/src/installers/windows-registry.ts @@ -0,0 +1,53 @@ +import cp from 'node:child_process'; + +type RegistryEntry = { + key: string; + type: string; + value: string; +}; + +function isRegistryEntry(value: unknown): value is RegistryEntry { + return ( + typeof value === 'object' && + value !== null && + 'key' in value && + typeof value.key === 'string' && + 'type' in value && + typeof value.type === 'string' && + 'value' in value && + typeof value.value === 'string' + ); +} + +/** + * Parse the putput of a "reg query" call. + */ +function parseQueryRegistryOutput(output: string): RegistryEntry[] { + const result = output.matchAll( + /^\s*(?\w+) +(?\w+) *(?.*)$/gm + ); + return [...result].map(({ groups }) => groups).filter(isRegistryEntry); +} + +/** + * Query the Windows registry by key. + */ +export function query(key: string) { + const result = cp.spawnSync('reg', ['query', key], { encoding: 'utf8' }); + if (result.status === 0) { + const entries = parseQueryRegistryOutput(result.stdout); + return Object.fromEntries(entries.map(({ key, value }) => [key, value])); + } else if ( + result.status === 1 && + result.stderr.trim() === + 'ERROR: The system was unable to find the specified registry key or value.' + ) { + return null; + } else { + throw Error( + `Expected either an entry or status code 1, got status ${ + result.status ?? '?' + }: ${result.stderr}` + ); + } +} diff --git a/packages/compass-smoke-tests/src/installers/windows-setup.ts b/packages/compass-smoke-tests/src/installers/windows-setup.ts new file mode 100644 index 00000000000..340d43a3083 --- /dev/null +++ b/packages/compass-smoke-tests/src/installers/windows-setup.ts @@ -0,0 +1,74 @@ +import path from 'node:path'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import cp from 'node:child_process'; + +import type { InstalledAppInfo, InstallablePackage } from './types'; +import { execute } from '../execute'; +import * as windowsRegistry from './windows-registry'; + +type UninstallOptions = { + /** + * Expect the app to already be uninstalled, warn if that's not the case. + */ + expectMissing?: boolean; +}; + +/** + * Install using the Windows installer. + */ +export function installWindowsSetup({ + appName, + filepath, +}: InstallablePackage): InstalledAppInfo { + const { LOCALAPPDATA: LOCAL_APPDATA_PATH } = process.env; + assert( + typeof LOCAL_APPDATA_PATH === 'string', + 'Expected a LOCALAPPDATA environment injected by the shell' + ); + const installDirectory = path.resolve(LOCAL_APPDATA_PATH, appName); + + function uninstall({ expectMissing = false }: UninstallOptions = {}) { + const registryEntry = windowsRegistry.query( + `HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${appName}` + ); + if (registryEntry) { + if (expectMissing) { + console.warn( + 'Found an existing registry entry (likely from a previous run)' + ); + } + const { UninstallString: uninstallCommand } = registryEntry; + assert( + typeof uninstallCommand === 'string', + 'Expected an UninstallString in the registry entry' + ); + console.log(`Running command to uninstall: ${uninstallCommand}`); + cp.execSync(uninstallCommand, { stdio: 'inherit' }); + } + // Removing the any remaining files + fs.rmSync(installDirectory, { recursive: true, force: true }); + } + + uninstall({ expectMissing: true }); + + // Assert the app is not on the filesystem at the expected location + assert( + !fs.existsSync(installDirectory), + `Delete any existing installations first (found ${installDirectory})` + ); + + // Run the installer + console.warn( + "Installing globally, since we haven't discovered a way to specify an install path" + ); + execute(filepath, []); + + const appPath = path.resolve(installDirectory, `${appName}.exe`); + execute(appPath, ['--version']); + + return { + appPath: installDirectory, + uninstall, + }; +} From 4735bc01d1c4138f7e69a60975a0f6029ac0cede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 31 Jan 2025 10:27:01 +0100 Subject: [PATCH 2/3] Add windows_setup to CI --- .evergreen/functions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.evergreen/functions.yml b/.evergreen/functions.yml index 0fd64ce3bcd..3a450402af7 100644 --- a/.evergreen/functions.yml +++ b/.evergreen/functions.yml @@ -677,7 +677,7 @@ functions: npm i -w packages/compass-smoke-tests https://x-access-token:${generated_token}@github.com/10gen/compass-mongodb-com --engine-strict=false if [[ "$IS_WINDOWS" == "true" ]]; then - # TODO: windows_setup + npm run --unsafe-perm --workspace @mongodb-js/compass-smoke-tests start -- --package=windows_setup --tests time-to-first-query npm run --unsafe-perm --workspace @mongodb-js/compass-smoke-tests start -- --package=windows_zip --tests auto-update-from npm run --unsafe-perm --workspace @mongodb-js/compass-smoke-tests start -- --package=windows_msi --tests auto-update-from fi From 7913f128de34ac2d4ec27e93fc79016cdae97b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 6 Feb 2025 10:51:42 +0100 Subject: [PATCH 3/3] Reading app location from registry --- .../src/installers/windows-setup.ts | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/compass-smoke-tests/src/installers/windows-setup.ts b/packages/compass-smoke-tests/src/installers/windows-setup.ts index 340d43a3083..fd6e4ec4fa3 100644 --- a/packages/compass-smoke-tests/src/installers/windows-setup.ts +++ b/packages/compass-smoke-tests/src/installers/windows-setup.ts @@ -21,54 +21,63 @@ export function installWindowsSetup({ appName, filepath, }: InstallablePackage): InstalledAppInfo { - const { LOCALAPPDATA: LOCAL_APPDATA_PATH } = process.env; - assert( - typeof LOCAL_APPDATA_PATH === 'string', - 'Expected a LOCALAPPDATA environment injected by the shell' - ); - const installDirectory = path.resolve(LOCAL_APPDATA_PATH, appName); - - function uninstall({ expectMissing = false }: UninstallOptions = {}) { - const registryEntry = windowsRegistry.query( + function queryRegistry() { + return windowsRegistry.query( `HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${appName}` ); - if (registryEntry) { + } + + function uninstall({ expectMissing = false }: UninstallOptions = {}) { + const entry = queryRegistry(); + if (entry) { + const { + InstallLocation: installLocation, + UninstallString: uninstallCommand, + } = entry; + assert( + typeof installLocation === 'string', + 'Expected an entry in the registry with the install location' + ); + assert( + typeof uninstallCommand === 'string', + 'Expected an entry in the registry with the uninstall command' + ); if (expectMissing) { console.warn( 'Found an existing registry entry (likely from a previous run)' ); } - const { UninstallString: uninstallCommand } = registryEntry; assert( typeof uninstallCommand === 'string', 'Expected an UninstallString in the registry entry' ); console.log(`Running command to uninstall: ${uninstallCommand}`); cp.execSync(uninstallCommand, { stdio: 'inherit' }); + // Removing the any remaining files manually + fs.rmSync(installLocation, { recursive: true, force: true }); } - // Removing the any remaining files - fs.rmSync(installDirectory, { recursive: true, force: true }); } uninstall({ expectMissing: true }); - // Assert the app is not on the filesystem at the expected location - assert( - !fs.existsSync(installDirectory), - `Delete any existing installations first (found ${installDirectory})` - ); - // Run the installer console.warn( "Installing globally, since we haven't discovered a way to specify an install path" ); execute(filepath, []); - const appPath = path.resolve(installDirectory, `${appName}.exe`); - execute(appPath, ['--version']); + const entry = queryRegistry(); + assert(entry !== null, 'Expected an entry in the registry after installing'); + const { InstallLocation: appPath } = entry; + assert( + typeof appPath === 'string', + 'Expected an entry in the registry with the install location' + ); + const appExecutablePath = path.resolve(appPath, `${appName}.exe`); + execute(appExecutablePath, ['--version']); return { - appPath: installDirectory, + appPath, uninstall, }; }