Introduction
heim
is a cross-platform Rust crate for retrieving information
about system processes and various system details
(such as CPU, memory, disks, networks and sensors).
heim
has few key goals which define its development and public interface:
- Async-first.
- Cross-platform.
Any code fromheim
should just work on all supported platforms. OS-specific things do exist, but API design forces users to pay attention to them. - Modular design.
- Idiomatic and easy to use.
License
Licensed under either of Apache License 2.0 or MIT license at your option.
Donations
heim
is an open-source project, developed in my spare time.
If you appreciate my work and want to support me
or speed up the project development,
you can do it here
or support this project at Open Collective.
Platforms support
Tier 1
heim
is still in a MVP phase and due to the lack of resources
right now it targets to support Rust Tier 1 platforms only:
OS | Architecture |
---|---|
Linux | i686 |
Linux | x86_64 |
macOS | i686 |
macOS | x86_64 |
Windows | i686 |
Windows | x86_64 |
In addition, most recent OS versions are targeted right now; for example, there might be a chance that it will not work on Linux 2.6.18. This should be considered as a bug and if you have compatibility issues, feel free to create an issue about it.
Tier 2
Following targets are explicitly tested in the CI.
Same to Rust Tier 2 platforms, these can be thought of as "guaranteed to build",
but no further guarantees about correctness are provided.
Target |
---|
aarch64-unknown-linux-gnu |
aarch64-unknown-linux-musl |
armv7-unknown-linux-gnueabihf |
armv7-unknown-linux-musleabihf |
arm-unknown-linux-gnueabihf |
arm-unknown-linux-musleabihf |
Other platforms
Other platforms are not implemented yet and there is no ETA on them. Contributions are welcomed!
Async-first approach
As it was mentioned before, heim
is created with the "async-first" idea in mind.
If you are not familiar with async programming in Rust, refer to "Asynchronous Programming in Rust" book to learn about wider view on the topic.
Motivation
It might be seen unusual for such a library to be so persistent on the "async-first" approach. The reason for that is because even the modern operating systems might spend a lot of time gathering requested system information.
Most common argument against it is the
"how much time will one syscall
take, am I right?",
but in a worse case scenarios, it might take some unknown and unpredictable
amount of time to make one simple operation, which is not what you
usually expect from your operating system.
For example, it might take seconds to read one file
from the Linux procfs
virtual filesystem; or if you accidentally
called access(2)
on a NFS mount, performance slaps you in the face,
because now OS needs to make a network request to do that.
Acknowledging this fact, you might consider async
keyword in the
heim
public API as a mere marker of uncertainty on how much time
this operation will take; it really depends on what information
you are requesting and what operating system is used.
Thanks to the modern Rust async runtimes, it is very easy and safe to do something else while waiting on this one operation, so why should not we embrace it already?
Runtime support
Internally heim
uses smol
crate to execute async operations
and blocking
crate to execute blocking operations on a separate thread pool.
Bundled support for tokio
and async-std
runtimes existed for some time in pre-release v0.1.0
versions,
but was removed as it was slowing down the development process and introduced too much maintenance burden.\
Public API
Platform-specific information
By default heim
exposes only that information, which is guaranteed to be available
on all supported platforms.
Let's check heim::cpu::CpuStats
struct as an example. It has two public methods: CpuStats::ctx_switches
and CpuStats::interrupts
,
which are returning amount of the context switches and interrupts correspondingly:
use heim::{cpu, Result}; #[tokio::main] async fn main() -> Result<()> { let stats = cpu::stats().await?; println!("Context switches: {}", stats.ctx_switches()); println!("Interrupts: {}", stats.interrupts()); Ok(()) }
But for Linux there is also an amount of software interrupts available,
and for Windows we might also want to get the syscalls amount too.
heim
follows Rust solution for providing OS-specific information
and exposes extension traits for some of the publicly available structs.
In order to show software interrupts and syscalls amount, we need to bring these traits into the scope and call the corresponding methods:
use heim::{cpu, Result}; #[cfg(target_os = "linux")] use heim::cpu::os::linux::CpuStatsExt; #[cfg(target_os = "windows")] use heim::cpu::os::windows::CpuStatsExt; #[tokio::main] async fn main() -> Result<()> { let stats = cpu::stats().await?; println!("Context switches: {}", stats.ctx_switches()); println!("Interrupts: {}", stats.interrupts()); #[cfg(target_os = "linux")] println!("Software interrupts: {}", stats.soft_interrupts()); #[cfg(target_os = "windows")] println!("Syscalls: {}", stats.syscalls()); Ok(()) }
With this approach it is very easy to write cross-platform code and platform-specific calls are more noticeable.
Measurement units
heim
heavily relies on the uom
crate
in order to expose proper measurement units where it is applicable.
In a short, that means that, for example, heim::cpu::CpuFrequency
struct methods are returning Frequency
type,
instead of the primitive types such as u64
,
which are prone to the logical bugs.
heim
re-exports all used measurement quantities and units at heim::units
module.
Refer to uom
documentation
on how to work with these types.
Example
use heim::{cpu, units, Result}; #[tokio::main] async fn main() -> Result<()> { let freq = cpu::frequency().await?; println!("Current CPU frequency: {} GHz", freq.current().get::<units::frequency::gigahertz>()); Ok(()) }
Components
heim
provides information about various system components,
and for the sake of clarity, they are split into the corresponding modules.
For example, there is
heim::process
module for system processes routines,
heim::cpu
for CPU related stuff,
heim::memory
for memory and swap info,
you got an idea.
See crate documentation for all available modules.
Note that all these modules are not included into heim
by default,
and you need to explicitly enable them with Cargo features, ex.
heim = { version = "*", features = ["process", "cpu", "memory"] }
Alternatively, you can use full
feature, which enables all components at once:
heim = { version = "*", features = ["full"] }
It is strongly discouraged to use full
feature, unless you are really planning
to use everything in heim
; prefer to enable separate features instead.
Contributing
First of all, see CONTRIBUTING.md document for a contributing guidelines.
If you want to create a PR, see "development" section for a quickstart on the project structure.
Development
Project structure
heim
is split into multiple sub-crates, which are gathered together
in the heim
facade crate.
Most of the crates has very obvious names, ex. heim-cpu
, heim-memory
or heim-process
— they are responsible for each separate heim
component.
heim-common
contains commonly used types, such as heim::Result
, heim::Error
,
prelude module for heim
development and also has a bunch
of platform-specific FFI bindings, which are utilized by other heim-*
crates.
heim-runtime
is an abtraction for all supported async runtimes.
Note that instead of the calling, for example, tokio::fs::read_link()
function,
you need to use heim_runtime::fs::read_link()
instead.
Same goes for pin!
/pin_mut!
, join!
and try_join!
macros, all of them are exported by heim-runtime
.
Crate structure
Each crate responsible for the corresponding heim
module has the same files structure.
For example, let's write heim-cpu
from scratch and provide information about CPU statistics.
$ ls /src
os/
sys/
lib.rs
stats.rs
Where:
os/
contains OS-specific traits and implementations, same as Rust doessys/
contains platform-specific implementationslib.rs
should expose all public types and functionsstats.rs
contains theCpuStats
struct and async function which returnsheim::Result<crate::CpuStats>
Public interface
#![allow(unused_variables)] fn main() { use heim_common::prelude::wrap; use crate::sys; /// System CPU stats. pub struct CpuStats(sys::CpuStats); wrap!(CpuStats, sys::CpuStats); }
wrap!
macro generates AsRef
, AsMut
and From
implementations,
which are allowing working with the "inner" sys::CpuStats
struct.
It is strictly important that struct should only has these methods, which are available on all platforms supported, as done in the following example.
#![allow(unused_variables)] fn main() { impl CpuStats { pub fn ctx_switches(&self) -> u64 { self.as_ref().ctx_switches() } pub fn interrupts(&self) -> u64 { self.as_ref().interrupts() } } }
Linux additionally provides the amount of "soft interrupts",
but we can't expose it here, because it would not be portable.
Instead, we should create the CpuStatsExt
trait at os/linux/stats.rs
:
#![allow(unused_variables)] fn main() { pub trait CpuStatsExt { fn soft_interrupts(&self) -> u64; } #[cfg(target_os = "linux")] impl CpuStatsExt for crate::CpuStats { fn soft_interrupts(&self) -> u64 { self.as_ref().soft_interrupts() } } }
Trait itself should be publicly accessible, but impl
block
for our crate::CpuStats
should be gated and implemented only
for target_os = "linux"
Platform implementations
Now we need to create platform-specific implementation
and we will start with the sys/linux/mod.rs
module.
sys/linux/mod.rs
should be compile-gated too with #[cfg(target_os = "linux")]
,
because it can only be used for Linux systems.
Same thing applies to all other platform-specific implementations.
Implementation for our src/stats.rs
goes into the sys/linux/stats.rs
module.
In the case of Linux it will contain few fields, which will be populated later:
#![allow(unused_variables)] fn main() { pub struct CpuStats { ctx_switches: u64, interrupts: u64, soft_interrupts: u64, } impl FromStr for CpuStats { type Err = Error; fn from_str(s: &str) -> Result<CpuStats, Self::Err> { // .. } } }
Now, we need to provide the async interface.
In case of Linux we need to parse the /proc/stat
file
and create the sys::CpuStats
struct with data from it.
Our sys/linux/stats.rs
should declare one function:
#![allow(unused_variables)] fn main() { use heim_common::Result; use heim_runtime as rt; pub async fn cpu_times() -> Result<CpuStats> { rt::fs::read_into("/proc/stat") } }
What will happen here: /proc/stat
will be read asynchronously
with the help of heim_runtime::fs
and then parsed with help of the FromStr
implementation.
Now let's go back to our public CpuStats
struct.
It should declare a similar function too, but its implementation will be much simpler:
#![allow(unused_variables)] fn main() { use heim_common::Result; use crate::sys; pub async fn stats() -> Result<CpuStats> { sys::stats().map(Into::into).await } }
Since that wrap!
macro from the start generates From<sys::CpuStats> for CpuStats
implementation,
all we need now is to call the platform-specific function and wrap the result into public struct.
Same thing applies to all structs and functions returning Future
s and Stream
s --
platform-specific implementations should be received from the Future
or Stream
and wrapped
into a public struct via Into::into
.
Additional
Please, stick to the Rust API guidelines where possible.
Showcases
These are some projects, which are using heim
to fetch and show system information:
If you know or maintain project, that is not mentioned in here, feel free to create an issue or send a Pull Request and it will be included to this page.
Nushell
Nushell ("A new type of shell") uses heim
to power two bundled plugins: ps
and sys
.
They are providing information about system processes and system components
correspondingly as a structured data:
heim::process
routines:
❯ ps | where name == node
━━━━━━━┯━━━━━━┯━━━━━━━━━┯━━━━━━━━┯━━━━━━━━━┯━━━━━━━━━
pid │ name │ status │ cpu │ mem │ virtual
───────┼──────┼─────────┼────────┼─────────┼─────────
15447 │ node │ Running │ 0.0000 │ 18.5 MB │ 4.7 GB
━━━━━━━┷━━━━━━┷━━━━━━━━━┷━━━━━━━━┷━━━━━━━━━┷━━━━━━━━━
> sys | get net | where sent > 0
───┬────────┬─────────┬──────────
# │ name │ sent │ recv
───┼────────┼─────────┼──────────
0 │ tun0 │ 30.3 MB │ 653.4 MB
1 │ wlp3s0 │ 97.7 MB │ 1.2 GB
2 │ lo │ 97.1 MB │ 97.1 MB
───┴────────┴─────────┴──────────
heim::host::platform
,
heim::host::uptime
, and
heim::host::users
:
> sys | get host
───┬───────┬────────────────┬──────────────────────────────────────────┬──────────┬────────┬────────────────────────────┬────────────────
# │ name │ release │ version │ hostname │ arch │ uptime │ users
───┼───────┼────────────────┼──────────────────────────────────────────┼──────────┼────────┼────────────────────────────┼────────────────
0 │ Linux │ 5.4.15-arch1-1 │ #1 SMP PREEMPT Sun, 26 Jan 2020 09:48:50 │ tardis │ x86_64 │ [row days hours mins secs] │ [table 1 rows]
│ │ │ +0000 │ │ │ │
───┴───────┴────────────────┴──────────────────────────────────────────┴──────────┴────────┴────────────────────────────┴────────────────
bottom
bottom
(Yet another cross-platform graphical process/system monitor)
uses heim
to show system information in a really cool text user interface:
Zenith
zenith
(sort of like top or htop but with histograms)
uses heim
to show system information in a really cool text user interface
with histograms: