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
-1
@@ -940,7 +940,7 @@
|
||||
|
||||
<div class="settings-card" data-settings-pane="updates" hidden>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const fileSystemSpies = vi.hoisted(() => ({
|
||||
readFileSync: vi.fn()
|
||||
readFileSync: vi.fn(),
|
||||
createReadStream: 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 };
|
||||
fileSystemSpies.createReadStream.mockImplementation(actual.createReadStream);
|
||||
return {
|
||||
...actual,
|
||||
readFileSync: fileSystemSpies.readFileSync,
|
||||
createReadStream: fileSystemSpies.createReadStream
|
||||
};
|
||||
});
|
||||
|
||||
import * as crypto from 'node:crypto';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
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 });
|
||||
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) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
const stream = fs.createReadStream(filePath);
|
||||
let digest: string | null = null;
|
||||
stream.on('data', (chunk) => {
|
||||
hash.update(chunk);
|
||||
});
|
||||
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