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
11 changes: 11 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,17 @@ test('sends a Mesh invitation only after the authenticated remote operator reque
result: {
localPeerId: 'peer-b',
available: true,
transit: {
meshId: null,
allowedMemberCount: 0,
activeReservationCount: 0,
activeCircuitCount: 0,
maxReservationCount: 32,
maxCircuitCount: 8,
maxCircuitsPerPeer: 2,
maxCircuitDurationSeconds: 7_200,
maxCircuitBytes: 256 * 1024 * 1024,
},
meshes: [
{
meshId: 'mesh-id',
Expand Down
40 changes: 27 additions & 13 deletions apps/desktop/src/main/runtime-host-peer-mesh-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
type PeerMeshInvitationResult,
type PeerMeshQueryResult,
} from '@maka/runtime-host/protocol';
import { projectPeerMeshStatus } from '@maka/runtime-host/server';
import { projectPeerMeshQuery } from '@maka/runtime-host/server';
import type {
DesktopRuntimeHostPeerMeshTarget,
} from '../preload/bridge-contract.js';
Expand Down Expand Up @@ -52,7 +52,12 @@ export function createDesktopRuntimeHostPeerMeshManagement(input: {
): Promise<PeerMeshQueryResult | PeerMeshInvitationResult> => {
const target = requireTarget(targetValue);
const action = requireAction(actionValue);
const meshId = actionNeedsMesh(action) ? requireIdentifier(meshIdValue, 'Mesh ID') : undefined;
const meshId =
action === 'transit' && meshIdValue === null
? null
: actionNeedsMesh(action)
? requireIdentifier(meshIdValue, 'Mesh ID')
: undefined;
const peerId = action === 'remove' ? requireIdentifier(peerIdValue, 'Peer ID') : undefined;
const invitation = action === 'join' ? requireInvitation(invitationValue) : undefined;
if (target.kind === 'desktop') {
Expand Down Expand Up @@ -80,7 +85,7 @@ export function createDesktopRuntimeHostPeerMeshManagement(input: {
rootId: managed.profile.rootId,
deploymentId: managed.deployment.deploymentId,
},
...(meshId ? { meshId } : {}),
...(meshId !== undefined ? { meshId } : {}),
...(peerId ? { peerId } : {}),
...(invitation ? { invitation: JSON.stringify(invitation) } : {}),
});
Expand All @@ -101,19 +106,15 @@ export function createDesktopRuntimeHostPeerMeshManagement(input: {
async function executeLocal(
mesh: PeerMeshNode | undefined,
action: PeerMeshAction,
meshId: string | undefined,
meshId: string | null | undefined,
peerId: string | undefined,
invitation: ReturnType<typeof decodePeerMeshInvitation> | undefined,
): Promise<PeerMeshQueryResult | PeerMeshInvitationResult> {
if (!mesh) {
if (action === 'status') return { available: false, meshes: [] };
throw new Error('This Desktop build does not include Direct peer support');
}
const snapshot = (): PeerMeshQueryResult => ({
available: true,
localPeerId: mesh.localPeerId(),
meshes: mesh.status().map(projectPeerMeshStatus),
});
const snapshot = (): PeerMeshQueryResult => projectPeerMeshQuery(mesh);
switch (action) {
case 'status':
return snapshot();
Expand All @@ -139,11 +140,14 @@ async function executeLocal(
case 'reconcile':
await mesh.reconcile();
return snapshot();
case 'transit':
await mesh.setTransitMesh(meshId ?? null);
return snapshot();
}
}

function requiredValue<T>(value: T | undefined, label: string): T {
if (value === undefined) throw new Error(`${label} is required`);
function requiredValue<T>(value: T | null | undefined, label: string): NonNullable<T> {
if (value === undefined || value === null) throw new Error(`${label} is required`);
return value;
}

Expand All @@ -168,13 +172,23 @@ function requireTarget(value: unknown): DesktopRuntimeHostPeerMeshTarget {
function requireAction(value: unknown): PeerMeshAction {
if (
value === 'status' || value === 'create' || value === 'invite' || value === 'join' ||
value === 'remove' || value === 'leave' || value === 'close' || value === 'reconcile'
value === 'remove' ||
value === 'leave' ||
value === 'close' ||
value === 'reconcile' ||
value === 'transit'
) return value;
throw new Error('Peer Mesh action is invalid');
}

function actionNeedsMesh(action: PeerMeshAction): boolean {
return action === 'invite' || action === 'remove' || action === 'leave' || action === 'close';
return (
action === 'invite' ||
action === 'remove' ||
action === 'leave' ||
action === 'close' ||
action === 'transit'
);
}

function requireIdentifier(value: unknown, label: string): string {
Expand Down
8 changes: 6 additions & 2 deletions apps/desktop/src/main/runtime-host-ssh-terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export interface DesktopRuntimeHostSshPeerMeshManagementInput {
readonly operatorPath: string;
readonly action: RuntimeHostPeerMeshManagementAction;
readonly expectedTarget: DesktopRuntimeHostSshManagementInput['expectedTarget'];
readonly meshId?: string;
readonly meshId?: string | null;
readonly peerId?: string;
readonly invitation?: string;
readonly signal?: AbortSignal;
Expand Down Expand Up @@ -1365,7 +1365,11 @@ function runtimeHostPeerMeshManagementRemoteCommand(
'mesh',
input.action,
'--framed',
...(input.meshId ? ['--mesh', input.meshId] : []),
...(typeof input.meshId === 'string'
? ['--mesh', input.meshId]
: input.meshId === null
? ['--off']
: []),
...(input.peerId ? ['--peer', input.peerId] : []),
...managedServiceTargetArgs(input.expectedTarget),
].map(quotePosix).join(' ');
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/preload/bridge-contract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,7 +746,11 @@ export interface MakaBridge {
execute(
target: DesktopRuntimeHostPeerMeshTarget,
action: DesktopRuntimeHostPeerMeshAction,
input?: { readonly meshId?: string; readonly peerId?: string; readonly invitation?: string },
input?: {
readonly meshId?: string | null;
readonly peerId?: string;
readonly invitation?: string;
},
): Promise<DesktopRuntimeHostPeerMeshResult>;
};

Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1380,7 +1380,11 @@ const makaBridge = {
execute(
target: import('./bridge-contract.js').DesktopRuntimeHostPeerMeshTarget,
action: import('./bridge-contract.js').DesktopRuntimeHostPeerMeshAction,
input: { readonly meshId?: string; readonly peerId?: string; readonly invitation?: string } = {},
input: {
readonly meshId?: string | null;
readonly peerId?: string;
readonly invitation?: string;
} = {},
) {
return ipcRenderer.invoke(
'runtime-host-peer-mesh:execute',
Expand Down
131 changes: 130 additions & 1 deletion apps/desktop/src/renderer/settings/runtime-host-peer-mesh-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,23 @@ import {
Button,
MoreMenu,
redactSecrets,
Switch,
Text,
TextArea,
useToast,
useUiLocale,
} from '@maka/ui';
import { ArrowLeft, Copy, ICON_SIZE, KeyRound, Network, Plus, RefreshCcw } from '@maka/ui/icons';
import {
ArrowLeft,
Copy,
HelpCircle,
ICON_SIZE,
KeyRound,
Network,
Plus,
RefreshCcw,
Workflow,
} from '@maka/ui/icons';
import type { DesktopRuntimeHostPeerMeshTarget } from '../../preload/bridge-contract.js';

type PeerMeshDialogView =
Expand Down Expand Up @@ -158,6 +169,22 @@ export function RuntimeHostPeerMeshDialog(props: {
}
}

async function setTransit(meshId: string, enabled: boolean): Promise<void> {
setWorking(true);
setError(undefined);
try {
const result = await window.maka.runtimeHostPeerMesh.execute(props.target, 'transit', {
meshId: enabled ? meshId : null,
});
if (!isSnapshot(result)) throw new Error(copy.invalidResult);
setSnapshot(result);
} catch (failure) {
setError(peerMeshErrorMessage(failure, copy.unknownError));
} finally {
setWorking(false);
}
}

async function copyInvitation(): Promise<void> {
if (view.kind !== 'invitation') return;
try {
Expand Down Expand Up @@ -209,6 +236,7 @@ export function RuntimeHostPeerMeshDialog(props: {
onJoin={() => setView({ kind: 'join' })}
onCreate={() => void run('create')}
onRefresh={() => void run('reconcile')}
onSetTransit={(meshId, enabled) => void setTransit(meshId, enabled)}
/>
)}
</div>
Expand Down Expand Up @@ -260,6 +288,7 @@ function Overview(props: {
readonly onJoin: () => void;
readonly onCreate: () => void;
readonly onRefresh: () => void;
readonly onSetTransit: (meshId: string, enabled: boolean) => void;
}) {
const { snapshot, copy } = props;
if (!snapshot) {
Expand Down Expand Up @@ -357,12 +386,14 @@ function Overview(props: {
<MeshCard
key={mesh.meshId}
mesh={mesh}
transit={snapshot.transit}
copy={copy}
working={props.working}
onInvite={() => props.onInvite(mesh.meshId)}
onRemove={(peerId) => props.onRemove(mesh.meshId, peerId)}
onLeave={() => props.onLeave(mesh.meshId)}
onClose={() => props.onClose(mesh.meshId)}
onSetTransit={(enabled) => props.onSetTransit(mesh.meshId, enabled)}
/>
))}
</div>
Expand Down Expand Up @@ -449,14 +480,17 @@ function InvitationView(props: {

function MeshCard(props: {
readonly mesh: PeerMeshProjection;
readonly transit: PeerMeshQueryResult['transit'];
readonly copy: ReturnType<typeof peerMeshCopy>;
readonly working: boolean;
readonly onInvite: () => void;
readonly onRemove: (peerId: string) => void;
readonly onLeave: () => void;
readonly onClose: () => void;
readonly onSetTransit: (enabled: boolean) => void;
}) {
const { mesh, copy } = props;
const transitEnabled = props.transit?.meshId === mesh.meshId;
return (
<section className="settingsPeerMeshCard">
<div className="settingsPeerMeshCardHeading">
Expand Down Expand Up @@ -509,6 +543,56 @@ function MeshCard(props: {
{copy.revision(mesh.revision)} · {copy.memberCount(mesh.members.length)}
{mesh.pendingInvitationCount > 0 ? ` · ${copy.pending(mesh.pendingInvitationCount)}` : ''}
</Text>
{!mesh.closed ? (
<div className="settingsPeerMeshTransit">
<div className="settingsPeerMeshTransitIdentity">
<span className="settingsPeerMeshTransitIcon" aria-hidden="true">
<Workflow size={ICON_SIZE.chrome} />
</span>
<div>
<div className="settingsPeerMeshTransitTitle">
<Text type="supporting" weight="semibold">
{copy.transit}
</Text>
<span
className="settingsPeerMeshTransitHelp"
role="img"
aria-label={copy.transitLimitsLabel}
title={copy.transitLimits(props.transit)}
>
<HelpCircle size={ICON_SIZE.meta} aria-hidden="true" />
</span>
</div>
<Text type="supporting" color="secondary">
{copy.transitHelp}
</Text>
</div>
</div>
<Switch
label={copy.transitToggle}
isLabelHidden
value={transitEnabled}
isDisabled={props.working}
onChange={props.onSetTransit}
/>
</div>
) : null}
{transitEnabled && props.transit ? (
<div className="settingsPeerMeshTransitMetrics" aria-label={copy.transitStatus}>
<TransitMetric
label={copy.allowedMembers}
value={String(props.transit.allowedMemberCount)}
/>
<TransitMetric
label={copy.reservations}
value={`${props.transit.activeReservationCount}/${props.transit.maxReservationCount}`}
/>
<TransitMetric
label={copy.circuits}
value={`${props.transit.activeCircuitCount}/${props.transit.maxCircuitCount}`}
/>
</div>
) : null}
<div className="settingsPeerMeshMembersHeading">
<Text type="supporting" color="secondary">
{copy.members}
Expand Down Expand Up @@ -561,6 +645,19 @@ function MeshCard(props: {
);
}

function TransitMetric(props: { readonly label: string; readonly value: string }) {
return (
<div>
<Text type="supporting" color="secondary">
{props.label}
</Text>
<Text type="body" weight="semibold">
{props.value}
</Text>
</div>
);
}

function isSnapshot(value: unknown): value is PeerMeshQueryResult {
return Boolean(value && typeof value === 'object' && 'available' in value && 'meshes' in value);
}
Expand Down Expand Up @@ -615,6 +712,18 @@ function peerMeshCopy(locale: string) {
revision: (value: number) => `版本 ${value}`,
memberCount: (value: number) => `${value} 个成员`,
pending: (value: number) => `${value} 个待使用邀请`,
transit: '成员转发',
transitHelp: '允许此 Mesh 的成员通过本机建立连接;会使用本机带宽。',
transitToggle: '为此 Mesh 提供转发',
transitStatus: '成员转发状态',
transitLimitsLabel: '成员转发限制',
transitLimits: (value: PeerMeshQueryResult['transit']) =>
value
? `固定上限:每个成员 ${value.maxCircuitsPerPeer} 条连接,每条最长 ${formatHours(value.maxCircuitDurationSeconds)},最多 ${formatMebibytes(value.maxCircuitBytes)}。一次只能为一个 Mesh 开启。`
: '成员转发使用固定资源上限,一次只能为一个 Mesh 开启。',
allowedMembers: '允许成员',
reservations: 'Reservation',
circuits: '连接',
routeState: {
local: '本机',
route_available: '路径可用',
Expand Down Expand Up @@ -669,6 +778,18 @@ function peerMeshCopy(locale: string) {
revision: (value: number) => `Revision ${value}`,
memberCount: (value: number) => `${value} members`,
pending: (value: number) => `${value} pending invites`,
transit: 'Member transit',
transitHelp: 'Let members of this Mesh connect through this device using its bandwidth.',
transitToggle: 'Provide transit for this Mesh',
transitStatus: 'Member transit status',
transitLimitsLabel: 'Member transit limits',
transitLimits: (value: PeerMeshQueryResult['transit']) =>
value
? `Fixed limits: ${value.maxCircuitsPerPeer} circuits per member, ${formatHours(value.maxCircuitDurationSeconds)} per circuit, and ${formatMebibytes(value.maxCircuitBytes)}. Only one Mesh can be served at a time.`
: 'Member transit uses fixed resource limits. Only one Mesh can be served at a time.',
allowedMembers: 'Allowed members',
reservations: 'Reservations',
circuits: 'Circuits',
routeState: {
local: 'Local',
route_available: 'Route known',
Expand Down Expand Up @@ -703,3 +824,11 @@ function peerMeshCopy(locale: string) {
memberActions: (peerId: string) => `Actions for ${peerId}`,
};
}

function formatHours(seconds: number): string {
return `${seconds / 3_600}h`;
}

function formatMebibytes(bytes: number): string {
return `${bytes / (1024 * 1024)} MiB`;
}
Loading
Loading