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:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## 1.0.20 - 2026-09-06
|
## 1.0.20 - 2026-09-06
|
||||||
|
|
||||||
|
- Fix automatic media-tool setup in long Windows paths by using the built-in archive extractor. Report extraction failures instead of accepting incomplete installations.
|
||||||
- Make queue cards easier to read with two-line titles, larger dates below the progress bar on the right, and 32-pixel remove and retry buttons.
|
- Make queue cards easier to read with two-line titles, larger dates below the progress bar on the right, and 32-pixel remove and retry buttons.
|
||||||
- Show clearer download states, preserve the progress percentage while paused, and wrap long error messages. Keep the waiting status simple with a yellow dot and no badge background or outline.
|
- Show clearer download states, preserve the progress percentage while paused, and wrap long error messages. Keep the waiting status simple with a yellow dot and no badge background or outline.
|
||||||
- Open and close download details by double-clicking the card surface or using the dedicated keyboard-accessible arrow.
|
- Open and close download details by double-clicking the card surface or using the dedicated keyboard-accessible arrow.
|
||||||
|
|||||||
+1
-1
@@ -81,7 +81,7 @@ npm run dev
|
|||||||
- Tatsächlicher Start über `scripts/dev.mjs` mit der umbenannten Windows-EXE erfolgreich: Anwendungsfenster geöffnet, TypeScript-Watcher aktiv mit null Fehlern. Renderer-Dateien werden automatisch neu geladen; Änderungen am Main-Prozess lösen einen Neustart aus.
|
- Tatsächlicher Start über `scripts/dev.mjs` mit der umbenannten Windows-EXE erfolgreich: Anwendungsfenster geöffnet, TypeScript-Watcher aktiv mit null Fehlern. Renderer-Dateien werden automatisch neu geladen; Änderungen am Main-Prozess lösen einen Neustart aus.
|
||||||
- Hot-Dev läuft mit den isolierten Entwicklungsdatenverzeichnissen. Der Launcher kann im Hintergrund über Node gestartet werden; Konsolenprotokolle liegen bei diesem Start außerhalb des Repositories im Windows-Temp-Verzeichnis.
|
- Hot-Dev läuft mit den isolierten Entwicklungsdatenverzeichnissen. Der Launcher kann im Hintergrund über Node gestartet werden; Konsolenprotokolle liegen bei diesem Start außerhalb des Repositories im Windows-Temp-Verzeichnis.
|
||||||
- Streamlink 8.4.0 im Entwicklungsverzeichnis eingerichtet und per `--version` geprüft. Der vorhandene Managed-Tool-Installer hat Archiv- und EXE-Prüfsummen verifiziert; zum Entpacken wurde lokal PowerShell 7 mit `Expand-Archive -LiteralPath` und über Umgebungsvariablen übergebenen Pfaden verwendet.
|
- Streamlink 8.4.0 im Entwicklungsverzeichnis eingerichtet und per `--version` geprüft. Der vorhandene Managed-Tool-Installer hat Archiv- und EXE-Prüfsummen verifiziert; zum Entpacken wurde lokal PowerShell 7 mit `Expand-Archive -LiteralPath` und über Umgebungsvariablen übergebenen Pfaden verwendet.
|
||||||
- Der automatische Installationsversuch mit Windows PowerShell meldete zuvor `required-executable-missing`. Die lokale Einrichtung ist behoben; die Ursache im allgemeinen Entpackablauf ist noch offen.
|
- Der automatische Installationsversuch mit Windows PowerShell meldete zuvor `required-executable-missing`. Bei der Release-Prüfung wurde die Ursache reproduziert: lange Entpackpfade führen zu Dateifehlern und Rollback in `Expand-Archive`, obwohl PowerShell mit Exitcode 0 endet. Seit der 1.0.20-Vorbereitung verwendet die Anwendung primär das integrierte Windows-`tar.exe` mit strukturierten Argumenten und Zeitlimit; nur wenn es fehlt, folgt Windows PowerShell mit LiteralPath, eigenen Pfadvariablen und `-ErrorAction Stop`.
|
||||||
- Anschließender Hot-Reload-Neustart tatsächlich ausgelöst und geprüft: Das neue Anwendungsfenster reagiert, und der reale Preflight meldet Internet, Streamlink, FFmpeg, FFprobe und beschreibbares Download-Verzeichnis als erfolgreich. Keine Live-Downloads gestartet.
|
- Anschließender Hot-Reload-Neustart tatsächlich ausgelöst und geprüft: Das neue Anwendungsfenster reagiert, und der reale Preflight meldet Internet, Streamlink, FFmpeg, FFprobe und beschreibbares Download-Verzeichnis als erfolgreich. Keine Live-Downloads gestartet.
|
||||||
|
|
||||||
6. September 2026, Queue-Card-Rework (unveröffentlicht, Version weiterhin 1.0.19):
|
6. September 2026, Queue-Card-Rework (unveröffentlicht, Version weiterhin 1.0.19):
|
||||||
|
|||||||
@@ -112,6 +112,8 @@
|
|||||||
"src/main/infra/format-helpers.ts",
|
"src/main/infra/format-helpers.ts",
|
||||||
"src/main/infra/fs-atomic.test.ts",
|
"src/main/infra/fs-atomic.test.ts",
|
||||||
"src/main/infra/fs-atomic.ts",
|
"src/main/infra/fs-atomic.ts",
|
||||||
|
"src/main/infra/extract-zip.ts",
|
||||||
|
"src/main/infra/extract-zip.test.ts",
|
||||||
"src/main/infra/loopback-server.test.ts",
|
"src/main/infra/loopback-server.test.ts",
|
||||||
"src/main/infra/loopback-server.ts",
|
"src/main/infra/loopback-server.ts",
|
||||||
"src/main/infra/schema-v5.ts",
|
"src/main/infra/schema-v5.ts",
|
||||||
|
|||||||
@@ -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 path from 'path';
|
||||||
import * as fs from 'fs';
|
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 axios from 'axios';
|
||||||
import { createManagedToolInstaller, type ManagedToolInstaller, type ManagedToolStatus } from './main/domain/managed-tools';
|
import { createManagedToolInstaller, type ManagedToolInstaller, type ManagedToolStatus } from './main/domain/managed-tools';
|
||||||
import { APPLICATION_TOOL_MANIFEST } from './main/domain/tool-manifest';
|
import { APPLICATION_TOOL_MANIFEST } from './main/domain/tool-manifest';
|
||||||
@@ -358,31 +359,7 @@ async function extractZip(zipPath: string, destinationDir: string): Promise<bool
|
|||||||
try {
|
try {
|
||||||
fs.mkdirSync(destinationDir, { recursive: true });
|
fs.mkdirSync(destinationDir, { recursive: true });
|
||||||
|
|
||||||
const command = `Expand-Archive -Path '${zipPath.replace(/'/g, "''")}' -DestinationPath '${destinationDir.replace(/'/g, "''")}' -Force`;
|
await extractZipArchive(zipPath, destinationDir);
|
||||||
|
|
||||||
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));
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user