diff --git a/ChangeLog.md b/ChangeLog.md index 3ae469aec7456..cc2a2ed3151c5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -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) +- 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 ----------------- diff --git a/src/library_fs.js b/src/library_fs.js index 575bf8feaac6b..3bfa8f2675477 100644 --- a/src/library_fs.js +++ b/src/library_fs.js @@ -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; @@ -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) { @@ -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) { @@ -1618,7 +1620,7 @@ FS.staticInit(); buffer[offset+i] = result; } if (bytesRead) { - stream.node.timestamp = Date.now(); + stream.node.atime = Date.now(); } return bytesRead; }, @@ -1631,7 +1633,7 @@ FS.staticInit(); } } if (length) { - stream.node.timestamp = Date.now(); + stream.node.mtime = stream.node.ctime = Date.now(); } return i; } diff --git a/src/library_lz4.js b/src/library_lz4.js index 061af9faf72a7..d989d99322009 100644 --- a/src/library_lz4.js +++ b/src/library_lz4.js @@ -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; @@ -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) { diff --git a/src/library_memfs.js b/src/library_memfs.js index b4beccd08aadf..35597c99056b5 100644 --- a/src/library_memfs.js +++ b/src/library_memfs.js @@ -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; }, @@ -161,9 +161,9 @@ 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; @@ -171,11 +171,10 @@ addToLibrary({ 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); @@ -207,14 +206,13 @@ 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); @@ -222,7 +220,7 @@ addToLibrary({ throw new FS.ErrnoError({{{ cDefs.ENOTEMPTY }}}); } delete parent.contents[name]; - parent.timestamp = Date.now(); + parent.ctime = parent.mtime = Date.now(); }, readdir(node) { var entries = ['.', '..']; @@ -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) { diff --git a/src/library_nodefs.js b/src/library_nodefs.js index 6d0544c8527cc..846a930c15c48 100644 --- a/src/library_nodefs.js +++ b/src/library_nodefs.js @@ -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); diff --git a/src/library_noderawfs.js b/src/library_noderawfs.js index 55b9e97a4c716..6c622d15078e5 100644 --- a/src/library_noderawfs.js +++ b/src/library_noderawfs.js @@ -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); }, diff --git a/src/library_proxyfs.js b/src/library_proxyfs.js index c98fe3755eadd..6a2a9d35efe49 100644 --- a/src/library_proxyfs.js +++ b/src/library_proxyfs.js @@ -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); diff --git a/src/library_syscall.js b/src/library_syscall.js index 3e857b344d141..821014a4af10f 100644 --- a/src/library_syscall.js +++ b/src/library_syscall.js @@ -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)); } @@ -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; diff --git a/src/library_tty.js b/src/library_tty.js index 28c1d5232d5a7..6889cf157588a 100644 --- a/src/library_tty.js +++ b/src/library_tty.js @@ -79,7 +79,7 @@ addToLibrary({ buffer[offset+i] = result; } if (bytesRead) { - stream.node.timestamp = Date.now(); + stream.node.atime = Date.now(); } return bytesRead; }, @@ -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; } diff --git a/src/library_workerfs.js b/src/library_workerfs.js index 28020bfac7e4d..b5245d201f181 100644 --- a/src/library_workerfs.js +++ b/src/library_workerfs.js @@ -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; @@ -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) { diff --git a/test/fs/test_fs_js_api.c b/test/fs/test_fs_js_api.c index ecd50cbdd799e..e5e58acff801f 100644 --- a/test/fs/test_fs_js_api.c +++ b/test/fs/test_fs_js_api.c @@ -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"); } diff --git a/test/stat/test_chmod.c b/test/stat/test_chmod.c index 995408147e791..b9428255efdbf 100644 --- a/test/stat/test_chmod.c +++ b/test/stat/test_chmod.c @@ -45,6 +45,7 @@ void cleanup() { void test() { int err; int lastctime; + int lastmtime; struct stat s; // @@ -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 @@ -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); @@ -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); @@ -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 @@ -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 @@ -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); diff --git a/test/utime/test_futimens.c b/test/utime/test_futimens.c index 562e957f071d4..d42f635fa43d6 100644 --- a/test/utime/test_futimens.c +++ b/test/utime/test_futimens.c @@ -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); diff --git a/test/utime/test_utime.c b/test/utime/test_utime.c index ab6a1a245a192..836735bb19909 100644 --- a/test/utime/test_utime.c +++ b/test/utime/test_utime.c @@ -27,7 +27,7 @@ void test() { // currently, the most recent timestamp is shared for atime, // ctime and mtime. using unique values for each in the test // will fail - struct utimbuf t = {1000000000, 1000000000}; + struct utimbuf t = {1000000000, 2000000000}; errno = 0; int rv = utime("writeable", &t); @@ -54,7 +54,7 @@ void test() { // write permissions aren't checked when setting node // attributes unless the user uid isn't the owner (so - // therefor, this should work fine) + // therefore, this should work fine) rv = utime("unwriteable", &t); assert(rv == 0); assert(!errno);