From 684ad8e191af2b8310b2282d3a483a4e301c82e0 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Mon, 30 Dec 2024 18:43:01 +0100 Subject: [PATCH] Improve OS detection and reduce data collected This improves some cases (e.g. distinguishes Linux distro, for notable major distros) but also reduces the fingerprintable details (kernel patch & variant was included and is relatively unique - all versions are now major+minor only). --- src/device.ts | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 52 +------------------- 2 files changed, 133 insertions(+), 50 deletions(-) create mode 100644 src/device.ts diff --git a/src/device.ts b/src/device.ts new file mode 100644 index 0000000..96c9568 --- /dev/null +++ b/src/device.ts @@ -0,0 +1,131 @@ +import * as os from 'os'; +import { promises as fs } from 'fs' +import { promisify } from 'util'; +import { exec } from 'child_process'; +const execAsync = promisify(exec); + +import { logError } from './errors'; + +export async function getDeviceDetails() { + const [ + realArch, + osDetails + ] = await Promise.all([ + getRealArch(), + getOsDetails() + ]); + + return { + ...osDetails, + runtimeArch: os.arch(), + realArch: realArch + } +} + +const majorMinorOnly = (input: I): I => + input?.split('.').slice(0, 2).join('.') as I; + +async function getOsDetails() { + const rawPlatform = os.platform(); + if (rawPlatform == 'linux') { + return await getLinuxOsDetails(); + } else if (rawPlatform === 'win32') { + // For Windows, the version numbers are oddly precise and weird. We simplify: + return { + platform: rawPlatform, + version: getWindowsVersion() + }; + } else { + return { + platform: rawPlatform, + release: majorMinorOnly(os.release()) + } + } +} + +function getWindowsVersion() { + const rawVersion = os.release(); + try { + if (rawVersion.startsWith('10.0.')) { + // Windows 10.0.x < 22000 is Windows 10, >= 22000 is Windows 11 + const buildNumber = parseInt(rawVersion.slice('10.0.'.length), 10); + if (isNaN(buildNumber)) return 'Unknown'; + else if (buildNumber >= 22000) return '11'; + else return '10' + } else { + // Other versions - e.g. 6.3 is Windows 8.1 + return majorMinorOnly(rawVersion); + } + } catch (e) { + logError(`Failed to detect windows version: ${e.message || e}`); + return 'Unknown'; + } +} + +async function getLinuxOsDetails() { + // For Linux, there's a relatively small number of users with a lot of variety. + // We do a bit more digging, to try to get meaningful data (e.g. distro) and + // drop unnecessary fingerprinting factors (kernel patch version & variants etc). End + // result is e.g. "ubuntu + 20.04" (just major+minor, for big distros supporting + // /etc/os-release) or "linux + 6.5" (just kernel major+minor). + try { + const osReleaseDetails = await fs.readFile('/etc/os-release', 'utf8') + .catch(() => ''); + const osRelease = osReleaseDetails.split('\n').reduce((acc, line) => { + const [key, value] = line.split('='); + if (key && value) { + acc[key] = value.replace(/(^")|("$)/g, ''); + } + return acc; + }, {} as { [key: string]: string | undefined }); + + return { + platform: osRelease['ID'] || osRelease['NAME'] || 'linux', + version: majorMinorOnly(osRelease['VERSION_ID'] || os.release()) + }; + } catch (e) { + logError(`Failed to detect Linux version: ${e.message}`); + return { + platform: 'linux', + version: majorMinorOnly(os.release()) + }; + } +} + +// Detect the 'real' architecture of the system. We're concerned here with detecting the real arch +// despite emulation here, to help with launch subprocs. Not too worried about x86 vs x64. +async function getRealArch() { + try { + switch (process.platform) { + case 'darwin': + const { stdout: armCheck } = await execAsync('sysctl -n hw.optional.arm64') + .catch((e: any) => { + const output = e.message + e.stdout + e.stderr; + // This id may not be available: + if (output?.includes?.("unknown oid")) return { stdout: "0" }; + else throw e; + }); + if (armCheck.trim() === '1') { + return 'arm64'; + } + + case 'linux': + const { stdout: cpuInfo } = await execAsync('cat /proc/cpuinfo'); + const lcCpuInfo = cpuInfo.toLowerCase(); + if (lcCpuInfo.includes('aarch64') || lcCpuInfo.includes('arm64')) { + return 'arm64'; + } + + case 'win32': + const arch = process.env.PROCESSOR_ARCHITEW6432 || process.env.PROCESSOR_ARCHITECTURE; + if (arch?.toLowerCase() === 'arm64') { + return 'arm64'; + } + } + } catch (e) { + console.warn(`Error querying system arch: ${e.message}`); + logError(e); + } + + return os.arch(); +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 418398a..3df64b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ const DEV_MODE = process.env.HTK_DEV === 'true'; // Set up error handling before everything else: import { logError, addBreadcrumb } from './errors'; -import { spawn, exec, ChildProcess } from 'child_process'; +import { spawn, ChildProcess } from 'child_process'; import * as os from 'os'; import { promises as fs, createWriteStream, WriteStream } from 'fs' import * as net from 'net'; @@ -18,7 +18,6 @@ import * as semver from 'semver'; import * as rimraf from 'rimraf'; const rmRF = promisify(rimraf); -const execAsync = promisify(exec); import * as windowStateKeeper from 'electron-window-state'; import { getSystemProxy } from 'os-proxy-config'; @@ -650,54 +649,7 @@ ipcMain.handle('open-context-menu', ipcHandler((options: ContextMenuDefinition) ipcMain.handle('get-desktop-version', ipcHandler(() => DESKTOP_VERSION)); ipcMain.handle('get-server-auth-token', ipcHandler(() => AUTH_TOKEN)); -ipcMain.handle('get-device-info', ipcHandler(async () => { - const realArch = await getRealArch(); - - return { - platform: os.platform(), - release: os.release(), - runtimeArch: os.arch(), - realArch: realArch - } -})); - -// Detect the 'real' architecture of the system. We're concerned here with detecting the real arch -// despite emulation here, to help with launch subprocs. Not too worried about x86 vs x64. -async function getRealArch() { - try { - switch (process.platform) { - case 'darwin': - const { stdout: armCheck } = await execAsync('sysctl -n hw.optional.arm64') - .catch((e: any) => { - const output = e.message + e.stdout + e.stderr; - // This id may not be available: - if (output?.includes?.("unknown oid")) return { stdout: "0" }; - else throw e; - }); - if (armCheck.trim() === '1') { - return 'arm64'; - } - - case 'linux': - const { stdout: cpuInfo } = await execAsync('cat /proc/cpuinfo'); - const lcCpuInfo = cpuInfo.toLowerCase(); - if (lcCpuInfo.includes('aarch64') || lcCpuInfo.includes('arm64')) { - return 'arm64'; - } - - case 'win32': - const arch = process.env.PROCESSOR_ARCHITEW6432 || process.env.PROCESSOR_ARCHITECTURE; - if (arch?.toLowerCase() === 'arm64') { - return 'arm64'; - } - } - } catch (e) { - console.warn(`Error querying system arch: ${e.message}`); - logError(e); - } - - return os.arch(); -} +ipcMain.handle('get-device-info', ipcHandler(() => getDeviceDetails())); let restarting = false; ipcMain.handle('restart-app', ipcHandler(() => {