Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to mount directories using the browser's native FileSystem API #337

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
143 changes: 143 additions & 0 deletions src/fs/actors.rs
Original file line number Diff line number Diff line change
@@ -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<Msg> {
type Output: Send + 'static;

async fn handle(&mut self, msg: Msg) -> Result<Self::Output, FsError>;
}

type Thunk<A> = Box<dyn FnOnce(&mut A) -> LocalBoxFuture<'_, ()> + Send>;

#[derive(Debug, Clone)]
pub(crate) struct Mailbox<A> {
original_thread: u32,
sender: mpsc::Sender<Thunk<A>>,
}

impl<A> Mailbox<A> {
/// Spawn an actor on the current thread.
pub(crate) fn spawn(mut actor: A) -> Self
where
A: 'static,
{
let (sender, mut receiver) = mpsc::channel::<Thunk<A>>(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<M>(&self, msg: M) -> Result<<A as Handler<M>>::Output, FsError>
where
A: Handler<M>,
M: Send + 'static,
{
let (ret_sender, ret_receiver) = oneshot::channel();

let thunk: Thunk<A> = 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::<M>(),
"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::<M>(),
actor_type = std::any::type_name::<A>(),
"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::<M>(),
actor_type = std::any::type_name::<A>(),
"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<M>(&self, msg: M) -> Result<<A as Handler<M>>::Output, FsError>
where
A: Handler<M>,
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))
}
}
22 changes: 22 additions & 0 deletions src/fs/directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use tracing::Instrument;
use virtual_fs::{AsyncReadExt, AsyncWriteExt, FileSystem, FileType};
use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
use wasmer_wasix::runtime::task_manager::InlineWaker;
use web_sys::FileSystemDirectoryHandle;

use crate::{utils::Error, StringOrBytes};

Expand All @@ -31,6 +32,27 @@ impl Directory {
}
}

/// 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)))
}

/// Read the contents of a directory.
#[wasm_bindgen(js_name = "readDir")]
pub async fn read_dir(&self, mut path: String) -> Result<ListOfDirEntry, Error> {
Expand Down
2 changes: 2 additions & 0 deletions src/fs/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod actors;
mod directory;
mod web;

pub use self::directory::{Directory, DirectoryInit};
Loading
Loading