Skip to content

JS file system: distinguish between atime, mtime, and ctime #22998

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

Merged
merged 17 commits into from
Dec 4, 2024
Merged
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
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ See docs/process.md for more on how version tagging works.
-----------------
- libunwind was updated to LLVM 19.1.4. (#22394)
- mimalloc was updated to 2.1.7. (#21548)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder how this line ended up here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I managed to remove it in #23017 and add it here...

- The file system was updated to independently track atime, mtime and ctime
instead of using the same time for all three. (#22998)

3.1.72 - 11/19/24
-----------------
Expand Down
10 changes: 6 additions & 4 deletions src/library_fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ FS.staticInit();
this.name = name;
this.mode = mode;
this.rdev = rdev;
this.atime = this.mtime = this.ctime = Date.now();
}
get read() {
return (this.mode & this.readMode) === this.readMode;
Expand Down Expand Up @@ -943,7 +944,7 @@ FS.staticInit();
}
node.node_ops.setattr(node, {
mode: (mode & {{{ cDefs.S_IALLUGO }}}) | (node.mode & ~{{{ cDefs.S_IALLUGO }}}),
timestamp: Date.now()
ctime: Date.now()
});
},
lchmod(path, mode) {
Expand Down Expand Up @@ -1016,7 +1017,8 @@ FS.staticInit();
var lookup = FS.lookupPath(path, { follow: true });
var node = lookup.node;
node.node_ops.setattr(node, {
timestamp: Math.max(atime, mtime)
atime: atime,
mtime: mtime
});
},
open(path, flags, mode = 0o666) {
Expand Down Expand Up @@ -1618,7 +1620,7 @@ FS.staticInit();
buffer[offset+i] = result;
}
if (bytesRead) {
stream.node.timestamp = Date.now();
stream.node.atime = Date.now();
}
return bytesRead;
},
Expand All @@ -1631,7 +1633,7 @@ FS.staticInit();
}
}
if (length) {
stream.node.timestamp = Date.now();
stream.node.mtime = stream.node.ctime = Date.now();
}
return i;
}
Expand Down
17 changes: 8 additions & 9 deletions src/library_lz4.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ addToLibrary({
node.mode = mode;
node.node_ops = LZ4.node_ops;
node.stream_ops = LZ4.stream_ops;
node.timestamp = (mtime || new Date).getTime();
this.atime = this.mtime = this.ctime = (mtime || new Date).getTime();
assert(LZ4.FILE_MODE !== LZ4.DIR_MODE);
if (mode === LZ4.FILE_MODE) {
node.size = contents.end - contents.start;
Expand All @@ -95,19 +95,18 @@ addToLibrary({
gid: 0,
rdev: 0,
size: node.size,
atime: new Date(node.timestamp),
mtime: new Date(node.timestamp),
ctime: new Date(node.timestamp),
atime: new Date(node.atime),
mtime: new Date(node.mtime),
ctime: new Date(node.ctime),
blksize: 4096,
blocks: Math.ceil(node.size / 4096),
};
},
setattr(node, attr) {
if (attr.mode !== undefined) {
node.mode = attr.mode;
}
if (attr.timestamp !== undefined) {
node.timestamp = attr.timestamp;
for (const key of ['mode', 'atime', 'mtime', 'ctime']) {
if (attr[key]) {
node[key] = attr[key];
}
}
},
lookup(parent, name) {
Expand Down
30 changes: 14 additions & 16 deletions src/library_memfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ addToLibrary({
node.node_ops = MEMFS.ops_table.chrdev.node;
node.stream_ops = MEMFS.ops_table.chrdev.stream;
}
node.timestamp = Date.now();
node.atime = node.mtime = node.ctime = Date.now();
// add the new node to the parent
if (parent) {
parent.contents[name] = node;
parent.timestamp = node.timestamp;
parent.atime = parent.mtime = parent.ctime = node.atime;
}
return node;
},
Expand Down Expand Up @@ -161,21 +161,20 @@ addToLibrary({
} else {
attr.size = 0;
}
attr.atime = new Date(node.timestamp);
attr.mtime = new Date(node.timestamp);
attr.ctime = new Date(node.timestamp);
attr.atime = new Date(node.atime);
attr.mtime = new Date(node.mtime);
attr.ctime = new Date(node.ctime);
// NOTE: In our implementation, st_blocks = Math.ceil(st_size/st_blksize),
// but this is not required by the standard.
attr.blksize = 4096;
attr.blocks = Math.ceil(attr.size / attr.blksize);
return attr;
},
setattr(node, attr) {
if (attr.mode !== undefined) {
node.mode = attr.mode;
}
if (attr.timestamp !== undefined) {
node.timestamp = attr.timestamp;
for (const key of ["mode", "atime", "mtime", "ctime"]) {
if (attr[key]) {
node[key] = attr[key];
}
}
if (attr.size !== undefined) {
MEMFS.resizeFileStorage(node, attr.size);
Expand Down Expand Up @@ -207,22 +206,21 @@ addToLibrary({
}
// do the internal rewiring
delete old_node.parent.contents[old_node.name];
old_node.parent.timestamp = Date.now()
old_node.name = new_name;
new_dir.contents[new_name] = old_node;
new_dir.timestamp = old_node.parent.timestamp;
old_node.name = new_name;
new_dir.ctime = new_dir.mtime = old_node.parent.ctime = old_node.parent.mtime = Date.now();
},
unlink(parent, name) {
delete parent.contents[name];
parent.timestamp = Date.now();
parent.ctime = parent.mtime = Date.now();
},
rmdir(parent, name) {
var node = FS.lookupNode(parent, name);
for (var i in node.contents) {
throw new FS.ErrnoError({{{ cDefs.ENOTEMPTY }}});
}
delete parent.contents[name];
parent.timestamp = Date.now();
parent.ctime = parent.mtime = Date.now();
},
readdir(node) {
var entries = ['.', '..'];
Expand Down Expand Up @@ -282,7 +280,7 @@ addToLibrary({

if (!length) return 0;
var node = stream.node;
node.timestamp = Date.now();
node.mtime = node.ctime = Date.now();

if (buffer.subarray && (!node.contents || node.contents.subarray)) { // This write is from a typed array to a typed array?
if (canOwn) {
Expand Down
7 changes: 4 additions & 3 deletions src/library_nodefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,10 @@ addToLibrary({
// update the common node structure mode as well
node.mode = attr.mode;
}
if (attr.timestamp !== undefined) {
var date = new Date(attr.timestamp);
fs.utimesSync(path, date, date);
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);
Expand Down
8 changes: 4 additions & 4 deletions src/library_noderawfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,13 @@ addToLibrary({
fs.ftruncateSync(stream.nfd, len);
},
utime(path, atime, mtime) {
// -1 here for atime or mtime means UTIME_OMIT was passed. Since node
// null here for atime or mtime means UTIME_OMIT was passed. Since node
// doesn't support this concept we need to first find the existing
// timestamps in order to preserve them.
if (atime == -1 || mtime == -1) {
if ((atime === null) || (mtime === null)) {
var st = fs.statSync(path);
if (atime == -1) atime = st.atimeMs;
if (mtime == -1) mtime = st.mtimeMs;
atime ||= st.atimeMs;
mtime ||= st.mtimeMs;
}
fs.utimesSync(path, atime/1000, mtime/1000);
},
Expand Down
7 changes: 4 additions & 3 deletions src/library_proxyfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ addToLibrary({
// update the common node structure mode as well
node.mode = attr.mode;
}
if (attr.timestamp !== undefined) {
var date = new Date(attr.timestamp);
node.mount.opts.fs.utime(path, date, date);
if (attr.atime || attr.mtime) {
var atime = new Date(attr.atime || attr.mtime);
var mtime = new Date(attr.mtime || attr.atime);
node.mount.opts.fs.utime(path, atime, mtime);
}
if (attr.size !== undefined) {
node.mount.opts.fs.truncate(path, attr.size);
Expand Down
11 changes: 5 additions & 6 deletions src/library_syscall.js
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,7 @@ var SyscallsLibrary = {
if (nanoseconds == {{{ cDefs.UTIME_NOW }}}) {
atime = now;
} else if (nanoseconds == {{{ cDefs.UTIME_OMIT }}}) {
atime = -1;
atime = null;
} else {
atime = (seconds*1000) + (nanoseconds/(1000*1000));
}
Expand All @@ -978,15 +978,14 @@ var SyscallsLibrary = {
if (nanoseconds == {{{ cDefs.UTIME_NOW }}}) {
mtime = now;
} else if (nanoseconds == {{{ cDefs.UTIME_OMIT }}}) {
mtime = -1;
mtime = null;
} else {
mtime = (seconds*1000) + (nanoseconds/(1000*1000));
}
}
// -1 here means UTIME_OMIT was passed. FS.utime tables the max of these
// two values and sets the timestamp to that single value. If both were
// set to UTIME_OMIT then we can skip the call completely.
if (mtime != -1 || atime != -1) {
// null here means UTIME_OMIT was passed. If both were set to UTIME_OMIT then
// we can skip the call completely.
if ((mtime ?? atime) !== null) {
FS.utime(path, atime, mtime);
}
return 0;
Expand Down
4 changes: 2 additions & 2 deletions src/library_tty.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ addToLibrary({
buffer[offset+i] = result;
}
if (bytesRead) {
stream.node.timestamp = Date.now();
stream.node.atime = Date.now();
}
return bytesRead;
},
Expand All @@ -95,7 +95,7 @@ addToLibrary({
throw new FS.ErrnoError({{{ cDefs.EIO }}});
}
if (length) {
stream.node.timestamp = Date.now();
stream.node.mtime = stream.node.ctime = Date.now();
}
return i;
}
Expand Down
17 changes: 8 additions & 9 deletions src/library_workerfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ addToLibrary({
node.mode = mode;
node.node_ops = WORKERFS.node_ops;
node.stream_ops = WORKERFS.stream_ops;
node.timestamp = (mtime || new Date).getTime();
node.atime = node.mtime = node.ctime = (mtime || new Date).getTime();
assert(WORKERFS.FILE_MODE !== WORKERFS.DIR_MODE);
if (mode === WORKERFS.FILE_MODE) {
node.size = contents.size;
Expand All @@ -82,19 +82,18 @@ addToLibrary({
gid: 0,
rdev: 0,
size: node.size,
atime: new Date(node.timestamp),
mtime: new Date(node.timestamp),
ctime: new Date(node.timestamp),
atime: new Date(node.atime),
mtime: new Date(node.mtime),
ctime: new Date(node.ctime),
blksize: 4096,
blocks: Math.ceil(node.size / 4096),
};
},
setattr(node, attr) {
if (attr.mode !== undefined) {
node.mode = attr.mode;
}
if (attr.timestamp !== undefined) {
node.timestamp = attr.timestamp;
for (const key of ["mode", "atime", "mtime", "ctime"]) {
if (attr[key]) {
node[key] = attr[key];
}
}
},
lookup(parent, name) {
Expand Down
7 changes: 0 additions & 7 deletions test/fs/test_fs_js_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -439,17 +439,10 @@ void test_fs_utime() {
assert(utimeStats.st_atime == 10);
assert(utimeStats.st_atim.tv_sec == 10);

// WasmFS correctly sets both times, but the legacy API sets both times to the max of atime and mtime
// and does not correctly handle nanseconds.
#if WASMFS
assert(utimeStats.st_atim.tv_nsec == 500000000);

assert(utimeStats.st_mtime == 8);
assert(utimeStats.st_mtim.tv_sec == 8);
#else
assert(utimeStats.st_mtime == 10);
assert(utimeStats.st_mtim.tv_sec == 10);
#endif

remove("utimetest");
}
Expand Down
9 changes: 9 additions & 0 deletions test/stat/test_chmod.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ void cleanup() {
void test() {
int err;
int lastctime;
int lastmtime;
struct stat s;

//
Expand All @@ -54,6 +55,7 @@ void test() {
memset(&s, 0, sizeof s);
stat("file", &s);
lastctime = s.st_ctime;
lastmtime = s.st_mtime;
sleep(1);

// do the actual chmod
Expand All @@ -64,11 +66,13 @@ void test() {
stat("file", &s);
assert(s.st_mode == (S_IWUSR | S_IFREG));
assert(s.st_ctime != lastctime);
assert(s.st_mtime == lastmtime);

//
// fchmod a file
//
lastctime = s.st_ctime;
lastmtime = s.st_mtime;
sleep(1);

err = fchmod(open("file", O_WRONLY), S_IXUSR);
Expand All @@ -78,11 +82,13 @@ void test() {
stat("file", &s);
assert(s.st_mode == (S_IXUSR | S_IFREG));
assert(s.st_ctime != lastctime);
assert(s.st_mtime == lastmtime);

//
// fchmodat a file
//
lastctime = s.st_ctime;
lastmtime = s.st_mtime;
sleep(1);
err = fchmodat(AT_FDCWD, "otherfile", S_IXUSR, 0);
assert(!err);
Expand All @@ -91,6 +97,7 @@ void test() {
stat("otherfile", &s);
assert(s.st_mode == (S_IXUSR | S_IFREG));
assert(s.st_ctime != lastctime);
assert(s.st_mtime == lastmtime);

//
// chmod a folder
Expand All @@ -99,6 +106,7 @@ void test() {
memset(&s, 0, sizeof s);
stat("folder", &s);
lastctime = s.st_ctime;
lastmtime = s.st_mtime;
sleep(1);

// do the actual chmod
Expand All @@ -108,6 +116,7 @@ void test() {
stat("folder", &s);
assert(s.st_mode == (S_IWUSR | S_IXUSR | S_IFDIR));
assert(s.st_ctime != lastctime);
assert(s.st_mtime == lastmtime);

#ifndef WASMFS // TODO https://github.com/emscripten-core/emscripten/issues/15948
lstat("file-link", &s);
Expand Down
6 changes: 0 additions & 6 deletions test/utime/test_futimens.c
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,6 @@ void test() {
err = futimens(fd, newtimes);
assert(!err);

#if defined(__EMSCRIPTEN__) && !defined(WASMFS) && !defined(NODERAWFS)
// The original emscripten FS (in JS) only supports a single timestamp so both
// mtime and atime will always be the same.
times[0].tv_sec = 42;
times[0].tv_nsec = 88;
#endif
times[1].tv_sec = 42;
times[1].tv_nsec = 88;
check_times(fd, times, 0);
Expand Down
Loading
Loading