/ utils / settings / mdm / rawRead.ts
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  }