Skip to content

[WASMFS] Workaround for missing directory rename in OPFS #24227

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions src/lib/libsigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ sigs = {
_wasmfs_opfs_init_root_directory__sig: 'vp',
_wasmfs_opfs_insert_directory__sig: 'vpipp',
_wasmfs_opfs_insert_file__sig: 'vpipp',
_wasmfs_opfs_move_dir__sig: 'vpiipp',
_wasmfs_opfs_move_file__sig: 'vpiipp',
_wasmfs_opfs_open_access__sig: 'vpip',
_wasmfs_opfs_open_blob__sig: 'vpip',
Expand Down
104 changes: 102 additions & 2 deletions src/lib/libwasmfs_opfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,69 @@ addToLibrary({
$wasmfsOPFSBlobs__deps: ["$HandleAllocator"],
$wasmfsOPFSBlobs: "new HandleAllocator()",

$wasmOPFSGetParentDir__deps: ['$wasmfsOPFSDirectoryHandles'],
$wasmOPFSGetParentDir: async function(handle) {
const root = wasmfsOPFSDirectoryHandles.get(1);
let fullPath = await root.resolve(handle);
if (!fullPath) {
throw new Error("Handle not found");
}
let parent = root;
for (let dir of fullPath.slice(0, -1)) {
parent = await parent.getDirectoryHandle(dir);
}
return parent;
},

$wasmOPFSFileMoveWorkaround__deps: ['$wasmOPFSGetParentDir'],
$wasmOPFSFileMoveWorkaround: async (fileHandle, newDirHandle, name) => {
let newHandle = await newDirHandle.getFileHandle(name, {create: true});
let oldParent = await wasmOPFSGetParentDir(fileHandle);

const fileSize = fileHandle.getSize();
const buffer = new Uint8Array(fileSize);
let totalRead = 0;
while (totalRead < fileSize) {
const readBuffer = fileHandle.read(buffer, { at: totalRead });
if (readBuffer === 0) {
break;
}
totalRead += readBuffer;
}
newHandle.truncate(0);
await newHandle.write(buffer);
newHandle.flush();
newHandle.close();
fileHandle.close();
await oldParent.removeEntry(fileHandle.name);
},

$wasmOPFSDirMoveWorkaround__deps: ['$wasmOPFSGetParentDir'],
$wasmOPFSDirMoveWorkaround: async (dirHandle, newDirHandle, name) => {
let oldParent = await wasmOPFSGetParentDir(dirHandle);
let toDir = await newDirHandle.getDirectoryHandle(name, {create: true});
const moveDir = async (fromDir, toDir) => {
const entries = await fromDir.entries();
let curr;
for (curr = await entries.next(); !curr.done; curr = await entries.next() ) {
const [name, child] = curr.value;
if (child.kind === 'directory') {
const newSub = await toDir.getDirectoryHandle(name, {create: true});
await moveDir(child, newSub);
await fromDir.removeEntry(name);
} else {
try {
await child.move(toDir);
} catch {
await wasmOPFSFileMoveWorkaround(child, toDir, name);
}
}
}
};
await moveDir(dirHandle, toDir);
await oldParent.removeEntry(dirHandle.name);
},

#if !PTHREADS
// OPFS will only be used on modern browsers that supports JS classes.
$FileSystemAsyncAccessHandle: class {
Expand Down Expand Up @@ -197,7 +260,8 @@ addToLibrary({

_wasmfs_opfs_move_file__deps: ['$wasmfsOPFSFileHandles',
'$wasmfsOPFSDirectoryHandles',
'$wasmfsOPFSProxyFinish'],
'$wasmfsOPFSProxyFinish',
'$wasmOPFSFileMoveWorkaround'],
_wasmfs_opfs_move_file: async function(ctx, fileID, newParentID, namePtr, errPtr) {
let name = UTF8ToString(namePtr);
let fileHandle = wasmfsOPFSFileHandles.get(fileID);
Expand All @@ -206,7 +270,43 @@ addToLibrary({
await fileHandle.move(newDirHandle, name);
} catch {
let err = -{{{ cDefs.EIO }}};
{{{ makeSetValue('errPtr', 0, 'err', 'i32') }}};
try {
await wasmOPFSFileMoveWorkaround(fileHandle, newDirHandle, name);
wasmfsOPFSFileHandles.allocated[fileID] =
await newDirHandle.getFileHandle(name);
err = undefined;
} catch {
// nothing to do, we already set the error above
}
if (err) {
{{{ makeSetValue('errPtr', 0, 'err', 'i32') }}};
}
}
wasmfsOPFSProxyFinish(ctx);
},

_wasmfs_opfs_move_dir__deps: ['$wasmfsOPFSDirectoryHandles',
'$wasmfsOPFSProxyFinish',
'$wasmOPFSDirMoveWorkaround'],

_wasmfs_opfs_move_dir: async function(ctx, dirID, newParentID, namePtr, errPtr) {
let name = UTF8ToString(namePtr);
let dirHandle = wasmfsOPFSDirectoryHandles.get(dirID);
let newDirHandle = wasmfsOPFSDirectoryHandles.get(newParentID);
try {
await dirHandle.move(newDirHandle, name);
} catch {
let err = -{{{ cDefs.EBUSY }}};
try {
await wasmOPFSDirMoveWorkaround(dirHandle, newDirHandle, name);
wasmfsOPFSDirectoryHandles.allocated[dirID] = await newDirHandle.getDirectoryHandle(name);
err = undefined;
} catch {
// nothing to do, we already set the error above
}
if (err) {
{{{ makeSetValue('errPtr', 0, 'err', 'i32')}}};
}
}
wasmfsOPFSProxyFinish(ctx);
},
Expand Down
11 changes: 7 additions & 4 deletions system/lib/wasmfs/backends/opfs_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -335,10 +335,13 @@ class OPFSDirectory : public Directory {
ctx.ctx, opfsFile->fileID, dirID, name.c_str(), &err);
});
} else {
// TODO: Support moving directories once OPFS supports that.
// EBUSY can be returned when the directory is "in use by the system,"
// which can mean whatever we want.
err = -EBUSY;
auto opfsDir = std::static_pointer_cast<OPFSDirectory>(file);
proxy([&](auto ctx) {
_wasmfs_opfs_move_dir(
ctx.ctx, opfsDir->dirID, dirID, name.c_str(), &err);
});
// opfs file handles are all invalid now, so have to clear the cache.
opfsDir->locked().removeAllCacheEntries();
}
return err;
}
Expand Down
6 changes: 6 additions & 0 deletions system/lib/wasmfs/backends/opfs_backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ void _wasmfs_opfs_move_file(em_proxying_ctx* ctx,
const char* name,
int* err);

void _wasmfs_opfs_move_dir(em_proxying_ctx* ctx,
int file_id,
int new_parent_id,
const char* name,
int* err);

void _wasmfs_opfs_remove_child(em_proxying_ctx* ctx,
int dir_id,
const char* name,
Expand Down
12 changes: 12 additions & 0 deletions system/lib/wasmfs/file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ Directory::Handle::insertSymlink(const std::string& name,
return child;
}

void Directory::Handle::removeAllCacheEntries() {
auto& dcache = getDir()->dcache;
for (const auto& [key, value] : dcache) {
if ((value.kind == DCacheKind::Normal) && (value.file->kind == DirectoryKind)) {
// Recursively remove all entries from child directories.
std::shared_ptr<Directory> childDir = value.file->cast<Directory>();
childDir->locked().removeAllCacheEntries();
}
}
dcache.clear();
}

// TODO: consider moving this to be `Backend::move` to avoid asymmetry between
// the source and destination directories and/or taking `Directory::Handle`
// arguments to prove that the directories have already been locked.
Expand Down
1 change: 1 addition & 0 deletions system/lib/wasmfs/file.h
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ class Directory::Handle : public File::Handle {

[[nodiscard]] ssize_t getNumEntries();
[[nodiscard]] MaybeEntries getEntries();
void removeAllCacheEntries();
};

inline File::Handle File::locked() { return Handle(shared_from_this()); }
Expand Down
26 changes: 24 additions & 2 deletions test/wasmfs/wasmfs_opfs.c
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,35 @@ int main(int argc, char* argv[]) {
assert(err == 0);
emscripten_console_log("moved file");

fd = open("/opfs/working/foo.txt", O_RDWR | O_CREAT | O_EXCL, 0777);
assert(fd > 0);
emscripten_console_log("created second OPFS file");
close(fd);


err = rename("/opfs/working", "/opfs/working2");
assert(err == 0);
err = access("/opfs/working", F_OK);
assert(err == -1);
err = access("/opfs/working2", F_OK);
assert(err == 0);
emscripten_console_log("moved directory");

err = access("/opfs/working2/foo.txt", F_OK);
assert(err == 0);

err = unlink("/opfs/working2/foo.txt");
assert(err == 0);

err = unlink("/opfs/foo.txt");
assert(err == 0);
err = access("/opfs/foo.txt", F_OK);
assert(err == -1);
emscripten_console_log("removed OPFS file");

err = rmdir("/opfs/working");
err = rmdir("/opfs/working2");
assert(err == 0);
err = access("/opfs/working", F_OK);
err = access("/opfs/working2", F_OK);
assert(err == -1);
emscripten_console_log("removed OPFS directory");

Expand All @@ -224,9 +244,11 @@ void cleanup(void) {

unlink("/opfs/working/foo.txt");
rmdir("/opfs/working");
rmdir("/opfs/working2");
unlink("/opfs/foo.txt");

assert(access("/opfs/working/foo.txt", F_OK) != 0);
assert(access("/opfs/working", F_OK) != 0);
assert(access("/opfs/working2", F_OK) != 0);
assert(access("/opfs/foo.txt", F_OK) != 0);
}
19 changes: 0 additions & 19 deletions test/wasmfs/wasmfs_opfs_errors.c
Original file line number Diff line number Diff line change
Expand Up @@ -122,25 +122,6 @@ int try_oob_write(void) {
return 2;
}

EMSCRIPTEN_KEEPALIVE
int try_rename_dir(void) {
int err = mkdir("/opfs/dir1", 0666);
if (err != 0) {
return 2;
}
err = rename("/opfs/dir1", "/opfs/dir2");
if (err == 0) {
return 1;
}
if (errno == EBUSY) {
rmdir("/opfs/dir1");
return 0;
}
emscripten_console_error(strerror(errno));
rmdir("/opfs/dir1");
return 2;
}

EMSCRIPTEN_KEEPALIVE
void report_result(int result) {
EM_ASM({ out(new Error().stack); });
Expand Down
4 changes: 0 additions & 4 deletions test/wasmfs/wasmfs_opfs_errors_post.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,5 @@ async function run_test() {
"nothing prevents it)";
}

if (Module._try_rename_dir() != 0) {
throw "Did not get expected EBUSY while renaming directory";
}

Module._report_result(0);
}