diff --git a/src/library/ps/DESCRIPTION b/src/library/ps/DESCRIPTION index d0be85932..4794469a0 100644 --- a/src/library/ps/DESCRIPTION +++ b/src/library/ps/DESCRIPTION @@ -1,6 +1,6 @@ Package: ps Title: List, Query, Manipulate System Processes -Version: 1.7.6 +Version: 1.8.1 Authors@R: c( person("Jay", "Loden", role = "aut"), person("Dave", "Daeschler", role = "aut"), @@ -16,14 +16,14 @@ BugReports: https://github.com/r-lib/ps/issues Depends: R (>= 3.4) Imports: utils Suggests: callr, covr, curl, pillar, pingr, processx (>= 3.1.0), R6, - rlang, testthat (>= 3.0.0), webfakes + rlang, testthat (>= 3.0.0), webfakes, withr Biarch: true Config/Needs/website: tidyverse/tidytemplate Config/testthat/edition: 3 Encoding: UTF-8 -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.2 NeedsCompilation: yes -Packaged: 2024-01-18 06:13:01 UTC; gaborcsardi +Packaged: 2024-10-28 21:43:41 UTC; gaborcsardi Author: Jay Loden [aut], Dave Daeschler [aut], Giampaolo Rodola' [aut], @@ -31,4 +31,4 @@ Author: Jay Loden [aut], Posit Software, PBC [cph, fnd] Maintainer: Gábor Csárdi Repository: CRAN -Date/Publication: 2024-01-18 06:40:02 UTC +Date/Publication: 2024-10-28 22:10:02 UTC diff --git a/src/library/ps/NAMESPACE b/src/library/ps/NAMESPACE index 61a047735..b80cb2b02 100644 --- a/src/library/ps/NAMESPACE +++ b/src/library/ps/NAMESPACE @@ -7,6 +7,7 @@ S3method(print,with_process_cleanup) export(CleanupReporter) export(errno) export(ps) +export(ps_apps) export(ps_boot_time) export(ps_children) export(ps_cmdline) @@ -16,12 +17,16 @@ export(ps_cpu_times) export(ps_create_time) export(ps_cwd) export(ps_descent) +export(ps_disk_io_counters) export(ps_disk_partitions) export(ps_disk_usage) export(ps_environ) export(ps_environ_raw) export(ps_exe) export(ps_find_tree) +export(ps_fs_info) +export(ps_fs_mount_point) +export(ps_fs_stat) export(ps_get_cpu_affinity) export(ps_get_nice) export(ps_gids) @@ -61,6 +66,7 @@ export(ps_tty_size) export(ps_uids) export(ps_username) export(ps_users) +export(ps_wait) export(ps_windows_nice_values) export(signals) export(with_process_cleanup) diff --git a/src/library/ps/NEWS.md b/src/library/ps/NEWS.md index 415644587..257f80f2b 100644 --- a/src/library/ps/NEWS.md +++ b/src/library/ps/NEWS.md @@ -1,3 +1,42 @@ +# ps 1.8.1 + +* ps can now be installed again on unsupported platforms. + +# ps 1.8.0 + +* New `ps_apps()` function to list all running applications on macOS. + +* New function `ps_disk_io_counters()` to query disk I/O counters + (#145, @michaelwalshe). + +* New `ps_fs_info()` to query information about the file system of one + or more files or directories. + +* New `ps_wait()` to start an interruptible wait on multiple processes, + with a timeout (#166). + +* `ps_handle()` now allows a numeric (double) scalar as the pid, as long + as its value is integer. + +* `ps_send_signal()`, `ps_suspend()`, `ps_resume()`, `ps_terminate()`, + `ps_kill()`, and `ps_interrupt()` can now operate on multiple processes, + if passed a list of process handles. + +* `ps_kill()` and `ps_kill_tree()` have a new `grace` argument. + On Unix, if this argument is not zero, then `ps_kill()` first sends a + `TERM` signal, and waits for the processes to quit gracefully, via + `ps_wait()`. The processes that are still alive after the grace period + are then killed with `SIGKILL`. + +* `ps_status()` (and thus `ps()`) is now better at getting the correct + status of processes on macOS. This usually requires calling the external + `ps` tool. See `?ps_status()` on how to opt out from the new + behavior (#31). + +# ps 1.7.7 + +* `ps_cpu_times()` values are now correct on newer arm64 macOS. + # ps 1.7.6 * `ps_name()` now does not fail in the rare case when `ps_cmdline()` returns an empty vector (#150). diff --git a/src/library/ps/R/cleancall.R b/src/library/ps/R/cleancall.R new file mode 100644 index 000000000..944cfd3c5 --- /dev/null +++ b/src/library/ps/R/cleancall.R @@ -0,0 +1,4 @@ + +call_with_cleanup <- function(ptr, ...) { + .Call(cleancall_call, pairlist(ptr, ...), parent.frame()) +} diff --git a/src/library/ps/R/disk.R b/src/library/ps/R/disk.R index e340cf9a4..b6bc362d9 100644 --- a/src/library/ps/R/disk.R +++ b/src/library/ps/R/disk.R @@ -120,3 +120,305 @@ ps__disk_usage_format_posix <- function(paths, l) { d } + +#' System-wide disk I/O counters +#' +#' Returns a data.frame of system-wide disk I/O counters. +#' +#' Includes the following non-NA fields for all supported platforms: +#' * `read_count`: number of reads +#' * `write_count`: number of writes +#' * `read_bytes`: number of bytes read +#' * `write_bytes`: number of bytes written +#' +#' And for only some platforms: +#' * `read_time`: time spent reading from disk (in milliseconds) +#' * `write_time`: time spent writing to disk (in milliseconds) +#' * `busy_time`: time spent doing actual I/Os (in milliseconds) +#' * `read_merged_count`: number of merged reads (see iostats doc) +#' * `write_merged_count`: number of merged writes (see iostats doc) +#' +#' @return A data frame of one row per disk of I/O stats, with columns +#' `name`, `read_count` `read_merged_count` `read_bytes`, `read_time`, +#' `write_count`, `write_merged_count`, `write_bytes` `write_time`, and +#' `busy_time`. +#' +#' @family disk functions +#' @export +#' @examplesIf ps::ps_is_supported() && ps:::ps_os_name() %in% c("LINUX", "WINDOWS") && !ps:::is_cran_check() +#' ps_disk_io_counters() +ps_disk_io_counters <- function() { + os <- ps_os_name() + tab <- if (os == "LINUX") { + ps__disk_io_counters_linux() + } else if (os == "WINDOWS") { + ps__disk_io_counters_windows() + } else { + ps__disk_io_counters_macos() + } + + class(tab) <- c("tbl", "data.frame") + tab +} + +ps__disk_io_counters_windows <- function() { + l <- .Call(ps__disk_io_counters) + disk_info <- data_frame( + name = l[[1]], + read_count = l[[2]], + read_merged_count = NA, + read_bytes = l[[3]], + read_time = l[[4]], + write_count = l[[5]], + write_merged_count = NA, + write_bytes = l[[6]], + write_time = l[[7]], + busy_time = NA + ) + + disk_info[disk_info$name != "",] +} + +ps__disk_io_counters_macos <- function() { + tab <- not_null(.Call(ps__disk_io_counters)) + data_frame( + name = names(tab), + read_count = map_dbl(tab, "[[", 1), + read_merged_count = NA, + read_bytes = map_dbl(tab, "[[", 3), + read_time = map_dbl(tab, "[[", 5), + write_count = map_dbl(tab, "[[", 2), + write_merged_count = NA, + write_bytes = map_dbl(tab, "[[", 4), + write_time = map_dbl(tab, "[[", 6), + busy_time = NA + ) +} + +#' File system information for files +#' +#' @param paths A path or a vector of paths. `ps_fs_info()` returns +#' information about the file systems of all paths. `path` may contain +#' direcories as well. +#' @return Data frame with file system information for each +#' path in `paths`, one row per path. Common columns for all +#' operating systems: +#' * `path`: The input paths, i.e. the `paths` argument. +#' * `mountpoint`: Directory where the file system is mounted. +#' On Linux there is a small chance that it was not possible to +#' look this up, and it is `NA_character_`. This is the drive letter +#' or the mount directory on Windows, with a trailing `\`. +#' * `name`: Device name. +#' On Linux there is a small chance that it was not possible to +#' look this up, and it is `NA_character_`. On Windows this is the +#' volume GUID path of the form `\\?\Volume{GUID}\`. +#' * `type`: File system type (character). +#' On Linux there is a tiny chance that it was not possible to +#' look this up, and it is `NA_character_`. +#' * `block_size`: File system block size. This is the sector size on +#' Windows, in bytes. +#' * `transfer_block_size`: Pptimal transfer block size. On Linux it is +#' currently always the same as `block_size`. This is the cluster size +#' on Windows, in bytes. +#' * `total_data_blocks`: Total data blocks in file system. On Windows +#' this is the number of sectors. +#' * `free_blocks`: Free blocks in file system. On Windows this is the +#' number of free sectors. +#' * `free_blocks_non_superuser`: Free blocks for a non-superuser, which +#' might be different on Unix. On Windows this is the number of free +#' sectors for the calling user. +#' * `id`: File system id. This is a raw vector. On Linux it is +#' often all zeros. It is always `NULL` on Windows. +#' * `owner`: User that mounted the file system. On Linux and Windows +#' this is currently always `NA_real_`. +#' * `type_code`: Type of file system, a numeric code. On Windows this +#' this is `NA_real_`. +#' * `subtype_code`: File system subtype (flavor). On Linux and Windows +#' this is always `NA_real_`. +#' +#' The rest of the columns are flags, and they are operating system +#' dependent. +#' +#' macOS: +#' +#' * `RDONLY`: A read-only filesystem. +#' * `SYNCHRONOUS`: File system is written to synchronously. +#' * `NOEXEC`: Can't exec from filesystem. +#' * `NOSUID`: Setuid bits are not honored on this filesystem. +#' * `NODEV`: Don't interpret special files. +#' * `UNION`: Union with underlying filesysten. +#' * `ASYNC`: File system written to asynchronously. +#' * `EXPORTED`: File system is exported. +#' * `LOCAL`: File system is stored locally. +#' * `QUOTA`: Quotas are enabled on this file system. +#' * `ROOTFS`: This file system is the root of the file system. +#' * `DOVOLFS`: File system supports volfs. +#' * `DONTBROWSE`: File system is not appropriate path to user data. +#' * `UNKNOWNPERMISSIONS`: VFS will ignore ownership information on +#' filesystem filesystemtem objects. +#' * `AUTOMOUNTED`: File system was mounted by automounter. +#' * `JOURNALED`: File system is journaled. +#' * `DEFWRITE`: File system should defer writes. +#' * `MULTILABEL`: MAC support for individual labels. +#' * `CPROTECT`: File system supports per-file encrypted data protection. +#' +#' Linux: +#' +#' * `MANDLOCK`: Mandatory locking is permitted on the filesystem +#' (see `fcntl(2)`). +#' * `NOATIME`: Do not update access times; see `mount(2)`. +#' * `NODEV`: Disallow access to device special files on this filesystem. +#' * `NODIRATIME`: Do not update directory access times; see mount(2). +#' * `NOEXEC`: Execution of programs is disallowed on this filesystem. +#' * `NOSUID`: The set-user-ID and set-group-ID bits are ignored by +#' `exec(3)` for executable files on this filesystem +#' * `RDONLY`: This filesystem is mounted read-only. +#' * `RELATIME`: Update atime relative to mtime/ctime; see `mount(2)`. +#' * `SYNCHRONOUS`: Writes are synched to the filesystem immediately +#' (see the description of `O_SYNC` in `open(2)``). +#' * `NOSYMFOLLOW`: Symbolic links are not followed when resolving paths; +#' see `mount(2)``. +#' +#' Windows: +#' +#' * `CASE_SENSITIVE_SEARCH`: Supports case-sensitive file names. +#' * `CASE_PRESERVED_NAMES`: Supports preserved case of file names when +#' it places a name on disk. +#' * `UNICODE_ON_DISK`: Supports Unicode in file names as they appear on +#' disk. +#' * `PERSISTENT_ACLS`: Preserves and enforces access control lists +#' (ACL). For example, the NTFS file system preserves and enforces +#' ACLs, and the FAT file system does not. +#' * `FILE_COMPRESSION`: Supports file-based compression. +#' * `VOLUME_QUOTAS`: Supports disk quotas. +#' * `SUPPORTS_SPARSE_FILES`: Supports sparse files. +#' * `SUPPORTS_REPARSE_POINTS`: Supports reparse points. +#' * `SUPPORTS_REMOTE_STORAGE`: Supports remote storage. +#' * `RETURNS_CLEANUP_RESULT_INFO`: On a successful cleanup operation, +#' the file system returns information that describes additional +#' actions taken during cleanup, such as deleting the file. File +#' system filters can examine this information in their post-cleanup +#' callback. +#' * `SUPPORTS_POSIX_UNLINK_RENAME`: Supports POSIX-style delete and +#' rename operations. +#' * `VOLUME_IS_COMPRESSED`: It is a compressed volume, for example, a +#' DoubleSpace volume. +#' * `SUPPORTS_OBJECT_IDS`: Supports object identifiers. +#' * `SUPPORTS_ENCRYPTION`: Supports the Encrypted File System (EFS). +#' * `NAMED_STREAMS`: Supports named streams. +#' * `READ_ONLY_VOLUME`: It is read-only. +#' * `SEQUENTIAL_WRITE_ONCE`: Supports a single sequential write. +#' * `SUPPORTS_TRANSACTIONS`: Supports transactions. +#' * `SUPPORTS_HARD_LINKS`: The volume supports hard links. +#' * `SUPPORTS_EXTENDED_ATTRIBUTES`: Supports extended attributes. +#' * `SUPPORTS_OPEN_BY_FILE_ID`: Supports open by FileID. +#' * `SUPPORTS_USN_JOURNAL`: Supports update sequence number (USN) +#' journals. +#' * `SUPPORTS_INTEGRITY_STREAMS`: Supports integrity streams. +#' * `SUPPORTS_BLOCK_REFCOUNTING`: The volume supports sharing logical +#' clusters between files on the same volume. +#' * `SUPPORTS_SPARSE_VDL`: The file system tracks whether each cluster +#' of a file contains valid data (either from explicit file writes or +#' automatic zeros) or invalid data (has not yet been written to or +#' zeroed). +#' * `DAX_VOLUME`: The volume is a direct access (DAX) volume. +#' * `SUPPORTS_GHOSTING`: Supports ghosting. +#' +#' @export +#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check() +#' ps_fs_info(c("/", "~", ".")) + +ps_fs_info <- function(paths = "/") { + assert_character(paths) + abspaths <- normalizePath(paths, mustWork = TRUE) + mps <- ps_fs_mount_point(paths) + res <- .Call(ps__fs_info, paths, abspaths, mps) + df <- as_data_frame(res) + + # this should not happen in practice, but just in case + if (ps_os_type()[["LINUX"]] && any(is.na(df$type))) { + miss <- which(is.na(df$type)) + df$type[miss] <- linux_fs_types$name[match(df$type_code[miss], linux_fs_types$id)] + } + + df +} + +linux_fs_types <- utils::read.table( + "tools/linux-fs-types.txt", + header = TRUE, + stringsAsFactors = FALSE +) + +posix_stat_types <- c( + "regular file", + "directory", + "character device", + "block device", + "FIFO", + "symbolic link", + "socket" +) + +#' File status +#' +#' This function is currently not implemented on Windows. +#' +#' @param paths Paths to files, directories, devices, etc. They must +#' exist. They are expanded using [base::path.expand()]. +#' @param follow Whether to follow symbolic links. If `FALSE` it returns +#' information on the links themselves. +#' @return Data frame with one row for each path in `paths`. Columns: +#' * `path`: Expanded `paths`. +#' * `dev_major`: Major device ID of the device the path resides on. +#' * `dev_minor`: Minor device ID of the device the path resodes on. +#' * `inode`: Inode number. +#' * `mode`: File type and mode (permissions). It is easier to use the +#' `type` and `permissions` columns. +#' * `type`: File type, character. One of +#' `r paste(posix_stat_types, collapse = ", ")`. +#' * `permissions`: Permissions, numeric code in an integer column. +#' * `nlink`: Number of hard links. +#' * `uid`: User id of owner. +#' * `gid`: Group id of owner. +#' * `rdev_major`: If the path is a device, its major device id, +#' otherwise `NA_integer_`. +#' * `rdev_minor`: IF the path is a device, its minor device id, +#' otherwise `NA_integer_`. +#' * `size`: File size in bytes. +#' * `block_size`: Block size for filesystem I/O. +#' * `blocks`: Number of 512B blocks allocated. +#' * `access_time`: Time of last access. +#' * `modification_time`: Time of last modification. +#' * `change_time`: Time of last status change. +#' +#' @export +#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check() && ps_os_type()[["POSIX"]] +#' ps_fs_stat(c(".", tempdir())) + +ps_fs_stat <- function(paths, follow = TRUE) { + assert_character(paths) + paths <- path.expand(paths) + res <- .Call(ps__stat, paths, follow) + res[["type"]] <- posix_stat_types[res[["type"]]] + res[["access_time"]] <- .POSIXct(res[["access_time"]], "UTC") + res[["modification_time"]] <- .POSIXct(res[["modification_time"]], "UTC") + res[["change_time"]] <- .POSIXct(res[["change_time"]], "UTC") + as_data_frame(res) +} + +#' Find the mount point of a file or directory +#' +#' @param paths Paths to files, directories, devices, etc. They must +#' exist. They are normalized using [base::normalizePath()]. +#' @return Character vector, paths to the mount points of the input +#' `paths`. +#' @export +#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check() +#' ps_fs_mount_point(".") + +ps_fs_mount_point <- function(paths) { + assert_character(paths) + paths <- normalizePath(paths, mustWork = TRUE) + call_with_cleanup(ps__mount_point, paths) +} diff --git a/src/library/ps/R/iso-date.R b/src/library/ps/R/iso-date.R new file mode 100644 index 000000000..687af22a3 --- /dev/null +++ b/src/library/ps/R/iso-date.R @@ -0,0 +1,144 @@ + +milliseconds <- function(x) as.difftime(as.numeric(x) / 1000, units = "secs") +seconds <- function(x) as.difftime(as.numeric(x), units = "secs") +minutes <- function(x) as.difftime(as.numeric(x), units = "mins") +hours <- function(x) as.difftime(as.numeric(x), units = "hours") +days <- function(x) as.difftime(as.numeric(x), units = "days") +weeks <- function(x) as.difftime(as.numeric(x), units = "weeks") +wday <- function(x) as.POSIXlt(x, tz = "UTC")$wday + 1 +with_tz <- function(x, tzone = "") as.POSIXct(as.POSIXlt(x, tz = tzone)) +ymd <- function(x) as.POSIXct(x, format = "%Y %m %d", tz = "UTC") +yj <- function(x) as.POSIXct(x, format = "%Y %j", tz = "UTC") + +parse_iso_8601 <- function(dates, default_tz = "UTC") { + if (default_tz == "") default_tz <- Sys.timezone() + dates <- as.character(dates) + match <- re_match(dates, iso_regex) + matching <- !is.na(match$.match) + result <- rep(.POSIXct(NA_real_, tz = ""), length.out = length(dates)) + result[matching] <- parse_iso_parts(match[matching, ], default_tz) + class(result) <- c("POSIXct", "POSIXt") + with_tz(result, "UTC") +} + +parse_iso_parts <- function(mm, default_tz) { + + num <- nrow(mm) + + ## ----------------------------------------------------------------- + ## Date first + + date <- .POSIXct(rep(NA_real_, num), tz = "") + + ## Years-days + fyd <- is.na(date) & mm$yearday != "" + date[fyd] <- yj(paste(mm$year[fyd], mm$yearday[fyd])) + + ## Years-weeks-days + fywd <- is.na(date) & mm$week != "" & mm$weekday != "" + date[fywd] <- iso_week(mm$year[fywd], mm$week[fywd], mm$weekday[fywd]) + + ## Years-weeks + fyw <- is.na(date) & mm$week != "" + date[fyw] <- iso_week(mm$year[fyw], mm$week[fyw], "1") + + ## Years-months-days + fymd <- is.na(date) & mm$month != "" & mm$day != "" + date[fymd] <- ymd(paste(mm$year[fymd], mm$month[fymd], mm$day[fymd])) + + ## Years-months + fym <- is.na(date) & mm$month != "" + date[fym] <- ymd(paste(mm$year[fym], mm$month[fym], "01")) + + ## Years + fy <- is.na(date) + date[fy] <- ymd(paste(mm$year, "01", "01")) + + ## ----------------------------------------------------------------- + ## Now the time + + th <- mm$hour != "" + date[th] <- date[th] + hours(mm$hour[th]) + + tm <- mm$min != "" + date[tm] <- date[tm] + minutes(mm$min[tm]) + + ts <- mm$sec != "" + date[ts] <- date[ts] + seconds(mm$sec[ts]) + + ## ----------------------------------------------------------------- + ## Fractional time + + frac <- as.numeric(sub(",", ".", mm$frac)) + + tfs <- !is.na(frac) & mm$sec != "" + date[tfs] <- date[tfs] + milliseconds(round(frac[tfs] * 1000)) + + tfm <- !is.na(frac) & mm$sec == "" & mm$min != "" + sec <- trunc(frac[tfm] * 60) + mil <- round((frac[tfm] * 60 - sec) * 1000) + date[tfm] <- date[tfm] + seconds(sec) + milliseconds(mil) + + tfh <- !is.na(frac) & mm$sec == "" & mm$min == "" + min <- trunc(frac[tfh] * 60) + sec <- trunc((frac[tfh] * 60 - min) * 60) + mil <- round((((frac[tfh] * 60) - min) * 60 - sec) * 1000) + date[tfh] <- date[tfh] + minutes(min) + seconds(sec) + milliseconds(mil) + + ## ----------------------------------------------------------------- + ## Time zone + + ftzpm <- mm$tzpm != "" + m <- ifelse(mm$tzpm[ftzpm] == "+", -1, 1) + ftzpmh <- ftzpm & mm$tzhour != "" + date[ftzpmh] <- date[ftzpmh] + m * hours(mm$tzhour[ftzpmh]) + ftzpmm <- ftzpm & mm$tzmin != "" + date[ftzpmm] <- date[ftzpmm] + m * minutes(mm$tzmin[ftzpmm]) + + ftzz <- mm$tz == "Z" + date[ftzz] <- as.POSIXct(date[ftzz], "UTC") + + ftz <- mm$tz != "Z" & mm$tz != "" + date[ftz] <- as.POSIXct(date[ftz], mm$tz[ftz]) + + if (default_tz != "UTC") { + ftna <- mm$tzpm == "" & mm$tz == "" + if (any(ftna)) { + dd <- as.POSIXct(format_iso_8601(date[ftna]), + "%Y-%m-%dT%H:%M:%S+00:00", tz = default_tz) + date[ftna] <- dd + } + } + + as.POSIXct(date, "UTC") +} + +iso_regex <- paste0( + "^\\s*", + "(?[\\+-]?\\d{4}(?!\\d{2}\\b))", + "(?:(?-?)", + "(?:(?0[1-9]|1[0-2])", + "(?:\\g{dash}(?[12]\\d|0[1-9]|3[01]))?", + "|W(?[0-4]\\d|5[0-3])(?:-?(?[1-7]))?", + "|(?00[1-9]|0[1-9]\\d|[12]\\d{2}|3", + "(?:[0-5]\\d|6[1-6])))", + "(?