OctoLauncher 1.3.6

This commit is contained in:
OctoWoW
2026-08-14 14:04:57 +00:00
parent f9d89601f1
commit eecda9381a
5 changed files with 82 additions and 34 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "octo-launcher", "name": "octo-launcher",
"version": "1.3.5", "version": "1.3.6",
"description": "An Electron application for launching and updating the OctoWoW client", "description": "An Electron application for launching and updating the OctoWoW client",
"author": "OctoWoW", "author": "OctoWoW",
"copyright": "Copyright © 2026 OctoWoW", "copyright": "Copyright © 2026 OctoWoW",
+3
View File
@@ -9,6 +9,7 @@ import icon from '~build/icon.png?asset';
import { PreferencesSchema } from '~common/schemas'; import { PreferencesSchema } from '~common/schemas';
import { appRouter } from './api/root'; import { appRouter } from './api/root';
import { stopSyncing, stopSeeding } from './modules/aria2';
import Preferences from './modules/preferences'; import Preferences from './modules/preferences';
import Updater from './modules/updater'; import Updater from './modules/updater';
import Addons from './modules/addons'; import Addons from './modules/addons';
@@ -201,6 +202,8 @@ if (!gotSingleInstanceLock) {
let settingsFlushed = false; let settingsFlushed = false;
app.on('before-quit', event => { app.on('before-quit', event => {
stopSyncing();
stopSeeding();
if (settingsFlushed) return; if (settingsFlushed) return;
settingsFlushed = true; settingsFlushed = true;
event.preventDefault(); event.preventDefault();
+17
View File
@@ -84,6 +84,15 @@ const parseProgress = (
}; };
}; };
let syncChild: ChildProcess | undefined;
// aria2's --stop-with-process is unreliable on Windows; kill the download
// child explicitly on quit or it keeps running headless
export const stopSyncing = (): void => {
syncChild?.kill();
syncChild = undefined;
};
export const syncClient = (opts: SyncOpts): Promise<void> => export const syncClient = (opts: SyncOpts): Promise<void> =>
new Promise<void>((resolve, reject) => { new Promise<void>((resolve, reject) => {
let child: ChildProcess | undefined; let child: ChildProcess | undefined;
@@ -120,6 +129,7 @@ export const syncClient = (opts: SyncOpts): Promise<void> =>
]; ];
Logger.log(`aria2c ${args.join(' ')}`); Logger.log(`aria2c ${args.join(' ')}`);
child = spawn(bin(), args, { windowsHide: true }); child = spawn(bin(), args, { windowsHide: true });
syncChild = child;
const onLine = (buf: Buffer) => { const onLine = (buf: Buffer) => {
for (const line of buf.toString().split(/\r?\n/)) { for (const line of buf.toString().split(/\r?\n/)) {
@@ -136,6 +146,7 @@ export const syncClient = (opts: SyncOpts): Promise<void> =>
child.on('error', reject); child.on('error', reject);
child.on('close', code => { child.on('close', code => {
if (syncChild === child) syncChild = undefined;
if (code === 0) resolve(); if (code === 0) resolve();
else reject(new Error(`aria2c exited with code ${code}`)); else reject(new Error(`aria2c exited with code ${code}`));
}); });
@@ -341,6 +352,7 @@ export const torrentDownloadSelection = async (
const files = torrent?.info?.files ?? []; const files = torrent?.info?.files ?? [];
if (!files.length) return null; if (!files.length) return null;
const need: number[] = []; const need: number[] = [];
let missing = false;
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const f = files[i]; const f = files[i];
if (!f.path?.length || typeof f.length !== 'number') return null; if (!f.path?.length || typeof f.length !== 'number') return null;
@@ -348,6 +360,7 @@ export const torrentDownloadSelection = async (
const dest = path.join(clientDir, ...f.path); const dest = path.join(clientDir, ...f.path);
const st = await fs.stat(dest).catch(() => null); const st = await fs.stat(dest).catch(() => null);
if (!st) { if (!st) {
missing = true;
need.push(i + 1); need.push(i + 1);
continue; continue;
} }
@@ -357,6 +370,10 @@ export const torrentDownloadSelection = async (
need.push(i + 1); need.push(i + 1);
} }
} }
// a deleted file poisons the resume state (its pieces are marked
// done, so aria2 skips them forever); partial files keep it so an
// interrupted download resumes instead of restarting
if (missing) await clearTorrentResumeState().catch(() => undefined);
return need; return need;
} catch (e) { } catch (e) {
Logger.warn('Torrent selection computation failed', e); Logger.warn('Torrent selection computation failed', e);
+47 -13
View File
@@ -58,6 +58,20 @@ const KNOWN_DLLS = new Set(
const AV_ERROR = const AV_ERROR =
'Windows Defender blocked this download. Use "Allow through antivirus" and apply again.'; 'Windows Defender blocked this download. Use "Allow through antivirus" and apply again.';
// pinned dxvk-gplasync v2.7.1-1 x32 d3d9.dll (same build the client ships)
const DXVK_DLL_SHA256 =
'a2cd6841e102f37189527c118ec416fa5071ac4d3120762973d9a0c6c5fd067e';
const fileSha256 = async (p: string): Promise<string | null> => {
try {
return createHash('sha256')
.update(await fs.readFile(p))
.digest('hex');
} catch {
return null;
}
};
const looksLikeAvBlock = (msg: string) => const looksLikeAvBlock = (msg: string) =>
/windows defender|virus|potentially unwanted/i.test(msg); /windows defender|virus|potentially unwanted/i.test(msg);
@@ -278,6 +292,7 @@ class ModsClass extends Observable<ModsStatus> {
} }
const missing: string[] = []; const missing: string[] = [];
let dxvkRepair = false;
for (const m of MODS) { for (const m of MODS) {
// disabled mods: leave dlls.txt and installed state untouched // disabled mods: leave dlls.txt and installed state untouched
if (m.disabled) continue; if (m.disabled) continue;
@@ -288,30 +303,41 @@ class ModsClass extends Observable<ModsStatus> {
// torrent mode: DLLs ship in the client; a missing file goes to `missing`, not dirty // torrent mode: DLLs ship in the client; a missing file goes to `missing`, not dirty
if (isTorrentMode()) { if (isTorrentMode()) {
const enabled = state?.enabled ?? DEFAULT_ENABLED_MODS.includes(m.id); const enabled = state?.enabled ?? DEFAULT_ENABLED_MODS.includes(m.id);
// dxvk loads by file presence, not dlls.txt; disabled parks // dxvk loads by file presence and torrent piece spillover can
// d3d9.dll as .off, enabled restores it // corrupt it; hash-verify every state: park/restore verified
// copies only, delete junk, re-download the pin when needed
if (m.id === 'dxvk' && clientDir) { if (m.id === 'dxvk' && clientDir) {
const live = path.join(clientDir, 'd3d9.dll'); const live = path.join(clientDir, 'd3d9.dll');
const off = path.join(clientDir, 'd3d9.dll.off'); const off = path.join(clientDir, 'd3d9.dll.off');
const liveStat = await fs.stat(live).catch(() => null); const liveSha = await fileSha256(live);
if (!enabled && liveStat && liveStat.size === 0) { if (!enabled) {
// 0-byte aria2 stub, never park it over a real copy if (
await fs.remove(live).catch(() => undefined); liveSha === DXVK_DLL_SHA256 &&
} else if (!enabled && liveStat) { !(await fs.pathExists(off))
await fs.remove(off).catch(() => undefined); ) {
await fs await fs
.move(live, off) .move(live, off)
.then(() => Logger.info('dxvk disabled: parked d3d9.dll')) .then(() => Logger.info('dxvk disabled: parked d3d9.dll'))
.catch(e => Logger.warn('Could not park d3d9.dll', e)); .catch(e => Logger.warn('Could not park d3d9.dll', e));
} else if ( } else if (liveSha !== null) {
enabled && await fs.remove(live).catch(() => undefined);
!(await fs.pathExists(live)) && }
(await fs.pathExists(off)) } else if (liveSha !== DXVK_DLL_SHA256) {
) { if (liveSha !== null) {
Logger.warn('dxvk: d3d9.dll failed verification; replacing');
await fs.remove(live).catch(() => undefined);
}
const offSha = await fileSha256(off);
if (offSha === DXVK_DLL_SHA256) {
await fs await fs
.move(off, live) .move(off, live)
.then(() => Logger.info('dxvk enabled: restored d3d9.dll')) .then(() => Logger.info('dxvk enabled: restored d3d9.dll'))
.catch(e => Logger.warn('Could not restore d3d9.dll', e)); .catch(e => Logger.warn('Could not restore d3d9.dll', e));
} else {
if (offSha !== null)
await fs.remove(off).catch(() => undefined);
dxvkRepair = true;
}
} }
} }
const files = modTargetFiles(m); const files = modTargetFiles(m);
@@ -371,6 +397,14 @@ class ModsClass extends Observable<ModsStatus> {
}); });
} }
if (dxvkRepair) {
const dm = getMod('dxvk');
if (dm)
await this.#install(dm).catch(e =>
Logger.warn('dxvk repair download failed', e)
);
}
this._value = { this._value = {
...this._value, ...this._value,
state: 'idle', state: 'idle',
+6 -12
View File
@@ -456,9 +456,6 @@ class UpdaterClass extends Observable<UpdaterStatus> {
} }
}); });
if (!(await torrentTreeIntact(clientPath, url))) { if (!(await torrentTreeIntact(clientPath, url))) {
// stale resume state makes aria2 skip pieces it thinks are
// done; drop it so the next attempt starts from the real files
await clearTorrentResumeState().catch(() => undefined);
await healRealmlist(clientPath).catch(() => undefined); await healRealmlist(clientPath).catch(() => undefined);
this.status = { this.status = {
state: 'updateAvailable', state: 'updateAvailable',
@@ -474,15 +471,12 @@ class UpdaterClass extends Observable<UpdaterStatus> {
); );
if (removed.length) if (removed.length)
Logger.log(`Removed stale archives: ${removed.join(', ')}`); Logger.log(`Removed stale archives: ${removed.join(', ')}`);
// a clean sync restores d3d9.dll; re-park it if dxvk is off // a sync can recreate d3d9.dll (clean pass, or piece spillover);
if (Preferences.data.mods?.dxvk?.enabled === false) { // drop it while dxvk is off, the mods verify owns park/restore
const live = path.join(clientPath, 'd3d9.dll'); if (Preferences.data.mods?.dxvk?.enabled === false)
const off = path.join(clientPath, 'd3d9.dll.off'); await fs
if (await fs.pathExists(live)) { .remove(path.join(clientPath, 'd3d9.dll'))
await fs.remove(off).catch(() => undefined); .catch(() => undefined);
await fs.move(live, off).catch(() => undefined);
}
}
await this.#reconcileClientPatch(clientPath); await this.#reconcileClientPatch(clientPath);
await this.#reconcileRaidVisuals(clientPath); await this.#reconcileRaidVisuals(clientPath);
Preferences.data = { Preferences.data = {