Skip to content
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f3f36d5
Make fstat work on file descriptor with no name
hoodmane Dec 3, 2024
260278f
Fix node rawfs
hoodmane Dec 3, 2024
f865bdb
Cleanup
hoodmane Dec 3, 2024
cac8b19
Fix nodefs handling of unnamed file descriptors
hoodmane Dec 3, 2024
cad0ddf
Fix indentation
hoodmane Dec 4, 2024
8b1711e
Apply review comments to test
hoodmane Dec 4, 2024
c961354
Fix flake8
hoodmane Dec 4, 2024
bebfc6b
Better argment names and comments
hoodmane Dec 4, 2024
7f4533e
Fix ftruncate
hoodmane Dec 4, 2024
2c41571
Fix directory fds for nameless file patch
hoodmane Dec 4, 2024
6a4dfc2
Implement both truncate and ftruncate via truncateCommon
hoodmane Dec 4, 2024
9d0c79a
Merge branch 'main' into anonymous-file-descriptors
hoodmane Dec 5, 2024
baadd84
Merge branch 'main' into anonymous-file-descriptors
hoodmane Dec 6, 2024
3aa358f
Fix merge
hoodmane Dec 6, 2024
04192f0
Merge branch 'main' into anonymous-file-descriptors
hoodmane Dec 9, 2024
989cf22
Refactor things a bit more
hoodmane Dec 9, 2024
ad02402
Change doStat to writeStat
hoodmane Dec 9, 2024
84bcf30
Fix tests
hoodmane Dec 9, 2024
ba35730
Fix reference error
hoodmane Dec 9, 2024
4e67e7f
Fix test
hoodmane Dec 10, 2024
9399402
Declare stream variable
hoodmane Dec 10, 2024
7593dc1
Merge branch 'main' into anonymous-file-descriptors
hoodmane Dec 12, 2024
efff804
Merge branch 'main' into anonymous-file-descriptors
hoodmane Dec 18, 2024
20de480
Merge branch 'main' into anonymous-file-descriptors
hoodmane Dec 19, 2024
e83ac75
Remove incorrect comment
hoodmane Dec 19, 2024
b509358
Fix merge
hoodmane Dec 19, 2024
8c556c8
Address review comment
hoodmane Dec 19, 2024
4ed8004
Merge branch 'main' into anonymous-file-descriptors
hoodmane Jan 6, 2025
bdfca83
Fix test
hoodmane Jan 6, 2025
4fdc681
Fix test
hoodmane Jan 8, 2025
ddb3fab
Merge branch 'main' into anonymous-file-descriptors
hoodmane Jan 9, 2025
bfd07c7
Fix test
hoodmane Jan 9, 2025
5d4c6d2
Merge branch 'main' into anonymous-file-descriptors
hoodmane Jan 9, 2025
ac100ab
Move stream ops access to streamGetAttr and streamSetAttr
hoodmane Jan 9, 2025
d1f1962
Fix syntax error
hoodmane Jan 9, 2025
02e0b69
Slightly shorter
hoodmane Jan 9, 2025
910b824
Slightly shorter
hoodmane Jan 9, 2025
37f8ee9
Some fixes
hoodmane Jan 9, 2025
5eef321
Fix more
hoodmane Jan 9, 2025
ad6f42a
More fixes
hoodmane Jan 9, 2025
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
69 changes: 55 additions & 14 deletions src/library_fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,16 @@ FS.staticInit();
}
return node.node_ops.getattr(node);
},
fstat(fd) {
var stream = FS.getStreamChecked(fd);
var node = stream.node;
if (stream.stream_ops.getattr) {
return stream.stream_ops.getattr(stream);
} else if (node.node_ops.getattr) {
return node.node_ops.getattr(node);
}
throw new FS.ErrnoError({{{ cDefs.EPERM }}});
},
lstat(path) {
return FS.stat(path, true);
},
Expand All @@ -962,7 +972,17 @@ FS.staticInit();
},
fchmod(fd, mode) {
var stream = FS.getStreamChecked(fd);
FS.chmod(stream.node, mode);
var node = stream.node;
var attrs = {
mode: (mode & {{{ cDefs.S_IALLUGO }}}) | (node.mode & ~{{{ cDefs.S_IALLUGO }}}),
ctime: Date.now()
};
if (stream.stream_ops.getattr) {
return stream.stream_ops.setattr(stream, attrs);
} else if (node.node_ops.getattr) {
return node.node_ops.setattr(node, attrs);
}
throw new FS.ErrnoError({{{ cDefs.EPERM }}});
},
chown(path, uid, gid, dontFollow) {
var node;
Expand All @@ -985,20 +1005,37 @@ FS.staticInit();
},
fchown(fd, uid, gid) {
var stream = FS.getStreamChecked(fd);
FS.chown(stream.node, uid, gid);
var node = stream.node;
var attrs = {
timestamp: Date.now()
// we ignore the uid / gid for now
};
if (stream.stream_ops.getattr) {
return stream.stream_ops.setattr(stream, attrs);
} else if (node.node_ops.getattr) {
return node.node_ops.setattr(node, attrs);
}
throw new FS.ErrnoError({{{ cDefs.EPERM }}});
},
truncate(path, len) {
truncateCommon(arg, len, ftruncate) {
if (len < 0) {
throw new FS.ErrnoError({{{ cDefs.EINVAL }}});
}
var node;
if (typeof path == 'string') {
var lookup = FS.lookupPath(path, { follow: true });
var stream;
if (ftruncate) {
stream = FS.getStreamChecked(arg);
if ((stream.flags & {{{ cDefs.O_ACCMODE }}}) === {{{ cDefs.O_RDONLY}}}) {
throw new FS.ErrnoError({{{ cDefs.EINVAL }}});
}
node = stream.node;
} else if (typeof arg == 'string') {
var lookup = FS.lookupPath(arg, { follow: true });
node = lookup.node;
} else {
node = path;
node = arg;
}
if (!node.node_ops.setattr) {
if (!node.node_ops.setattr && !stream?.stream_ops.setattr) {
throw new FS.ErrnoError({{{ cDefs.EPERM }}});
}
if (FS.isDir(node.mode)) {
Expand All @@ -1011,17 +1048,21 @@ FS.staticInit();
if (errCode) {
throw new FS.ErrnoError(errCode);
}
node.node_ops.setattr(node, {
var attrs = {
size: len,
timestamp: Date.now()
});
};
if (stream?.stream_ops.setattr) {
stream.stream_ops.setattr(stream, attrs);
} else {
node.node_ops.setattr(node, attrs);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a common pattern where we try to do an operation first on the stream and then on the node if it fails?

Do we need the fallback?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well we have a map from file descriptors to streams, and a map from paths to nodes. Each stream has a reference to its node. The difficulty is that not every node has a valid path.

Most file systems store all relevant information about a node on the node itself. However, in the nodefs, each stream has a distinct native file descriptor. In the case where the node has no valid path, the only way to talk to the native object is with fstat and friends using the native file descriptor. So in that case we need these stream_ops. For every other FS, it's sufficient to have setattr on the node_ops.

Also, we do need the node_ops case because if we call stat instead of fstat then we have a path but no file descriptor. We could open the path to get the file descriptor and then stat it and close it. But we're better off just having a separate handler for gettattr/setattr on a path/node than on a file descriptor/stream. But only nodefs seems to need it.

}
},
truncate(path, len) {
FS.truncateCommon(path, len, false);
},
ftruncate(fd, len) {
var stream = FS.getStreamChecked(fd);
if ((stream.flags & {{{ cDefs.O_ACCMODE }}}) === {{{ cDefs.O_RDONLY}}}) {
throw new FS.ErrnoError({{{ cDefs.EINVAL }}});
}
FS.truncate(stream.node, len);
FS.truncateCommon(fd, len, true);
},
utime(path, atime, mtime) {
var lookup = FS.lookupPath(path, { follow: true });
Expand Down
131 changes: 73 additions & 58 deletions src/library_nodefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,65 +116,76 @@ addToLibrary({
}
return newFlags;
},

getattr(func) {
var stat = NODEFS.tryFSOperation(func);
if (NODEFS.isWindows) {
// node.js v0.10.20 doesn't report blksize and blocks on Windows. Fake
// them with default blksize of 4096.
// See http://support.microsoft.com/kb/140365
if (!stat.blksize) {
stat.blksize = 4096;
}
if (!stat.blocks) {
stat.blocks = (stat.size+stat.blksize-1)/stat.blksize|0;
}
// Windows does not report the 'x' permission bit, so propagate read
// bits to execute bits.
stat.mode |= (stat.mode & {{{ cDefs.S_IRUGO }}}) >> 2;
}
return {
dev: stat.dev,
ino: stat.ino,
mode: stat.mode,
nlink: stat.nlink,
uid: stat.uid,
gid: stat.gid,
rdev: stat.rdev,
size: stat.size,
atime: stat.atime,
mtime: stat.mtime,
ctime: stat.ctime,
blksize: stat.blksize,
blocks: stat.blocks
};
},
// Common code for both node and stream setattr
// For node getatrr:
// - arg is a native path
// - chmod, utimes, truncate are fs.chmodSync, fs.utimesSync, fs.truncateSync
// For stream getatrr:
// - arg is a native file descriptor
// - chmod, utimes, truncate are fs.fchmodSync, fs.futimesSync, fs.ftruncateSync
setattr(arg, node, attr, chmod, utimes, truncate) {
NODEFS.tryFSOperation(() => {
if (attr.mode !== undefined) {
var mode = attr.mode;
if (NODEFS.isWindows) {
// Windows only supports S_IREAD / S_IWRITE (S_IRUSR / S_IWUSR)
// https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/chmod-wchmod
mode &= {{{ cDefs.S_IRUSR | cDefs.S_IWUSR }}};
}
chmod(arg, mode);
// update the common node structure mode as well
node.mode = attr.mode;
}
if (attr.atime || attr.mtime) {
var atime = attr.atime && new Date(attr.atime);
var mtime = attr.mtime && new Date(attr.mtime);
fs.utimesSync(path, atime, mtime);
}
if (attr.size !== undefined) {
truncate(arg, attr.size);
}
});
},
node_ops: {
getattr(node) {
var path = NODEFS.realPath(node);
var stat;
NODEFS.tryFSOperation(() => stat = fs.lstatSync(path));
if (NODEFS.isWindows) {
// node.js v0.10.20 doesn't report blksize and blocks on Windows. Fake
// them with default blksize of 4096.
// See http://support.microsoft.com/kb/140365
if (!stat.blksize) {
stat.blksize = 4096;
}
if (!stat.blocks) {
stat.blocks = (stat.size+stat.blksize-1)/stat.blksize|0;
}
// Windows does not report the 'x' permission bit, so propagate read
// bits to execute bits.
stat.mode |= (stat.mode & {{{ cDefs.S_IRUGO }}}) >> 2;
}
return {
dev: stat.dev,
ino: stat.ino,
mode: stat.mode,
nlink: stat.nlink,
uid: stat.uid,
gid: stat.gid,
rdev: stat.rdev,
size: stat.size,
atime: stat.atime,
mtime: stat.mtime,
ctime: stat.ctime,
blksize: stat.blksize,
blocks: stat.blocks
};
return NODEFS.getattr(() => fs.lstatSync(path));
},
setattr(node, attr) {
var path = NODEFS.realPath(node);
NODEFS.tryFSOperation(() => {
if (attr.mode !== undefined) {
var mode = attr.mode;
if (NODEFS.isWindows) {
// Windows only supports S_IREAD / S_IWRITE (S_IRUSR / S_IWUSR)
// https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/chmod-wchmod
mode &= {{{ cDefs.S_IRUSR | cDefs.S_IWUSR }}};
}
fs.chmodSync(path, mode);
// update the common node structure mode as well
node.mode = attr.mode;
}
if (attr.atime || attr.mtime) {
var atime = attr.atime && new Date(attr.atime);
var mtime = attr.mtime && new Date(attr.mtime);
fs.utimesSync(path, atime, mtime);
}
if (attr.size !== undefined) {
fs.truncateSync(path, attr.size);
}
});
NODEFS.setattr(path, node, attr, fs.chmodSync, fs.utimesSync, fs.truncateSync);
},
lookup(parent, name) {
var path = PATH.join2(NODEFS.realPath(parent), name);
Expand Down Expand Up @@ -232,18 +243,22 @@ addToLibrary({
}
},
stream_ops: {
getattr(stream) {
return NODEFS.getattr(() => fs.fstatSync(stream.nfd));
},
setattr(stream, attr) {
NODEFS.setattr(stream.nfd, stream.node, attr, fs.fchmodSync, fs.futimesSync, fs.ftruncateSync);
},
open(stream) {
var path = NODEFS.realPath(stream.node);
NODEFS.tryFSOperation(() => {
if (FS.isFile(stream.node.mode)) {
stream.shared.refcount = 1;
stream.nfd = fs.openSync(path, NODEFS.flagsForNode(stream.flags));
}
stream.shared.refcount = 1;
stream.nfd = fs.openSync(path, NODEFS.flagsForNode(stream.flags));
});
},
close(stream) {
NODEFS.tryFSOperation(() => {
if (FS.isFile(stream.node.mode) && stream.nfd && --stream.shared.refcount === 0) {
if (stream.nfd && --stream.shared.refcount === 0) {
fs.closeSync(stream.nfd);
}
});
Expand Down
4 changes: 4 additions & 0 deletions src/library_noderawfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ addToLibrary({
}
return stat;
},
fstat(fd) {
var stream = FS.getStreamChecked(fd);
return fs.fstatSync(stream.nfd);
},
chmod(path, mode, dontFollow) {
mode &= {{{ cDefs.S_IALLUGO }}};
if (NODEFS.isWindows) {
Expand Down
10 changes: 5 additions & 5 deletions src/library_syscall.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ var SyscallsLibrary = {
}
return PATH.join2(dir, path);
},

doStat(func, path, buf) {
var stat = func(path);
// When called by stat, arg is a path. When called by fstat, arg is a file
// descriptor.
doStat(func, arg, buf) {
var stat = func(arg);
{{{ makeSetValue('buf', C_STRUCTS.stat.st_dev, 'stat.dev', 'i32') }}};
{{{ makeSetValue('buf', C_STRUCTS.stat.st_mode, 'stat.mode', 'i32') }}};
{{{ makeSetValue('buf', C_STRUCTS.stat.st_nlink, 'stat.nlink', SIZE_TYPE) }}};
Expand Down Expand Up @@ -679,8 +680,7 @@ var SyscallsLibrary = {
return SYSCALLS.doStat(FS.lstat, path, buf);
},
__syscall_fstat64: (fd, buf) => {
var stream = SYSCALLS.getStreamFromFD(fd);
return SYSCALLS.doStat(FS.stat, stream.path, buf);
return SYSCALLS.doStat(FS.fstat, fd, buf);
},
__syscall_fchown32: (fd, owner, group) => {
FS.fchown(fd, owner, group);
Expand Down
44 changes: 44 additions & 0 deletions test/fs/test_stat_unnamed_file_descriptor.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <assert.h>
#include "stdio.h"

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

void makedir(const char *dir) {
int rtn = mkdir(dir, 0777);
assert(rtn == 0);
}

void changedir(const char *dir) {
int rtn = chdir(dir);
assert(rtn == 0);
}

void setup() {
#if defined(__EMSCRIPTEN__) && defined(NODEFS)
makedir("working");
EM_ASM(FS.mount(NODEFS, { root: '.' }, 'working'));
changedir("working");
#endif
}


int main() {
setup();
int fd = open("file.txt", O_RDWR | O_CREAT, 0666);
unlink("file.txt");
int res;
struct stat buf;
res = fstat(fd, &buf);
assert(res == 0);
assert(buf.st_atime > 1000000000);
res = fchmod(fd, 0777);
assert(res == 0);
res = ftruncate(fd, 10);
assert(res == 0);
printf("success\n");
}
14 changes: 14 additions & 0 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5874,6 +5874,20 @@ def test_fs_64bit(self):
self.set_setting('FORCE_FILESYSTEM')
self.do_runf('fs/test_64bit.c', 'success')

@requires_node
@parameterized({
'': ([],),
'nodefs': (['-DNODEFS', '-lnodefs.js'],),
'noderawfs': (['-sNODERAWFS'],),
})
def test_fs_stat_unnamed_file_descriptor(self, args):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_fs_stat_unnamed_file_descriptor(self, args):
def test_fs_stat_unnamed_file_descriptor(self):

nodefs = '-DNODEFS' in args or '-DNODERAWFS' in args
if self.get_setting('WASMFS'):
if nodefs:
self.skipTest('NODEFS in WasmFS')
self.set_setting('FORCE_FILESYSTEM')
self.do_runf('fs/test_stat_unnamed_file_descriptor.c', 'success', emcc_args=args)

@parameterized({
'': ([],),
'nodefs': (['-DNODEFS', '-lnodefs.js'],),
Expand Down
Loading