// Native metadata diagnostic owned by the local REA provider. No target injection or termination. import { spawn } from 'node:child_process'; import os from 'node:os'; import path from 'node:path'; import fs from 'node:fs/promises'; import { createEvidence } from '../domain/evidence.js'; export async function inspectDesktopElectronProcesses(processName) { if (process.platform !== 'win32') throw new Error('Windows process metadata provider requires Windows'); if (!/^[A-Za-z0-9_.-]{1,80}\.exe$/i.test(processName)) throw new Error('Select one literal executable basename'); const script = `$targets=Get-CimInstance Win32_Process -Filter "Name='${processName}'"; $rows=foreach($p in $targets){$debugPort=$null;$processType='main';if($p.CommandLine -match '--remote-debugging-port[= ]+(\\d+)'){$debugPort=[int]$Matches[1]};if($p.CommandLine -match '--type[= ]+([^\\s]+)'){$processType=$Matches[1]};[pscustomobject]@{pid=$p.ProcessId;parentPid=$p.ParentProcessId;executable=$p.ExecutablePath;type=$processType;debugPort=$debugPort}};$listeners=Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue|Where-Object{$_.OwningProcess -in $targets.ProcessId}|Select-Object LocalAddress,LocalPort,OwningProcess;[pscustomobject]@{processes=@($rows);listeners=@($listeners)}|ConvertTo-Json -Depth 5 -Compress`; const result = await new Promise((resolve,reject)=>{ const child=spawn('C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe',['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',Buffer.from(script,'utf16le').toString('base64')],{windowsHide:true,stdio:['ignore','pipe','pipe']}); const chunks=[];let size=0;const timer=setTimeout(()=>{child.kill();reject(new Error('Windows metadata query timed out'))},15000); child.stdout.on('data',chunk=>{size+=chunk.length;if(size>1024*1024){child.kill();reject(new Error('Windows metadata budget exceeded'))}else chunks.push(chunk)}); child.stderr.on('data',()=>{}); child.once('error',error=>{clearTimeout(timer);reject(error)}); child.once('close',code=>{clearTimeout(timer);if(code!==0)return reject(new Error('Windows metadata query failed'));try{resolve(JSON.parse(Buffer.concat(chunks).toString('utf8').replace(/^\uFEFF/,'')))}catch(error){reject(error)}}); }); return createEvidence(undefined,{id:'rea-windows-electron-processes',name:'Local REA Windows Electron metadata provider',version:'1+state-local.1'},{ predicateType:'rea.desktop-electron-processes',operation:'inspect_desktop_electron_processes',parameters:{process_name:processName},result, confidence:'observed',authority:'external-service',environment:{id:`${os.platform()}-${os.release()}`,platform:os.platform(),architecture:os.arch(),isolation:'none'}, limitations:['Native process metadata and listening sockets only; no target injection, file extraction, restart or termination.','Command lines are reduced to process type and declared remote-debugging port; other argument values are omitted.'] }); } // Explicit lifecycle operation. The caller must already have authorization to interrupt WorkBuddy. export async function restartDesktopElectronDebug(executable,port,authorized) { if(process.platform!=='win32'||authorized!==true)throw new Error('An authorized Windows restart is required'); if(!Number.isInteger(port)||port<1024||port>65535)throw new Error('Select one debugging port from 1024 to 65535'); const expected=path.join(process.env.LOCALAPPDATA,'Programs','WorkBuddy','WorkBuddy.exe'); const actual=await fs.realpath(executable),allowed=await fs.realpath(expected); if(actual.toLowerCase()!==allowed.toLowerCase())throw new Error('Restart is limited to the installed WorkBuddy executable'); const quoted=actual.replaceAll("'","''"); const script=`$ErrorActionPreference='Stop';$exe='${quoted}';$before=@(Get-CimInstance Win32_Process -Filter "Name='WorkBuddy.exe'"|Where-Object{$_.ExecutablePath -eq $exe});$close=@();foreach($p in $before){$native=Get-Process -Id $p.ProcessId -ErrorAction SilentlyContinue;if($native -and $native.MainWindowHandle -ne 0){$close+= [pscustomobject]@{pid=$p.ProcessId;requested=$native.CloseMainWindow()}}};$deadline=[datetime]::UtcNow.AddSeconds(8);do{$remaining=@(Get-CimInstance Win32_Process -Filter "Name='WorkBuddy.exe'"|Where-Object{$_.ExecutablePath -eq $exe});if($remaining.Count -eq 0){break};Start-Sleep -Milliseconds 200}while([datetime]::UtcNow -lt $deadline);$forced=@();foreach($p in $before){$current=Get-CimInstance Win32_Process -Filter ("ProcessId="+$p.ProcessId);if($current -and $current.ExecutablePath -eq $exe -and $current.CreationDate -eq $p.CreationDate){Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue;$forced+=$p.ProcessId}};$deadline=[datetime]::UtcNow.AddSeconds(4);do{$remaining=@(Get-CimInstance Win32_Process -Filter "Name='WorkBuddy.exe'"|Where-Object{$_.ExecutablePath -eq $exe});if($remaining.Count -eq 0){break};Start-Sleep -Milliseconds 100}while([datetime]::UtcNow -lt $deadline);if($remaining.Count -ne 0){throw 'Selected WorkBuddy processes did not exit; no replacement was launched'};[pscustomobject]@{beforePids=@($before|ForEach-Object{$_.ProcessId});closeRequests=@($close);forcedPids=@($forced);remaining=0}|ConvertTo-Json -Depth 4 -Compress`; const stopped=await new Promise((resolve,reject)=>{ const child=spawn('C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe',['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',Buffer.from(script,'utf16le').toString('base64')],{windowsHide:true,stdio:['ignore','pipe','pipe']}); let output='',errors='';const timer=setTimeout(()=>{child.kill();reject(new Error('Authorized restart shutdown timed out'))},20000); child.stdout.on('data',b=>{output+=b;if(output.length>65536){child.kill();reject(new Error('Restart result budget exceeded'))}});child.stderr.on('data',b=>errors+=b); child.once('error',e=>{clearTimeout(timer);reject(e)});child.once('close',code=>{clearTimeout(timer);if(code!==0)return reject(new Error('Restart shutdown failed: '+errors.slice(0,1000)));try{resolve(JSON.parse(output.replace(/^\uFEFF/,'')))}catch(e){reject(e)}}); }); const env={...process.env};for(const key of Object.keys(env))if(key.toUpperCase()==='ELECTRON_RUN_AS_NODE')delete env[key]; const args=[`--remote-debugging-port=${port}`,'--remote-debugging-address=127.0.0.1']; const launched=spawn(actual,args,{detached:true,windowsHide:true,stdio:'ignore',env}); await new Promise((resolve,reject)=>{launched.once('spawn',resolve);launched.once('error',reject)});launched.unref(); return createEvidence(undefined,{id:'rea-windows-electron-processes',name:'Local REA Windows Electron lifecycle provider',version:'1+state-local.2'},{ predicateType:'rea.desktop-electron-restart',operation:'restart_desktop_electron_debug',parameters:{executable:actual,port,restart_authorized:true},result:{...stopped,launchedPid:launched.pid,arguments:args},confidence:'observed',authority:'controlled-replay',environment:{id:`${os.platform()}-${os.release()}`,platform:os.platform(),architecture:os.arch(),isolation:'none'}, limitations:['Caller-authorized restart can interrupt in-flight tasks.','Graceful close is attempted first; surviving launch identities belonging to the exact selected executable are terminated.','Launch evidence confirms process creation only; inspect the debugging endpoint separately.','No original file contents or application data are extracted by this lifecycle operation.'] }); }