mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-29 03:57:44 +00:00
Merge branch 'main' into zizmor-fix-02
This commit is contained in:
commit
183063fa14
6 changed files with 384 additions and 5 deletions
2
.github/workflows/android.yml
vendored
2
.github/workflows/android.yml
vendored
|
@ -178,7 +178,7 @@ jobs:
|
||||||
util/android-commands.sh sync_host
|
util/android-commands.sh sync_host
|
||||||
util/android-commands.sh build
|
util/android-commands.sh build
|
||||||
util/android-commands.sh tests
|
util/android-commands.sh tests
|
||||||
if [[ "${{ steps.rust-cache.outputs.cache-hit }}" != 'true' ]]; then util/android-commands.sh sync_image; fi; exit 0
|
if [ "${{ steps.rust-cache.outputs.cache-hit }}" != 'true' ]; then util/android-commands.sh sync_image; fi; exit 0
|
||||||
- name: Collect information about runner ressources
|
- name: Collect information about runner ressources
|
||||||
if: always()
|
if: always()
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
|
@ -87,6 +87,7 @@ lines = []
|
||||||
format = ["itertools", "quoting-style"]
|
format = ["itertools", "quoting-style"]
|
||||||
mode = ["libc"]
|
mode = ["libc"]
|
||||||
perms = ["libc", "walkdir"]
|
perms = ["libc", "walkdir"]
|
||||||
|
buf-copy = []
|
||||||
pipes = []
|
pipes = []
|
||||||
process = ["libc"]
|
process = ["libc"]
|
||||||
proc-info = ["tty", "walkdir"]
|
proc-info = ["tty", "walkdir"]
|
||||||
|
|
|
@ -39,11 +39,13 @@ pub mod version_cmp;
|
||||||
pub mod mode;
|
pub mod mode;
|
||||||
|
|
||||||
// ** unix-only
|
// ** unix-only
|
||||||
|
#[cfg(all(any(target_os = "linux", target_os = "android"), feature = "buf-copy"))]
|
||||||
|
pub mod buf_copy;
|
||||||
#[cfg(all(unix, feature = "entries"))]
|
#[cfg(all(unix, feature = "entries"))]
|
||||||
pub mod entries;
|
pub mod entries;
|
||||||
#[cfg(all(unix, feature = "perms"))]
|
#[cfg(all(unix, feature = "perms"))]
|
||||||
pub mod perms;
|
pub mod perms;
|
||||||
#[cfg(all(unix, feature = "pipes"))]
|
#[cfg(all(unix, any(feature = "pipes", feature = "buf-copy")))]
|
||||||
pub mod pipes;
|
pub mod pipes;
|
||||||
#[cfg(all(target_os = "linux", feature = "proc-info"))]
|
#[cfg(all(target_os = "linux", feature = "proc-info"))]
|
||||||
pub mod proc_info;
|
pub mod proc_info;
|
||||||
|
|
373
src/uucore/src/lib/features/buf_copy.rs
Normal file
373
src/uucore/src/lib/features/buf_copy.rs
Normal file
|
@ -0,0 +1,373 @@
|
||||||
|
// This file is part of the uutils coreutils package.
|
||||||
|
//
|
||||||
|
// For the full copyright and license information, please view the LICENSE
|
||||||
|
// file that was distributed with this source code.
|
||||||
|
|
||||||
|
//! This module provides several buffer-based copy/write functions that leverage
|
||||||
|
//! the `splice` system call in Linux systems, thus increasing the I/O
|
||||||
|
//! performance of copying between two file descriptors. This module is mostly
|
||||||
|
//! used by utilities to work around the limitations of Rust's `fs::copy` which
|
||||||
|
//! does not handle copying special files (e.g pipes, character/block devices).
|
||||||
|
|
||||||
|
use crate::error::{UError, UResult};
|
||||||
|
use nix::unistd;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::{
|
||||||
|
io::{self, Read, Write},
|
||||||
|
os::{
|
||||||
|
fd::AsFd,
|
||||||
|
unix::io::{AsRawFd, RawFd},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use nix::{errno::Errno, libc::S_IFIFO, sys::stat::fstat};
|
||||||
|
|
||||||
|
use super::pipes::{pipe, splice, splice_exact, vmsplice};
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
/// Error types used by buffer-copying functions from the `buf_copy` module.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Io(io::Error),
|
||||||
|
WriteError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::WriteError(msg) => write!(f, "splice() write error: {}", msg),
|
||||||
|
Error::Io(err) => write!(f, "I/O error: {}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
|
impl UError for Error {
|
||||||
|
fn code(&self) -> i32 {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to determine whether a given handle (such as a file) is a pipe or not.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `out` - path of handle
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A `bool` indicating whether the given handle is a pipe or not.
|
||||||
|
#[inline]
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn is_pipe<P>(path: &P) -> Result<bool>
|
||||||
|
where
|
||||||
|
P: AsRawFd,
|
||||||
|
{
|
||||||
|
Ok(fstat(path.as_raw_fd())?.st_mode as nix::libc::mode_t & S_IFIFO != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SPLICE_SIZE: usize = 1024 * 128;
|
||||||
|
const BUF_SIZE: usize = 1024 * 16;
|
||||||
|
|
||||||
|
/// Copy data from `Read` implementor `source` into a `Write` implementor
|
||||||
|
/// `dest`. This works by reading a chunk of data from `source` and writing the
|
||||||
|
/// data to `dest` in a loop.
|
||||||
|
///
|
||||||
|
/// This function uses the Linux-specific `splice` call when possible which does
|
||||||
|
/// not use any intermediate user-space buffer. It falls backs to
|
||||||
|
/// `std::io::copy` under other platforms or when the call fails and is still
|
||||||
|
/// recoverable.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `source` - `Read` implementor to copy data from.
|
||||||
|
/// * `dest` - `Write` implementor to copy data to.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Result of operation and bytes successfully written (as a `u64`) when
|
||||||
|
/// operation is successful.
|
||||||
|
pub fn copy_stream<R, S>(src: &mut R, dest: &mut S) -> UResult<u64>
|
||||||
|
where
|
||||||
|
R: Read + AsFd + AsRawFd,
|
||||||
|
S: Write + AsFd + AsRawFd,
|
||||||
|
{
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||||
|
{
|
||||||
|
// If we're on Linux or Android, try to use the splice() system call
|
||||||
|
// for faster writing. If it works, we're done.
|
||||||
|
let result = splice_write(src, &dest.as_fd())?;
|
||||||
|
if !result.1 {
|
||||||
|
return Ok(result.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we're not on Linux or Android, or the splice() call failed,
|
||||||
|
// fall back on slower writing.
|
||||||
|
let result = std::io::copy(src, dest)?;
|
||||||
|
|
||||||
|
// If the splice() call failed and there has been some data written to
|
||||||
|
// stdout via while loop above AND there will be second splice() call
|
||||||
|
// that will succeed, data pushed through splice will be output before
|
||||||
|
// the data buffered in stdout.lock. Therefore additional explicit flush
|
||||||
|
// is required here.
|
||||||
|
dest.flush()?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write from source `handle` into destination `write_fd` using Linux-specific
|
||||||
|
/// `splice` system call.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - `source` - source handle
|
||||||
|
/// - `dest` - destination handle
|
||||||
|
#[inline]
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||||
|
fn splice_write<R, S>(source: &R, dest: &S) -> UResult<(u64, bool)>
|
||||||
|
where
|
||||||
|
R: Read + AsFd + AsRawFd,
|
||||||
|
S: AsRawFd + AsFd,
|
||||||
|
{
|
||||||
|
let (pipe_rd, pipe_wr) = pipe()?;
|
||||||
|
let mut bytes: u64 = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match splice(&source, &pipe_wr, SPLICE_SIZE) {
|
||||||
|
Ok(n) => {
|
||||||
|
if n == 0 {
|
||||||
|
return Ok((bytes, false));
|
||||||
|
}
|
||||||
|
if splice_exact(&pipe_rd, dest, n).is_err() {
|
||||||
|
// If the first splice manages to copy to the intermediate
|
||||||
|
// pipe, but the second splice to stdout fails for some reason
|
||||||
|
// we can recover by copying the data that we have from the
|
||||||
|
// intermediate pipe to stdout using normal read/write. Then
|
||||||
|
// we tell the caller to fall back.
|
||||||
|
copy_exact(pipe_rd.as_raw_fd(), dest, n)?;
|
||||||
|
return Ok((bytes, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes += n as u64;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
return Ok((bytes, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move exactly `num_bytes` bytes from `read_fd` to `write_fd` using the `read`
|
||||||
|
/// and `write` calls.
|
||||||
|
fn copy_exact(read_fd: RawFd, write_fd: &impl AsFd, num_bytes: usize) -> std::io::Result<usize> {
|
||||||
|
let mut left = num_bytes;
|
||||||
|
let mut buf = [0; BUF_SIZE];
|
||||||
|
let mut written = 0;
|
||||||
|
while left > 0 {
|
||||||
|
let read = unistd::read(read_fd, &mut buf)?;
|
||||||
|
assert_ne!(read, 0, "unexpected end of pipe");
|
||||||
|
while written < read {
|
||||||
|
let n = unistd::write(write_fd, &buf[written..read])?;
|
||||||
|
written += n;
|
||||||
|
}
|
||||||
|
left -= read;
|
||||||
|
}
|
||||||
|
Ok(written)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write input `bytes` to a file descriptor. This uses the Linux-specific
|
||||||
|
/// `vmsplice()` call to write into a file descriptor directly, which only works
|
||||||
|
/// if the destination is a pipe.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `bytes` - data to be written
|
||||||
|
/// * `dest` - destination handle
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// When write succeeds, the amount of bytes written is returned as a
|
||||||
|
/// `u64`. The `bool` indicates if we need to fall back to normal copying or
|
||||||
|
/// not. `true` means we need to fall back, `false` means we don't have to.
|
||||||
|
///
|
||||||
|
/// A `UError` error is returned when the operation is not supported or when an
|
||||||
|
/// I/O error occurs.
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||||
|
pub fn splice_data_to_pipe<T>(bytes: &[u8], dest: &T) -> UResult<(u64, bool)>
|
||||||
|
where
|
||||||
|
T: AsRawFd + AsFd,
|
||||||
|
{
|
||||||
|
let mut n_bytes: u64 = 0;
|
||||||
|
let mut bytes = bytes;
|
||||||
|
while !bytes.is_empty() {
|
||||||
|
let len = match vmsplice(dest, bytes) {
|
||||||
|
Ok(n) => n,
|
||||||
|
// The maybe_unsupported call below may emit an error, when the
|
||||||
|
// error is considered as unrecoverable error (ones that won't make
|
||||||
|
// us fall back to other method)
|
||||||
|
Err(e) => return Ok(maybe_unsupported(e)?),
|
||||||
|
};
|
||||||
|
bytes = &bytes[len..];
|
||||||
|
n_bytes += len as u64;
|
||||||
|
}
|
||||||
|
Ok((n_bytes, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write input `bytes` to a handle using a temporary pipe. A `vmsplice()` call
|
||||||
|
/// is issued to write to the temporary pipe, which then gets written to the
|
||||||
|
/// final destination using `splice()`.
|
||||||
|
///
|
||||||
|
/// # Arguments * `bytes` - data to be written * `dest` - destination handle
|
||||||
|
///
|
||||||
|
/// # Returns When write succeeds, the amount of bytes written is returned as a
|
||||||
|
/// `u64`. The `bool` indicates if we need to fall back to normal copying or
|
||||||
|
/// not. `true` means we need to fall back, `false` means we don't have to.
|
||||||
|
///
|
||||||
|
/// A `UError` error is returned when the operation is not supported or when an
|
||||||
|
/// I/O error occurs.
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||||
|
pub fn splice_data_to_fd<T: AsFd>(
|
||||||
|
bytes: &[u8],
|
||||||
|
read_pipe: &File,
|
||||||
|
write_pipe: &File,
|
||||||
|
dest: &T,
|
||||||
|
) -> UResult<(u64, bool)> {
|
||||||
|
loop {
|
||||||
|
let mut bytes = bytes;
|
||||||
|
while !bytes.is_empty() {
|
||||||
|
let len = match vmsplice(&write_pipe, bytes) {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(e) => return Ok(maybe_unsupported(e)?),
|
||||||
|
};
|
||||||
|
if let Err(e) = splice_exact(&read_pipe, dest, len) {
|
||||||
|
return Ok(maybe_unsupported(e)?);
|
||||||
|
}
|
||||||
|
bytes = &bytes[len..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Conversion from a `nix::Error` into our `Error` which implements `UError`.
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||||
|
impl From<nix::Error> for Error {
|
||||||
|
fn from(error: nix::Error) -> Self {
|
||||||
|
Self::Io(io::Error::from_raw_os_error(error as i32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Several error values from `nix::Error` (`EINVAL`, `ENOSYS`, and `EBADF`) get
|
||||||
|
/// treated as errors indicating that the `splice` call is not available, i.e we
|
||||||
|
/// can still recover from the error. Thus, return the final result of the call
|
||||||
|
/// as `Result` and indicate that we have to fall back using other write method.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `error` - the `nix::Error` received
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Result with tuple containing a `u64` `0` indicating that no data had been
|
||||||
|
/// written and a `true` indicating we have to fall back, if error is still
|
||||||
|
/// recoverable. Returns an `Error` implementing `UError` otherwise.
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||||
|
fn maybe_unsupported(error: nix::Error) -> Result<(u64, bool)> {
|
||||||
|
match error {
|
||||||
|
Errno::EINVAL | Errno::ENOSYS | Errno::EBADF => Ok((0, true)),
|
||||||
|
_ => Err(error.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::pipes;
|
||||||
|
|
||||||
|
fn new_temp_file() -> File {
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
File::create(temp_dir.path().join("file.txt")).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_file_is_pipe() {
|
||||||
|
let temp_file = new_temp_file();
|
||||||
|
let (pipe_read, pipe_write) = pipes::pipe().unwrap();
|
||||||
|
|
||||||
|
assert!(is_pipe(&pipe_read).unwrap());
|
||||||
|
assert!(is_pipe(&pipe_write).unwrap());
|
||||||
|
assert!(!is_pipe(&temp_file).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_splice_errs() {
|
||||||
|
let err = nix::Error::from(Errno::EINVAL);
|
||||||
|
assert_eq!(maybe_unsupported(err).unwrap(), (0, true));
|
||||||
|
|
||||||
|
let err = nix::Error::from(Errno::ENOSYS);
|
||||||
|
assert_eq!(maybe_unsupported(err).unwrap(), (0, true));
|
||||||
|
|
||||||
|
let err = nix::Error::from(Errno::EBADF);
|
||||||
|
assert_eq!(maybe_unsupported(err).unwrap(), (0, true));
|
||||||
|
|
||||||
|
let err = nix::Error::from(Errno::EPERM);
|
||||||
|
assert!(maybe_unsupported(err).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_splice_data_to_pipe() {
|
||||||
|
let (pipe_read, pipe_write) = pipes::pipe().unwrap();
|
||||||
|
let data = b"Hello, world!";
|
||||||
|
let (bytes, _) = splice_data_to_pipe(data, &pipe_write).unwrap();
|
||||||
|
let mut buf = [0; 1024];
|
||||||
|
let n = unistd::read(pipe_read.as_raw_fd(), &mut buf).unwrap();
|
||||||
|
assert_eq!(&buf[..n], data);
|
||||||
|
assert_eq!(bytes as usize, data.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_splice_data_to_file() {
|
||||||
|
let mut temp_file = new_temp_file();
|
||||||
|
let (pipe_read, pipe_write) = pipes::pipe().unwrap();
|
||||||
|
let data = b"Hello, world!";
|
||||||
|
let (bytes, _) = splice_data_to_fd(data, &pipe_read, &pipe_write, &temp_file).unwrap();
|
||||||
|
let mut buf = [0; 1024];
|
||||||
|
let n = temp_file.read(&mut buf).unwrap();
|
||||||
|
assert_eq!(&buf[..n], data);
|
||||||
|
assert_eq!(bytes as usize, data.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_exact() {
|
||||||
|
let (mut pipe_read, mut pipe_write) = pipes::pipe().unwrap();
|
||||||
|
let data = b"Hello, world!";
|
||||||
|
let n = pipe_write.write(data).unwrap();
|
||||||
|
assert_eq!(n, data.len());
|
||||||
|
let mut buf = [0; 1024];
|
||||||
|
let n = copy_exact(pipe_read.as_raw_fd(), &pipe_write, data.len()).unwrap();
|
||||||
|
let n2 = pipe_read.read(&mut buf).unwrap();
|
||||||
|
assert_eq!(n, n2);
|
||||||
|
assert_eq!(&buf[..n], data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_stream() {
|
||||||
|
let (mut pipe_read, mut pipe_write) = pipes::pipe().unwrap();
|
||||||
|
let data = b"Hello, world!";
|
||||||
|
let n = pipe_write.write(data).unwrap();
|
||||||
|
assert_eq!(n, data.len());
|
||||||
|
let mut buf = [0; 1024];
|
||||||
|
let n = copy_stream(&mut pipe_read, &mut pipe_write).unwrap();
|
||||||
|
let n2 = pipe_read.read(&mut buf).unwrap();
|
||||||
|
assert_eq!(n as usize, n2);
|
||||||
|
assert_eq!(&buf[..n as usize], data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_splice_write() {
|
||||||
|
let (mut pipe_read, pipe_write) = pipes::pipe().unwrap();
|
||||||
|
let data = b"Hello, world!";
|
||||||
|
let (bytes, _) = splice_write(&pipe_read, &pipe_write).unwrap();
|
||||||
|
let mut buf = [0; 1024];
|
||||||
|
let n = pipe_read.read(&mut buf).unwrap();
|
||||||
|
assert_eq!(&buf[..n], data);
|
||||||
|
assert_eq!(bytes as usize, data.len());
|
||||||
|
}
|
||||||
|
}
|
|
@ -83,13 +83,14 @@ pub fn get_groups() -> IOResult<Vec<gid_t>> {
|
||||||
if res == -1 {
|
if res == -1 {
|
||||||
let err = IOError::last_os_error();
|
let err = IOError::last_os_error();
|
||||||
if err.raw_os_error() == Some(libc::EINVAL) {
|
if err.raw_os_error() == Some(libc::EINVAL) {
|
||||||
// Number of groups changed, retry
|
// Number of groups has increased, retry
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
groups.truncate(ngroups.try_into().unwrap());
|
// Number of groups may have decreased
|
||||||
|
groups.truncate(res.try_into().unwrap());
|
||||||
return Ok(groups);
|
return Ok(groups);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,11 +70,13 @@ pub use crate::features::version_cmp;
|
||||||
#[cfg(all(not(windows), feature = "mode"))]
|
#[cfg(all(not(windows), feature = "mode"))]
|
||||||
pub use crate::features::mode;
|
pub use crate::features::mode;
|
||||||
// ** unix-only
|
// ** unix-only
|
||||||
|
#[cfg(all(any(target_os = "linux", target_os = "android"), feature = "buf-copy"))]
|
||||||
|
pub use crate::features::buf_copy;
|
||||||
#[cfg(all(unix, feature = "entries"))]
|
#[cfg(all(unix, feature = "entries"))]
|
||||||
pub use crate::features::entries;
|
pub use crate::features::entries;
|
||||||
#[cfg(all(unix, feature = "perms"))]
|
#[cfg(all(unix, feature = "perms"))]
|
||||||
pub use crate::features::perms;
|
pub use crate::features::perms;
|
||||||
#[cfg(all(unix, feature = "pipes"))]
|
#[cfg(all(unix, any(feature = "pipes", feature = "buf-copy")))]
|
||||||
pub use crate::features::pipes;
|
pub use crate::features::pipes;
|
||||||
#[cfg(all(unix, feature = "process"))]
|
#[cfg(all(unix, feature = "process"))]
|
||||||
pub use crate::features::process;
|
pub use crate::features::process;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue