Skip to content

Commit

Permalink
docs(Judger): 📝 update development documents
Browse files Browse the repository at this point in the history
  • Loading branch information
Eason0729 committed May 27, 2024
1 parent 7f37c97 commit 66c812c
Show file tree
Hide file tree
Showing 24 changed files with 367 additions and 98 deletions.
7 changes: 7 additions & 0 deletions judger/src/filesystem/adapter/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/// Error occurred in the filesystem adapter.
///
/// It's only used to manage the error in a centralized way.
///
/// User shouldn't rely on this error to as value in another error,
/// and should always call [`Into::<fuse3::Errno>>::into`]
/// immediately after the error is returned.
#[derive(thiserror::Error, Debug)]
pub enum FuseError {
#[error("not a readable file")]
Expand Down
20 changes: 12 additions & 8 deletions judger/src/filesystem/adapter/fuse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,26 @@ use spin::Mutex;
use tokio::io::{AsyncRead, AsyncSeek};
use tokio::sync::Mutex as AsyncMutex;

use crate::filesystem::entry::{Entry, TarTree, BLOCKSIZE};
use crate::filesystem::entry::{Entry, BLOCKSIZE};
use crate::filesystem::resource::Resource;
use crate::filesystem::table::to_internal_path;
use crate::filesystem::table::{to_internal_path, AdjTable};

use super::{error::FuseError, handle::HandleTable, reply::*};
use fuse3::{
raw::{reply::*, *},
Result as FuseResult, *,
};

/// A asynchorized stream from vector
type VecStream<I> = tokio_stream::Iter<std::vec::IntoIter<I>>;

// filesystem is an adapter, it should not contain any business logic.
pub struct Filesystem<F>
where
F: AsyncRead + AsyncSeek + Unpin + Send + 'static,
{
handle_table: HandleTable<AsyncMutex<Entry<F>>>,
tree: Mutex<TarTree<F>>,
tree: Mutex<AdjTable<Entry<F>>>,
resource: Arc<Resource>,
}

Expand All @@ -30,15 +33,16 @@ where
F: AsyncRead + AsyncSeek + Unpin + Send + Sync + 'static,
{
/// Create a new filesystem
pub(super) fn new(tree: TarTree<F>, fs_size: u64) -> Self {
pub(super) fn new(tree: AdjTable<Entry<F>>, fs_size: u64) -> Self {
Self {
handle_table: HandleTable::new(),
tree: Mutex::new(tree),
resource: Arc::new(Resource::new(fs_size)),
}
}
/// Mount the filesystem to a path
pub async fn mount_with_path(
/// Mount the filesystem to a path,
/// return a raw handle from `libfuse`
pub async fn raw_mount_with_path(
self,
path: impl AsRef<Path> + Clone,
) -> std::io::Result<MountHandle> {
Expand All @@ -53,13 +57,13 @@ where
.mount_with_unprivileged(self, path.as_ref())
.await
}
/// Insert a file by path
/// Insert a file by path before actual mounts.
pub fn insert_by_path(&self, path: impl AsRef<Path>, content: Vec<u8>) {
let mut tree = self.tree.lock();
tree.insert_by_path(
to_internal_path(path.as_ref()),
|| Entry::Directory,
Entry::new_file_with_content(content),
Entry::new_file_with_vec(content),
);
}
}
Expand Down
8 changes: 5 additions & 3 deletions judger/src/filesystem/adapter/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use std::{

use spin::Mutex;

pub type FileHandle = u64;
/// Lookup table for file handles
pub struct HandleTable<E> {
handle_generator: AtomicU64,
table: Mutex<BTreeMap<u64, Arc<E>>>,
Expand All @@ -19,7 +21,7 @@ impl<E> HandleTable<E> {
}
}
/// Add an entry to the table
pub fn add(&self, entry: E) -> u64 {
pub fn add(&self, entry: E) -> FileHandle {
let handle = self
.handle_generator
.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
Expand All @@ -28,12 +30,12 @@ impl<E> HandleTable<E> {
handle
}
/// Get an entry from the table
pub fn get(&self, handle: u64) -> Option<Arc<E>> {
pub fn get(&self, handle: FileHandle) -> Option<Arc<E>> {
log::trace!("get handle: {}", handle);
self.table.lock().get(&handle).cloned()
}
/// Remove an entry from the table
pub fn remove(&self, handle: u64) -> Option<Arc<E>> {
pub fn remove(&self, handle: FileHandle) -> Option<Arc<E>> {
log::trace!("deallocate handle: {}", handle);
self.table.lock().remove(&handle)
}
Expand Down
2 changes: 1 addition & 1 deletion judger/src/filesystem/adapter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ mod test {
log::info!("mounting test tarball in .temp ...");
let template = Template::new("plugins/rlua-54.lang").await.unwrap();
let filesystem = template.as_filesystem(1024 * 1024 * 1024);
let mut mount_handle = filesystem.mount_with_path("./.temp/5").await.unwrap();
let mut mount_handle = filesystem.raw_mount_with_path("./.temp/5").await.unwrap();
let handle = &mut mount_handle;

tokio::select! {
Expand Down
11 changes: 7 additions & 4 deletions judger/src/filesystem/adapter/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ use tokio::{
io::{AsyncRead, AsyncSeek},
};

use crate::filesystem::entry::TarTree;
use crate::filesystem::{
entry::{Entry, TarTree},
table::AdjTable,
};

use super::fuse::Filesystem;

pub struct Template<F>(TarTree<F>)
pub struct Template<F>(AdjTable<Entry<F>>)
where
F: AsyncRead + AsyncSeek + Unpin + Send + 'static;

Expand All @@ -23,14 +26,14 @@ where
}
/// read a file by path
pub async fn read_by_path(&self, path: impl AsRef<Path>) -> Option<Vec<u8>> {
self.0.read_by_path(path).await
self.read_by_path(path).await
}
}

impl Template<File> {
/// Create a new template from a tar file
pub async fn new(path: impl AsRef<Path> + Clone) -> std::io::Result<Self> {
let tree = TarTree::new(path).await?;
Ok(Self(tree))
Ok(Self(tree.0))
}
}
118 changes: 118 additions & 0 deletions judger/src/filesystem/dev.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
## Module Layout

- `table.rs`: adjacency table
- Tree data structure on vector
- Inode allocation by `MIN_ID + index`
- Not `MT-Safe`
- `handle.rs`: Mount Handle
- NewType wrapper for dropping
- `adapter` module: adapter between internal data structure(tree-like) and `libfuse`
- `error.rs`: a centralized way to handle error
- `fuse.rs`: adaptor between internal data structure and `libfuse`
- `reply.rs`: collection of constructor for replay from `libfuse`
- `handle.rs`: file handle table
- `template.rs`: A NewType wrapper to force user explicitly clone(`as_filesystem`) filesystem
- `entry` module: collection of single file
- `tar.rs`: a NewType wrapper for `Tree<Entry>`
- `ro.rs`: read only normal file(mapped from tar ball)
- `rw.rs`: read/write normal file(in memory)
- `resource.rs`: Resource counter, much like `semaphore`
- `mkdtemp.rs`: a safe wrapper around `libc::mkdtemp`

## Prerequisite knowledge

### Filesystem in Userspace

> FUSE, or Filesystem in Userspace, is a software interface that allows non-privileged users to create their own file systems in Linux without modifying the kernel. It acts as a bridge between the kernel's virtual filesystem layer and user-space programs.
Traditionally, we have to develop a dedicated kernel module for a filesystem.

FUSE workaround this problem by providing connection, to set up a FUSE, program need to...

1. acquire a FUSE connection(similar to unix socket).
2. poll the socket until a connection(similar to a new connection on tcp socket)
3. read that connection to acquire an OPCODE
4. follow OPCODE to parse the request

example of OPCODE: `READ`, `LOOKUP`, `OPEN`, `RMDIR`...

[list](https://github.com/libfuse/libfuse/blob/6476b1c3ccde2fc4e8755858c96debf55aa0574b/lib/fuse_lowlevel.c#L2619) of OPCODE

In this project, we use `fuse3`, which is a wrapper over `libfuse-sys`.

To get started, you can follow [main.rs](https://github.com/Sherlock-Holo/fuse3/blob/master/examples/src/memfs/main.rs) from `fuse3`'s example.

#### `INODE`

`INODE` is an id generate by filesystem, program providing that can set `INODE` to whatever you want, but make sure it's unique for same file(dictionary).

You can get inode with `stat`
```
❯ stat .
File: .
Size: 128 Blocks: 0 IO Block: 4096 directory
Device: 2ah/42d Inode: 13553287 Links: 1
Access: (0775/drwxrwxr-x) Uid: ( 1000/ eason) Gid: ( 1000/ eason)
Access: 2024-04-28 19:44:43.208376257 +0800
Modify: 2024-05-20 21:39:42.300855512 +0800
Change: 2024-05-20 21:39:42.300855512 +0800
Birth: 2024-04-28 19:44:43.208376257 +0800
```

Note that zero `INODE` means unspecified(null in C's language).

#### `File Handle`

In the context of filesystem of libc, you might be familiar with `file descriptor`, `file descriptor` is a `uint64` generate secquetially by kernel(unique for each process).

`File handle` is similar to `file descriptor`, but it sit between kernel and FUSE provider, generated by FUSE provider and unique for each FUSE connection.

When a file open, FUSE provider generate a `uint64`, and file handle is pass as parameter for later operations.

FUSE provider should implement a way to retrieve file's session by `file handle`.

> file's session includes `bytes readed`, `open flag`...
Note that zero `File Handle` means unspecified(null in C's language), generating a 0 `File Handle` is not allowed.

#### OPCODE `READDIR`

> Read directory.
similar to what `ls -a` provide, list dictionary including `.` and `..`

parameters:
1. parent `INODE`(could be unspecified, unspecified is root)
2. offset
3. size

return: list of file(dictionary)

example(ignore the fact that many file is missing):

```
❯ ls /
❯ ls -a /
. .. boot etc lib lib64 media
```

| offset | size | output |
| ---- | ---- | --- |
| 0 | 1 | `.` |
| 2 | 3 | `etc`, `lib`, `lib64` |

#### OPCODE `OPEN`

> Open a file. Open flags (with the exception of O_CREAT, O_EXCL, O_NOCTTY and O_TRUNC) are available in flags
parameters:
1. file `INODE`(could be unspecified, unspecified is root)
2. flag

return: File Handle

`O_CREAT` should be handle by kernel instead.

### mkdtemp

> See `man mkdtemp`
18 changes: 8 additions & 10 deletions judger/src/filesystem/entry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ mod tar;
use self::{ro::TarBlock, rw::MemBlock};
use bytes::Bytes;
use fuse3::FileType;
use std::{ffi::OsString, ops::Deref, sync::Arc};
use std::{ffi::OsString, sync::Arc};
use tokio::{
io::{AsyncRead, AsyncSeek},
sync::{Mutex, OwnedMutexGuard},
sync::Mutex,
};

use super::resource::Resource;
Expand All @@ -25,12 +25,6 @@ pub trait FuseWriteTrait {
async fn write(&mut self, offset: u64, data: &[u8]) -> std::io::Result<u32>;
}

async fn clone_arc<T: Clone>(arc: &Arc<Mutex<T>>) -> Arc<Mutex<T>> {
let inner = arc.deref();
let lock = inner.lock().await;
Arc::new(Mutex::new(lock.deref().clone()))
}

/// Entry in the filesystem
///
/// cloning the entry would clone file state
Expand Down Expand Up @@ -66,13 +60,16 @@ impl<F> Entry<F>
where
F: AsyncRead + AsyncSeek + Unpin + Send + 'static,
{
/// create a new file entry with empty content
pub fn new_file() -> Self {
Self::MemFile(MemBlock::default())
}
pub fn new_file_with_content(content: Vec<u8>) -> Self {
/// create a new file entry with content
pub fn new_file_with_vec(content: Vec<u8>) -> Self {
Self::MemFile(MemBlock::new(content))
}
pub fn kind(&self) -> FileType {
/// get kind of the file
pub(super) fn kind(&self) -> FileType {
match self {
Self::SymLink(_) => FileType::Symlink,
Self::HardLink(_) => FileType::RegularFile,
Expand All @@ -81,6 +78,7 @@ where
Self::MemFile(_) => FileType::RegularFile,
}
}
/// get size of the file
pub fn get_size(&self) -> u64 {
match self {
Self::SymLink(x) => x.len() as u64,
Expand Down
2 changes: 1 addition & 1 deletion judger/src/filesystem/entry/ro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ where
{
async fn read(&mut self, offset: u64, size: u32) -> std::io::Result<bytes::Bytes> {
let size = size.min(self.size - self.cursor) as usize;
let size=size.min(BLOCKSIZE*MAX_READ_BLK);
let size = size.min(BLOCKSIZE * MAX_READ_BLK);

let mut lock = self.file.lock().await;
let seek_from = self.get_seek_from(offset).ok_or(io::Error::new(
Expand Down
32 changes: 3 additions & 29 deletions judger/src/filesystem/entry/tar.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
use std::{
ffi::OsString,
io::Read,
ops::{Deref, DerefMut},
os::unix::ffi::OsStringExt,
path::Path,
sync::Arc,
};
use std::{ffi::OsString, io::Read, os::unix::ffi::OsStringExt, path::Path, sync::Arc};

#[cfg(test)]
use std::io::Cursor;
Expand All @@ -22,7 +15,7 @@ use crate::filesystem::table::{to_internal_path, AdjTable};

use super::{ro::TarBlock, Entry};

pub struct TarTree<F>(AdjTable<Entry<F>>)
pub struct TarTree<F>(pub AdjTable<Entry<F>>)
where
F: AsyncRead + AsyncSeek + Unpin + Send + 'static;

Expand All @@ -35,26 +28,6 @@ where
}
}

impl<F> DerefMut for TarTree<F>
where
F: AsyncRead + AsyncSeek + Unpin + Send + 'static,
{
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

impl<F> Deref for TarTree<F>
where
F: AsyncRead + AsyncSeek + Unpin + Send + 'static,
{
type Target = AdjTable<Entry<F>>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<F> Default for TarTree<F>
where
F: AsyncRead + AsyncSeek + Unpin + Send + 'static,
Expand Down Expand Up @@ -142,6 +115,7 @@ mod test {
macro_rules! assert_kind {
($tree:expr,$path:expr, $kind:ident) => {{
let node = $tree
.0
.get_by_path(to_internal_path(Path::new($path)))
.unwrap();
let entry = node;
Expand Down
2 changes: 1 addition & 1 deletion judger/src/filesystem/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ where
{
pub async fn mount(self) -> std::io::Result<MountHandle> {
let mountpoint = MkdTemp::new();
let handle = self.mount_with_path(mountpoint.get_path()).await?;
let handle = self.raw_mount_with_path(mountpoint.get_path()).await?;
Ok(MountHandle(Some(handle), Some(mountpoint)))
}
}
Loading

0 comments on commit 66c812c

Please sign in to comment.