diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index c5474f4847718..e4e7d0c20ece8 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -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', diff --git a/src/lib/libwasmfs_opfs.js b/src/lib/libwasmfs_opfs.js index 4dd7598226298..26ce83f795777 100644 --- a/src/lib/libwasmfs_opfs.js +++ b/src/lib/libwasmfs_opfs.js @@ -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 { @@ -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); @@ -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); }, diff --git a/system/lib/wasmfs/backends/opfs_backend.cpp b/system/lib/wasmfs/backends/opfs_backend.cpp index 6a5be884cc38b..235f9b0cde6b2 100644 --- a/system/lib/wasmfs/backends/opfs_backend.cpp +++ b/system/lib/wasmfs/backends/opfs_backend.cpp @@ -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(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; } diff --git a/system/lib/wasmfs/backends/opfs_backend.h b/system/lib/wasmfs/backends/opfs_backend.h index fc484bdd067b9..507cc24348549 100644 --- a/system/lib/wasmfs/backends/opfs_backend.h +++ b/system/lib/wasmfs/backends/opfs_backend.h @@ -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, diff --git a/system/lib/wasmfs/file.cpp b/system/lib/wasmfs/file.cpp index c16af931c7228..031c23b5845db 100644 --- a/system/lib/wasmfs/file.cpp +++ b/system/lib/wasmfs/file.cpp @@ -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 childDir = value.file->cast(); + 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. diff --git a/system/lib/wasmfs/file.h b/system/lib/wasmfs/file.h index 039d265c21899..fa3d0d85b8b3f 100644 --- a/system/lib/wasmfs/file.h +++ b/system/lib/wasmfs/file.h @@ -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()); } diff --git a/test/wasmfs/wasmfs_opfs.c b/test/wasmfs/wasmfs_opfs.c index 8b1c3e6086458..3bd4c1b5abbad 100644 --- a/test/wasmfs/wasmfs_opfs.c +++ b/test/wasmfs/wasmfs_opfs.c @@ -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"); @@ -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); } diff --git a/test/wasmfs/wasmfs_opfs_errors.c b/test/wasmfs/wasmfs_opfs_errors.c index 019cd25317431..fdc7e7bb20cb7 100644 --- a/test/wasmfs/wasmfs_opfs_errors.c +++ b/test/wasmfs/wasmfs_opfs_errors.c @@ -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); }); diff --git a/test/wasmfs/wasmfs_opfs_errors_post.js b/test/wasmfs/wasmfs_opfs_errors_post.js index e76675a899898..49069cb7376d7 100644 --- a/test/wasmfs/wasmfs_opfs_errors_post.js +++ b/test/wasmfs/wasmfs_opfs_errors_post.js @@ -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); }