Files
OctoLauncher-personal/src/main/modules/defender.ts
T
OctoWoW 5dca94a3fc OctoLauncher 1.3.1
Manifest-based CDN updater and mod manager for the OctoWoW 1.12.1 client:
launcher-owned realmlist, torrent-backed content sync with bundled aria2c,
antivirus and Defender exclusion handling, hardware-aware render distance,
optional client tweaks and mods, and the in-launcher news feed.
2026-08-14 01:45:50 +00:00

193 lines
5.2 KiB
TypeScript

import { spawn } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { app } from 'electron';
import Logger from 'electron-log/main';
import Preferences from './preferences';
export type ExclusionResult = { ok: boolean; error?: string; paths?: string[] };
const psSingleQuote = (s: string) => `'${s.replace(/'/g, "''")}'`;
export const addDefenderExclusions = async (): Promise<ExclusionResult> => {
if (os.platform() !== 'win32')
return {
ok: false,
error: 'Antivirus exclusions are only needed on Windows.'
};
const clientDir = Preferences.data.clientDir;
if (!clientDir)
return {
ok: false,
error: 'Set your game folder first, then add the exclusion.'
};
const launcherDir =
process.env.PORTABLE_EXECUTABLE_DIR ?? path.dirname(app.getPath('exe'));
const paths = [...new Set([clientDir, launcherDir])];
const resultFile = path.join(
os.tmpdir(),
`octo-defender-${process.pid}-${Date.now()}.txt`
);
const write = (v: string) =>
`Set-Content -LiteralPath ${psSingleQuote(
resultFile
)} -Value "${v}" -Encoding ASCII`;
const inner = [
'try {',
...paths.map(
p =>
` Add-MpPreference -ExclusionPath ${psSingleQuote(
p
)} -ErrorAction Stop`
),
' Add-MpPreference -ExclusionProcess "WoW.exe" -ErrorAction Stop',
' Add-MpPreference -ExclusionProcess "VanillaFixes.exe" -ErrorAction Stop',
` ${write('OK')}`,
'} catch {',
' $t = $false',
' try { $t = (Get-MpComputerStatus).IsTamperProtected } catch {}',
` if ($t) { ${write('TAMPER')} } else { ${write('FAIL')} }`,
'}'
].join('\n');
const encoded = Buffer.from(inner, 'utf16le').toString('base64');
const outer =
'try { Start-Process powershell -Verb RunAs -WindowStyle Hidden -Wait ' +
"-ArgumentList '-NoProfile','-NonInteractive'," +
`'-EncodedCommand','${encoded}' } catch { exit 1 }`;
return new Promise<ExclusionResult>(resolve => {
const child = spawn(
'powershell.exe',
['-NoProfile', '-NonInteractive', '-Command', outer],
{ windowsHide: true }
);
let stderr = '';
child.stderr.on('data', d => (stderr += String(d)));
child.on('error', e => {
Logger.error('Failed to launch PowerShell for Defender exclusion', e);
resolve({ ok: false, error: 'Could not run Windows PowerShell.' });
});
child.on('exit', code => {
let result: string | null = null;
try {
result = fs.readFileSync(resultFile, 'utf8').trim();
} catch {}
try {
fs.rmSync(resultFile, { force: true });
} catch {}
if (result === 'OK') {
Logger.info(`Added Defender exclusions: ${paths.join(', ')}`);
resolve({ ok: true, paths });
return;
}
if (result === 'TAMPER') {
Logger.error('Defender exclusion blocked by Tamper Protection');
resolve({
ok: false,
error:
'Windows Security Tamper Protection is blocking this. Turn it off in Windows Security, or add your game folder by hand under Exclusions.'
});
return;
}
if (result === 'FAIL') {
Logger.error(`Defender exclusion failed: ${stderr}`.trim());
resolve({
ok: false,
error:
'Windows would not add the exclusion. You can add your game folder by hand in Windows Security, under Exclusions.'
});
return;
}
Logger.warn(
`Defender exclusion: no result (exit ${code}) ${stderr}`.trim()
);
resolve({
ok: false,
error:
'Windows did not grant permission. Click Yes on the User Account Control prompt to add the exclusion.'
});
});
});
};
const SENSITIVE_FILES = [
'WoW.exe',
'VanillaFixes.exe',
'd3d9.dll',
'UnitXP_SP3.dll',
'nampower.dll',
'VfPatcher.dll',
'VanillaHelpers.dll',
'VanillaMultiMonitorFix.dll',
'transmogfix.dll'
];
export const detectAntivirusBlocks = async (): Promise<string[]> => {
if (os.platform() !== 'win32') return [];
const clientDir = Preferences.data.clientDir;
const launcherDir =
process.env.PORTABLE_EXECUTABLE_DIR ?? path.dirname(app.getPath('exe'));
const roots = [clientDir, launcherDir]
.filter((p): p is string => !!p)
.map(p => p.toLowerCase());
if (!roots.length) return [];
const blocked = new Set<string>();
if (clientDir && Preferences.data.syncedTorrentHash)
for (const name of SENSITIVE_FILES)
if (!fs.existsSync(path.join(clientDir, name))) blocked.add(name);
const script =
'Get-MpThreatDetection | Where-Object ' +
'{ $_.InitialDetectionTime -gt (Get-Date).AddHours(-12) } | ' +
'Select-Object -ExpandProperty Resources';
await new Promise<void>(resolve => {
const child = spawn(
'powershell.exe',
['-NoProfile', '-NonInteractive', '-Command', script],
{ windowsHide: true }
);
let out = '';
let settled = false;
const timer = setTimeout(() => {
try {
child.kill();
} catch {}
}, 15_000);
const finish = () => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve();
};
child.stdout.on('data', d => (out += String(d)));
child.on('error', finish);
child.on('exit', () => {
for (const line of out.split(/\r?\n/)) {
const m = /^file:_?(.+)$/.exec(line.trim());
if (!m) continue;
const full = m[1];
if (
roots.some(r => full.toLowerCase().startsWith(r)) &&
!fs.existsSync(full)
)
blocked.add(path.basename(full));
}
finish();
});
});
return [...blocked];
};