fix: extract managed media tools reliably in long Windows paths
Windows CI / verify (push) Canceled after 0s
Windows CI / verify (push) Canceled after 0s
This commit is contained in:
@@ -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);
|
||||
});
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user