forked from OctoWoW/OctoLauncher
OctoLauncher 1.3.6
This commit is contained in:
+1
-1
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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',
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user