fix(launcher): address review feedback on setup and runtime cleanup

This commit is contained in:
2026-09-10 13:32:12 -07:00
parent 8c7ad75033
commit 962442ccd5
7 changed files with 126 additions and 35 deletions
+1 -4
View File
@@ -1523,10 +1523,7 @@ const firstRunSetupService = createFirstRunSetupService({
},
installCommandLineLauncher: async () => {
const snapshot = await installCommandLineLauncher(createCommandLineLauncherRuntimeOptions());
const ok =
snapshot.status === 'ready' ||
snapshot.status === 'installed_bun_missing' ||
snapshot.status === 'not_on_path';
const ok = snapshot.status === 'ready' || snapshot.status === 'not_on_path';
return {
ok,
installPath: snapshot.installPath,
+30
View File
@@ -305,3 +305,33 @@ test('Windows stages a new runtime version while the prior Bun executable is run
assert.equal(fs.existsSync(path.dirname(first.bunPath)), false);
assert.ok(fs.existsSync(expectedSecond.bunPath));
});
test('Windows cleanup removes an obsolete runtime with no Bun executable', (t) => {
if (process.platform !== 'win32') return;
const root = workspace(t);
const current = windowsManagedRuntimePaths({
platform: 'win32',
localAppData: root,
appVersion: '2.0.0',
});
const obsolete = windowsManagedRuntimePaths({
platform: 'win32',
localAppData: root,
appVersion: '1.0.0',
});
fs.mkdirSync(path.join(path.dirname(obsolete.bunPath), 'licenses'), { recursive: true });
fs.writeFileSync(
path.join(path.dirname(obsolete.bunPath), 'licenses', 'Bun-LICENSE.md'),
'license',
);
fs.mkdirSync(path.dirname(current.bunPath), { recursive: true });
cleanupOldWindowsManagedRuntimes({
platform: 'win32',
localAppData: root,
appVersion: '2.0.0',
});
assert.equal(fs.existsSync(path.dirname(obsolete.bunPath)), false);
assert.ok(fs.existsSync(path.dirname(current.bunPath)));
});
+1 -1
View File
@@ -101,7 +101,7 @@ export function cleanupOldWindowsManagedRuntimes(
if (!entry.isDirectory() || entry.name === path.win32.basename(paths.directory)) continue;
const oldDirectory = path.win32.join(paths.rootDirectory, entry.name);
try {
fs.rmSync(path.win32.join(oldDirectory, 'bun.exe'));
fs.rmSync(path.win32.join(oldDirectory, 'bun.exe'), { force: true });
} catch {
continue;
}
@@ -160,7 +160,7 @@ test('app-managed wrappers are never overwritten by the standalone release scrip
});
test('GUI updates defer recognized standalone launcher migration to app startup', async () => {
let accessed = false;
const accessed: string[] = [];
let downloaded = false;
const result = await updateLauncherAtPath({
launcherPath: '/home/tester/.local/bin/subminer',
@@ -174,8 +174,8 @@ test('GUI updates defer recognized standalone launcher migration to app startup'
fs: {
stat: async () => ({ isFile: () => true }),
readFile: async () => Buffer.from('#!/bin/sh\n# SubMiner launcher\n'),
access: async () => {
accessed = true;
access: async (targetPath) => {
accessed.push(targetPath);
},
writeFile: async () => {},
chmod: async () => {},
@@ -189,7 +189,7 @@ test('GUI updates defer recognized standalone launcher migration to app startup'
path: '/home/tester/.local/bin/subminer',
message: 'Launcher migration is deferred until the updated SubMiner app starts.',
});
assert.equal(accessed, false);
assert.deepEqual(accessed, ['/home/tester/.local/bin/subminer', '/home/tester/.local/bin']);
assert.equal(downloaded, false);
});
@@ -213,9 +213,7 @@ test('release launcher updater propagates GUI migration deferral', async () => {
fs: {
stat: async () => ({ isFile: () => true }),
readFile: async () => Buffer.from('#!/bin/sh\n# SubMiner launcher\n'),
access: async () => {
throw new Error('must not check writability before app startup');
},
access: async () => {},
writeFile: async () => {},
chmod: async () => {},
rename: async () => {},
@@ -227,3 +225,36 @@ test('release launcher updater propagates GUI migration deferral', async () => {
assert.match(result.message ?? '', /deferred until the updated SubMiner app starts/);
assert.equal(downloaded, false);
});
test('GUI migration reports a protected launcher when its file or parent is not writable', async () => {
const launcherPath = '/usr/local/bin/subminer';
for (const protectedPath of [launcherPath, '/usr/local/bin']) {
const result = await updateLauncherAtPath({
launcherPath,
assetUrl: 'https://example.test/subminer',
expectedSha256: launcherHash,
deferRecognizedLauncherUpdate: true,
download: async () => {
throw new Error('Protected launchers must not download a replacement.');
},
fs: {
stat: async () => ({ isFile: () => true }),
readFile: async () => Buffer.from('#!/usr/bin/env bun\n// SubMiner launcher\n'),
access: async (targetPath) => {
if (targetPath === protectedPath) throw new Error('EACCES');
},
writeFile: async () => {
throw new Error('Protected launchers must not be written.');
},
chmod: async () => {},
rename: async () => {},
unlink: async () => {},
},
});
assert.equal(result.status, 'protected', protectedPath);
assert.equal(
result.command,
buildProtectedLauncherUpdateCommand('https://example.test/subminer', launcherPath),
);
}
});
+9 -8
View File
@@ -112,16 +112,9 @@ export async function updateLauncherAtPath(options: {
message: 'Existing executable does not look like a SubMiner launcher.',
};
}
if (options.deferRecognizedLauncherUpdate) {
return {
status: 'skipped',
path: options.launcherPath,
message: 'Launcher migration is deferred until the updated SubMiner app starts.',
};
}
try {
await fsDeps.access(options.launcherPath);
await fsDeps.access(path.dirname(options.launcherPath));
} catch {
return {
status: 'protected',
@@ -130,6 +123,14 @@ export async function updateLauncherAtPath(options: {
};
}
if (options.deferRecognizedLauncherUpdate) {
return {
status: 'skipped',
path: options.launcherPath,
message: 'Launcher migration is deferred until the updated SubMiner app starts.',
};
}
const data = await options.download();
const actualSha256 = sha256(data);
if (actualSha256 !== options.expectedSha256.toLowerCase()) {