Skip to content
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

Times built-in #351

Merged
merged 5 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 8 additions & 0 deletions yash-builtin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub mod set;
pub mod shift;
#[cfg(feature = "yash-semantics")]
pub mod source;
pub mod times;
pub mod trap;
#[cfg(feature = "yash-semantics")]
pub mod r#type;
Expand Down Expand Up @@ -265,6 +266,13 @@ pub const BUILTINS: &[(&str, Builtin)] = &[
execute: |env, args| Box::pin(source::main(env, args)),
},
),
(
"times",
Builtin {
r#type: Special,
execute: |env, args| Box::pin(times::main(env, args)),
},
),
(
"trap",
Builtin {
Expand Down
95 changes: 95 additions & 0 deletions yash-builtin/src/times.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// This file is part of yash, an extended POSIX shell.
// Copyright (C) 2024 WATANABE Yuki
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! Times built-in
//!
//! The **`times`** built-in is used to display the accumulated user and system
//! times for the shell and its children.
//!
//! # Synopsis
//!
//! ```sh
//! times
//! ```
//!
//! # Description
//!
//! The built-in prints the accumulated user and system times for the shell and
//! its children.
//!
//! # Options
//!
//! None.
//!
//! # Operands
//!
//! None.
//!
//! # Standard output
//!
//! Two lines are printed to the standard output, each in the following format:
//!
//! ```text
//! 1m2.345678s 3m4.567890s
//! ```
//!
//! The first field of each line is the user time, and the second field is the
//! system time.
//! The first line shows the times consumed by the shell itself, and the
//! second line shows the times consumed by its children.
//!
//! # Errors
//!
//! It is an error if the times cannot be obtained or the standard output is not
//! writable.
//!
//! # Exit status
//!
//! Zero unless an error occurred.
//!
//! # Portability
//!
//! The `times` built-in is defined in POSIX.
//!
//! POSIX requires each field to be printed with six digits after the decimal
//! point, but many implementations print less. Note that the number of digits
//! does not necessarily indicate the precision of the times.

use crate::common::output;
use crate::common::report_error;
use crate::common::report_simple_failure;
use yash_env::semantics::Field;
use yash_env::Env;
use yash_env::System;

mod format;
mod syntax;

/// Entry point of the `times` built-in
pub async fn main(env: &mut Env, args: Vec<Field>) -> crate::Result {
match syntax::parse(env, args) {
Ok(()) => match env.system.times() {
Ok(times) => {
let result = format::format(&times);
output(env, &result).await
}
Err(error) => {
report_simple_failure(env, &format!("cannot obtain times: {error}")).await
}
},
Err(error) => report_error(env, &error).await,
}
}
109 changes: 109 additions & 0 deletions yash-builtin/src/times/format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// This file is part of yash, an extended POSIX shell.
// Copyright (C) 2024 WATANABE Yuki
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! Formatting the result of the times built-in

use yash_env::system::Times;

/// Formats a single time.
///
/// This function panics if the `ticks_per_second` is zero.
fn format_one_time<W>(ticks: u64, ticks_per_second: u64, result: &mut W) -> std::fmt::Result
where
W: std::fmt::Write,
{
let seconds = ticks / ticks_per_second;
let minutes = seconds / 60;
let sub_minute_ticks = ticks - minutes * 60 * ticks_per_second;
let seconds = sub_minute_ticks as f64 / ticks_per_second as f64;
write!(result, "{minutes}m{seconds:.6}s")
}
Copy link

Choose a reason for hiding this comment

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

The format_one_time function correctly formats time values but panics if ticks_per_second is zero. Consider handling this case more gracefully, perhaps by returning an error, to improve robustness.


/// Formats the result of the times built-in.
///
/// This function takes a `Times` structure and returns a string that is to be
/// printed to the standard output. See the
/// [parent module documentation](crate::times) for the format.
pub fn format(times: &Times) -> String {
let mut result = String::with_capacity(64);

format_one_time(times.self_user, times.ticks_per_second, &mut result).unwrap();
result.push(' ');
format_one_time(times.self_system, times.ticks_per_second, &mut result).unwrap();
result.push('\n');
format_one_time(times.children_user, times.ticks_per_second, &mut result).unwrap();
result.push(' ');
format_one_time(times.children_system, times.ticks_per_second, &mut result).unwrap();
result.push('\n');

result
}
magicant marked this conversation as resolved.
Show resolved Hide resolved

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn format_one_time_zero() {
let mut result = String::new();
format_one_time(0, 100, &mut result).unwrap();
assert_eq!(result, "0m0.000000s");
}

#[test]
fn format_one_time_less_than_one_second() {
let mut result = String::new();
format_one_time(50, 100, &mut result).unwrap();
assert_eq!(result, "0m0.500000s");
}

#[test]
fn format_one_time_one_second() {
let mut result = String::new();
format_one_time(1000, 1000, &mut result).unwrap();
assert_eq!(result, "0m1.000000s");
}

#[test]
fn format_one_time_more_than_one_second() {
let mut result = String::new();
format_one_time(1225, 100, &mut result).unwrap();
assert_eq!(result, "0m12.250000s");
}

#[test]
fn format_one_time_more_than_one_minute() {
let mut result = String::new();
format_one_time(123450, 100, &mut result).unwrap();
assert_eq!(result, "20m34.500000s");
}

#[test]
fn format_times() {
let times = Times {
self_user: 1250,
self_system: 6525,
children_user: 2475,
children_system: 60000,
ticks_per_second: 100,
};
let result = format(&times);
assert_eq!(
result,
"0m12.500000s 1m5.250000s\n0m24.750000s 10m0.000000s\n"
);
}
}
67 changes: 67 additions & 0 deletions yash-builtin/src/times/syntax.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// This file is part of yash, an extended POSIX shell.
// Copyright (C) 2024 WATANABE Yuki
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! Command line syntax parsing for the times built-in

use crate::common::syntax::{parse_arguments, Mode};
use std::borrow::Cow;
use thiserror::Error;
use yash_env::semantics::Field;
use yash_env::Env;
use yash_syntax::source::pretty::{Annotation, AnnotationType, MessageBase};

/// Error in parsing command line arguments
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum Error {
/// An error occurred in the common parser.
#[error(transparent)]
CommonError(#[from] crate::common::syntax::ParseError<'static>),

/// One or more operands are given.
#[error("unexpected operand")]
UnexpectedOperands(Vec<Field>),
}

impl MessageBase for Error {
fn message_title(&self) -> Cow<str> {
self.to_string().into()
}

fn main_annotation(&self) -> Annotation<'_> {
use Error::*;
match self {
CommonError(e) => e.main_annotation(),
UnexpectedOperands(operands) => Annotation::new(
AnnotationType::Error,
format!("{}: unexpected operand", operands[0].value).into(),
&operands[0].origin,
),
}
}
}

/// Parses command line arguments for the times built-in.
pub fn parse(env: &Env, args: Vec<Field>) -> Result<(), Error> {
let (options, operands) = parse_arguments(&[], Mode::with_env(env), args)?;
debug_assert_eq!(options, []);

if operands.is_empty() {
Ok(())
} else {
Err(Error::UnexpectedOperands(operands))
}
}
30 changes: 30 additions & 0 deletions yash-env/src/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ pub trait System: Debug {
#[must_use]
fn now(&self) -> Instant;

/// Returns consumed CPU times.
fn times(&self) -> nix::Result<Times>;
Comment on lines +199 to +200
Copy link

Choose a reason for hiding this comment

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

The addition of the times method to the System trait is a crucial enhancement for supporting the new time tracking functionality. This method is expected to return consumed CPU times, which aligns with the PR objectives. However, it's important to ensure that all implementors of the System trait provide a meaningful implementation of this method to avoid runtime errors or unimplemented method panics.

Ensure that all concrete implementations of the System trait (e.g., RealSystem, VirtualSystem) provide an implementation for the times method. This is crucial for maintaining the integrity of the system and avoiding runtime issues.


/// Gets and/or sets the signal blocking mask.
///
/// This is a low-level function used internally by
Expand Down Expand Up @@ -397,6 +400,30 @@ pub trait System: Debug {
/// [`System::fstatat`].
pub const AT_FDCWD: Fd = Fd(nix::libc::AT_FDCWD);

/// Set of consumed CPU time
///
/// This structure contains four CPU time values, all in clock ticks. To convert
/// them to seconds, divide each value by the number of clock ticks per second
/// (`ticks_per_second`).
///
/// This structure is returned by [`System::times`].
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct Times {
/// User CPU time consumed by the current process
pub self_user: u64,
/// System CPU time consumed by the current process
pub self_system: u64,
/// User CPU time consumed by the children of the current process
pub children_user: u64,
/// System CPU time consumed by the children of the current process
pub children_system: u64,

/// Number of clock ticks per second
///
/// This value is used to convert the consumed CPU time to seconds.
pub ticks_per_second: u64,
}

/// How to handle a signal.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SignalHandling {
Expand Down Expand Up @@ -883,6 +910,9 @@ impl System for SharedSystem {
fn now(&self) -> Instant {
self.0.borrow().now()
}
fn times(&self) -> nix::Result<Times> {
self.0.borrow().times()
}
fn sigmask(
&mut self,
how: SigmaskHow,
Expand Down
25 changes: 25 additions & 0 deletions yash-env/src/system/real.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ use super::SigmaskHow;
use super::Signal;
use super::System;
use super::TimeSpec;
use super::Times;
use crate::io::Fd;
use crate::job::Pid;
use crate::job::ProcessState;
Expand All @@ -54,6 +55,7 @@ use std::ffi::OsStr;
use std::ffi::OsString;
use std::future::Future;
use std::io::SeekFrom;
use std::mem::MaybeUninit;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::IntoRawFd;
use std::path::Path;
Expand Down Expand Up @@ -260,6 +262,29 @@ impl System for RealSystem {
Instant::now()
}

fn times(&self) -> nix::Result<Times> {
let mut tms = MaybeUninit::<nix::libc::tms>::uninit();
let raw_result = unsafe { nix::libc::times(tms.as_mut_ptr()) };
if raw_result == -1 {
return Err(Errno::last());
}
let tms = unsafe { tms.assume_init() };

let ticks_per_second = unsafe { nix::libc::sysconf(nix::libc::_SC_CLK_TCK) };
if ticks_per_second <= 0 {
return Err(Errno::last());
}
let ticks_per_second: u64 = ticks_per_second.try_into().map_err(|_| Errno::EOVERFLOW)?;

Ok(Times {
self_user: tms.tms_utime.try_into().map_err(|_| Errno::EOVERFLOW)?,
self_system: tms.tms_stime.try_into().map_err(|_| Errno::EOVERFLOW)?,
children_user: tms.tms_cutime.try_into().map_err(|_| Errno::EOVERFLOW)?,
children_system: tms.tms_cstime.try_into().map_err(|_| Errno::EOVERFLOW)?,
ticks_per_second,
})
}

fn sigmask(
&mut self,
how: SigmaskHow,
Expand Down
Loading
Loading