Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
44 changes: 44 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Build

on:
push:
pull_request:

jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7

# Alire brings the GNAT toolchain, and picks PJ_OS for the system it runs on
- uses: alire-project/setup-alire@v6

- name: Build the static library
run: alr --non-interactive build

- name: Build the relocatable (shared) library
run: alr exec -- gprbuild -P cpuload.gpr -XCPULOAD_LIBRARY_TYPE=relocatable -p

- name: Build the Ada example
run: alr exec -- gprbuild -P example/example.gpr -p

- name: Build the C example (Linux and macOS)
if: runner.os != 'Windows'
run: alr exec -- make -C example/c

- name: Build the C example (Windows)
if: runner.os == 'Windows'
run: |
# make is not installed on the Windows runner, unlike gcc
# The Makefile runs its commands in cmd, so the default PowerShell shell of the runner is fine
choco install make --no-progress -y
alr exec -- make -C example/c

# The Python example is not compiled, ctypes calls the shared library directly, so this only builds the library it loads and puts it where the example looks
# make is on every runner by this point, the Windows step above having installed it
- name: Build the library the Python example loads
run: alr exec -- make -C example/python
31 changes: 21 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ The library is thread-safe, written in Ada, and also provides a [C interface](in
| What is measured | OS | Method |
|---|---|---|
| The whole system | Linux | The `cpu` line of `/proc/stat` |
| The whole system | macOS | `host_statistics`, the machine's own CPU counters |
| The whole system | Windows | `GetSystemTimes` |
| One process, by its number | Linux | `utime` + `stime` of `/proc/<pid>/stat` |
| One process, by its number | macOS | `proc_pidinfo`, the user and system time of the process |
| One process, by its number | Windows | `OpenProcess` + `GetProcessTimes` |
| An application, every process of it | Linux | `/proc` scanned, each process named by `/proc/<pid>/comm` |
| An application, every process of it | macOS | `proc_listpids`, each process named by `proc_pidpath` |
| An application, every process of it | Windows | `EnumProcesses` + `QueryFullProcessImageNameW` |

macOS and BSD support is planned and will come in a future version.
macOS is supported on Apple Silicon.
BSD support is planned and will come in a future version.

## Building

Expand All @@ -32,18 +36,23 @@ alr build
Or directly with GNAT:

```bash
gprbuild -P cpuload.gpr
gprbuild -P cpuload.gpr -XPJ_OS=macos
```

The build produces a static library by default, and will detect the OS to compile the appropriate version. You can specify a specific OS to compile with `-XPJ_OS` (ex. `-XPJ_OS=windows`) to gprbuild (Alire sets it on its own).
The build produces a static library by default. `-XPJ_OS` says which system to build for: `linux`, `macos` or `windows`. Windows is the only one recognised on its own, as nothing tells macOS from Linux at build time, so **pass `-XPJ_OS` yourself on the other two**. Alire sets it on its own, and so do the Makefiles of the examples, which ask `uname`. A library built for another system reads no counters at all and reports 0% for everything.

For other library types (shared, etc.), set `-XCPULOAD_LIBRARY_TYPE`:

```bash
gprbuild -P cpuload.gpr -XCPULOAD_LIBRARY_TYPE=relocatable
gprbuild -P cpuload.gpr -XPJ_OS=macos -XCPULOAD_LIBRARY_TYPE=relocatable
```

`relocatable` builds the shared library (`libCPU_Load.so` / `.dll`) that carries the C interface, is standalone (it starts itself up when loaded), and is encapsulated (it carries the Ada runtime too, so it is one self-contained file).
`relocatable` builds the shared library (`libCPU_Load.so` / `.dll` / `.dylib`) that carries the C interface and is standalone: it starts itself up when loaded. On Linux and Windows it is encapsulated as well, carrying the Ada runtime with it, so it is one self-contained file.
gprbuild cannot encapsulate on macOS, so the library reads the Ada runtime from its own file there, and looks for it next to itself. Copy it in once the library is built (the Makefiles of the examples do this for you):

```bash
cp "$(gnatls -v | grep adalib | tr -d ' ')"/libgnat-*.dylib lib/relocatable/
```

## Using from Ada

Expand Down Expand Up @@ -75,10 +84,12 @@ The whole interface is five functions: `Sample`, the three `Take` functions (the
A full example program is in [example/src/example_cpu_load.adb](example/src/example_cpu_load.adb). It follows the machine, itself, and an application named on the command line, once per second until stopped with Ctrl+C:

```bash
gprbuild -P example/example.gpr -p
gprbuild -P example/example.gpr -XPJ_OS=macos -p
./example/example_cpu_load firefox
```

Give `-XPJ_OS` here too: the example builds the library with it, and one built for another system reads no counters at all and reports 0% for everything.

With Alire, add the library to your project with `alr with cpuload`.

## Using from C (and any other language)
Expand Down Expand Up @@ -107,7 +118,7 @@ make -C example/c run APP=firefox
To build it by hand instead, from the root of the repository, first compile the library:

```bash
gprbuild -P cpuload.gpr -XCPULOAD_LIBRARY_TYPE=relocatable
gprbuild -P cpuload.gpr -XPJ_OS=macos -XCPULOAD_LIBRARY_TYPE=relocatable
```

Then compile the C program:
Expand All @@ -116,7 +127,7 @@ Then compile the C program:
gcc example/c/main.c -Iinclude -Llib/relocatable -lCPU_Load -Wl,-rpath,"$PWD/lib/relocatable" -o example/c/example_c
```

`-I` is the folder holding `cpuload.h`, `-L` and `-l` the library to link with, and `-rpath` the folder where the program looks for the library when it runs. Without `-rpath`, the program still compiles but stops on start with a "library not loaded" error, unless you set `LD_LIBRARY_PATH` yourself. Windows has no `-rpath`: put a copy of the DLL next to the program instead (which is what the Makefile does).
`-I` is the folder holding `cpuload.h`, `-L` and `-l` the library to link with, and `-rpath` the folder where the program looks for the library when it runs. Without `-rpath`, the program still compiles but stops on start with a "library not loaded" error, unless you set `LD_LIBRARY_PATH` yourself. macOS works the same way, as long as the Ada runtime sits next to the library as above. Windows has no `-rpath`: put a copy of the DLL next to the program instead (which is what the Makefile does).

## Using from Python

Expand All @@ -128,7 +139,7 @@ import ctypes, time
class Sample(ctypes.Structure):
_fields_ = [("busy", ctypes.c_int64), ("total", ctypes.c_int64), ("used", ctypes.c_int64)]

lib = ctypes.CDLL("lib/relocatable/libCPU_Load.so")
lib = ctypes.CDLL("lib/relocatable/libCPU_Load.so") # libCPU_Load.dylib on macOS, CPU_Load.dll on Windows
lib.cpuload_system_usage.restype = ctypes.c_double
lib.cpuload_process_usage.restype = ctypes.c_double
lib.cpuload_version.restype = ctypes.c_char_p
Expand All @@ -149,7 +160,7 @@ Java (through FFM or JNA), Rust (through `libloading` or FFI declarations), and

## Adding a new OS

The package spec [src/cpu_load.ads](src/cpu_load.ads) is shared by every OS, and holds the two usage functions, which are pure arithmetic on a pair of samples. Each OS brings its own body of the three `Take` functions ([src/linux](src/linux/cpu_load.adb), [src/windows](src/windows/cpu_load.adb)), and [cpuload.gpr](cpuload.gpr) picks the folder for the OS being built from the `PJ_OS` symbol. To support a new OS, write the implementation body and add its folder there.
The package spec [src/cpu_load.ads](src/cpu_load.ads) is shared by every OS, and holds the usage functions. Each OS brings its own body of the three `Take` functions ([src/linux](src/linux/cpu_load.adb), [src/macos](src/macos/cpu_load.adb), [src/windows](src/windows/cpu_load.adb)), and [cpuload.gpr](cpuload.gpr) picks the folder for the OS being built from the `PJ_OS` symbol. To support a new OS, write the implementation body and add its folder there.

## 📜 License

Expand Down
14 changes: 11 additions & 3 deletions alire.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "cpuload"
description = "Library to monitor processes, applications and system CPU usage"
version = "0.0.1"
version = "0.0.2"

long-description = """
CPU Load reports system, processes and applications (every process of it) CPU load.
Expand All @@ -11,17 +11,25 @@ authors = ["Adel Noureddine"]
maintainers = ["Adel Noureddine <adel.noureddine@outlook.com>"]
maintainers-logins = ["adelnoureddine"]

tags = ["cpu", "process", "monitoring", "linux", "windows", "application", "usage", "load"]
tags = ["cpu", "process", "monitoring", "linux", "windows", "macos", "application", "usage", "load"]


licenses = "LGPL-3.0-only"
website = "https://www.noureddine.org/research/joular"

[gpr-externals]
PJ_OS = ["linux", "windows"]
PJ_OS = ["linux", "windows", "macos"]
CPULOAD_LIBRARY_TYPE = ["static", "relocatable", "static-pic"]

[gpr-set-externals."case(os)".linux]
PJ_OS = "linux"
[gpr-set-externals."case(os)".windows]
PJ_OS = "windows"
[gpr-set-externals."case(os)".macos]
PJ_OS = "macos"

[available.'case(os)']
linux = true
windows = true
macos = true
'...' = false
25 changes: 14 additions & 11 deletions cpuload.gpr
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ library project CpuLoad is

-- Symbols to indicate which OS to compile for
-- Build with: gprbuild -XPJ_OS=windows
type OS_Platform is ("linux", "windows"); --, "macos", "bsd");
type OS_Platform is ("linux", "windows", "macos"); --, "bsd");
PJ_OS : OS_Platform := external ("PJ_OS", Default_OS);

-- Default platform directory for each CPU load implementation
Expand All @@ -25,8 +25,8 @@ library project CpuLoad is
Platform_Dir := "src/linux";
when "windows" =>
Platform_Dir := "src/windows";
-- when "macos" =>
-- Platform_Dir := "src/macos";
when "macos" =>
Platform_Dir := "src/macos";
-- when "bsd" =>
-- Platform_Dir := "src/bsd";
end case;
Expand All @@ -47,25 +47,28 @@ library project CpuLoad is
for Library_Kind use Library_Type;
for Create_Missing_Dirs use "True";

-- The relocatable (shared) library is stand-alone and encapsulated: it carries the Ada runtime and starts itself up when loaded, so programs in other languages (C, Python, Java, Rust, etc.) can simply load it and call the functions of include/cpuload.h, with nothing else needed
-- The relocatable (shared) library is stand-alone: it starts itself up when loaded, so programs in other languages (C, Python, Java, Rust, etc.) can simply load it and call the functions of include/cpuload.h, with nothing else needed
-- It is encapsulated as well where it can be, meaning it carries the Ada runtime and is one self-contained file
case Library_Type is
when "relocatable" =>
for Library_Interface use ("CPU_Load", "CPU_Load.C_API");
for Library_Standalone use "encapsulated";
-- case PJ_OS is
-- when "macos" =>
case PJ_OS is
when "macos" =>
-- gprbuild cannot encapsulate on macOS, so the library loads the Ada runtime from its own file there
-- for Library_Standalone use "standard";
-- when others =>
-- for Library_Standalone use "encapsulated";
-- end case;
for Library_Standalone use "standard";
when others =>
for Library_Standalone use "encapsulated";
end case;

case PJ_OS is
when "linux" =>
for Library_Version use "libCPU_Load.so.0";
when "windows" =>
-- Linker_Options below is for whoever links against this library, and it comes too late for the library itself: the DLL calls EnumProcesses, so it has to link psapi on its own
for Library_Options use ("-lpsapi");
when "macos" =>
-- Nothing to add: the counters the library reads are in libSystem, which every program links already
null;
end case;

when others =>
Expand Down
25 changes: 17 additions & 8 deletions example/c/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Builds the C example of CPU Load, on Linux and Windows
# Builds the C example of CPU Load, on Linux, macOS and Windows
#
# make builds the shared library and the example
# make run builds it, then runs it (stop it with Ctrl+C)
Expand Down Expand Up @@ -34,20 +34,29 @@ ifeq ($(OS),Windows_NT)

else

# CPU Load has a body for Linux and for Windows only
# Nothing but Windows sets OS, so ask uname which of the others this is
ifeq ($(shell uname),Darwin)
$(error CPU Load has no macOS implementation yet, so build this on Linux or Windows)

PJ_OS = macos

# gprbuild cannot put the Ada runtime inside the library on macOS, and the library looks for it next to itself, so put a copy there
ADALIB = $(shell gnatls -v | grep adalib | head -1 | tr -d ' ')
COPY_LIBRARY = cp -f $(ADALIB)/libgnat-*.dylib $(LIB_DIR)

else

PJ_OS = linux

# The library carries the Ada runtime inside it, so nothing to copy
COPY_LIBRARY = true

endif

PJ_OS = linux
EXE = example_c

# Linux records the folder of the library inside the program, so it is found when running
# Linux and macOS record the folder of the library inside the program, so it is found when running
LDFLAGS += -Wl,-rpath,$(abspath $(LIB_DIR))

# Nothing to copy
COPY_LIBRARY = true

RUN = ./$(EXE) $(APP)
DELETE = rm -f

Expand Down
23 changes: 16 additions & 7 deletions example/python/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Builds the shared library the Python example of CPU Load needs, on Linux and Windows
# Builds the shared library the Python example of CPU Load needs, on Linux, macOS and Windows
#
# make builds the shared library
# make run builds it, then runs the example (stop it with Ctrl+C)
Expand Down Expand Up @@ -31,17 +31,26 @@ ifeq ($(OS),Windows_NT)

else

# CPU Load has a body for Linux and for Windows only
# Nothing but Windows sets OS, so ask uname which of the others this is
ifeq ($(shell uname),Darwin)
$(error CPU Load has no macOS implementation yet, so build this on Linux or Windows)

PJ_OS = macos

# gprbuild cannot put the Ada runtime inside the library on macOS, and the library looks for it next to itself, so put a copy there
ADALIB = $(shell gnatls -v | grep adalib | head -1 | tr -d ' ')
COPY_LIBRARY = cp -f $(ADALIB)/libgnat-*.dylib $(LIB_DIR)

else

PJ_OS = linux

# Linux records the folder of the Ada runtime inside the library, so nothing to copy
COPY_LIBRARY = true

endif

PJ_OS = linux
PYTHON = python3

# Linux records the folder of the Ada runtime inside the library, so nothing to copy
COPY_LIBRARY = true

CLEAN = rm -f *.dll && rm -rf __pycache__

endif
Expand Down
5 changes: 2 additions & 3 deletions example/python/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,13 @@ def library_names():
"""The name the shared library takes on this OS."""
if sys.platform == "win32":
return ("libCPU_Load.dll", "CPU_Load.dll")
if sys.platform == "darwin":
return ("libCPU_Load.dylib",)
return ("libCPU_Load.so",)


def find_library():
"""The file holding the shared library, or a message on how to build it when there is none."""
if sys.platform == "darwin":
sys.exit("CPU Load has no macOS implementation yet, so run this on Linux or Windows")

# Look next to this program first, as Windows has no rpath and wants a copy
# of the DLL there, then in the folder gprbuild builds the library into
for folder in (Path(__file__).resolve().parent, LIBRARY_DIR):
Expand Down
19 changes: 17 additions & 2 deletions example/src/example_cpu_load.adb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

-- Prints the CPU load of the machine, of this very program, and of an application named on the command line, every second, until stopped with Ctrl+C
--
-- Build and run it with:
-- gprbuild -P example/example.gpr -p
-- Build and run it with (-XPJ_OS says which system to build for: linux, macos or windows):
-- gprbuild -P example/example.gpr -XPJ_OS=macos -p
-- ./example/example_cpu_load firefox

with Ada.Command_Line; use Ada.Command_Line;
Expand All @@ -25,6 +25,10 @@ with System.Multiprocessors;

with CPU_Load; use CPU_Load;

-- A Sample's counters are Integer_64, so comparing one needs its operators here
with Interfaces;
use type Interfaces.Integer_64;

procedure Example_CPU_Load is

-- Time between two samples
Expand Down Expand Up @@ -52,6 +56,7 @@ procedure Example_CPU_Load is
Mine_Colour : constant String := Escape & "[1;35m";
App_Colour : constant String := Escape & "[1;33m";
Ready_Colour : constant String := Escape & "[1;32m";
Trouble_Colour : constant String := Escape & "[1;31m";

-- Goes back to the beginning of the line and erases it, so each reading overwrites the previous one instead of scrolling
Clear_Line : constant String := ASCII.CR & Escape & "[2K";
Expand Down Expand Up @@ -116,6 +121,16 @@ begin
Mine_Before := Take (Ours);
App_Before := Take (App);

-- A Total of zero is the library saying it could not read the machine's counters at all
-- It is what a library built for another system does here, and it would otherwise show as a row of 0% every second, which looks like an idle machine rather than a build to redo
if Machine_Before.Total = 0 then
Put_Line (Trouble_Colour
& "The machine's counters could not be read at all."
& " This is what a library built for another system does: build it again with -XPJ_OS for this one (linux, macos or windows)."
& Reset);
return;
end if;

while not Stop_Asked loop
delay Interval;

Expand Down
3 changes: 2 additions & 1 deletion include/cpuload.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
/*
* C interface of CPU Load, a library reporting how much of a machine's CPU is in use: the whole system, one process by its number, or an application, meaning every process running it
*
* Use it with the relocatable (shared) build of the library (libCPU_Load.so on Linux, CPU_Load.dll on Windows), which starts itself up when loaded: no other initialization call is needed
* Use it with the relocatable (shared) build of the library (libCPU_Load.so on Linux, libCPU_Load.dylib on macOS, CPU_Load.dll on Windows), which starts itself up when loaded: no other initialization call is needed
*
* Library is thread-safe
*
Expand Down Expand Up @@ -50,6 +50,7 @@ void cpuload_take_pid(unsigned int pid, cpuload_sample *out);

/* Take a sample of the system and of every process running the named application
* The name is the program's own, without its folder, and it is exactly matched and case insensitive, so "firefox" finds "Firefox"
* On macOS the program's own name is matched the same way, so "firefox" finds the firefox inside Firefox.app
* On Windows a trailing ".exe" is ignored as well, so "firefox" also finds "firefox.exe"
*/
void cpuload_take_app(const char *app, cpuload_sample *out);
Expand Down
Loading