diff --git a/tests/snippets/stdlib_os.py b/tests/snippets/stdlib_os.py index 26a101ecb5..7d8d84a9d4 100644 --- a/tests/snippets/stdlib_os.py +++ b/tests/snippets/stdlib_os.py @@ -88,6 +88,9 @@ def __exit__(self, exc_type, exc_val, exc_tb): assert os.read(fd, len(CONTENT3)) == CONTENT3 os.close(fd) + # wait a little bit to ensure that the file times aren't the same + time.sleep(0.1) + fname2 = os.path.join(tmpdir, FILE_NAME2) with open(fname2, "wb"): pass @@ -143,6 +146,46 @@ def __exit__(self, exc_type, exc_val, exc_tb): print(stat_res.st_gid) print(stat_res.st_size) assert stat_res.st_size == len(CONTENT2) + len(CONTENT3) + print(stat_res.st_atime) + print(stat_res.st_ctime) + print(stat_res.st_mtime) + # test that it all of these times are greater than the 10 May 2019, when this test was written + assert stat_res.st_atime > 1557500000 + assert stat_res.st_ctime > 1557500000 + assert stat_res.st_mtime > 1557500000 + + stat_file2 = os.stat(fname2) + print(stat_file2.st_ctime) + assert stat_file2.st_ctime > stat_res.st_ctime + + # wait a little bit to ensures that the access/modify time will change + time.sleep(0.1) + + old_atime = stat_res.st_atime + old_mtime = stat_res.st_mtime + + fd = os.open(fname, os.O_RDWR) + os.write(fd, CONTENT) + os.fsync(fd) + + # wait a little bit to ensures that the access/modify time is different + time.sleep(0.1) + + os.read(fd, 1) + os.fsync(fd) + os.close(fd) + + # retrieve update file stats + stat_res = os.stat(fname) + print(stat_res.st_atime) + print(stat_res.st_ctime) + print(stat_res.st_mtime) + if os.name != "nt": + # access time on windows has a resolution ranging from 1 hour to 1 day + # https://docs.microsoft.com/en-gb/windows/desktop/api/minwinbase/ns-minwinbase-filetime + assert stat_res.st_atime > old_atime, "Access time should be update" + assert stat_res.st_atime > stat_res.st_mtime + assert stat_res.st_mtime > old_mtime, "Modified time should be update" # stat default is follow_symlink=True os.stat(fname).st_ino == os.stat(symlink_file).st_ino diff --git a/vm/src/stdlib/os.rs b/vm/src/stdlib/os.rs index e688737673..6fd2daf724 100644 --- a/vm/src/stdlib/os.rs +++ b/vm/src/stdlib/os.rs @@ -1,7 +1,8 @@ use std::cell::RefCell; use std::fs::File; use std::fs::OpenOptions; -use std::io::{ErrorKind, Read, Write}; +use std::io::{self, ErrorKind, Read, Write}; +use std::time::{Duration, SystemTime}; use std::{env, fs}; use num_traits::cast::ToPrimitive; @@ -88,6 +89,7 @@ pub fn os_open(vm: &VirtualMachine, args: PyFuncArgs) -> PyResult { let handle = match objint::get_value(mode).to_u16().unwrap() { 0 => OpenOptions::new().read(true).open(&fname), 1 => OpenOptions::new().write(true).open(&fname), + 2 => OpenOptions::new().read(true).write(true).open(&fname), 512 => OpenOptions::new().write(true).create(true).open(&fname), _ => OpenOptions::new().read(true).open(&fname), } @@ -123,6 +125,15 @@ fn os_error(vm: &VirtualMachine, args: PyFuncArgs) -> PyResult { Err(vm.new_os_error(msg)) } +fn os_fsync(fd: PyIntRef, vm: &VirtualMachine) -> PyResult<()> { + let file = rust_file(fd.as_bigint().to_i64().unwrap()); + file.sync_all() + .map_err(|s| vm.new_os_error(s.to_string()))?; + // Avoid closing the fd + raw_file_number(file); + Ok(()) +} + fn os_read(fd: PyIntRef, n: PyIntRef, vm: &VirtualMachine) -> PyResult { let mut buffer = vec![0u8; n.as_bigint().to_usize().unwrap()]; let mut file = rust_file(fd.as_bigint().to_i64().unwrap()); @@ -261,7 +272,7 @@ impl DirEntryRef { .is_symlink()) } - fn stat(self, follow_symlinks: FollowSymlinks, vm: &VirtualMachine) -> PyResult { + fn stat(self, follow_symlinks: FollowSymlinks, vm: &VirtualMachine) -> PyResult { os_stat(self.path(vm).try_into_ref(vm)?, follow_symlinks, vm) } } @@ -317,6 +328,9 @@ struct StatResult { st_uid: u32, st_gid: u32, st_size: u64, + st_atime: f64, + st_ctime: f64, + st_mtime: f64, } impl PyValue for StatResult { @@ -355,47 +369,94 @@ impl StatResultRef { fn st_size(self, _vm: &VirtualMachine) -> u64 { self.st_size } + + fn st_atime(self, _vm: &VirtualMachine) -> f64 { + self.st_atime + } + + fn st_ctime(self, _vm: &VirtualMachine) -> f64 { + self.st_ctime + } + + fn st_mtime(self, _vm: &VirtualMachine) -> f64 { + self.st_mtime + } +} + +// Copied code from Duration::as_secs_f64 as it's still unstable +fn duration_as_secs_f64(duration: Duration) -> f64 { + (duration.as_secs() as f64) + (duration.subsec_nanos() as f64) / (1_000_000_000 as f64) +} + +fn to_seconds_from_unix_epoch(sys_time: SystemTime) -> f64 { + match sys_time.duration_since(SystemTime::UNIX_EPOCH) { + Ok(duration) => duration_as_secs_f64(duration), + Err(err) => -duration_as_secs_f64(err.duration()), + } } #[cfg(unix)] -macro_rules! os_unix_stat_inner { - ( $path:expr, $follow_symlinks:expr, $vm:expr) => {{ - let metadata = match $follow_symlinks.follow_symlinks { - true => fs::metadata($path), - false => fs::symlink_metadata($path), - }; - let meta = metadata.map_err(|s| $vm.new_os_error(s.to_string()))?; +fn to_seconds_from_nanos(secs: i64, nanos: i64) -> f64 { + let duration = Duration::new(secs as u64, nanos as u32); + duration_as_secs_f64(duration) +} - Ok(StatResult { - st_mode: meta.st_mode(), - st_ino: meta.st_ino(), - st_dev: meta.st_dev(), - st_nlink: meta.st_nlink(), - st_uid: meta.st_uid(), - st_gid: meta.st_gid(), - st_size: meta.st_size(), +#[cfg(unix)] +macro_rules! os_unix_stat_inner { + ( $path:expr, $follow_symlinks:expr, $vm:expr ) => {{ + fn get_stats(path: &str, follow_symlinks: bool) -> io::Result { + let meta = match follow_symlinks { + true => fs::metadata(path)?, + false => fs::symlink_metadata(path)?, + }; + + Ok(StatResult { + st_mode: meta.st_mode(), + st_ino: meta.st_ino(), + st_dev: meta.st_dev(), + st_nlink: meta.st_nlink(), + st_uid: meta.st_uid(), + st_gid: meta.st_gid(), + st_size: meta.st_size(), + st_atime: to_seconds_from_unix_epoch(meta.accessed()?), + st_mtime: to_seconds_from_unix_epoch(meta.modified()?), + st_ctime: to_seconds_from_nanos(meta.st_ctime(), meta.st_ctime_nsec()), + }) } - .into_ref($vm) - .into_object()) + + get_stats(&$path.value, $follow_symlinks.follow_symlinks) + .map_err(|s| $vm.new_os_error(s.to_string())) }}; } #[cfg(target_os = "linux")] -fn os_stat(path: PyStringRef, follow_symlinks: FollowSymlinks, vm: &VirtualMachine) -> PyResult { +fn os_stat( + path: PyStringRef, + follow_symlinks: FollowSymlinks, + vm: &VirtualMachine, +) -> PyResult { use std::os::linux::fs::MetadataExt; - os_unix_stat_inner!(&path.value, follow_symlinks, vm) + os_unix_stat_inner!(path, follow_symlinks, vm) } #[cfg(target_os = "macos")] -fn os_stat(path: PyStringRef, follow_symlinks: FollowSymlinks, vm: &VirtualMachine) -> PyResult { +fn os_stat( + path: PyStringRef, + follow_symlinks: FollowSymlinks, + vm: &VirtualMachine, +) -> PyResult { use std::os::macos::fs::MetadataExt; - os_unix_stat_inner!(&path.value, follow_symlinks, vm) + os_unix_stat_inner!(path, follow_symlinks, vm) } #[cfg(target_os = "android")] -fn os_stat(path: PyStringRef, follow_symlinks: FollowSymlinks, vm: &VirtualMachine) -> PyResult { +fn os_stat( + path: PyStringRef, + follow_symlinks: FollowSymlinks, + vm: &VirtualMachine, +) -> PyResult { use std::os::android::fs::MetadataExt; - os_unix_stat_inner!(&path.value, follow_symlinks, vm) + os_unix_stat_inner!(path, follow_symlinks, vm) } // Copied from CPython fileutils.c @@ -420,24 +481,35 @@ fn attributes_to_mode(attr: u32) -> u32 { } #[cfg(windows)] -fn os_stat(path: PyStringRef, follow_symlinks: FollowSymlinks, vm: &VirtualMachine) -> PyResult { +fn os_stat( + path: PyStringRef, + follow_symlinks: FollowSymlinks, + vm: &VirtualMachine, +) -> PyResult { use std::os::windows::fs::MetadataExt; - let metadata = match follow_symlinks.follow_symlinks { - true => fs::metadata(&path.value), - false => fs::symlink_metadata(&path.value), - }; - let meta = metadata.map_err(|s| vm.new_os_error(s.to_string()))?; - Ok(StatResult { - st_mode: attributes_to_mode(meta.file_attributes()), - st_ino: 0, // TODO: Not implemented in std::os::windows::fs::MetadataExt. - st_dev: 0, // TODO: Not implemented in std::os::windows::fs::MetadataExt. - st_nlink: 0, // TODO: Not implemented in std::os::windows::fs::MetadataExt. - st_uid: 0, // 0 on windows - st_gid: 0, // 0 on windows - st_size: meta.file_size(), + + fn get_stats(path: &str, follow_symlinks: bool) -> io::Result { + let meta = match follow_symlinks { + true => fs::metadata(path)?, + false => fs::symlink_metadata(path)?, + }; + + Ok(StatResult { + st_mode: attributes_to_mode(meta.file_attributes()), + st_ino: 0, // TODO: Not implemented in std::os::windows::fs::MetadataExt. + st_dev: 0, // TODO: Not implemented in std::os::windows::fs::MetadataExt. + st_nlink: 0, // TODO: Not implemented in std::os::windows::fs::MetadataExt. + st_uid: 0, // 0 on windows + st_gid: 0, // 0 on windows + st_size: meta.file_size(), + st_atime: to_seconds_from_unix_epoch(meta.accessed()?), + st_mtime: to_seconds_from_unix_epoch(meta.modified()?), + st_ctime: to_seconds_from_unix_epoch(meta.created()?), + }) } - .into_ref(vm) - .into_object()) + + get_stats(&path.value, follow_symlinks.follow_symlinks) + .map_err(|s| vm.new_os_error(s.to_string())) } #[cfg(not(any( @@ -510,12 +582,16 @@ pub fn make_module(vm: &VirtualMachine) -> PyObjectRef { "st_uid" => ctx.new_property(StatResultRef::st_uid), "st_gid" => ctx.new_property(StatResultRef::st_gid), "st_size" => ctx.new_property(StatResultRef::st_size), + "st_atime" => ctx.new_property(StatResultRef::st_atime), + "st_ctime" => ctx.new_property(StatResultRef::st_ctime), + "st_mtime" => ctx.new_property(StatResultRef::st_mtime), }); py_module!(vm, "_os", { "open" => ctx.new_rustfunc(os_open), "close" => ctx.new_rustfunc(os_close), "error" => ctx.new_rustfunc(os_error), + "fsync" => ctx.new_rustfunc(os_fsync), "read" => ctx.new_rustfunc(os_read), "write" => ctx.new_rustfunc(os_write), "remove" => ctx.new_rustfunc(os_remove),