diff --git a/src/fs/actors.rs b/src/fs/actors.rs new file mode 100644 index 00000000..6d826fe5 --- /dev/null +++ b/src/fs/actors.rs @@ -0,0 +1,143 @@ +//! A quick'n'dirty JS-friendly actor framework, inspired by Actix. +//! +//! ## Deadlocks +//! +//! Most [`FileSystem`] methods are synchronous, whereas all +//! [`FileSystemDirectoryHandle`] operations are asynchronous. To implement a +//! synchronous API on top of an inherently asynchronous mechanism, we use +//! [`InlineWaker`] to block in-place until a future is resolved. +//! +//! When a blocking method is invoked from the same thread that called +//! [`spawn()`], we open ourselves up to a chicken-and-egg scenario where the +//! synchronous operation can't return until the future resolves, but in order +//! for the future to resolve we have to yield to the JavaScript event loop so +//! the asynchronous operations get a chance to make progress. +//! +//! This causes a deadlock. +//! +//! In the spirit of [Pre-Pooping Your Pants][poop], we use +//! [`wasmer::current_thread_id()`] to detect these scenarios and crash instead. +//! +//! [poop]: https://cglab.ca/~abeinges/blah/everyone-poops/ + +use futures::{channel::mpsc, future::LocalBoxFuture, SinkExt, StreamExt}; +use tokio::sync::oneshot; +use tracing::Instrument; +use virtual_fs::FsError; +use wasmer_wasix::runtime::task_manager::InlineWaker; + +#[async_trait::async_trait(?Send)] +pub(crate) trait Handler { + type Output: Send + 'static; + + async fn handle(&mut self, msg: Msg) -> Result; +} + +type Thunk = Box LocalBoxFuture<'_, ()> + Send>; + +#[derive(Debug, Clone)] +pub(crate) struct Mailbox { + original_thread: u32, + sender: mpsc::Sender>, +} + +impl Mailbox { + /// Spawn an actor on the current thread. + pub(crate) fn spawn(mut actor: A) -> Self + where + A: 'static, + { + let (sender, mut receiver) = mpsc::channel::>(1); + let original_thread = wasmer::current_thread_id(); + + wasm_bindgen_futures::spawn_local( + async move { + while let Some(thunk) = receiver.next().await { + thunk(&mut actor).await; + } + } + .in_current_span(), + ); + + Mailbox { + original_thread, + sender, + } + } + + /// Asynchronously send a message to the actor. + pub(crate) async fn send(&self, msg: M) -> Result<>::Output, FsError> + where + A: Handler, + M: Send + 'static, + { + let (ret_sender, ret_receiver) = oneshot::channel(); + + let thunk: Thunk = Box::new(move |actor: &mut A| { + Box::pin(async move { + let result = actor.handle(msg).await; + + if ret_sender.send(result).is_err() { + tracing::warn!( + message_type = std::any::type_name::(), + "Unable to send the result back", + ); + } + }) + }); + + // Note: This isn't technically necessary, but it means our methods can + // take &self rather than forcing higher layers to add unnecessary + // synchronisation. + let mut sender = self.sender.clone(); + + if let Err(e) = sender.send(thunk).await { + tracing::warn!( + error = &e as &dyn std::error::Error, + message_type = std::any::type_name::(), + actor_type = std::any::type_name::(), + "Message sending failed", + ); + return Err(FsError::UnknownError); + } + + match ret_receiver.await { + Ok(result) => result, + Err(e) => { + tracing::warn!( + error = &e as &dyn std::error::Error, + message_type = std::any::type_name::(), + actor_type = std::any::type_name::(), + "Unable to receive the result", + ); + Err(FsError::UnknownError) + } + } + } + + /// Send a message to the actor and synchronously block until a response + /// is received. + /// + /// # Deadlocks + /// + /// To avoid deadlocks, this will error out with [`FsError::Lock`] if called + /// from the thread that the actor was spawned on. + pub(crate) fn handle(&self, msg: M) -> Result<>::Output, FsError> + where + A: Handler, + M: Send + 'static, + { + // Note: See the module doc-comments for more context on deadlocks + let current_thread = wasmer::current_thread_id(); + if self.original_thread == current_thread { + tracing::error!( + thread.id=current_thread, + caller=%std::panic::Location::caller(), + "Running a synchronous FileSystem operation on this thread will result in a deadlock" + ); + return Err(FsError::Lock); + } + + InlineWaker::block_on(self.send(msg)) + } +} diff --git a/src/fs/directory.rs b/src/fs/directory.rs index a6060a3e..33d9c31d 100644 --- a/src/fs/directory.rs +++ b/src/fs/directory.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use tracing::Instrument; use virtual_fs::{AsyncReadExt, AsyncWriteExt, FileSystem}; use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; +use web_sys::FileSystemDirectoryHandle; use crate::{utils::Error, StringOrBytes}; @@ -19,6 +20,27 @@ impl Directory { Directory::default() } + /// Create a new [`Directory`] using the [File System API][mdn]. + /// + /// > **Important:** this API will only work inside a secure context. + /// + /// Some ways a [`FileSystemDirectoryHandle`] can be created are... + /// + /// - Using the [Origin private file system API][opfs] + /// - Calling [`window.showDirectoryPicker()`][sdp] + /// to access a file on the host machine (i.e. outside of the browser) + /// - From the [HTML Drag & Drop API][dnd] by calling [`DataTransferItem.getAsFileSystemHandle()`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/getAsFileSystemHandle) + /// + /// + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/File_System_API + /// [dnd]: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API + /// [sdp]: https://developer.mozilla.org/en-US/docs/Web/API/window/showDirectoryPicker + /// [opfs]: https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system + #[wasm_bindgen(js_name = "fromBrowser")] + pub fn from_browser(handle: FileSystemDirectoryHandle) -> Self { + Directory(Arc::new(crate::fs::web::spawn(handle))) + } + #[wasm_bindgen(js_name = "readDir")] pub async fn read_dir(&self, mut path: String) -> Result { if !path.starts_with('/') { diff --git a/src/fs/mod.rs b/src/fs/mod.rs index f1e552a3..a8e469d2 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -1,3 +1,5 @@ mod directory; +mod web; +mod actors; pub use self::directory::Directory; diff --git a/src/fs/web.rs b/src/fs/web.rs new file mode 100644 index 00000000..042121f5 --- /dev/null +++ b/src/fs/web.rs @@ -0,0 +1,543 @@ +//! A [`FileSystem`] implementation backed by the browser's +//! [`FileSystemDirectoryHandle`]. +//! +//! Similar to the [`crate::tasks`] module, this provides multi-threaded access +//! to single-threaded JavaScript objects by encapsulating them in actors that +//! run in the original JavaScript context, then invoking methods by sending +//! messages back and forth. +//! +//! ## Deadlocks +//! +//! Most [`FileSystem`] methods are synchronous, whereas all +//! [`FileSystemDirectoryHandle`] operations are asynchronous. To implement a +//! synchronous API on top of an inherently asynchronous mechanism, we use +//! [`InlineWaker`] to block in-place until a future is resolved. +//! +//! When a blocking method is invoked from the same thread that called +//! [`spawn()`], we open ourselves up to a chicken-and-egg scenario where the +//! synchronous operation can't return until the future resolves, but in order +//! for the future to resolve we have to yield to the JavaScript event loop so +//! the asynchronous operations get a chance to make progress. +//! +//! This causes a deadlock. +//! +//! In the spirit of [Pre-Pooping Your Pants][poop], we use +//! [`wasmer::current_thread_id()`] to detect these scenarios and crash instead. +//! +//! [poop]: https://cglab.ca/~abeinges/blah/everyone-poops/ + +use std::path::{Path, PathBuf}; + +use async_trait::async_trait; +use futures::{SinkExt, TryStream, TryStreamExt}; +use js_sys::{JsString, Reflect}; +use virtual_fs::{ + DirEntry, FileOpener, FileSystem, FileType, FsError, OpenOptionsConfig, ReadDir, VirtualFile, +}; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{DomException, FileSystemDirectoryHandle, FileSystemGetDirectoryOptions}; + +use crate::fs::actors::{Handler, Mailbox}; + +#[tracing::instrument(level = "debug", skip_all)] +pub(crate) fn spawn(handle: FileSystemDirectoryHandle) -> impl FileSystem + 'static { + Mailbox::spawn(Directory(handle)) +} + +#[derive(Debug, Clone)] +struct Directory(FileSystemDirectoryHandle); + +impl Directory { + /// Execute a thunk and use [`JsCast`] to downcast its result. + /// + /// # Implementation Details + /// + /// Note that this will do string matching against [`DomException::name()`] + /// to try and interpret an exception as a [`FsError`]. Technically, there + /// is [`DomException::code()`] that we could `match` against, but the docs + /// mark it as a legacy feature and recomend against using it. + /// + /// See [the MDN page][mdn] for more. + /// + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/DOMException + async fn exec(&self, error_codes: &[(&str, FsError)], thunk: F) -> Result + where + F: FnOnce(&FileSystemDirectoryHandle) -> js_sys::Promise, + Ret: JsCast, + { + let promise = thunk(&self.0); + + match wasm_bindgen_futures::JsFuture::from(promise).await { + Ok(value) => match value.dyn_into() { + Ok(v) => Ok(v), + Err(other) => unreachable!( + "Unable to cast {other:?} to a {}", + std::any::type_name::() + ), + }, + Err(e) => { + if let Some(e) = e + .dyn_ref::() + .and_then(|ex| interpret_dom_exception(ex, error_codes)) + { + Err(e) + } else { + let error = crate::utils::js_error(e); + tracing::error!( + error = &*error, + operation = %std::any::type_name::().replace("::{{closure}}", ""), + "An error occurred while interacting with the FileSystemDirectoryHandle", + ); + Err(FsError::UnknownError) + } + } + } + } + + /// Evaluate a thunk and throw away the result. + async fn eval(&self, error_codes: &[(&str, FsError)], thunk: F) -> Result<(), FsError> + where + F: FnOnce(&FileSystemDirectoryHandle) -> js_sys::Promise, + { + let _: JsValue = self.exec(error_codes, thunk).await?; + Ok(()) + } +} + +/// Convert a [`DomException`] into a known [`FsError`]. +/// +fn interpret_dom_exception(e: &DomException, error_codes: &[(&str, FsError)]) -> Option { + let name = e.name(); + + for (error_name, fs_error) in error_codes.iter().copied() { + if name == error_name { + return Some(fs_error); + } + } + + None +} + +#[derive(Debug, Clone)] +struct ReadDirectory(PathBuf); + +#[async_trait(?Send)] +impl Handler for Directory { + type Output = ReadDir; + + #[tracing::instrument(level = "trace", skip(self))] + async fn handle(&mut self, msg: ReadDirectory) -> Result { + let path = msg.0.as_os_str().to_str().ok_or(FsError::InvalidInput)?; + + let error_codes = [ + ("NotAllowedError", FsError::PermissionDenied), + ("TypeMismatchError", FsError::BaseNotDirectory), + ("NotFoundError", FsError::EntryNotFound), + ]; + + let dir: FileSystemDirectoryHandle = self + .exec(&error_codes, |d| { + d.get_directory_handle_with_options( + path, + FileSystemGetDirectoryOptions::new().create(true), + ) + }) + .await?; + + let iter: js_sys::AsyncIterator = Reflect::get(&dir, &JsValue::from_str("entries")) + .unwrap() + .unchecked_into::() + .call0(&dir) + .unwrap() + .unchecked_into(); + + let dir_entries = async_iterator_to_stream(iter) + .and_then(|pair| async { + let array: js_sys::Array = pair.dyn_into().unwrap(); + + let key: JsString = array.get(0).dyn_into().unwrap(); + let name = String::from(key); + let full_path = msg.0.join(name); + + let handle: web_sys::FileSystemHandle = array.get(1).dyn_into().unwrap(); + + let metadata = metadata_from_fs_handle(&handle).await.map_err(|e| { + tracing::debug!( + path=%full_path.display(), + error=&*e, + "Unable to get an item's metadata" + ); + FsError::UnknownError + }); + + Ok(DirEntry { + path: full_path, + metadata, + }) + }) + .try_collect::>() + .await; + + match dir_entries { + Ok(entries) => Ok(ReadDir::new(entries)), + Err(e) => { + let error = crate::utils::js_error(e); + tracing::debug!( + path, + error = &*error, + "Unable to read a directory's metadata", + ); + Err(FsError::UnknownError) + } + } + } +} + +async fn metadata_from_fs_handle( + handle: &web_sys::FileSystemHandle, +) -> Result { + if let Some(_dir_handle) = handle.dyn_ref::() { + Ok(virtual_fs::Metadata { + ft: FileType::new_dir(), + accessed: 0, + created: 0, + modified: 0, + len: 0, + }) + } else if let Some(file_handle) = handle.dyn_ref::() { + let file: web_sys::File = wasm_bindgen_futures::JsFuture::from(file_handle.get_file()) + .await + .map_err(crate::utils::js_error)? + .dyn_into() + .unwrap(); + + Ok(virtual_fs::Metadata { + ft: FileType { + file: true, + ..Default::default() + }, + accessed: 0, + created: 0, + modified: 0, + len: file.size() as u64, + }) + } else { + unreachable!() + } +} + +fn async_iterator_to_stream( + iter: js_sys::AsyncIterator, +) -> impl TryStream { + let (mut sender, receiver) = futures::channel::mpsc::channel(1); + + async fn iter_next(iter: &js_sys::AsyncIterator) -> Result, JsValue> { + let promise = iter.next()?; + let result = wasm_bindgen_futures::JsFuture::from(promise).await?; + + let done = js_sys::Reflect::get(&result, &JsValue::from_str("done"))? + .as_bool() + .unwrap_or(true); + + if done { + return Ok(None); + } + + let value = js_sys::Reflect::get(&result, &JsValue::from_str("value")) + .unwrap_or(JsValue::UNDEFINED); + + Ok(Some(value)) + } + + wasm_bindgen_futures::spawn_local(async move { + loop { + match iter_next(&iter).await { + Ok(Some(value)) => { + if sender.send(Ok(value)).await.is_err() { + break; + } + } + Ok(None) => break, + Err(e) => { + let _ = sender.send(Err(e)).await; + break; + } + } + } + }); + + receiver +} + +#[derive(Debug, Clone)] +struct CreateDirectory(PathBuf); + +#[async_trait(?Send)] +impl Handler for Directory { + type Output = (); + + #[tracing::instrument(level = "trace", skip(self))] + async fn handle(&mut self, msg: CreateDirectory) -> Result { + let path = msg.0.as_os_str().to_str().ok_or(FsError::InvalidInput)?; + + let error_codes = [ + ("NotAllowedError", FsError::PermissionDenied), + ("TypeMismatchError", FsError::BaseNotDirectory), + ("NotFoundError", FsError::EntryNotFound), + ]; + + self.eval(&error_codes, |d| { + d.get_directory_handle_with_options( + path, + FileSystemGetDirectoryOptions::new().create(true), + ) + }) + .await?; + + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct RemoveDirectory(PathBuf); + +#[async_trait(?Send)] +impl Handler for Directory { + type Output = (); + + #[tracing::instrument(level = "trace", skip(self))] + async fn handle(&mut self, msg: RemoveDirectory) -> Result { + let path = msg.0.as_os_str().to_str().ok_or(FsError::InvalidInput)?; + + let error_codes = [ + ("NotAllowedError", FsError::PermissionDenied), + ("InvalidModificationError", FsError::DirectoryNotEmpty), + ("NotFoundError", FsError::EntryNotFound), + ]; + + self.eval(&error_codes, |d| d.remove_entry(path)).await?; + + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct Rename { + from: PathBuf, + to: PathBuf, +} + +#[async_trait(?Send)] +impl Handler for Directory { + type Output = (); + + #[tracing::instrument(level = "trace", skip(self))] + async fn handle(&mut self, Rename { from, to }: Rename) -> Result { + // TODO: Add a polyfill for renaming an item + tracing::warn!( + ?from, + ?to, + "Renaming isn't implemented by the FileSystem API" + ); + Err(FsError::UnknownError) + } +} + +#[derive(Debug, Clone)] +struct RemoveFile(PathBuf); + +#[async_trait(?Send)] +impl Handler for Directory { + type Output = (); + + #[tracing::instrument(level = "trace", skip(self))] + async fn handle(&mut self, msg: RemoveFile) -> Result { + let path = msg.0.as_os_str().to_str().ok_or(FsError::InvalidInput)?; + + let error_codes = [ + ("NotAllowedError", FsError::PermissionDenied), + ("InvalidModificationError", FsError::DirectoryNotEmpty), + ("NotFoundError", FsError::EntryNotFound), + ]; + + self.eval(&error_codes, |d| d.remove_entry(path)).await?; + + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct Open { + path: PathBuf, + conf: OpenOptionsConfig, +} + +#[async_trait(?Send)] +impl Handler for Directory { + type Output = Box; + + #[tracing::instrument(level = "trace", skip(self))] + async fn handle(&mut self, _msg: Open) -> Result { + todo!(); + } +} + +#[derive(Debug, Clone)] +struct Metadata(PathBuf); + +#[async_trait(?Send)] +impl Handler for Directory { + type Output = virtual_fs::Metadata; + + #[tracing::instrument(level = "trace", skip(self))] + async fn handle(&mut self, _msg: Metadata) -> Result { + todo!(); + } +} + +impl FileSystem for Mailbox { + fn read_dir(&self, path: &Path) -> virtual_fs::Result { + self.handle(ReadDirectory(path.to_path_buf())) + } + + fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> { + self.handle(CreateDirectory(path.to_path_buf())) + } + + fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> { + self.handle(RemoveDirectory(path.to_path_buf())) + } + + fn rename<'a>( + &'a self, + from: &'a Path, + to: &'a Path, + ) -> futures::prelude::future::BoxFuture<'a, virtual_fs::Result<()>> { + Box::pin(self.send(Rename { + from: from.to_path_buf(), + to: to.to_path_buf(), + })) + } + + fn metadata(&self, path: &Path) -> virtual_fs::Result { + self.handle(Metadata(path.to_path_buf())) + } + + fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> { + self.handle(RemoveFile(path.to_path_buf())) + } + + fn new_open_options(&self) -> virtual_fs::OpenOptions { + virtual_fs::OpenOptions::new(self) + } +} + +impl FileOpener for Mailbox { + fn open( + &self, + path: &Path, + conf: &OpenOptionsConfig, + ) -> virtual_fs::Result> { + let path = path.to_path_buf(); + let conf = conf.clone(); + + self.handle(Open { path, conf }) + } +} + +#[cfg(test)] +mod tests { + use std::{fmt::Debug, num::NonZeroUsize}; + + use tokio::sync::oneshot; + use wasm_bindgen::JsCast; + use wasm_bindgen_futures::JsFuture; + use wasm_bindgen_test::wasm_bindgen_test; + use wasmer_wasix::VirtualTaskManager; + + use super::*; + + #[wasm_bindgen_test] + async fn detect_deadlocks() { + let storage = web_sys::window().unwrap().navigator().storage(); + let handle: FileSystemDirectoryHandle = JsFuture::from(storage.get_directory()) + .await + .unwrap() + .dyn_into() + .unwrap(); + let fs = spawn(handle); + + let err = fs.read_dir("/".as_ref()).unwrap_err(); + + assert_eq!(err, FsError::Lock); + } + + /// Create a [`FileSystem`] from a [`FileSystemDirectoryHandle`] and + /// interact with it in a way that won't create deadlocks. + async fn with_fs(handle: FileSystemDirectoryHandle, op: F) -> Ret + where + F: FnOnce(&(dyn FileSystem + 'static)) -> Ret + Send + 'static, + Ret: Debug + Send + 'static, + { + let fs = spawn(handle); + let thread_pool = crate::tasks::ThreadPool::new(NonZeroUsize::new(2).unwrap()); + let (sender, receiver) = oneshot::channel(); + + thread_pool + .task_dedicated(Box::new(move || { + sender.send(op(&fs)).unwrap(); + })) + .unwrap(); + + receiver.await.unwrap() + } + + #[wasm_bindgen_test] + async fn read_dir() { + crate::on_start(); + let _ = crate::initialize_logger(Some("info,wasmer_js::fs=trace".into())); + let storage = web_sys::window().unwrap().navigator().storage(); + let handle: FileSystemDirectoryHandle = JsFuture::from(storage.get_directory()) + .await + .unwrap() + .dyn_into() + .unwrap(); + + let read_dir = with_fs(handle, |fs| fs.read_dir("/".as_ref())).await; + + let entries: Vec<_> = read_dir.unwrap().map(|e| e.unwrap().path()).collect(); + assert!(entries.is_empty()); + } + + #[wasm_bindgen_test] + async fn create_dir() { + crate::on_start(); + let _ = crate::initialize_logger(Some("info,wasmer_js::fs=trace".into())); + let storage = web_sys::window().unwrap().navigator().storage(); + let handle: FileSystemDirectoryHandle = JsFuture::from(storage.get_directory()) + .await + .unwrap() + .dyn_into() + .unwrap(); + + let entries: Vec<_> = with_fs(handle.clone(), |fs| { + fs.create_dir("root".as_ref()).unwrap(); + fs.create_dir("root/nested".as_ref()).unwrap(); + fs.read_dir("root".as_ref()).unwrap() + }) + .await + .map(|result| result.unwrap()) + .collect(); + + assert_eq!( + entries, + vec![DirEntry { + path: PathBuf::from("/root/nested"), + metadata: Ok(virtual_fs::Metadata { + ft: FileType::new_dir(), + ..Default::default() + }) + }] + ); + } +} diff --git a/tests/directory.test.ts b/tests/directory.test.ts index 97f32be7..01ec1fc0 100644 --- a/tests/directory.test.ts +++ b/tests/directory.test.ts @@ -49,3 +49,63 @@ describe("In-Memory Directory", function() { }); }); +describe("Web FileSystem", function() { + this.beforeAll(async () => await initialized); + + it("can read an empty dir", async() => { + const dirHandle = await navigator.storage.getDirectory(); + + const dir = Directory.fromBrowser(dirHandle); + const entries = await dir.readDir("/"); + + expect(entries).to.eql([]); + }); + + it("can read a file", async() => { + const dirHandle = await navigator.storage.getDirectory(); + const f = await dirHandle.getFileHandle("file.txt"); + const writer = await f.createWritable(); + await writer.write("Hello, World!"); + await writer.close(); + const dir = Directory.fromBrowser(dirHandle); + + const contents = await dir.readFile("/file.txt"); + + expect(contents).to.equal("Hello, World!"); + }); + + it("can create a file", async() => { + const dirHandle = await navigator.storage.getDirectory(); + const dir = Directory.fromBrowser(dirHandle); + + await dir.writeFile("/file.txt", "Hello, World!"); + + const handle = await dirHandle.getFileHandle("file.txt"); + const f = await handle.getFile(); + expect(await f.text()).to.equal("Hello, World!"); + }); + + it("can list a directory", async() => { + const dirHandle = await navigator.storage.getDirectory(); + const f = await dirHandle.getFileHandle("file.txt", {create: true}); + const dir = Directory.fromBrowser(dirHandle); + + const entries = await dir.readDir("/"); + + expect(entries).to.eql(["/file.txt"]); + }); + + it("can delete a file", async() => { + const dirHandle = await navigator.storage.getDirectory(); + const f = await dirHandle.getFileHandle("file.txt", {create: true}); + const dir = Directory.fromBrowser(dirHandle); + + await dir.removeFile("/file.txt"); + + const entries: string[] = []; + for await (const key of (dirHandle as any).keys()) { + entries.push(key); + } + expect(entries).to.eql([]); + }); +}); diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 09618203..2bfdae6b 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -6,12 +6,12 @@ const decoder = new TextDecoder("utf-8"); const initialized = (async () => { await init(); - initializeLogger("info"); + initializeLogger("info,wasmer_js::fs=trace"); })(); const ansiEscapeCode = /\u001B\[[\d;]*[JDm]/g; -describe("Wasmer.spawn", function() { +describe.skip("Wasmer.spawn", function() { let wasmer: Wasmer; this.timeout("120s") @@ -294,6 +294,50 @@ describe("Wasmer.spawn", function() { }); }); +describe("fs tests", function() { + let wasmer: Wasmer; + + this.timeout("120s") + .beforeAll(async () => { + await initialized; + + // Note: technically we should use a separate Wasmer instance so tests can't + // interact with each other, but in this case the caching benefits mean we + // complete in tens of seconds rather than several minutes. + wasmer = new Wasmer(); + }); + + it("can do all fs operations using the FileSystem API", async () => { + const dirHandle = await navigator.storage.getDirectory(); + const dir = Directory.fromBrowser(dirHandle); + const script = ` + ls / + ls /mounted + echo "Hello, World!" > /mounted/file.txt + cat /mounted/file.txt + mkdir /mounted/nested + ls /mounted + rm /mounted/file.txt + rmdir /mounted/nested/ + `; + + const instance = await wasmer.spawn("sharrattj/bash", { + command: "bash", + args: ["-xe", "-c", script], + mount: { "/mounted": dir }, + }); + const output = await instance.wait(); + + console.log({ + ...output, + stdout: decoder.decode(output.stdout), + stderr: decoder.decode(output.stderr), + }); + expect(output.ok).to.be.true; + expect(decoder.decode(output.stdout)).to.equal("asdf"); + }); +}); + // FIXME: Re-enable these test and move it to the "Wasmer.spawn" test suite // when we fix TTY handling with static inputs. describe.skip("failing tty handling tests", function() { diff --git a/tests/run.test.ts b/tests/run.test.ts index 4c320011..ddcde011 100644 --- a/tests/run.test.ts +++ b/tests/run.test.ts @@ -7,11 +7,11 @@ const encoder = new TextEncoder(); const initialized = (async () => { await init(); - initializeLogger("info"); + initializeLogger("info,wasmer_js::run=trace,wasmer_js::fs=trace,wasmer_wasix::syscalls=trace"); wasmer = new Wasmer(); })(); -describe("run", function() { +describe.skip("run", function() { this.timeout("60s") .beforeAll(async () => await initialized);