blob: d42007a7574a872205b348effc357a2d3c50145a [file]
/*
* 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 {
installRuntimeHostLogCapture,
runRuntimeHostProcessLifecycle,
startExecutionRuntimeHostService,
} from '@maka/runtime-host/server';
import {
RUNTIME_HOST_COMPATIBILITY_EPOCH,
RUNTIME_HOST_PROTOCOL_VERSION,
} from '@maka/runtime-host/protocol';
import type { SignedPeerReachabilityLeaseV1 } from '@maka/runtime-host/peer-reachability';
import type { RuntimeHostManagedLaunchClaim } from '@maka/runtime-host/operator';
import { readFile } from 'node:fs/promises';
export interface RuntimeHostServiceCliOptions {
readonly rootPath: string;
readonly json?: boolean;
readonly managedLaunchClaim?: RuntimeHostManagedLaunchClaim;
readonly projectDirectoryRoots?: readonly { readonly label: string; readonly path: string }[];
readonly websocket?: {
readonly host: string;
readonly port: number;
readonly path?: string;
readonly tlsCertificatePath?: string;
readonly tlsPrivateKeyPath?: string;
readonly allowInsecureRemote?: boolean;
readonly allowedOrigins?: readonly string[];
};
readonly peer?: {
readonly nativePath: string;
readonly keyPath: string;
readonly expectedPeerId?: string;
readonly listenAddresses?: readonly string[];
readonly coordinationRelays?: readonly string[];
readonly automaticRelayDiscovery?: boolean;
readonly webRtcStunUrls?: readonly string[];
readonly meshDataRoot?: string;
};
}
export async function runRuntimeHostServiceCli(
options: RuntimeHostServiceCliOptions,
): Promise<number> {
installRuntimeHostLogCapture();
const websocket = options.websocket
? {
host: options.websocket.host,
port: options.websocket.port,
...(options.websocket.path ? { path: options.websocket.path } : {}),
...(options.websocket.allowedOrigins
? { allowedOrigins: options.websocket.allowedOrigins }
: {}),
...(options.websocket.allowInsecureRemote ? { allowInsecureRemote: true } : {}),
...(options.websocket.tlsCertificatePath && options.websocket.tlsPrivateKeyPath
? {
tls: {
certificate: await readFile(options.websocket.tlsCertificatePath),
privateKey: await readFile(options.websocket.tlsPrivateKeyPath),
},
}
: {}),
}
: undefined;
const host = await startExecutionRuntimeHostService({
rootPath: options.rootPath,
...(options.managedLaunchClaim ? { managedLaunchClaim: options.managedLaunchClaim } : {}),
...(options.projectDirectoryRoots
? { projectDirectoryRoots: options.projectDirectoryRoots }
: {}),
...(websocket ? { websocket } : {}),
...(options.peer ? { peer: options.peer } : {}),
});
let lifecycleEnd: Awaited<ReturnType<typeof runRuntimeHostProcessLifecycle>> | undefined;
try {
lifecycleEnd = await runRuntimeHostProcessLifecycle(host, {
onReady: () => {
if (options.json) {
process.stdout.write(`${JSON.stringify(createRuntimeHostServiceReadyEvent(host))}\n`);
return;
}
process.stdout.write(`Runtime Host service is ready at ${host.endpoint}\n`);
for (const endpoint of host.websocketEndpoints) {
process.stdout.write(`Runtime Host WebSocket is ready at ${endpoint}\n`);
}
for (const peer of host.peerListeners) {
process.stdout.write(
`Runtime Host direct peer is ready as ${peer.reachability.lease.peerId} at ${peer.reachability.lease.directRoutes.join(', ')}\n`,
);
}
},
});
} catch (error) {
if (host.shutdownReason !== 'retirement') throw error;
console.error('[runtime-host] retirement shutdown did not complete cleanly:', error);
}
return lifecycleEnd === 'host_closed' && host.shutdownReason !== 'retirement' ? 1 : 0;
}
export interface RuntimeHostServiceReadyEvent {
readonly schemaVersion: 1;
readonly event: 'runtime_host_ready';
readonly rootId: string;
readonly hostEpoch: string;
readonly protocol: {
readonly version: number;
readonly compatibilityEpoch: number;
};
readonly composition: {
readonly id: string;
readonly revision: string;
};
readonly listeners: readonly (
| { readonly kind: 'local_ipc'; readonly endpoint: string }
| {
readonly kind: 'websocket';
readonly tls: boolean;
readonly host: string;
readonly port: number;
readonly path: string;
}
| {
readonly kind: 'libp2p_direct';
readonly peerId: string;
readonly listenAddresses: readonly string[];
}
)[];
}
export function createRuntimeHostServiceReadyEvent(host: {
readonly rootId: string;
readonly hostEpoch: string;
readonly endpoint: string;
readonly websocketEndpoints: readonly string[];
readonly peerListeners: readonly {
readonly reachability: SignedPeerReachabilityLeaseV1;
}[];
readonly compositionDescriptor: { readonly id: string; readonly revision: string };
}): RuntimeHostServiceReadyEvent {
return {
schemaVersion: 1,
event: 'runtime_host_ready',
rootId: host.rootId,
hostEpoch: host.hostEpoch,
protocol: {
version: RUNTIME_HOST_PROTOCOL_VERSION,
compatibilityEpoch: RUNTIME_HOST_COMPATIBILITY_EPOCH,
},
composition: host.compositionDescriptor,
listeners: [
{ kind: 'local_ipc', endpoint: host.endpoint },
...host.websocketEndpoints.map((endpoint) => {
const url = new URL(endpoint);
return {
kind: 'websocket' as const,
tls: url.protocol === 'wss:',
host: url.hostname,
port: url.port === '' ? (url.protocol === 'wss:' ? 443 : 80) : Number(url.port),
path: url.pathname,
};
}),
...host.peerListeners.map((peer) => ({
kind: 'libp2p_direct' as const,
peerId: peer.reachability.lease.peerId,
listenAddresses: peer.reachability.lease.directRoutes,
})),
],
};
}