diff --git a/libs/as-sdk-integration-tests/assembly/index.ts b/libs/as-sdk-integration-tests/assembly/index.ts index 3b124af..333d7de 100644 --- a/libs/as-sdk-integration-tests/assembly/index.ts +++ b/libs/as-sdk-integration-tests/assembly/index.ts @@ -1,4 +1,5 @@ import { httpFetch, Process } from '../../as-sdk/assembly/index'; +import { testTallyVmHttp, testTallyVmMode } from './vm-tests'; const args = Process.args().at(1); @@ -6,6 +7,10 @@ if (args === 'testHttpRejection') { testHttpRejection(); } else if (args === 'testHttpSuccess') { testHttpSuccess(); +} else if (args === 'testTallyVmMode') { + testTallyVmMode(); +} else if (args === 'testTallyVmHttp') { + testTallyVmHttp(); } export function testHttpRejection(): void { @@ -18,7 +23,7 @@ export function testHttpRejection(): void { Process.exit_with_result(0, buffer); } else { - Process.exit_with_message(1, "Test failed"); + Process.exit_with_message(1, 'Test failed'); } } @@ -35,5 +40,3 @@ export function testHttpSuccess(): void { Process.exit_with_message(31, 'My custom test failed'); } } - - diff --git a/libs/as-sdk-integration-tests/assembly/vm-tests.ts b/libs/as-sdk-integration-tests/assembly/vm-tests.ts new file mode 100644 index 0000000..80fdd56 --- /dev/null +++ b/libs/as-sdk-integration-tests/assembly/vm-tests.ts @@ -0,0 +1,28 @@ +import { Process, httpFetch } from '../../as-sdk/assembly/index'; + +export function testTallyVmMode(): void { + const envs = Process.envs(); + const vmMode = envs.get('VM_MODE'); + + if (vmMode === 'tally') { + Process.exit_with_message(0, 'tally'); + } else { + Process.exit_with_message(1, 'dr'); + } +} + +export function testTallyVmHttp(): void { + const response = httpFetch('https://swapi.dev/api/planets/1/'); + const fulfilled = response.fulfilled; + const rejected = response.rejected; + + if (fulfilled !== null) { + Process.exit_with_message(1, 'this should not be allowed in tally mode'); + } + + if (rejected !== null) { + Process.exit_with_result(0, rejected.bytes); + } +} + + diff --git a/libs/as-sdk-integration-tests/src/http.test.ts b/libs/as-sdk-integration-tests/src/http.test.ts index fa34c8d..2f1dcc8 100644 --- a/libs/as-sdk-integration-tests/src/http.test.ts +++ b/libs/as-sdk-integration-tests/src/http.test.ts @@ -10,6 +10,7 @@ jest.setTimeout(15_000); const TestVmAdapter = jest.fn().mockImplementation(() => { return { + modifyVmCallData: (v) => v, setProcessId: () => {}, httpFetch: mockHttpFetch }; diff --git a/libs/as-sdk-integration-tests/src/tallyvm.test.ts b/libs/as-sdk-integration-tests/src/tallyvm.test.ts new file mode 100644 index 0000000..019d4a6 --- /dev/null +++ b/libs/as-sdk-integration-tests/src/tallyvm.test.ts @@ -0,0 +1,40 @@ +import { TallyVmAdapter, callVm } from '../../../dist/libs/vm'; +import { readFile } from 'node:fs/promises'; + +describe('TallyVm', () => { + it('should run in tally vm mode', async () => { + const wasmBinary = await readFile( + 'dist/libs/as-sdk-integration-tests/debug.wasm' + ); + const result = await callVm( + { + args: ['testTallyVmMode'], + envs: {}, + binary: new Uint8Array(wasmBinary), + }, + undefined, + new TallyVmAdapter() + ); + + expect(result.resultAsString).toEqual('tally'); + expect(result.exitCode).toBe(0); + }); + + it('should fail to make an http call', async () => { + const wasmBinary = await readFile( + 'dist/libs/as-sdk-integration-tests/debug.wasm' + ); + const result = await callVm( + { + args: ['testTallyVmHttp'], + envs: {}, + binary: new Uint8Array(wasmBinary), + }, + undefined, + new TallyVmAdapter() + ); + + expect(result.resultAsString).toEqual('http_fetch is not allowed in tally'); + expect(result.exitCode).toBe(0); + }); +}); diff --git a/libs/as-sdk/assembly/process.ts b/libs/as-sdk/assembly/process.ts index dd14f35..b54ca88 100644 --- a/libs/as-sdk/assembly/process.ts +++ b/libs/as-sdk/assembly/process.ts @@ -1,4 +1,4 @@ -import { Process as WasiProcess, CommandLine } from 'as-wasi/assembly'; +import { Process as WasiProcess, CommandLine, Environ } from 'as-wasi/assembly'; import { execution_result } from './bindings/seda_v1'; export default class Process { @@ -17,6 +17,28 @@ export default class Process { return CommandLine.all; } + /** + * Gets all the environment variables as a Map + * + * @returns {Map} key, value pair with all environment variables + * @example + * ```ts + * const env = Process.env(); + * + * const vmMode = env.get('VM_MODE'); + * ``` + */ + static envs(): Map { + const result: Map = new Map(); + + for (let i: i32 = 0; i < Environ.all.length; i++) { + const entry = Environ.all[i]; + result.set(entry.key, entry.value); + } + + return result; + } + /** * Exits the process with a message * This sets the result of the Data Request execution to the message diff --git a/libs/vm/src/default-vm-adapter.ts b/libs/vm/src/data-request-vm-adapter.ts similarity index 78% rename from libs/vm/src/default-vm-adapter.ts rename to libs/vm/src/data-request-vm-adapter.ts index a9d097a..bda3ecf 100644 --- a/libs/vm/src/default-vm-adapter.ts +++ b/libs/vm/src/data-request-vm-adapter.ts @@ -1,12 +1,23 @@ -import type { HttpFetchAction } from './types/vm-actions'; +import type { HttpFetchAction } from './types/vm-actions.js'; import { HttpFetchResponse } from './types/vm-actions.js'; -import type { VmAdapter } from './types/vm-adapter'; +import type { VmAdapter } from './types/vm-adapter.js'; import fetch from 'node-fetch'; import { PromiseStatus } from './types/vm-promise.js'; +import { VmCallData } from './vm.js'; -export default class DefaultVmAdapter implements VmAdapter { +export default class DataRequestVmAdapter implements VmAdapter { private processId?: string; + modifyVmCallData(input: VmCallData): VmCallData { + return { + ...input, + envs: { + ...input.envs, + VM_MODE: 'dr', + }, + }; + } + setProcessId(processId: string) { this.processId = processId; } diff --git a/libs/vm/src/index.ts b/libs/vm/src/index.ts index f93f1c2..20f2e07 100644 --- a/libs/vm/src/index.ts +++ b/libs/vm/src/index.ts @@ -2,11 +2,14 @@ import { Worker } from "node:worker_threads"; import type { VmCallData, VmResult } from "./vm"; import { VmCallWorkerMessage, WorkerMessage, WorkerMessageType } from "./types/worker-messages.js"; import type { VmAdapter } from "./types/vm-adapter.js"; -import DefaultVmAdapter from "./default-vm-adapter.js"; +import DataRequestVmAdapter from "./data-request-vm-adapter.js"; import { parse, format } from "node:path"; import { HostToWorker } from "./worker-host-communication.js"; import { createProcessId } from "./services/create-process-id.js"; +export { default as TallyVmAdapter } from './tally-vm-adapter.js'; +export { default as DataRequestVmAdapter } from './data-request-vm-adapter.js'; + const CURRENT_FILE_PATH = parse(import.meta.url); CURRENT_FILE_PATH.base = 'worker.js'; const DEFAULT_WORKER_PATH = format(CURRENT_FILE_PATH); @@ -19,13 +22,17 @@ const DEFAULT_WORKER_PATH = format(CURRENT_FILE_PATH); * @param vmAdapter Option to insert a custom VM adapter, can be used to mock * @returns */ -export function callVm(callData: VmCallData, workerUrl = DEFAULT_WORKER_PATH, vmAdapter: VmAdapter = new DefaultVmAdapter()): Promise { +export function callVm( + callData: VmCallData, + workerUrl = DEFAULT_WORKER_PATH, + vmAdapter: VmAdapter = new DataRequestVmAdapter() +): Promise { return new Promise((resolve) => { - const finalCallData: VmCallData = { + const finalCallData: VmCallData = vmAdapter.modifyVmCallData({ ...callData, // First argument matches the Rust Wasmer standard (_start for WASI) args: ['_start', ...callData.args], - }; + }); const processId = createProcessId(finalCallData); vmAdapter.setProcessId(processId); @@ -74,7 +81,7 @@ export function callVm(callData: VmCallData, workerUrl = DEFAULT_WORKER_PATH, vm exitCode, stderr: `[${processId}] - The worker has been terminated`, stdout: '', - }) + }); }); worker.postMessage(workerMessage); diff --git a/libs/vm/src/tally-vm-adapter.ts b/libs/vm/src/tally-vm-adapter.ts new file mode 100644 index 0000000..08a602d --- /dev/null +++ b/libs/vm/src/tally-vm-adapter.ts @@ -0,0 +1,40 @@ +import type { HttpFetchAction } from './types/vm-actions'; +import { HttpFetchResponse } from './types/vm-actions.js'; +import type { VmAdapter } from './types/vm-adapter'; +import { PromiseStatus } from './types/vm-promise.js'; +import type { VmCallData } from './vm'; + +export default class TallyVmAdapter implements VmAdapter { + private processId?: string; + + modifyVmCallData(input: VmCallData): VmCallData { + return { + ...input, + envs: { + ...input.envs, + VM_MODE: 'tally' + } + }; + } + + setProcessId(processId: string) { + this.processId = processId; + } + + async httpFetch( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _action: HttpFetchAction + ): Promise> { + const error = new TextEncoder().encode('http_fetch is not allowed in tally'); + + return PromiseStatus.rejected( + new HttpFetchResponse({ + bytes: Array.from(error), + content_length: error.length, + headers: {}, + status: 0, + url: '', + }) + ); + } +} diff --git a/libs/vm/src/types/vm-adapter.ts b/libs/vm/src/types/vm-adapter.ts index c02b226..290f50c 100644 --- a/libs/vm/src/types/vm-adapter.ts +++ b/libs/vm/src/types/vm-adapter.ts @@ -1,8 +1,28 @@ +import type { VmCallData } from "../vm"; import type { HttpFetchAction, HttpFetchResponse } from "./vm-actions"; import { PromiseStatus } from "./vm-promise.js"; export interface VmAdapter { + /** + * Allows the adapter to modify the call data before executing + * this can be used to inject arguments, environment variables, etc. + * + * @param input + */ + modifyVmCallData(input: VmCallData): VmCallData; + + /** + * Sets the process id in order to identify a vm call in the logs + * + * @param processId + */ setProcessId(processId: string): void, + + /** + * Method to do a remote http fetch call + * + * @param action + */ httpFetch(action: HttpFetchAction): Promise>; } diff --git a/libs/vm/src/vm.ts b/libs/vm/src/vm.ts index f06be74..61321c8 100644 --- a/libs/vm/src/vm.ts +++ b/libs/vm/src/vm.ts @@ -16,7 +16,6 @@ export interface VmResult { } export async function executeVm(callData: VmCallData, notifierBuffer: SharedArrayBuffer, processId: string): Promise { - await init(); const wasi = new WASI({ args: callData.args,