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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Submit only work you have the right to contribute, and record third-party source

## Quick start

Requires Node `>=22.19.0` and npm `11.19.0` (root `package.json`); desktop work needs macOS Apple Silicon.
Requires Node `>=22.19.0` and npm `11.19.0` (root `package.json`). Direct Peer or Peer Mesh Desktop development additionally needs Rust stable 1.98 or newer and Xcode Command Line Tools on macOS, or MSVC Build Tools on Windows.

```sh
git clone https://github.com/apache/maka.git
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

## 快速开始

需要 Node `>=22.19.0` 和 npm `11.19.0`(见根 `package.json`);桌面端开发需要 macOS Apple Silicon
需要 Node `>=22.19.0` 和 npm `11.19.0`(见根 `package.json`)。开发 Desktop Direct Peer 或 Peer Mesh 还需要 Rust stable 1.98 或更高版本,以及 macOS 的 Xcode Command Line Tools 或 Windows 的 MSVC Build Tools

```sh
git clone https://github.com/apache/maka.git
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ npm run dev
npm run dev:full
```

Direct Peer and Peer Mesh development additionally requires Rust stable 1.98 or newer and the
platform linker (Xcode Command Line Tools on macOS, MSVC Build Tools on Windows). Use the
peer-enabled entry point so the native addon is built before Desktop starts:

```sh
npm run dev:peer # HMR
npm run dev:full:peer # full build
```

If dependencies were installed with `ELECTRON_SKIP_BINARY_DOWNLOAD=1`, install the Electron platform binary before starting:

```sh
Expand Down
9 changes: 9 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ npm run dev
npm run dev:full
```

开发 Direct Peer 和 Peer Mesh 还需要 Rust stable 1.98 或更高版本及平台 linker
(macOS 使用 Xcode Command Line Tools,Windows 使用 MSVC Build Tools)。使用 Peer 开发入口,
Desktop 会在启动前构建原生 addon:

```sh
npm run dev:peer # HMR
npm run dev:full:peer # 完整构建
```

如果安装时设置过 `ELECTRON_SKIP_BINARY_DOWNLOAD=1`,启动前需要补装 Electron 平台二进制:

```sh
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
"main": "dist/main/main.js",
"scripts": {
"start": "node scripts/start-dev-app.mjs",
"start:peer": "npm run prepare:runtime-host-peer && node scripts/start-dev-app.mjs --runtime-host-peer",
"prepare:dev-app": "node scripts/prepare-dev-app.mjs",
"prepare:runtime-host-peer": "node ../../native/runtime-host-peer/build.mjs",
"dev": "node scripts/dev.mjs",
"dev:peer": "npm run prepare:runtime-host-peer && node scripts/dev.mjs --runtime-host-peer",
"dev:hmr": "node scripts/dev.mjs",
"storybook": "storybook dev -p 6006 -c .storybook",
"build-storybook": "storybook build -c .storybook --output-dir storybook-static",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,19 @@
*/

import assert from 'node:assert/strict';
import type { spawn } from 'node:child_process';
import { createHash } from 'node:crypto';
import { EventEmitter } from 'node:events';
import { PassThrough } from 'node:stream';
import test from 'node:test';
import { runtimeHostLocalSetupCommand } from '../runtime-host-local-operator.js';
import {
encodeRuntimeHostSetupFrame,
RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV,
} from '@maka/runtime-host/operator';
import {
createDesktopRuntimeHostLocalOperator,
runtimeHostLocalSetupCommand,
} from '../runtime-host-local-operator.js';

test('local setup installs one managed service for the Desktop root with Direct peer enabled', () => {
assert.deepEqual(
Expand Down Expand Up @@ -56,3 +67,55 @@ test('local setup installs one managed service for the Desktop root with Direct
},
);
});

test('local setup forwards the exact development archive evidence', async (t) => {
const archive = '/tmp/maka-agent-development.tgz';
const archiveBytes = Buffer.from('development package');
const integrity = `sha512-${createHash('sha512').update(archiveBytes).digest('base64')}`;
let environment: NodeJS.ProcessEnv | undefined;
const spawnProcess = ((_command, _args, options) => {
environment = options?.env;
const child = new EventEmitter() as ReturnType<typeof spawn>;
const stdout = new PassThrough();
const stderr = new PassThrough();
Object.assign(child, { pid: 1234, stdout, stderr, kill: () => true });
process.nextTick(() => {
stdout.end(encodeRuntimeHostSetupFrame({
schemaVersion: 1,
sequence: 0,
kind: 'complete',
version: '0.2.0-development',
serviceId: 'b'.repeat(64),
deploymentId: '00000000-0000-4000-8000-000000000001',
operatorPath: '/tmp/maka/operator',
rootPath: '/tmp/maka/root',
rootId: 'a'.repeat(64),
endpoint: 'ws://127.0.0.1:7443/runtime-host',
credentialId: 'credential-1',
credential: 'secret-access-token',
}));
stderr.end();
child.emit('close', 0, null);
});
return child;
}) as typeof spawn;
const operator = createDesktopRuntimeHostLocalOperator({
environment: { PATH: process.env.PATH },
spawnProcess,
});
t.after(() => operator.close());

await operator.runSetup({
setupPackage: { kind: 'development_archive', path: archive, integrity },
clientDataRoot: '/tmp/maka/client',
rootPath: '/tmp/maka/root',
principalId: 'desktop-owner:pairing',
expectedTarget: {
serviceId: 'b'.repeat(64),
rootPath: '/tmp/maka/root',
rootId: 'a'.repeat(64),
},
}, () => undefined);

assert.equal(environment?.[RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV], integrity);
});
67 changes: 67 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-peer-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import test from 'node:test';
import { configureDesktopRuntimeHostPeerClient } from '../runtime-host-peer-client.js';

test('development uses the native peer addon only for the peer-enabled launch', async (t) => {
const root = await mkdtemp(join(tmpdir(), 'maka-desktop-peer-client-'));
t.after(() => rm(root, { recursive: true, force: true }));
const appPath = join(root, 'apps', 'desktop');
const nativePath = join(
root,
'native',
'runtime-host-peer',
'target',
'release',
'maka_runtime_host_peer.node',
);
const clientDataRoot = join(root, 'client');
await mkdir(appPath, { recursive: true });
await mkdir(dirname(nativePath), { recursive: true });
await writeFile(nativePath, 'native addon');

const ordinaryEnvironment: NodeJS.ProcessEnv = {};
assert.equal(await configureDesktopRuntimeHostPeerClient({
isPackaged: false,
appPath,
resourcesPath: join(root, 'resources'),
clientDataRoot,
environment: ordinaryEnvironment,
}), undefined);
assert.equal(ordinaryEnvironment.MAKA_RUNTIME_HOST_PEER_NATIVE_PATH, undefined);

const peerEnvironment: NodeJS.ProcessEnv = {};
assert.deepEqual(await configureDesktopRuntimeHostPeerClient({
isPackaged: false,
enableDevelopmentPeer: true,
appPath,
resourcesPath: join(root, 'resources'),
clientDataRoot,
environment: peerEnvironment,
}), {
nativePath,
keyPath: join(clientDataRoot, 'runtime-host-client.peer.key'),
});
assert.equal(peerEnvironment.MAKA_RUNTIME_HOST_PEER_NATIVE_PATH, nativePath);
});
63 changes: 51 additions & 12 deletions apps/desktop/src/main/__tests__/runtime-host-setup-package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,23 @@
*/

import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { test } from 'node:test';
import { createRuntimeHostSetupPackageResolver } from '../runtime-host-setup-package.js';

test('development setup lazily caches CLI archives by peer target unless overridden', async () => {
const ARCHIVE_BYTES = Buffer.from('development archive');
const ARCHIVE_INTEGRITY = `sha512-${createHash('sha512').update(ARCHIVE_BYTES).digest('base64')}`;

test('development setup lazily caches CLI archives by peer target unless overridden', async (t) => {
const repoRoot = resolve('/workspace');
const archive = join(repoRoot, 'packages', 'cli', 'release', 'maka-agent-dev.tgz');
const directory = await mkdtemp(join(tmpdir(), 'maka-runtime-host-setup-package-'));
t.after(() => rm(directory, { recursive: true, force: true }));
const archive = join(directory, 'maka-agent-dev.tgz');
await writeFile(archive, ARCHIVE_BYTES);
const canonicalArchive = await realpath(archive);
let builds = 0;
let closes = 0;
const targets: string[] = [];
Expand Down Expand Up @@ -53,33 +62,59 @@ test('development setup lazily caches CLI archives by peer target unless overrid
]), [
{
kind: 'development_archive',
path: archive,
path: canonicalArchive,
integrity: ARCHIVE_INTEGRITY,
},
{
kind: 'development_archive',
path: archive,
path: canonicalArchive,
integrity: ARCHIVE_INTEGRITY,
},
]);
await resolvePackage.resolve('none');
assert.equal(builds, 2);
assert.deepEqual(targets, ['linux-x64', 'none']);

const override = join(tmpdir(), 'explicit.tgz');
const override = join(directory, 'explicit.tgz');
await writeFile(override, ARCHIVE_BYTES);
const resolveOverride = createRuntimeHostSetupPackageResolver({
isPackaged: false,
appPath: join(repoRoot, 'apps', 'desktop'),
environment: { MAKA_RUNTIME_HOST_SETUP_ARCHIVE: override },
startDevelopmentArchiveBuild: () => assert.fail('override must bypass the local build'),
});
assert.deepEqual(await resolveOverride.resolve('none'), {
kind: 'development_archive',
path: override,
const snapshot = await resolveOverride.resolve('none');
assert.equal(snapshot.kind, 'development_archive');
assert.notEqual(snapshot.path, await realpath(override));
assert.equal(snapshot.integrity, ARCHIVE_INTEGRITY);
await writeFile(override, 'replacement archive');
assert.deepEqual(await readFile(snapshot.path), ARCHIVE_BYTES);
assert.deepEqual(await resolveOverride.resolve('none'), snapshot);

await resolveOverride.close();
await assert.rejects(readFile(snapshot.path), { code: 'ENOENT' });

const invalidOverride = join(directory, 'explicit.zip');
await writeFile(invalidOverride, ARCHIVE_BYTES);
const resolveInvalidOverride = createRuntimeHostSetupPackageResolver({
isPackaged: false,
appPath: join(repoRoot, 'apps', 'desktop'),
environment: { MAKA_RUNTIME_HOST_SETUP_ARCHIVE: invalidOverride },
});
await Promise.all([resolvePackage.close(), resolveOverride.close()]);
await assert.rejects(resolveInvalidOverride.resolve('none'), /must be a \.tgz file/u);
await Promise.all([
resolvePackage.close(),
resolveInvalidOverride.close(),
]);
assert.equal(closes, 2);
});

test('cancelling the last waiter closes its build before a new setup starts', async () => {
test('cancelling the last waiter closes its build before a new setup starts', async (t) => {
const directory = await mkdtemp(join(tmpdir(), 'maka-runtime-host-setup-package-'));
t.after(() => rm(directory, { recursive: true, force: true }));
const freshArchive = join(directory, 'fresh.tgz');
await writeFile(freshArchive, ARCHIVE_BYTES);
const canonicalFreshArchive = await realpath(freshArchive);
const cancelled = new AbortController();
let builds = 0;
let rejectBuild!: (error: Error) => void;
Expand All @@ -99,7 +134,10 @@ test('cancelling the last waiter closes its build before a new setup starts', as
startDevelopmentArchiveBuild: () => {
builds += 1;
if (builds > 1) {
return { result: Promise.resolve('/workspace/fresh.tgz'), close: async () => undefined };
return {
result: Promise.resolve(freshArchive),
close: async () => undefined,
};
}
return {
result: new Promise((_resolve, reject) => {
Expand All @@ -126,7 +164,8 @@ test('cancelling the last waiter closes its build before a new setup starts', as
await assert.rejects(first, /setup cancelled/u);
assert.deepEqual(await second, {
kind: 'development_archive',
path: '/workspace/fresh.tgz',
path: canonicalFreshArchive,
integrity: ARCHIVE_INTEGRITY,
});
assert.equal(builds, 2);
assert.equal(closes, 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
Expand Down Expand Up @@ -861,6 +862,7 @@ test('uploads a development release archive before running the same remote setup
t.after(() => rm(directory, { recursive: true, force: true }));
const archive = join(directory, 'maka-agent-development.tgz');
await writeFile(archive, 'development package');
const integrity = `sha512-${createHash('sha512').update('development package').digest('base64')}`;
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const launches: Array<{ file: string; args: string[]; pty: FakePty }> = [];
const terminal = createDesktopRuntimeHostSshTerminal({
Expand All @@ -879,7 +881,11 @@ test('uploads a development release archive before running the same remote setup

const setupInput = {
destination: 'operator@example.com',
setupPackage: { kind: 'development_archive', path: archive } as const,
setupPackage: {
kind: 'development_archive',
path: archive,
integrity,
} as const,
principalId: 'desktop:stable-client',
};
const setup = terminal.runSetup(setupInput, () => undefined);
Expand All @@ -896,6 +902,8 @@ test('uploads a development release archive before running the same remote setup
assert.equal(launches[1]?.file, 'ssh');
const remoteCommand = launches[1]?.args.at(-1) ?? '';
assert.match(remoteCommand, /--package.*maka-runtime-host-setup-.+\.tgz/u);
assert.match(remoteCommand, /MAKA_RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY=/u);
assert.ok(remoteCommand.includes(integrity));
assert.match(remoteCommand, /--defer-pairing-commit/u);
assert.match(remoteCommand, /cd.*\$HOME/u);
assert.match(remoteCommand, /rm -f/u);
Expand Down
Loading
Loading