fix: extract managed media tools reliably in long Windows paths
Windows CI / verify (push) Canceled after 0s

This commit is contained in:
Sucukdeluxe
2026-09-06 12:56:20 +02:00
parent 0c01668e55
commit 6b41767a71
6 changed files with 76 additions and 27 deletions
+43
View File
@@ -0,0 +1,43 @@
import { beforeEach, expect, it, vi } from 'vitest';
import { execFile } from 'node:child_process';
import { extractZipArchive } from './extract-zip';
vi.mock('node:child_process', () => ({ execFile: vi.fn() }));
const execute = vi.mocked(execFile);
const archive = "C:\\Downloads\\Sascha's [tools] & media.zip";
const destination = "C:\\Downloads\\Sascha's [tools] & media";
beforeEach(() => { execute.mockReset(); });
it('passes archive paths literally to the built-in Windows extractor without a shell', async () => {
execute.mockImplementation((...args: unknown[]) => {
(args[3] as (error: Error | null) => void)(null);
return {} as ReturnType<typeof execFile>;
});
await extractZipArchive(archive, destination);
expect(execute).toHaveBeenCalledWith(expect.stringMatching(/System32[\\/]tar\.exe$/), ['-xf', archive, '-C', destination], expect.objectContaining({ windowsHide: true, timeout: 120000 }), expect.any(Function));
});
it('falls back only when tar is unavailable and keeps paths out of PowerShell source', async () => {
execute.mockImplementation((...args: unknown[]) => {
(args[3] as (error: Error | null) => void)(execute.mock.calls.length === 1 ? Object.assign(new Error('missing'), { code: 'ENOENT' }) : null);
return {} as ReturnType<typeof execFile>;
});
await extractZipArchive(archive, destination);
expect(execute).toHaveBeenCalledTimes(2);
const fallback = execute.mock.calls[1];
expect(fallback[1]).toEqual(expect.arrayContaining([expect.stringContaining('-ErrorAction Stop')]));
expect(JSON.stringify(fallback[1])).not.toContain('Sascha');
expect(fallback[2]).toMatchObject({ env: { TVM_ARCHIVE_PATH: archive, TVM_EXTRACT_PATH: destination } });
});
it('rejects extraction errors instead of reporting an incomplete archive as successful', async () => {
const failure = Object.assign(new Error('invalid archive'), { code: 1 });
execute.mockImplementation((...args: unknown[]) => {
(args[3] as (error: Error | null) => void)(failure);
return {} as ReturnType<typeof execFile>;
});
await expect(extractZipArchive(archive, destination)).rejects.toBe(failure);
expect(execute).toHaveBeenCalledTimes(1);
});
+26
View File
@@ -0,0 +1,26 @@
import { execFile } from 'node:child_process';
import * as path from 'node:path';
function runExtractor(executable: string, args: string[], env = process.env): Promise<void> {
return new Promise((resolve, reject) => {
execFile(executable, args, { windowsHide: true, timeout: 120000, maxBuffer: 1024 * 1024, env }, (error) => {
if (error) reject(error);
else resolve();
});
});
}
export async function extractZipArchive(archivePath: string, destinationPath: string): Promise<void> {
const systemDirectory = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32');
try {
await runExtractor(path.join(systemDirectory, 'tar.exe'), ['-xf', archivePath, '-C', destinationPath]);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
await runExtractor(path.join(systemDirectory, 'WindowsPowerShell', 'v1.0', 'powershell.exe'), [
'-NoProfile',
'-NonInteractive',
'-Command',
"$env:PSModulePath = Join-Path $PSHOME 'Modules'; Expand-Archive -LiteralPath $env:TVM_ARCHIVE_PATH -DestinationPath $env:TVM_EXTRACT_PATH -Force -ErrorAction Stop",
], { ...process.env, TVM_ARCHIVE_PATH: archivePath, TVM_EXTRACT_PATH: destinationPath });
}
}
+3 -26
View File
@@ -1,6 +1,7 @@
import * as path from 'path';
import * as fs from 'fs';
import { spawn, execSync, spawnSync } from 'child_process';
import { extractZipArchive } from './main/infra/extract-zip';
import { execSync, spawnSync } from 'child_process';
import axios from 'axios';
import { createManagedToolInstaller, type ManagedToolInstaller, type ManagedToolStatus } from './main/domain/managed-tools';
import { APPLICATION_TOOL_MANIFEST } from './main/domain/tool-manifest';
@@ -358,31 +359,7 @@ async function extractZip(zipPath: string, destinationDir: string): Promise<bool
try {
fs.mkdirSync(destinationDir, { recursive: true });
const command = `Expand-Archive -Path '${zipPath.replace(/'/g, "''")}' -DestinationPath '${destinationDir.replace(/'/g, "''")}' -Force`;
await new Promise<void>((resolve, reject) => {
const proc = spawn('powershell', [
'-NoProfile',
'-ExecutionPolicy', 'Bypass',
'-Command',
command
], { windowsHide: true });
let stderr = '';
proc.stderr?.on('data', (data) => {
stderr += data.toString();
});
proc.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Expand-Archive exit code ${code}: ${stderr.trim()}`));
}
});
proc.on('error', (err) => reject(err));
});
await extractZipArchive(zipPath, destinationDir);
return true;
} catch (e) {