fix(managed-tools): wait for checksum handles before promotion
Windows CI / verify (push) Successful in 3m18s
Windows CI / verify (push) Successful in 3m18s
Delay managed-tool installation promotion until checksum read streams have emitted close, preventing intermittent Windows rename failures caused by open file handles. Add a deterministic end-before-close regression test and prepare the 1.0.9 patch release across package metadata, UI version text, README, changelog, and release contracts.
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.0.9 - 2026-08-12
|
||||||
|
|
||||||
|
- Waited for managed-tool checksum streams to close before promoting verified installations, preventing intermittent Windows repair failures caused by open file handles.
|
||||||
|
|
||||||
## 1.0.8 - 2026-08-12
|
## 1.0.8 - 2026-08-12
|
||||||
|
|
||||||
- Updated the Windows CI runtime to Node.js 24.11.1, resolving the SQLite native-module crash that interrupted verification workers on Node.js 22.13.0.
|
- Updated the Windows CI runtime to Node.js 24.11.1, resolving the SQLite native-module crash that interrupted verification workers on Node.js 22.13.0.
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ The application works in public mode without a Twitch login. Connecting a Twitch
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Open the [latest GitHub release](https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/latest).
|
1. Open the [latest GitHub release](https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/latest).
|
||||||
2. Download `Twitch-VOD-Manager-Setup-1.0.8.exe`.
|
2. Download `Twitch-VOD-Manager-Setup-1.0.9.exe`.
|
||||||
3. Run the installer and choose the installation directory.
|
3. Run the installer and choose the installation directory.
|
||||||
4. Start Twitch VOD Manager and add a streamer.
|
4. Start Twitch VOD Manager and add a streamer.
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "1.0.8",
|
"version": "1.0.9",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "1.0.8",
|
"version": "1.0.9",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.16.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "1.0.8",
|
"version": "1.0.9",
|
||||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@ if (process.platform === 'win32') {
|
|||||||
sourcePath: electronSourceExecutable,
|
sourcePath: electronSourceExecutable,
|
||||||
destinationPath: resolve(rootDirectory, 'node_modules', 'electron', 'dist', 'Twitch VOD Manager.exe'),
|
destinationPath: resolve(rootDirectory, 'node_modules', 'electron', 'dist', 'Twitch VOD Manager.exe'),
|
||||||
iconPath: resolve(rootDirectory, 'build', 'icon.ico'),
|
iconPath: resolve(rootDirectory, 'build', 'icon.ico'),
|
||||||
version: '1.0.8',
|
version: '1.0.9',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ function check(condition, message) {
|
|||||||
if (!condition) failures.push(message);
|
if (!condition) failures.push(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
check(packageJson.version === '1.0.8', `package version is ${packageJson.version}`);
|
check(packageJson.version === '1.0.9', `package version is ${packageJson.version}`);
|
||||||
check(packageLock.version === '1.0.8', `lockfile version is ${packageLock.version}`);
|
check(packageLock.version === '1.0.9', `lockfile version is ${packageLock.version}`);
|
||||||
check(packageLock.packages?.['']?.version === '1.0.8', `lockfile root package version is ${packageLock.packages?.['']?.version}`);
|
check(packageLock.packages?.['']?.version === '1.0.9', `lockfile root package version is ${packageLock.packages?.['']?.version}`);
|
||||||
check(packageJson.build?.appId === 'io.github.sucukdeluxe.twitch-vod-manager', `appId is ${packageJson.build?.appId}`);
|
check(packageJson.build?.appId === 'io.github.sucukdeluxe.twitch-vod-manager', `appId is ${packageJson.build?.appId}`);
|
||||||
check(packageJson.build?.publish?.provider === 'generic', `publish provider is ${packageJson.build?.publish?.provider}`);
|
check(packageJson.build?.publish?.provider === 'generic', `publish provider is ${packageJson.build?.publish?.provider}`);
|
||||||
check(packageJson.build?.publish?.url === 'https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/latest/download/', `publish URL is ${packageJson.build?.publish?.url}`);
|
check(packageJson.build?.publish?.url === 'https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/latest/download/', `publish URL is ${packageJson.build?.publish?.url}`);
|
||||||
@@ -62,7 +62,7 @@ check(mainSource.includes('GITHUB_RELEASES_DOWNLOAD_BASE_URL'), 'GitHub releases
|
|||||||
check(mainSource.includes('https://api.github.com/repos/Sucukdeluxe/Twitch-VOD-Manager/releases/latest'), 'GitHub latest release API URL is missing');
|
check(mainSource.includes('https://api.github.com/repos/Sucukdeluxe/Twitch-VOD-Manager/releases/latest'), 'GitHub latest release API URL is missing');
|
||||||
check(mainSource.includes('https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/download'), 'GitHub release download URL is missing');
|
check(mainSource.includes('https://github.com/Sucukdeluxe/Twitch-VOD-Manager/releases/download'), 'GitHub release download URL is missing');
|
||||||
check(!/storyboards\/\d{8,12}(?:-|\/)/.test(mainSource), 'numeric Twitch VOD example remains in the public source');
|
check(!/storyboards\/\d{8,12}(?:-|\/)/.test(mainSource), 'numeric Twitch VOD example remains in the public source');
|
||||||
check(indexSource.includes('Version: v1.0.8'), 'initial version label is not 1.0.8');
|
check(indexSource.includes('Version: v1.0.9'), 'initial version label is not 1.0.9');
|
||||||
check(!indexSource.includes('Version: v4.1.13'), 'legacy version label is still present');
|
check(!indexSource.includes('Version: v4.1.13'), 'legacy version label is still present');
|
||||||
check(fs.existsSync(manifestPath), 'public release manifest is missing');
|
check(fs.existsSync(manifestPath), 'public release manifest is missing');
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -940,7 +940,7 @@
|
|||||||
|
|
||||||
<div class="settings-card" data-settings-pane="updates" hidden>
|
<div class="settings-card" data-settings-pane="updates" hidden>
|
||||||
<h3 id="updateTitle">Updates</h3>
|
<h3 id="updateTitle">Updates</h3>
|
||||||
<p id="versionInfo" class="card-intro">Version: v1.0.8</p>
|
<p id="versionInfo" class="card-intro">Version: v1.0.9</p>
|
||||||
<button type="button" class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
<button type="button" class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
const fileSystemSpies = vi.hoisted(() => ({
|
const fileSystemSpies = vi.hoisted(() => ({
|
||||||
readFileSync: vi.fn()
|
readFileSync: vi.fn(),
|
||||||
|
createReadStream: vi.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('node:fs', async (importOriginal) => {
|
vi.mock('node:fs', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('node:fs')>();
|
const actual = await importOriginal<typeof import('node:fs')>();
|
||||||
fileSystemSpies.readFileSync.mockImplementation(actual.readFileSync);
|
fileSystemSpies.readFileSync.mockImplementation(actual.readFileSync);
|
||||||
return { ...actual, readFileSync: fileSystemSpies.readFileSync };
|
fileSystemSpies.createReadStream.mockImplementation(actual.createReadStream);
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
readFileSync: fileSystemSpies.readFileSync,
|
||||||
|
createReadStream: fileSystemSpies.createReadStream
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
import * as crypto from 'node:crypto';
|
import * as crypto from 'node:crypto';
|
||||||
|
import { EventEmitter } from 'node:events';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
@@ -380,4 +387,39 @@ describe('managed tool installer', () => {
|
|||||||
await expect(installer.status(manifest())).resolves.toMatchObject({ state: 'corrupt', verified: false });
|
await expect(installer.status(manifest())).resolves.toMatchObject({ state: 'corrupt', verified: false });
|
||||||
expect(fileSystemSpies.readFileSync).not.toHaveBeenCalled();
|
expect(fileSystemSpies.readFileSync).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('waits for hash streams to close before promoting a verified installation', async () => {
|
||||||
|
const createReadStream = fileSystemSpies.createReadStream.getMockImplementation();
|
||||||
|
if (!createReadStream) throw new Error('createReadStream mock is not initialized');
|
||||||
|
let openStreams = 0;
|
||||||
|
fileSystemSpies.createReadStream.mockImplementation((filePath: fs.PathLike) => {
|
||||||
|
const stream = new EventEmitter();
|
||||||
|
openStreams += 1;
|
||||||
|
queueMicrotask(() => {
|
||||||
|
stream.emit('data', fs.readFileSync(filePath));
|
||||||
|
stream.emit('end');
|
||||||
|
setImmediate(() => {
|
||||||
|
openStreams -= 1;
|
||||||
|
stream.emit('close');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return stream as fs.ReadStream;
|
||||||
|
});
|
||||||
|
|
||||||
|
const installPath = path.join(directory, 'installed');
|
||||||
|
const { installer } = createInstaller({
|
||||||
|
rename: (sourcePath, destinationPath) => {
|
||||||
|
if (sourcePath.includes('.stage-') && destinationPath === installPath && openStreams > 0) {
|
||||||
|
throw new Error('hash stream still open');
|
||||||
|
}
|
||||||
|
fs.renameSync(sourcePath, destinationPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(installer.repair(manifest())).resolves.toMatchObject({ success: true });
|
||||||
|
} finally {
|
||||||
|
fileSystemSpies.createReadStream.mockImplementation(createReadStream);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -96,11 +96,17 @@ function sha256File(filePath: string): Promise<string> {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const hash = crypto.createHash('sha256');
|
const hash = crypto.createHash('sha256');
|
||||||
const stream = fs.createReadStream(filePath);
|
const stream = fs.createReadStream(filePath);
|
||||||
|
let digest: string | null = null;
|
||||||
stream.on('data', (chunk) => {
|
stream.on('data', (chunk) => {
|
||||||
hash.update(chunk);
|
hash.update(chunk);
|
||||||
});
|
});
|
||||||
stream.once('error', reject);
|
stream.once('error', reject);
|
||||||
stream.once('end', () => resolve(hash.digest('hex')));
|
stream.once('end', () => {
|
||||||
|
digest = hash.digest('hex');
|
||||||
|
});
|
||||||
|
stream.once('close', () => {
|
||||||
|
if (digest !== null) resolve(digest);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user