diff --git a/Makefile b/Makefile index d67863bd..bf9f001b 100644 --- a/Makefile +++ b/Makefile @@ -144,13 +144,14 @@ clean_core_js: ############## build_packages: build_python_host build_nodejs_host build_cfw_host deps_packages: deps_python_host deps_nodejs_host deps_cfw_host +test_packages: test_nodejs_host test_cfw_host test_python_host # Node.js Host deps_nodejs_host: cd packages/nodejs_host && yarn install build_nodejs_host: deps_nodejs_host ${CORE_ASYNCIFY_WASM} mkdir -p ${NODEJS_HOST_ASSETS} - cp ${CORE_ASYNCIFY_WASM} ${NODEJS_HOST_ASSETS}/core-async.wasm # copy wasm always because cached docker artifacts can have older timestamp + cp ${CORE_ASYNCIFY_WASM} ${NODEJS_HOST_ASSETS}/core-async.wasm cd packages/nodejs_host && yarn build test_nodejs_host: build_nodejs_host ${TEST_CORE_ASYNCIFY_WASM} cp ${TEST_CORE_ASYNCIFY_WASM} ${NODEJS_HOST_ASSETS}/test-core-async.wasm @@ -161,7 +162,7 @@ deps_cfw_host: cd packages/cloudflare_worker_host && yarn install build_cfw_host: deps_cfw_host ${CORE_ASYNCIFY_WASM} mkdir -p ${CFW_HOST_ASSETS} - cp ${CORE_ASYNCIFY_WASM} ${CFW_HOST_ASSETS}/core-async.wasm # copy wasm always because cached docker artifacts can have older timestamp + cp ${CORE_ASYNCIFY_WASM} ${CFW_HOST_ASSETS}/core-async.wasm cd packages/cloudflare_worker_host && yarn build test_cfw_host: build_cfw_host ${TEST_CORE_ASYNCIFY_WASM} cp ${TEST_CORE_ASYNCIFY_WASM} ${CFW_HOST_ASSETS}/test-core-async.wasm diff --git a/core/core/src/sf_core/metrics.rs b/core/core/src/sf_core/metrics.rs index 2ae456d2..ee129a06 100644 --- a/core/core/src/sf_core/metrics.rs +++ b/core/core/src/sf_core/metrics.rs @@ -28,26 +28,29 @@ impl<'a> PerformMetricsData<'a> { pub fn get_profile(&self) -> Cow<'_, str> { match self.profile { Some(profile) => Cow::Borrowed(profile), - None => Cow::Owned( - self.profile_url - .split('/') - .last() - .and_then(|b| b.strip_suffix(".profile")) - .unwrap() - .replace('.', "/"), - ), + None => match self.profile_url.split('/').last() { + // treat anything from .profile until the end of the url as an extension + // this works for both .profile and .profile.ts extensions + Some(last_segment) => match last_segment.rfind(".profile") { + Some(ext_start) => Cow::Owned(last_segment[..ext_start].replace('.', "/")), + None => Cow::Borrowed("unknown"), + }, + None => Cow::Borrowed("unknown"), + }, } } pub fn get_provider(&self) -> &str { match self.provider { Some(provider) => provider, - None => self - .provider_url - .split('/') - .last() - .and_then(|b| b.strip_suffix(".provider.json")) - .unwrap(), + None => match self.profile_url.split('/').last() { + // treat anything from .provider until the end of the url as an extension + Some(last_segment) => match last_segment.rfind(".provider") { + Some(ext_start) => &last_segment[..ext_start], + None => "unknown", + }, + None => "unknown", + }, } } } diff --git a/examples/cloudflare_worker/src/comlinks.ts b/examples/cloudflare_worker/src/comlinks.ts index ff0493d9..d9dda53f 100644 --- a/examples/cloudflare_worker/src/comlinks.ts +++ b/examples/cloudflare_worker/src/comlinks.ts @@ -15,7 +15,7 @@ import mapSendSms from '../superface/communication.send-sms.twilio.map.js'; import providerTwilio from '../superface/twilio.provider.json'; // @ts-ignore -import profileExample from '../superface/wasm-sdk.example.profile'; +import profileExample from '../superface/wasm-sdk.example.profile.ts'; // @ts-ignore import mapExample from '../superface/wasm-sdk.example.localhost.map.js'; // @ts-ignore @@ -28,7 +28,7 @@ export const COMLINK_IMPORTS = { 'superface/communication.send-sms.profile': new Uint8Array(profileSendSms), 'superface/communication.send-sms.twilio.map.js': new Uint8Array(mapSendSms), 'superface/twilio.provider.json': new Uint8Array(providerTwilio as any), - 'superface/wasm-sdk.example.profile': new Uint8Array(profileExample), + 'superface/wasm-sdk.example.profile.ts': new Uint8Array(profileExample), 'superface/wasm-sdk.example.localhost.map.js': new Uint8Array(mapExample), 'superface/localhost.provider.json': new Uint8Array(providerLocalhost as any) }; diff --git a/examples/comlinks/src/wasm-sdk.example.localhost.map.js b/examples/comlinks/src/wasm-sdk.example.localhost.map.js index 689ca804..833718b5 100644 --- a/examples/comlinks/src/wasm-sdk.example.localhost.map.js +++ b/examples/comlinks/src/wasm-sdk.example.localhost.map.js @@ -1,4 +1,5 @@ /// +/// // @ts-check const manifest = { @@ -6,9 +7,8 @@ const manifest = { provider: 'localhost' }; -// var to hoist it like a function would be -// can't be a function because then the type annotation doesn't work -/** @type {Usecase<{ safety: 'safe', input: { id: AnyValue }, result: { url: AnyValue, method: AnyValue, query: AnyValue, headers: AnyValue }, error: { title: string, detail: string } }>} */ +// var to hoist it like a function would be - can't be a function because then the type annotation doesn't work +/** @type {Example} */ var Example = ({ input, parameters, services }) => { // @ts-ignore __ffi.unstable.printDebug('Input:', input); diff --git a/examples/comlinks/src/wasm-sdk.example.profile b/examples/comlinks/src/wasm-sdk.example.profile deleted file mode 100644 index 92fa96d5..00000000 --- a/examples/comlinks/src/wasm-sdk.example.profile +++ /dev/null @@ -1,20 +0,0 @@ -name = "wasm-sdk/example" -version = "0.1.0" - -usecase Example { - input { - id! - } - - result { - url! - method! - query! - headers! - } - - error { - title! - detail - } -} \ No newline at end of file diff --git a/examples/comlinks/src/wasm-sdk.example.profile.ts b/examples/comlinks/src/wasm-sdk.example.profile.ts new file mode 100644 index 00000000..fc4d3993 --- /dev/null +++ b/examples/comlinks/src/wasm-sdk.example.profile.ts @@ -0,0 +1,7 @@ +/// +type Example = Usecase<{ + safety: 'safe' + input: { id: AnyValue } + result: { url: AnyValue, method: AnyValue, query: AnyValue, headers: AnyValue } + error: { title: AnyValue, detail?: AnyValue } +}>; diff --git a/examples/run.sh b/examples/run.sh index 956f3331..56989714 100755 --- a/examples/run.sh +++ b/examples/run.sh @@ -22,7 +22,7 @@ case $1 in make build_cfw_host $MAKE_FLAGS fi cd "$base/cloudflare_worker" - yarn install + yarn install --force yarn dev ;; diff --git a/packages/cloudflare_worker_host/src/index.ts b/packages/cloudflare_worker_host/src/index.ts index 3609cc04..76ce5c9a 100644 --- a/packages/cloudflare_worker_host/src/index.ts +++ b/packages/cloudflare_worker_host/src/index.ts @@ -31,6 +31,10 @@ class CfwFileSystem implements FileSystem { this.files = new HandleMap(); } + async exists(path: string): Promise { + return this.preopens[path] !== undefined; + } + async open(path: string, options: { createNew?: boolean, create?: boolean, truncate?: boolean, append?: boolean, write?: boolean, read?: boolean }): Promise { if (options.read !== true) { throw new WasiError(WasiErrno.EROFS); @@ -88,7 +92,11 @@ class CfwNetwork implements Network { try { response = await fetch(input, init); } catch (err: unknown) { - throw err; // TODO: are there any errors that we need to handle here? + if (typeof err === 'object' && err !== null && 'message' in err) { + // found a `Error: Network connection lost` caused by `kj/async-io-unix.c++:186: disconnected` in the wild + throw new HostError(ErrorCode.NetworkError, `${err.message}`); + } + throw err; } if (response.status === 530) { @@ -275,10 +283,12 @@ export type ClientPerformOptions = { class InternalClient { private readonly app: App; private ready = false; + private readonly fileSystem: CfwFileSystem; constructor(readonly options: ClientOptions = {}) { + this.fileSystem = new CfwFileSystem(options.preopens ?? {}); this.app = new App({ - fileSystem: new CfwFileSystem(options.preopens ?? {}), + fileSystem: this.fileSystem, textCoder: new CfwTextCoder(), timers: new CfwTimers(), network: new CfwNetwork(), @@ -324,9 +334,20 @@ class InternalClient { const resolvedProfile = profile.replace(/\//g, '.'); // TODO: be smarter about this const assetsPath = this.options.assetsPath ?? 'superface'; // TODO: path join? - not sure if we are going to stick with this VFS + let profilePath = `${assetsPath}/${resolvedProfile}.profile.ts`; + // migration from Comlink to TypeScript profiles + const profilePathComlink = `${assetsPath}/${resolvedProfile}.profile`; + if ( + !(await this.fileSystem.exists(profilePath)) + && (await this.fileSystem.exists(profilePathComlink)) + ) { + profilePath = profilePathComlink; + } + profilePath = `file://${profilePath}`; + try { return await this.app.perform( - `file://${assetsPath}/${resolvedProfile}.profile`, + profilePath, `file://${assetsPath}/${provider}.provider.json`, `file://${assetsPath}/${resolvedProfile}.${provider}.map.js`, usecase, diff --git a/packages/javascript_common/src/app.ts b/packages/javascript_common/src/app.ts index f3d26eda..63a384f2 100644 --- a/packages/javascript_common/src/app.ts +++ b/packages/javascript_common/src/app.ts @@ -1,108 +1,9 @@ -import { Asyncify } from './asyncify.js'; +import type { SecurityValuesMap } from './security.js'; +import type { AppContext, FileSystem, Network, Persistence, TextCoder, Timers, WasiContext } from './interfaces.js'; import { PerformError, UnexpectedError, UninitializedError, ValidationError, WasiErrno, WasiError } from './error.js'; -import { HandleMap } from './handle_map.js'; -import { AppContext, FileSystem, Network, Persistence, TextCoder, Timers, WasiContext } from './interfaces.js'; -import { SecurityValuesMap } from './security.js'; +import { AsyncMutex, Asyncify, HandleMap, ReadableStreamAdapter, Stream } from './lib/index.js'; import * as sf_host from './sf_host.js'; -class ReadableStreamAdapter implements Stream { - private chunks: Uint8Array[]; - private readonly reader?: ReadableStreamDefaultReader; - constructor(stream: ReadableStream | null) { - this.reader = stream?.getReader(); - this.chunks = []; - } - async read(out: Uint8Array): Promise { - if (this.reader === undefined) { - return 0; - } - - if (this.chunks.length === 0) { - const readResult = await this.reader.read(); - if (readResult.value === undefined) { - return 0; - } - - this.chunks.push(readResult.value); - } - - // TODO: coalesce multiple smaller chunks into one read - let chunk = this.chunks.shift()!; - if (chunk.byteLength > out.byteLength) { - const remaining = chunk.subarray(out.byteLength); - chunk = chunk.subarray(0, out.byteLength); - - this.chunks.unshift(remaining); - } - - const count = Math.min(chunk.byteLength, out.byteLength); - for (let i = 0; i < count; i += 1) { - out[i] = chunk[i]; - } - - return count; - } - async write(data: Uint8Array): Promise { - throw new Error('not implemented'); - } - async close(): Promise { - // TODO: what to do here? - } -} - -/** Async mutex allows us to synchronize multiple async tasks. - * - * For example, if a perform is in-flight but is waiting for I/O the async task is suspended. If at the same time - * the periodic timer fires, this could cause core to be invoked twice within the same asyncify context, causing undefined behavior. - * - * We can avoid this by synchronizing over core. - * - * Note that this is not thread safe (concurrent), but merely task safe (asynchronous). - */ -export class AsyncMutex { - /** Promise to be awaited to synchronize between tasks. */ - private condvar: Promise; - /** Indicator of whether the mutex is currently locked. */ - private isLocked: boolean; - private value: T; - - constructor(value: T) { - this.condvar = Promise.resolve(); - this.isLocked = false; - this.value = value; - } - - /** - * Get the protected value without respecting the lock. - * - * This is unsafe, but it is needed to get access to memory in sf_host imports. - */ - get unsafeValue(): T { - return this.value; - } - - public async withLock(fn: (value: T) => R): Promise> { - do { - // Under the assumption that we do not have concurrency it can never happen that two tasks - // pass over the condition of this loop and think they both have a lock - that would imply there exists task preemption in synchronous code. - // - // If there ever is threading or task preemption, we will need to use other means (atomics, spinlocks). - await this.condvar; - } while (this.isLocked); - - this.isLocked = true; - let notify: () => void; - this.condvar = new Promise((resolve) => { notify = resolve; }); - - try { - return await fn(this.value); - } finally { - this.isLocked = false; - notify!(); - } - } -} - function headersToMultimap(headers: Headers): Record { const result: Record = {}; @@ -118,14 +19,6 @@ function headersToMultimap(headers: Headers): Record { return result; } -type Stream = { - /** Reads up to `out.length` bytes from the stream, returns number of bytes read or throws a `WasiError`. */ - read(out: Uint8Array): Promise; - /** Writes up to `data.length` bytes into the stream, returns number of bytes written or throws a `WasiError`. */ - write(data: Uint8Array): Promise; - /** Closes the stream, returns undefined or throws a `WasiError`. */ - close(): Promise; -}; type AppCore = { instance: WebAssembly.Instance; asyncify: Asyncify; diff --git a/packages/javascript_common/src/index.ts b/packages/javascript_common/src/index.ts index 78a5c031..3b0758b9 100644 --- a/packages/javascript_common/src/index.ts +++ b/packages/javascript_common/src/index.ts @@ -1,6 +1,5 @@ -export { App, AsyncMutex } from './app.js'; +export { App } from './app.js'; export * from './error.js'; -export { HandleMap } from './handle_map.js'; export type { FileSystem, Network, Persistence, TextCoder, Timers, WasiContext } from './interfaces.js'; -export { SecurityValuesMap } from './security.js'; -export * from './wasm.js'; +export type { SecurityValuesMap } from './security.js'; +export { HandleMap, AsyncMutex, corePathURL } from './lib/index.js'; \ No newline at end of file diff --git a/packages/javascript_common/src/interfaces.ts b/packages/javascript_common/src/interfaces.ts index 5dd9a579..d05e05c7 100644 --- a/packages/javascript_common/src/interfaces.ts +++ b/packages/javascript_common/src/interfaces.ts @@ -8,6 +8,8 @@ export interface AppContext { closeStream(handle: number): Promise; } export interface FileSystem { + /** Return true if the file exists (can be `stat`ed). */ + exists(path: string): Promise; open(path: string, options: { createNew?: boolean, create?: boolean, truncate?: boolean, append?: boolean, write?: boolean, read?: boolean }): Promise; /** Read bytes and write them to `out`. Returns number of bytes read. */ read(handle: number, out: Uint8Array): Promise; diff --git a/packages/javascript_common/src/lib/async_mutex.ts b/packages/javascript_common/src/lib/async_mutex.ts new file mode 100644 index 00000000..bfc38821 --- /dev/null +++ b/packages/javascript_common/src/lib/async_mutex.ts @@ -0,0 +1,52 @@ +/** Async mutex allows us to synchronize multiple async tasks. + * + * For example, if a perform is in-flight but is waiting for I/O the async task is suspended. If at the same time + * the periodic timer fires, this could cause core to be invoked twice within the same asyncify context, causing undefined behavior. + * + * We can avoid this by synchronizing over core. + * + * Note that this is not thread safe (concurrent), but merely task safe (asynchronous). + */ +export class AsyncMutex { + /** Promise to be awaited to synchronize between tasks. */ + private condvar: Promise; + /** Indicator of whether the mutex is currently locked. */ + private isLocked: boolean; + private value: T; + + constructor(value: T) { + this.condvar = Promise.resolve(); + this.isLocked = false; + this.value = value; + } + + /** + * Get the protected value without respecting the lock. + * + * This is unsafe, but it is needed to get access to memory in sf_host imports. + */ + get unsafeValue(): T { + return this.value; + } + + public async withLock(fn: (value: T) => R): Promise> { + do { + // Under the assumption that we do not have concurrency it can never happen that two tasks + // pass over the condition of this loop and think they both have a lock - that would imply there exists task preemption in synchronous code. + // + // If there ever is threading or task preemption, we will need to use other means (atomics, spinlocks). + await this.condvar; + } while (this.isLocked); + + this.isLocked = true; + let notify: () => void; + this.condvar = new Promise((resolve) => { notify = resolve; }); + + try { + return await fn(this.value); + } finally { + this.isLocked = false; + notify!(); + } + } +} \ No newline at end of file diff --git a/packages/javascript_common/src/asyncify.ts b/packages/javascript_common/src/lib/asyncify.ts similarity index 100% rename from packages/javascript_common/src/asyncify.ts rename to packages/javascript_common/src/lib/asyncify.ts diff --git a/packages/javascript_common/src/handle_map.ts b/packages/javascript_common/src/lib/handle_map.ts similarity index 100% rename from packages/javascript_common/src/handle_map.ts rename to packages/javascript_common/src/lib/handle_map.ts diff --git a/packages/javascript_common/src/lib/index.ts b/packages/javascript_common/src/lib/index.ts new file mode 100644 index 00000000..5c053141 --- /dev/null +++ b/packages/javascript_common/src/lib/index.ts @@ -0,0 +1,5 @@ +export * from './asyncify.js'; +export * from './async_mutex.js'; +export * from './handle_map.js'; +export * from './stream.js'; +export * from './wasm.js'; \ No newline at end of file diff --git a/packages/javascript_common/src/lib/stream.ts b/packages/javascript_common/src/lib/stream.ts new file mode 100644 index 00000000..b8e30ca1 --- /dev/null +++ b/packages/javascript_common/src/lib/stream.ts @@ -0,0 +1,52 @@ +export type Stream = { + /** Reads up to `out.length` bytes from the stream, returns number of bytes read or throws a `WasiError`. */ + read(out: Uint8Array): Promise; + /** Writes up to `data.length` bytes into the stream, returns number of bytes written or throws a `WasiError`. */ + write(data: Uint8Array): Promise; + /** Closes the stream, returns undefined or throws a `WasiError`. */ + close(): Promise; +}; +export class ReadableStreamAdapter implements Stream { + private chunks: Uint8Array[]; + private readonly reader?: ReadableStreamDefaultReader; + constructor(stream: ReadableStream | null) { + this.reader = stream?.getReader(); + this.chunks = []; + } + async read(out: Uint8Array): Promise { + if (this.reader === undefined) { + return 0; + } + + if (this.chunks.length === 0) { + const readResult = await this.reader.read(); + if (readResult.value === undefined) { + return 0; + } + + this.chunks.push(readResult.value); + } + + // TODO: coalesce multiple smaller chunks into one read + let chunk = this.chunks.shift()!; + if (chunk.byteLength > out.byteLength) { + const remaining = chunk.subarray(out.byteLength); + chunk = chunk.subarray(0, out.byteLength); + + this.chunks.unshift(remaining); + } + + const count = Math.min(chunk.byteLength, out.byteLength); + for (let i = 0; i < count; i += 1) { + out[i] = chunk[i]; + } + + return count; + } + async write(data: Uint8Array): Promise { + throw new Error('not implemented'); + } + async close(): Promise { + // TODO: what to do here? + } +} \ No newline at end of file diff --git a/packages/javascript_common/src/lib/wasm.ts b/packages/javascript_common/src/lib/wasm.ts new file mode 100644 index 00000000..e23c1166 --- /dev/null +++ b/packages/javascript_common/src/lib/wasm.ts @@ -0,0 +1,5 @@ +export function corePathURL(): URL { + // relative to where common is symlinked + // up three times gets us from dist/common/lib into the outer package + return new URL('../../../assets/core-async.wasm', import.meta.url); +} diff --git a/packages/javascript_common/src/sf_host.ts b/packages/javascript_common/src/sf_host.ts index a96ce3c7..cba1562c 100644 --- a/packages/javascript_common/src/sf_host.ts +++ b/packages/javascript_common/src/sf_host.ts @@ -1,6 +1,5 @@ import type { TextCoder, AppContext } from './interfaces'; -import { HandleMap } from './handle_map.js'; -import { Asyncify, AsyncifyState } from './asyncify.js'; +import { Asyncify, AsyncifyState, HandleMap } from './lib/index.js'; import { WasiErrno } from './error.js'; type AbiResult = number; diff --git a/packages/javascript_common/src/wasm.ts b/packages/javascript_common/src/wasm.ts deleted file mode 100644 index f2055769..00000000 --- a/packages/javascript_common/src/wasm.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function corePathURL(): URL { - return new URL('../../assets/core-async.wasm', import.meta.url); // path is relative to location where common is simlinked -} diff --git a/packages/nodejs_host/src/index.ts b/packages/nodejs_host/src/index.ts index 18b43ead..9b63f971 100644 --- a/packages/nodejs_host/src/index.ts +++ b/packages/nodejs_host/src/index.ts @@ -5,7 +5,7 @@ import process from 'node:process'; import { fileURLToPath } from 'node:url'; import { WASI } from 'node:wasi'; -import { AsyncMutex } from './common/app.js'; +import { AsyncMutex } from './common/lib/index.js'; import { App, FileSystem, @@ -45,6 +45,15 @@ class NodeTextCoder implements TextCoder { class NodeFileSystem implements FileSystem { private readonly files: HandleMap = new HandleMap(); + async exists(path: string): Promise { + try { + await fs.stat(path); + return true; + } catch { + return false; + } + } + async open(path: string, options: { createNew?: boolean, create?: boolean, truncate?: boolean, append?: boolean, write?: boolean, read?: boolean }): Promise { let flags = ''; @@ -223,6 +232,7 @@ class InternalClient { private app: App; private readyState: AsyncMutex<{ ready: boolean }>; + private readonly fileSystem: NodeFileSystem; constructor(readonly options: ClientOptions = {}) { if (options.assetsPath !== undefined) { @@ -230,10 +240,11 @@ class InternalClient { } this.readyState = new AsyncMutex({ ready: false }); + this.fileSystem = new NodeFileSystem(); this.app = new App({ network: new NodeNetwork(), - fileSystem: new NodeFileSystem(), + fileSystem: this.fileSystem, textCoder: new NodeTextCoder(), timers: new NodeTimers(), persistence: new NodePersistence(options.token, options.superfaceApiUrl, this.userAgent) @@ -305,7 +316,15 @@ class InternalClient { public async resolveProfileUrl(profile: string): Promise { const resolvedProfile = profile.replace(/\//g, '.'); - const path = resolvePath(this.assetsPath, `${resolvedProfile}.profile`); + let path = resolvePath(this.assetsPath, `${resolvedProfile}.profile.ts`); + // migration from Comlink to TypeScript profiles + const pathComlink = resolvePath(this.assetsPath, `${resolvedProfile}.profile`); + if ( + !(await this.fileSystem.exists(path)) + && (await this.fileSystem.exists(pathComlink)) + ) { + path = pathComlink; + } return `file://${path}`; } diff --git a/packages/python_host/src/one_sdk/client.py b/packages/python_host/src/one_sdk/client.py index f7334112..fea69e2e 100644 --- a/packages/python_host/src/one_sdk/client.py +++ b/packages/python_host/src/one_sdk/client.py @@ -21,15 +21,20 @@ def __init__( self._assets_path = assets_path self._core_path = CORE_PATH self._ready = False + self._file_system = PythonFilesystem() self._app = WasiApp( - PythonFilesystem(), + self._file_system, PythonNetwork(), PythonPersistence(token, superface_api_url, WasiApp.user_agent()) ) def resolve_profile_url(self, profile: str) -> str: resolved_profile = profile.replace('/', '.') - path = os.path.abspath(os.path.join(self._assets_path, f"{resolved_profile}.profile")) + path = os.path.abspath(os.path.join(self._assets_path, f"{resolved_profile}.profile.ts")) + # migration from Comlink to TypeScript profiles + path_comlink = os.path.abspath(os.path.join(self._assets_path, f"{resolved_profile}.profile")) + if not self._file_system.exists(path) and self._file_system.exists(path_comlink): + path = path_comlink return f"file://{path}" diff --git a/packages/python_host/src/one_sdk/platform.py b/packages/python_host/src/one_sdk/platform.py index bc240abc..4d048fcf 100644 --- a/packages/python_host/src/one_sdk/platform.py +++ b/packages/python_host/src/one_sdk/platform.py @@ -1,5 +1,6 @@ from typing import BinaryIO, List, Mapping, Optional, Union, cast +import os.path from datetime import datetime from collections import defaultdict @@ -82,7 +83,6 @@ def write(self, handle: int, data: bytes) -> int: # TODO: map system exception to wasi raise WasiError(WasiErrno.EINVAL) from e - def close(self, handle: int): file = self._files.get(handle) if file is None: @@ -94,6 +94,9 @@ def close(self, handle: int): # TODO: map system exception to wasi raise WasiError(WasiErrno.EINVAL) from e + def exists(self, path: str) -> bool: + return os.path.exists(path) + class HttpResponse(BinaryIO): def __init__(self, response): self._response = response