rawRead.ts
1 /** 2 * Minimal module for firing MDM subprocess reads without blocking the event loop. 3 * Has minimal imports — only child_process, fs, and mdmConstants (which only imports os). 4 * 5 * Two usage patterns: 6 * 1. Startup: startMdmRawRead() fires at main.tsx module evaluation, results consumed later via getMdmRawReadPromise() 7 * 2. Poll/fallback: fireRawRead() creates a fresh read on demand (used by changeDetector and SDK entrypoint) 8 * 9 * Raw stdout is consumed by mdmSettings.ts via consumeRawReadResult(). 10 */ 11 12 import { execFile } from 'child_process' 13 import { existsSync } from 'fs' 14 import { 15 getMacOSPlistPaths, 16 MDM_SUBPROCESS_TIMEOUT_MS, 17 PLUTIL_ARGS_PREFIX, 18 PLUTIL_PATH, 19 WINDOWS_REGISTRY_KEY_PATH_HKCU, 20 WINDOWS_REGISTRY_KEY_PATH_HKLM, 21 WINDOWS_REGISTRY_VALUE_NAME, 22 } from './constants.js' 23 24 export type RawReadResult = { 25 plistStdouts: Array<{ stdout: string; label: string }> | null 26 hklmStdout: string | null 27 hkcuStdout: string | null 28 } 29 30 let rawReadPromise: Promise<RawReadResult> | null = null 31 32 function execFilePromise( 33 cmd: string, 34 args: string[], 35 ): Promise<{ stdout: string; code: number | null }> { 36 return new Promise(resolve => { 37 execFile( 38 cmd, 39 args, 40 { encoding: 'utf-8', timeout: MDM_SUBPROCESS_TIMEOUT_MS }, 41 (err, stdout) => { 42 // biome-ignore lint/nursery/noFloatingPromises: resolve() is not a floating promise 43 resolve({ stdout: stdout ?? '', code: err ? 1 : 0 }) 44 }, 45 ) 46 }) 47 } 48 49 /** 50 * Fire fresh subprocess reads for MDM settings and return raw stdout. 51 * On macOS: spawns plutil for each plist path in parallel, picks first winner. 52 * On Windows: spawns reg query for HKLM and HKCU in parallel. 53 * On Linux: returns empty (no MDM equivalent). 54 */ 55 export function fireRawRead(): Promise<RawReadResult> { 56 return (async (): Promise<RawReadResult> => { 57 if (process.platform === 'darwin') { 58 const plistPaths = getMacOSPlistPaths() 59 60 const allResults = await Promise.all( 61 plistPaths.map(async ({ path, label }) => { 62 // Fast-path: skip the plutil subprocess if the plist file does not 63 // exist. Spawning plutil takes ~5ms even for an immediate ENOENT, 64 // and non-MDM machines never have these files. 65 // Uses synchronous existsSync to preserve the spawn-during-imports 66 // invariant: execFilePromise must be the first await so plutil 67 // spawns before the event loop polls (see main.tsx:3-4). 68 if (!existsSync(path)) { 69 return { stdout: '', label, ok: false } 70 } 71 const { stdout, code } = await execFilePromise(PLUTIL_PATH, [ 72 ...PLUTIL_ARGS_PREFIX, 73 path, 74 ]) 75 return { stdout, label, ok: code === 0 && !!stdout } 76 }), 77 ) 78 79 // First source wins (array is in priority order) 80 const winner = allResults.find(r => r.ok) 81 return { 82 plistStdouts: winner 83 ? [{ stdout: winner.stdout, label: winner.label }] 84 : [], 85 hklmStdout: null, 86 hkcuStdout: null, 87 } 88 } 89 90 if (process.platform === 'win32') { 91 const [hklm, hkcu] = await Promise.all([ 92 execFilePromise('reg', [ 93 'query', 94 WINDOWS_REGISTRY_KEY_PATH_HKLM, 95 '/v', 96 WINDOWS_REGISTRY_VALUE_NAME, 97 ]), 98 execFilePromise('reg', [ 99 'query', 100 WINDOWS_REGISTRY_KEY_PATH_HKCU, 101 '/v', 102 WINDOWS_REGISTRY_VALUE_NAME, 103 ]), 104 ]) 105 return { 106 plistStdouts: null, 107 hklmStdout: hklm.code === 0 ? hklm.stdout : null, 108 hkcuStdout: hkcu.code === 0 ? hkcu.stdout : null, 109 } 110 } 111 112 return { plistStdouts: null, hklmStdout: null, hkcuStdout: null } 113 })() 114 } 115 116 /** 117 * Fire raw subprocess reads once for startup. Called at main.tsx module evaluation. 118 * Results are consumed via getMdmRawReadPromise(). 119 */ 120 export function startMdmRawRead(): void { 121 if (rawReadPromise) return 122 rawReadPromise = fireRawRead() 123 } 124 125 /** 126 * Get the startup promise. Returns null if startMdmRawRead() wasn't called. 127 */ 128 export function getMdmRawReadPromise(): Promise<RawReadResult> | null { 129 return rawReadPromise 130 }