fix(tools): recover staged first installs asynchronously

Promote a verified staged first installation after interruption and clear stale first-install journals. Stream archive and managed executable hashes, make status recovery non-blocking, and await the propagated tool and IPC status contracts.
This commit is contained in:
Sucukdeluxe
2026-08-12 01:23:15 +02:00
parent 60b5aa8ca9
commit aa06d486be
4 changed files with 158 additions and 76 deletions
+4 -4
View File
@@ -7977,9 +7977,9 @@ registerTrustedIpcHandler(ipcMain, 'run-preflight', isTrustedRendererEvent, () =
return await runPreflight(autoFix);
});
ipcMain.handle('get-managed-tool-status', (event) => {
ipcMain.handle('get-managed-tool-status', async (event) => {
if (!isTrustedRendererEvent(event)) return null;
return getManagedToolStatuses();
return await getManagedToolStatuses();
});
ipcMain.handle('repair-managed-tools', async (event) => {
@@ -7987,9 +7987,9 @@ ipcMain.handle('repair-managed-tools', async (event) => {
return await repairManagedTools();
});
ipcMain.handle('reset-managed-tools', (event) => {
ipcMain.handle('reset-managed-tools', async (event) => {
if (!isTrustedRendererEvent(event)) return null;
return resetManagedTools();
return await resetManagedTools();
});
registerTrustedIpcHandler(ipcMain, 'get-debug-log', isTrustedRendererEvent, () => Promise.resolve(''), async (_, lines: number = 200) => {
+69 -9
View File
@@ -1,4 +1,15 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const fileSystemSpies = vi.hoisted(() => ({
readFileSync: vi.fn()
}));
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>();
fileSystemSpies.readFileSync.mockImplementation(actual.readFileSync);
return { ...actual, readFileSync: fileSystemSpies.readFileSync };
});
import * as crypto from 'node:crypto';
import * as fs from 'node:fs';
import * as os from 'node:os';
@@ -187,7 +198,7 @@ describe('managed tool installer', () => {
});
});
it('marks a mismatched installed version as unverified', () => {
it('marks a mismatched installed version as unverified', async () => {
const { installer, installPath } = createInstaller();
writeExistingInstallation(installPath);
fs.writeFileSync(path.join(installPath, '.tool-manifest.json'), JSON.stringify({
@@ -198,7 +209,7 @@ describe('managed tool installer', () => {
sha256: sha256('old archive')
}));
expect(installer.status(manifest())).toMatchObject({
await expect(installer.status(manifest())).resolves.toMatchObject({
version: '8.3.0',
state: 'unverified',
verified: false
@@ -222,8 +233,7 @@ describe('managed tool installer', () => {
const first = installer.repair(manifest());
const second = installer.repair(manifest());
await Promise.resolve();
expect(download).toHaveBeenCalledTimes(1);
await vi.waitFor(() => expect(download).toHaveBeenCalledTimes(1));
releaseDownload?.();
const [firstResult, secondResult] = await Promise.all([first, second]);
@@ -233,7 +243,7 @@ describe('managed tool installer', () => {
expect(download).toHaveBeenCalledTimes(1);
});
it('restores a valid previous tool at the active path after interruption between backup and promotion', () => {
it('restores a valid previous tool at the active path after interruption between backup and promotion', async () => {
const { installer, installPath } = createInstaller();
const backupPath = `${installPath}.backup-interrupted`;
const stagingPath = `${installPath}.stage-interrupted`;
@@ -241,14 +251,14 @@ describe('managed tool installer', () => {
writeVerifiedInstallation(stagingPath, 'new executable');
writeInterruptedPromotionJournal(installPath, backupPath, stagingPath, 'backup-created');
expect(installer.status(manifest())).toMatchObject({ state: 'verified', verified: true });
await expect(installer.status(manifest())).resolves.toMatchObject({ state: 'verified', verified: true });
expect(fs.readFileSync(path.join(installPath, 'bin', 'streamlink.exe'), 'utf8')).toBe('old executable');
expect(fs.existsSync(backupPath)).toBe(false);
expect(fs.existsSync(stagingPath)).toBe(false);
expect(fs.existsSync(`${installPath}.transaction.json`)).toBe(false);
});
it('keeps a valid promoted tool after interruption between promotion and journal cleanup', () => {
it('keeps a valid promoted tool after interruption between promotion and journal cleanup', async () => {
const { installer, installPath } = createInstaller();
const backupPath = `${installPath}.backup-interrupted`;
const stagingPath = `${installPath}.stage-interrupted`;
@@ -256,7 +266,7 @@ describe('managed tool installer', () => {
writeVerifiedInstallation(backupPath, 'old executable');
writeInterruptedPromotionJournal(installPath, backupPath, stagingPath, 'backup-created');
expect(installer.status(manifest())).toMatchObject({ state: 'verified', verified: true });
await expect(installer.status(manifest())).resolves.toMatchObject({ state: 'verified', verified: true });
expect(fs.readFileSync(path.join(installPath, 'bin', 'streamlink.exe'), 'utf8')).toBe('new executable');
expect(fs.existsSync(backupPath)).toBe(false);
expect(fs.existsSync(`${installPath}.transaction.json`)).toBe(false);
@@ -313,11 +323,61 @@ describe('managed tool installer', () => {
expect(installed.success).toBe(true);
fs.writeFileSync(path.join(installPath, 'bin', 'streamlink.exe'), 'changed executable');
expect(installer.status(manifest())).toMatchObject({ state: 'corrupt', verified: false });
await expect(installer.status(manifest())).resolves.toMatchObject({ state: 'corrupt', verified: false });
const repaired = await installer.repair(manifest());
expect(repaired.success).toBe(true);
expect(repaired.status).toMatchObject({ state: 'verified', verified: true });
expect(fs.readFileSync(path.join(installPath, 'bin', 'streamlink.exe'), 'utf8')).toBe('new executable');
});
it('promotes a verified staged first install after interruption before promotion', async () => {
const { installer, installPath, download } = createInstaller();
const stagingPath = `${installPath}.stage-interrupted`;
writeVerifiedInstallation(stagingPath, 'first executable');
writeInterruptedPromotionJournal(installPath, `${installPath}.backup-interrupted`, stagingPath, 'staged');
await expect(installer.status(manifest())).resolves.toMatchObject({ state: 'verified', verified: true });
expect(fs.readFileSync(path.join(installPath, 'bin', 'streamlink.exe'), 'utf8')).toBe('first executable');
expect(fs.existsSync(stagingPath)).toBe(false);
expect(fs.existsSync(`${installPath}.transaction.json`)).toBe(false);
const result = await installer.install(manifest());
expect(result.success).toBe(true);
expect(download).not.toHaveBeenCalled();
});
it('clears a staged first-install journal after promotion before its phase update', async () => {
const { installer, installPath, download } = createInstaller();
writeVerifiedInstallation(installPath, 'first executable');
writeInterruptedPromotionJournal(installPath, `${installPath}.backup-interrupted`, `${installPath}.stage-interrupted`, 'staged');
await expect(installer.status(manifest())).resolves.toMatchObject({ state: 'verified', verified: true });
expect(fs.readFileSync(path.join(installPath, 'bin', 'streamlink.exe'), 'utf8')).toBe('first executable');
expect(fs.existsSync(`${installPath}.transaction.json`)).toBe(false);
const result = await installer.install(manifest());
expect(result.success).toBe(true);
expect(download).not.toHaveBeenCalled();
});
it('streams multi-megabyte executable verification without readFileSync and detects corruption', async () => {
const multiMegabyteExecutable = Buffer.alloc(3 * 1024 * 1024, 0x5a);
const { installer, installPath } = createInstaller({
extract: async (_archivePath, destinationPath) => {
fs.mkdirSync(path.join(destinationPath, 'bin'), { recursive: true });
fs.writeFileSync(path.join(destinationPath, 'bin', 'streamlink.exe'), multiMegabyteExecutable);
}
});
expect((await installer.repair(manifest())).success).toBe(true);
fileSystemSpies.readFileSync.mockClear();
await expect(installer.status(manifest())).resolves.toMatchObject({ state: 'verified', verified: true });
expect(fileSystemSpies.readFileSync).not.toHaveBeenCalled();
fs.writeFileSync(path.join(installPath, 'bin', 'streamlink.exe'), Buffer.alloc(3 * 1024 * 1024, 0x2a));
await expect(installer.status(manifest())).resolves.toMatchObject({ state: 'corrupt', verified: false });
expect(fileSystemSpies.readFileSync).not.toHaveBeenCalled();
});
});
+73 -53
View File
@@ -92,8 +92,16 @@ function findFileRecursive(rootDir: string, fileName: string): string | null {
return null;
}
function sha256File(filePath: string): string {
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
function sha256File(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('data', (chunk) => {
hash.update(chunk);
});
stream.once('error', reject);
stream.once('end', () => resolve(hash.digest('hex')));
});
}
function isRecord(value: unknown): value is ManagedToolRecord {
@@ -146,20 +154,20 @@ export class ManagedToolInstaller {
constructor(private readonly options: ManagedToolInstallerOptions) {}
status(manifest: ExternalToolManifest, includeInstallState = true): ManagedToolStatus {
async status(manifest: ExternalToolManifest, includeInstallState = true): Promise<ManagedToolStatus> {
if (includeInstallState && this.inFlight.has(manifest.id)) {
return this.createStatus(manifest, 'installing', false);
}
const recovery = this.recover(manifest, []);
const recovery = await this.recover(manifest, []);
if (!recovery.success) {
return this.createStatus(manifest, 'corrupt', false);
}
return this.statusFromInspection(manifest);
return await this.statusFromInspection(manifest);
}
install(manifest: ExternalToolManifest): Promise<ManagedToolInstallResult> {
const currentStatus = this.status(manifest);
async install(manifest: ExternalToolManifest): Promise<ManagedToolInstallResult> {
const currentStatus = await this.status(manifest);
if (currentStatus.verified) {
return Promise.resolve({ success: true, status: currentStatus, diagnostics: [] });
}
@@ -170,19 +178,19 @@ export class ManagedToolInstaller {
return this.start(manifest);
}
reset(manifest: ExternalToolManifest): ManagedToolStatus {
async reset(manifest: ExternalToolManifest): Promise<ManagedToolStatus> {
if (this.inFlight.has(manifest.id)) {
return this.status(manifest);
return await this.status(manifest);
}
const diagnostics: string[] = [];
const journal = this.readJournal();
const journal = await this.readJournal();
if (isJournal(journal, this.options.installationDirectory)) {
this.cleanup(journal.backupDirectory, true, diagnostics);
this.cleanup(journal.stagingDirectory, true, diagnostics);
}
this.cleanup(this.options.installationDirectory, true, diagnostics);
this.cleanup(this.journalPath(), false, diagnostics);
return this.status(manifest);
return await this.status(manifest);
}
private start(manifest: ExternalToolManifest): Promise<ManagedToolInstallResult> {
@@ -192,7 +200,7 @@ export class ManagedToolInstaller {
let operation!: Promise<ManagedToolInstallResult>;
operation = Promise.resolve()
.then(() => this.installOnce(manifest))
.catch((error: unknown) => this.failure(manifest, 'download-failed', this.errorText(error), []))
.catch(async (error: unknown) => await this.failure(manifest, 'download-failed', this.errorText(error), []))
.finally(() => {
if (this.inFlight.get(manifest.id) === operation) {
this.inFlight.delete(manifest.id);
@@ -209,29 +217,29 @@ export class ManagedToolInstaller {
const diagnostics: string[] = [];
try {
const recovery = this.recover(manifest, diagnostics);
const recovery = await this.recover(manifest, diagnostics);
if (!recovery.success) {
return this.failure(manifest, recovery.error!, recovery.detail, diagnostics);
return await this.failure(manifest, recovery.error!, recovery.detail, diagnostics);
}
fs.mkdirSync(this.options.temporaryDirectory, { recursive: true });
await this.options.download(manifest.sourceUrl, archivePath);
if (sha256File(archivePath).toLowerCase() !== manifest.sha256.toLowerCase()) {
return this.failure(manifest, 'archive-hash-mismatch', undefined, diagnostics);
if ((await sha256File(archivePath)).toLowerCase() !== manifest.sha256.toLowerCase()) {
return await this.failure(manifest, 'archive-hash-mismatch', undefined, diagnostics);
}
try {
await this.options.extract(archivePath, stagingDirectory);
} catch (error) {
return this.failure(manifest, 'extract-failed', this.errorText(error), diagnostics);
return await this.failure(manifest, 'extract-failed', this.errorText(error), diagnostics);
}
const executablePaths = this.executablePaths(stagingDirectory, manifest.executables);
if (!executablePaths) {
return this.failure(manifest, 'required-executable-missing', undefined, diagnostics);
return await this.failure(manifest, 'required-executable-missing', undefined, diagnostics);
}
const executableHashes = Object.fromEntries(executablePaths.map(([name, executablePath]) => [name, sha256File(executablePath)]));
const executableHashes = Object.fromEntries(await Promise.all(executablePaths.map(async ([name, executablePath]) => [name, await sha256File(executablePath)] as const)));
const record: ManagedToolRecord = {
id: manifest.id,
version: manifest.version,
@@ -242,21 +250,21 @@ export class ManagedToolInstaller {
};
fs.writeFileSync(path.join(stagingDirectory, RECORD_FILE_NAME), JSON.stringify(record));
const promotion = this.promote(manifest, stagingDirectory, diagnostics);
const promotion = await this.promote(manifest, stagingDirectory, diagnostics);
if (!promotion.success) {
return this.failure(manifest, promotion.error!, promotion.detail, diagnostics);
return await this.failure(manifest, promotion.error!, promotion.detail, diagnostics);
}
return { success: true, status: this.statusFromInspection(manifest), diagnostics };
return { success: true, status: await this.statusFromInspection(manifest), diagnostics };
} catch (error) {
return this.failure(manifest, 'download-failed', this.errorText(error), diagnostics);
return await this.failure(manifest, 'download-failed', this.errorText(error), diagnostics);
} finally {
this.cleanup(archivePath, false, diagnostics);
this.cleanup(stagingDirectory, true, diagnostics);
}
}
private promote(manifest: ExternalToolManifest, stagingDirectory: string, diagnostics: string[]): RecoveryResult {
private async promote(manifest: ExternalToolManifest, stagingDirectory: string, diagnostics: string[]): Promise<RecoveryResult> {
const backupDirectory = `${this.options.installationDirectory}.backup-${crypto.randomUUID()}`;
const journal: InstallationJournal = {
installationDirectory: this.options.installationDirectory,
@@ -279,26 +287,26 @@ export class ManagedToolInstaller {
this.cleanup(this.journalPath(), false, diagnostics);
return { success: true };
} catch (error) {
const recovery = this.recover(manifest, diagnostics);
const recovery = await this.recover(manifest, diagnostics);
return recovery.success
? { success: false, error: 'promotion-failed', detail: this.errorText(error) }
: recovery;
}
}
private recover(manifest: ExternalToolManifest, diagnostics: string[]): RecoveryResult {
const journal = this.readJournal();
private async recover(manifest: ExternalToolManifest, diagnostics: string[]): Promise<RecoveryResult> {
const journal = await this.readJournal();
if (journal === undefined) {
return { success: true };
}
if (!isJournal(journal, this.options.installationDirectory)) {
return { success: false, error: 'recovery-journal-invalid', detail: 'Ungültiges Installationsjournal.' };
}
return this.recoverJournal(manifest, journal, diagnostics);
return await this.recoverJournal(manifest, journal, diagnostics);
}
private recoverJournal(manifest: ExternalToolManifest, journal: InstallationJournal, diagnostics: string[]): RecoveryResult {
const active = this.inspectInstallation(this.options.installationDirectory, manifest);
private async recoverJournal(manifest: ExternalToolManifest, journal: InstallationJournal, diagnostics: string[]): Promise<RecoveryResult> {
const active = await this.inspectInstallation(this.options.installationDirectory, manifest);
if (active.strictValid || (journal.phase === 'staged' && active.layoutUsable)) {
this.cleanup(journal.backupDirectory, true, diagnostics);
this.cleanup(journal.stagingDirectory, true, diagnostics);
@@ -306,36 +314,48 @@ export class ManagedToolInstaller {
return { success: true };
}
const backup = this.inspectInstallation(journal.backupDirectory, manifest);
if (!backup.layoutUsable) {
const backup = await this.inspectInstallation(journal.backupDirectory, manifest);
if (backup.layoutUsable) {
let displacedDirectory: string | undefined;
try {
if (fs.existsSync(this.options.installationDirectory)) {
displacedDirectory = `${this.options.installationDirectory}.recovery-${crypto.randomUUID()}`;
this.rename(this.options.installationDirectory, displacedDirectory);
}
this.rename(journal.backupDirectory, this.options.installationDirectory);
} catch (error) {
return { success: false, error: 'recovery-restore-failed', detail: this.errorText(error) };
}
this.cleanup(journal.stagingDirectory, true, diagnostics);
if (displacedDirectory) {
this.cleanup(displacedDirectory, true, diagnostics);
}
this.cleanup(this.journalPath(), false, diagnostics);
return { success: true };
}
const staging = await this.inspectInstallation(journal.stagingDirectory, manifest);
if (journal.phase !== 'staged' || !staging.strictValid || fs.existsSync(this.options.installationDirectory)) {
return { success: false, error: 'recovery-restore-failed', detail: 'Kein nutzbares Tool-Backup für die Wiederherstellung vorhanden.' };
}
let displacedDirectory: string | undefined;
try {
if (fs.existsSync(this.options.installationDirectory)) {
displacedDirectory = `${this.options.installationDirectory}.recovery-${crypto.randomUUID()}`;
this.rename(this.options.installationDirectory, displacedDirectory);
}
this.rename(journal.backupDirectory, this.options.installationDirectory);
this.rename(journal.stagingDirectory, this.options.installationDirectory);
} catch (error) {
return { success: false, error: 'recovery-restore-failed', detail: this.errorText(error) };
}
this.cleanup(journal.stagingDirectory, true, diagnostics);
if (displacedDirectory) {
this.cleanup(displacedDirectory, true, diagnostics);
}
this.cleanup(this.journalPath(), false, diagnostics);
return { success: true };
}
private statusFromInspection(manifest: ExternalToolManifest): ManagedToolStatus {
const inspection = this.inspectInstallation(this.options.installationDirectory, manifest);
private async statusFromInspection(manifest: ExternalToolManifest): Promise<ManagedToolStatus> {
const inspection = await this.inspectInstallation(this.options.installationDirectory, manifest);
return this.createStatus(manifest, inspection.state, inspection.strictValid, inspection.version);
}
private inspectInstallation(directory: string, manifest: ExternalToolManifest): InstallationInspection {
private async inspectInstallation(directory: string, manifest: ExternalToolManifest): Promise<InstallationInspection> {
if (!fs.existsSync(directory)) {
return { state: 'missing', version: manifest.version, layoutUsable: false, strictValid: false };
}
@@ -345,7 +365,7 @@ export class ManagedToolInstaller {
return { state: 'corrupt', version: manifest.version, layoutUsable: false, strictValid: false };
}
const record = this.readRecord(directory);
const record = await this.readRecord(directory);
if (!record || !isRecord(record)) {
return { state: 'unverified', version: manifest.version, layoutUsable: true, strictValid: false };
}
@@ -357,7 +377,7 @@ export class ManagedToolInstaller {
}
try {
const hashesMatch = executablePaths.every(([name, executablePath]) => sha256File(executablePath).toLowerCase() === record.executableHashes[name].toLowerCase());
const hashesMatch = (await Promise.all(executablePaths.map(async ([name, executablePath]) => (await sha256File(executablePath)).toLowerCase() === record.executableHashes[name].toLowerCase()))).every(Boolean);
return hashesMatch
? { state: 'verified', version: record.version, layoutUsable: true, strictValid: true }
: { state: 'corrupt', version: record.version, layoutUsable: true, strictValid: false };
@@ -374,20 +394,20 @@ export class ManagedToolInstaller {
return paths as Array<[string, string]>;
}
private readRecord(directory: string): unknown {
private async readRecord(directory: string): Promise<unknown> {
try {
return JSON.parse(fs.readFileSync(path.join(directory, RECORD_FILE_NAME), 'utf8'));
return JSON.parse(await fs.promises.readFile(path.join(directory, RECORD_FILE_NAME), 'utf8'));
} catch {
return null;
}
}
private readJournal(): unknown | typeof INVALID_JOURNAL | undefined {
private async readJournal(): Promise<unknown | typeof INVALID_JOURNAL | undefined> {
try {
if (!fs.existsSync(this.journalPath())) {
return undefined;
}
return JSON.parse(fs.readFileSync(this.journalPath(), 'utf8'));
return JSON.parse(await fs.promises.readFile(this.journalPath(), 'utf8'));
} catch {
return INVALID_JOURNAL;
}
@@ -433,12 +453,12 @@ export class ManagedToolInstaller {
};
}
private failure(manifest: ExternalToolManifest, error: NonNullable<ManagedToolInstallResult['error']>, detail: string | undefined, diagnostics: string[]): ManagedToolInstallResult {
private async failure(manifest: ExternalToolManifest, error: NonNullable<ManagedToolInstallResult['error']>, detail: string | undefined, diagnostics: string[]): Promise<ManagedToolInstallResult> {
return {
success: false,
error,
detail,
status: this.statusFromInspection(manifest),
status: await this.statusFromInspection(manifest),
diagnostics
};
}
+12 -10
View File
@@ -409,10 +409,10 @@ export interface ManagedToolStatuses {
ffmpeg: ManagedToolStatus;
}
export function getManagedToolStatuses(): ManagedToolStatuses {
export async function getManagedToolStatuses(): Promise<ManagedToolStatuses> {
return {
streamlink: getManagedToolInstaller('streamlink').status(APPLICATION_TOOL_MANIFEST.streamlink),
ffmpeg: getManagedToolInstaller('ffmpeg').status(APPLICATION_TOOL_MANIFEST.ffmpeg)
streamlink: await getManagedToolInstaller('streamlink').status(APPLICATION_TOOL_MANIFEST.streamlink),
ffmpeg: await getManagedToolInstaller('ffmpeg').status(APPLICATION_TOOL_MANIFEST.ffmpeg)
};
}
@@ -424,17 +424,19 @@ export async function repairManagedTools(): Promise<{ success: boolean; statuses
refreshBundledToolPaths(true);
return {
success: streamlink.success && ffmpeg.success,
statuses: getManagedToolStatuses()
statuses: await getManagedToolStatuses()
};
}
export function resetManagedTools(): { success: boolean; statuses: ManagedToolStatuses } {
const streamlink = getManagedToolInstaller('streamlink').reset(APPLICATION_TOOL_MANIFEST.streamlink);
const ffmpeg = getManagedToolInstaller('ffmpeg').reset(APPLICATION_TOOL_MANIFEST.ffmpeg);
export async function resetManagedTools(): Promise<{ success: boolean; statuses: ManagedToolStatuses }> {
const [streamlink, ffmpeg] = await Promise.all([
getManagedToolInstaller('streamlink').reset(APPLICATION_TOOL_MANIFEST.streamlink),
getManagedToolInstaller('ffmpeg').reset(APPLICATION_TOOL_MANIFEST.ffmpeg)
]);
refreshBundledToolPaths(true);
return {
success: streamlink.state !== 'installing' && ffmpeg.state !== 'installing',
statuses: getManagedToolStatuses()
statuses: await getManagedToolStatuses()
};
}
@@ -445,7 +447,7 @@ export async function ensureStreamlinkInstalled(): Promise<boolean> {
refreshBundledToolPaths();
const manifest = APPLICATION_TOOL_MANIFEST.streamlink;
const managedStatus = getManagedToolInstaller('streamlink').status(manifest);
const managedStatus = await getManagedToolInstaller('streamlink').status(manifest);
const requiresManagedRepair = Boolean(bundledStreamlinkPath) && !managedStatus.verified;
const current = getStreamlinkCommand();
const versionArgs = [...current.prefixArgs, '--version'];
@@ -491,7 +493,7 @@ export async function ensureFfmpegInstalled(): Promise<boolean> {
refreshBundledToolPaths();
const manifest = APPLICATION_TOOL_MANIFEST.ffmpeg;
const managedStatus = getManagedToolInstaller('ffmpeg').status(manifest);
const managedStatus = await getManagedToolInstaller('ffmpeg').status(manifest);
const requiresManagedRepair = Boolean(bundledFFmpegPath || bundledFFprobePath) && !managedStatus.verified;
const ffmpegPath = getFFmpegPath();
const ffprobePath = getFFprobePath();