Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ function serviceResult(
operatorAccess = false,
): Exclude<
Extract<RuntimeHostServiceManagementFrame, { kind: 'result' }>,
{ action: 'check_update' | 'update' }
{ action: 'check_update' | 'update' | 'update_policy' | 'reconcile_update' }
> {
const result = {
schemaVersion: 1 as const,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ test('reads a framed service result without projecting it into the SSH terminal'

const result = await management;
assert.equal(result.kind, 'result');
if (result.kind !== 'result') assert.fail('expected service management result');
if (result.kind !== 'result' || result.action !== 'status') {
assert.fail('expected service status result');
}
assert.equal(result.service.installedVersion, '1.2.3');
assert.doesNotMatch(JSON.stringify(harness.events), /MAKA_RUNTIME_HOST_SERVICE/u);
assert.match(JSON.stringify(harness.events), /Password/u);
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/main/runtime-host-ssh-terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ export interface DesktopRuntimeHostSshManagementInput {
readonly destination: string;
readonly sshPort?: number;
readonly operatorPath: string;
readonly action: Exclude<RuntimeHostServiceManagementAction, 'check_update' | 'update'>;
readonly action: Exclude<
RuntimeHostServiceManagementAction,
'check_update' | 'update' | 'update_policy' | 'reconcile_update'
>;
readonly expectedTarget: {
readonly serviceId: string;
readonly rootPath: string;
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,20 @@ installs or switches a package. Installation-management callers can pass the sam
to the existing exact-package update transaction, and does not mutate a candidate that requires
manual review.

The installation owner can persist one update target and reconcile it with the same verified
transaction:

```sh
maka runtime-host service update-policy --target latest \
--expected-service-id <service-id> \
--expected-root-path <state-root> \
--expected-root-id <root-id>
maka runtime-host service reconcile-update --json
```

Use `update-policy --target manual` to disable automatic reconciliation. Reconciliation is a
bounded one-shot command: it never interrupts active work and does not install a scheduler.

## Uninstall

```sh
Expand Down
13 changes: 13 additions & 0 deletions packages/cli/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,19 @@ maka runtime-host service check-update --target next --json
selector 传给 `service update --target`。该路径会先校验 archive 与解包后的 manifest,再委托给
现有的精确 package 更新事务;需要人工审查的候选不会改变当前 Host。

Installation owner 可以持久化一个更新目标,并通过同一套已验证事务执行 reconciliation:

```sh
maka runtime-host service update-policy --target latest \
--expected-service-id <service-id> \
--expected-root-path <state-root> \
--expected-root-id <root-id>
maka runtime-host service reconcile-update --json
```

使用 `update-policy --target manual` 关闭自动 reconciliation。Reconciliation 是有界的单次命令:
它不会中断 active work,也不会安装 scheduler。

## 卸载

```sh
Expand Down
38 changes: 37 additions & 1 deletion packages/cli/src/__tests__/runtime-host-selected-update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { decodeRuntimeHostServiceManagementFrame } from '@maka/runtime-host/operator';
import {
runManagedRuntimeHostUpdateCli,
runManagedRuntimeHostSelectedUpdateCli,
type RuntimeHostSelectedUpdateCliOptions,
type RuntimeHostUpdateCliOptions,
Expand All @@ -45,6 +46,41 @@ const OPTIONS: RuntimeHostSelectedUpdateCliOptions = {
};

describe('managed Runtime Host selected update', () => {
it('revalidates selection inside the deployment lock before reading service state', async () => {
let lockHeld = false;
let output = '';
assert.equal(
await runManagedRuntimeHostUpdateCli(
{
...OPTIONS,
sourcePackageRoot: '/verified/package',
version: '2.0.0',
},
{
withDeploymentLock: async (_root, operation) => {
lockHeld = true;
try {
return await operation();
} finally {
lockHeld = false;
}
},
revalidateSelection: async () => {
assert.equal(lockHeld, true);
return { code: 'update_policy_changed', message: 'The policy changed' };
},
manage: async () => assert.fail('service state must not be read'),
writeOutput: (value) => {
output += value;
},
},
),
1,
);
const frame = decodeRuntimeHostServiceManagementFrame(output.trim());
assert.equal(frame?.kind === 'error' ? frame.error.code : undefined, 'update_policy_changed');
});

it('parses an optional target without changing the exact-package command', () => {
assert.deepEqual(
parseRuntimeHostCommand([
Expand Down Expand Up @@ -111,7 +147,7 @@ describe('managed Runtime Host selected update', () => {
});
});

it('lets the exact transaction decide whether a current candidate needs repair', async () => {
it('lets the exact transaction inspect the current deployment without downloading it again', async () => {
const selection = updateSelection({ kind: 'current' });
let updateInput: RuntimeHostUpdateCliOptions | undefined;
assert.equal(
Expand Down
41 changes: 36 additions & 5 deletions packages/cli/src/__tests__/runtime-host-service-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ import {
type RuntimeHostManagedServiceResult,
type RuntimeHostServiceBackend,
} from '../runtime-host-service-manager.js';
import {
readRuntimeHostManagedUpdatePolicy,
writeRuntimeHostManagedUpdatePolicy,
} from '../runtime-host-update-policy-store.js';
import {
createSystemdUserRuntimeHostService,
renderSystemdUnit,
Expand Down Expand Up @@ -394,6 +398,25 @@ describe('managed Runtime Host service', () => {
rootPath: root.canonicalPath,
rootId: root.rootId,
} as const;
const updatePolicy = {
schemaVersion: 1 as const,
policy: { kind: 'channel' as const, channel: 'latest' as const },
target: expectedTarget,
};
await writeRuntimeHostManagedUpdatePolicy(deploymentRoot, updatePolicy);
await assert.rejects(
manageRuntimeHostService(
{
...common,
action: 'uninstall',
expectedTarget: { ...expectedTarget, rootId: 'f'.repeat(64) },
},
backend(),
),
(error: unknown) =>
error instanceof RuntimeHostServiceManagerError && error.code === 'target_mismatch',
);
assert.deepEqual(await readRuntimeHostManagedUpdatePolicy(deploymentRoot), updatePolicy);
await assert.rejects(
cleanupRuntimeHostManagedDeployment(
{ clientDataRoot, cliPath: canonicalCliPath, expectedTarget },
Expand All @@ -412,6 +435,7 @@ describe('managed Runtime Host service', () => {
},
backend(),
);
assert.equal(await readRuntimeHostManagedUpdatePolicy(deploymentRoot), null);
await cleanupRuntimeHostManagedDeployment(
{ clientDataRoot, cliPath: canonicalCliPath, expectedTarget },
backend(),
Expand Down Expand Up @@ -822,15 +846,19 @@ describe('managed Runtime Host service', () => {
const legacyFrame = decodeRuntimeHostServiceManagementFrame(await run());
assert.equal(legacyFrame?.kind, 'result');
assert.equal(
legacyFrame?.kind === 'result' ? legacyFrame.operatorCapabilities : undefined,
legacyFrame?.kind === 'result' && legacyFrame.action === 'status'
? legacyFrame.operatorCapabilities
: undefined,
undefined,
);

process.env[RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST_ENV] =
RUNTIME_HOST_OPERATOR_ACCESS_MANAGEMENT_CAPABILITY;
const frame = decodeRuntimeHostServiceManagementFrame(await run());
assert.equal(frame?.kind, 'result');
if (frame?.kind !== 'result') assert.fail('Expected a service result frame');
if (frame?.kind !== 'result' || frame.action !== 'status') {
assert.fail('Expected a service status result frame');
}
assert.equal(frame.service.installedVersion, '1.2.3');
assert.deepEqual(frame.operatorCapabilities, ['access-management-v1']);
assert.equal(frame.service.stateRoot, '/srv/maka');
Expand All @@ -839,9 +867,12 @@ describe('managed Runtime Host service', () => {
process.env[RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST_ENV] =
RUNTIME_HOST_OPERATOR_PROCESS_LIFETIME_LOCK_CAPABILITY;
const lockFrame = decodeRuntimeHostServiceManagementFrame(await run());
assert.deepEqual(lockFrame?.kind === 'result' ? lockFrame.operatorCapabilities : undefined, [
'process-lifetime-lock-v1',
]);
assert.deepEqual(
lockFrame?.kind === 'result' && lockFrame.action === 'status'
? lockFrame.operatorCapabilities
: undefined,
['process-lifetime-lock-v1'],
);
});

it('reads service logs when an interrupted install left no config', async (t) => {
Expand Down
Loading