Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/src/main/runtime-host-wsl-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ function runtimeHostWslSetupCommand(
'--lifecycle',
'on-demand',
'--repair-root-after-remount',
'--update-existing',
...(input.projectDirectoryRoots === undefined
? []
: input.projectDirectoryRoots.length === 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ describe('managed Runtime Host service', () => {
'--client-data-root',
'/var/lib/maka-client',
'--defer-pairing-commit',
'--update-existing',
'--enable-direct-peer',
'--coordination-relay',
'/dns4/discovery.example/udp/443/quic-v1',
Expand All @@ -469,6 +470,7 @@ describe('managed Runtime Host service', () => {
clientDataRoot: '/var/lib/maka-client',
lifecycle: 'supervised',
deferPairingCommit: true,
updateExisting: true,
directPeer: {
coordinationRelays: ['/dns4/discovery.example/udp/443/quic-v1'],
},
Expand Down
49 changes: 49 additions & 0 deletions packages/cli/src/__tests__/runtime-host-setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,55 @@ test('on-demand setup installs one exact deployment without a service backend',
'unsupported_lifecycle_configuration',
);

const replacementIntegrity = `sha512-${Buffer.alloc(64, 9).toString('base64')}`;
const replacementOptions = { ...options, version: '1.2.4' } as const;
const replacementPackage = {
...overrides,
resolveRegistryCandidate: async () => ({
kind: 'npm_registry' as const,
version: '1.2.4',
integrity: replacementIntegrity,
}),
};
const refused: string[] = [];
assert.equal(
await runRuntimeHostSetupCli(replacementOptions, {
...replacementPackage,
writeOutput: (value) => refused.push(value),
}),
1,
);
const refusedReplacement = refused
.map(decodeRuntimeHostSetupFrame)
.find((frame) => frame?.kind === 'error');
assert.equal(
refusedReplacement?.kind === 'error' ? refusedReplacement.error.code : undefined,
'version_change_requires_update',
);

const replacementOutputs: string[] = [];
assert.equal(
await runRuntimeHostSetupCli(
{ ...replacementOptions, updateExisting: true },
{
...replacementPackage,
openDeployment: async () =>
assert.fail('a changed exact package must be staged before replacement'),
writeOutput: (value) => replacementOutputs.push(value),
},
),
0,
);
const replaced = JSON.parse(
await readFile(resolveRuntimeHostManagedDeploymentConfigPath(rootId), 'utf8'),
) as RuntimeHostManagedDeploymentConfig;
assert.equal(replaced.launch.package.version, '1.2.4');
assert.equal(replaced.launch.package.integrity, replacementIntegrity);
assert.equal(
replacementOutputs.map(decodeRuntimeHostSetupFrame).some((frame) => frame?.kind === 'complete'),
true,
);

const uninstalled = await manageRuntimeHostManagedLifecycle(
rootId,
{
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/cli-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ export async function runMakaCli(
deferPairingCommit: command.deferPairingCommit,
bindPairingToClient: command.bindPairingToClient,
...(command.repairRootAfterRemount ? { repairRootAfterRemount: true } : {}),
updateExisting: command.updateExisting,
...(command.rootPath ? { rootPath: command.rootPath } : {}),
...(command.projectDirectoryRoots
? { projectDirectoryRoots: command.projectDirectoryRoots }
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/src/runtime-host-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export type RuntimeHostCliCommand =
deferPairingCommit: boolean;
bindPairingToClient?: true;
repairRootAfterRemount?: true;
updateExisting?: true;
clientDataRoot?: string;
rootPath?: string;
projectDirectoryRoots?: { label: string; path: string }[];
Expand Down Expand Up @@ -555,6 +556,7 @@ function parseSetupCommand(argv: string[]): RuntimeHostCliCommand {
let deferPairingCommit = false;
let bindPairingToClient = false;
let repairRootAfterRemount = false;
let updateExisting = false;
let clientDataRoot: string | undefined;
let enableDirectPeer = false;
const coordinationRelays: string[] = [];
Expand Down Expand Up @@ -603,6 +605,10 @@ function parseSetupCommand(argv: string[]): RuntimeHostCliCommand {
if (repairRootAfterRemount) return error('Duplicate --repair-root-after-remount');
repairRootAfterRemount = true;
},
'--update-existing': () => {
if (updateExisting) return error('Duplicate --update-existing');
updateExisting = true;
},
},
});
if ('kind' in options) return options;
Expand All @@ -625,6 +631,7 @@ function parseSetupCommand(argv: string[]): RuntimeHostCliCommand {
deferPairingCommit,
...(bindPairingToClient ? { bindPairingToClient: true } : {}),
...(repairRootAfterRemount ? { repairRootAfterRemount: true } : {}),
...(updateExisting ? { updateExisting: true } : {}),
...(clientDataRoot ? { clientDataRoot } : {}),
...(enableDirectPeer ? { directPeer: { coordinationRelays } } : {}),
};
Expand Down
64 changes: 38 additions & 26 deletions packages/cli/src/runtime-host-setup-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export interface RuntimeHostSetupCliOptions {
readonly deferPairingCommit?: boolean;
readonly bindPairingToClient?: boolean;
readonly repairRootAfterRemount?: true;
readonly updateExisting?: boolean;
readonly rootPath?: string;
readonly projectDirectoryRoots?: readonly {
readonly label: string;
Expand Down Expand Up @@ -743,7 +744,8 @@ async function runRuntimeHostOnDemandSetupLocked(
const serviceId = capability.rootId;
const deploymentRoot =
current?.deploymentRoot ?? resolveRuntimeHostManagedDeploymentRoot(serviceId);
if (current && !sameExactPackage(current, candidate)) {
const packageChanged = current !== undefined && !sameExactPackage(current, candidate);
if (current && packageChanged && !options.updateExisting) {
throw new RuntimeHostSetupError(
'version_change_requires_update',
`Runtime Host ${current.launch.package.version} is already installed; changing its exact package requires the update workflow`,
Expand Down Expand Up @@ -801,27 +803,30 @@ async function runRuntimeHostOnDemandSetupLocked(
await resolvedPackage.use(async (packageRoot) => {
let committed = false;
const created = !current;
let deployment: Awaited<ReturnType<typeof deps.prepareDeployment>> | undefined;
try {
emit({ kind: 'progress', phase: 'installing_package' });
const deployment = current
? await deps.openDeployment({
serviceId,
clientDataRoot: options.clientDataRoot,
deploymentRoot,
cliPath: resolveRuntimeHostManagedPackageCliPath(
deployment =
current && !packageChanged
? await deps.openDeployment({
serviceId,
clientDataRoot: options.clientDataRoot,
deploymentRoot,
candidate.version,
candidate.integrity,
),
version: candidate.version,
})
: await deps.prepareDeployment({
serviceId,
clientDataRoot: options.clientDataRoot,
sourcePackageRoot: packageRoot,
version: candidate.version,
packageIntegrity: candidate.integrity,
});
cliPath: resolveRuntimeHostManagedPackageCliPath(
deploymentRoot,
candidate.version,
candidate.integrity,
),
version: candidate.version,
})
: await deps.prepareDeployment({
serviceId,
clientDataRoot: options.clientDataRoot,
sourcePackageRoot: packageRoot,
version: candidate.version,
packageIntegrity: candidate.integrity,
...(current ? { deploymentRoot } : {}),
});
const desiredConfig: RuntimeHostManagedDeploymentConfig = current
? config
: { ...config, deploymentRoot: deployment.root };
Expand All @@ -839,11 +844,13 @@ async function runRuntimeHostOnDemandSetupLocked(
const replacement = await deps.replaceLifecycle({
operation: legacyToMigrate
? 'legacy_migration'
: current
? isDeepStrictEqual(current.lifecycle, config.lifecycle)
? 'configure'
: 'lifecycle_change'
: 'install',
: packageChanged
? 'update'
: current
? isDeepStrictEqual(current.lifecycle, config.lifecycle)
? 'configure'
: 'lifecycle_change'
: 'install',
...(current ? { current } : {}),
desired: desiredConfig,
...(legacyToMigrate && legacyBackend ? { retirementSupervisor: legacyBackend } : {}),
Expand All @@ -867,8 +874,13 @@ async function runRuntimeHostOnDemandSetupLocked(
(await resolveRuntimeHostManagedDeployment(capability.rootId)).config,
);
} catch (error) {
if (created && !committed && canDiscardRuntimeHostLifecycleDesiredArtifacts(error)) {
await removeRuntimeHostManagedDeployment(deploymentRoot, serviceId).catch(() => undefined);
if (!committed && canDiscardRuntimeHostLifecycleDesiredArtifacts(error)) {
if (packageChanged && deployment) await deployment.rollback().catch(() => undefined);
else if (created) {
await removeRuntimeHostManagedDeployment(deploymentRoot, serviceId).catch(
() => undefined,
);
}
}
throw error;
}
Expand Down
Loading