Skip to content

Commit a8cd635

Browse files
author
Tobias de Bruijn
committed
Added CPU usage and Memory usage endpoints, started updating documentation for the HTTP API
1 parent a909e18 commit a8cd635

File tree

14 files changed

+326
-13
lines changed

14 files changed

+326
-13
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ web/node_modules:
3737
cd web; \
3838
npm i
3939

40-
web/dist/dist.js: $(wildcard web/node_modules) ${TS_SOURCE_FILES} ${SCSS_SOURCE_FILES}
40+
web/dist/dist.js: web/node_modules ${TS_SOURCE_FILES} ${SCSS_SOURCE_FILES}
4141
cd web; \
4242
npx webpack
4343

44-
web/dist/ansi_up.js: $(wildcard web/node_modules/ansi_up/ansi_up.js)
44+
web/dist/ansi_up.js: web/node_modules
4545
cp web/node_modules/ansi_up/ansi_up.js web/dist/ansi_up.js
4646

4747
web/dist.zip: web/dist/dist.js web/dist/ansi_up.js ${STATIC_WEB_CONTENT}

docs/http/stats/cpu.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# CPU
2+
Get information about the CPU and CPU usage
3+
4+
## CPU Usage (load)
5+
Endpoint: `/stats/cpu/load`
6+
Method: `POST`
7+
8+
Request payload: (x-www-form-urlencoded)
9+
```
10+
session_id: The user's session ID
11+
```
12+
13+
Request response;
14+
```jsonc
15+
{
16+
"status": 200, //200 if okay, 401 if session_id is invalid
17+
"load": { //For linux, see alsso: https://man7.org/linux/man-pages/man3/getloadavg.3.html
18+
"one": 1.0, //On *nix: Average over 1 minute. On Windows: CPU utilization with a 500ms sampling period, or -1.0 if an error occurred
19+
"five": 1.0, //On *nix: Average over 5 minutes. -1.0 on Windows (Not supported)
20+
"fifteen": 1.0 //On *nix: Average over 15 minutes. -1.0 on Windows (Not supported)
21+
}
22+
}
23+
```

docs/http/stats/jvm_mem_explained.png

20.1 KB
Loading

docs/http/stats/mem.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Memory
2+
Get information about Memory (RAM) usage and availability
3+
4+
## Memory usage
5+
Endpoint: `/stats/mem`
6+
Method: `POST`
7+
8+
Request payload: (x-www-form-urlencoded)
9+
```
10+
session_id: The user's session ID
11+
```
12+
13+
Response:
14+
```jsonc
15+
{
16+
"status": 200, //200 if okay, 401 if session_id is invalid
17+
"total_mem": 1.0, //Total memory available for new Objects
18+
"free_mem": 1.0, //Memory currently used by instantiated Objects
19+
"max_mem": 1.0, //Unalocated memory, but designated for future objects
20+
}
21+
```
22+
![Java memory guide](jvm_mem_explained.png)

librconsole/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,9 @@ bcrypt = "0.9.0"
2525
sha2 = "0.9.3"
2626
base64 = "0.13.0"
2727
chrono = "0.4.19"
28+
29+
[target.'cfg(unix)'.dependencies]
30+
libc = "0.2.93"
31+
32+
[target.'cfg(windows)'.dependencies]
33+
winapi = { version = "0.3.9", features = ["std", "processthreadsapi", "minwindef"]}

librconsole/src/endpoints/mod.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,28 @@
1+
use crate::jni::JvmCommand;
2+
use crate::webserver::AppData;
3+
use crate::database::Database;
4+
use crate::jni::logging::{ConsoleLogItem, LogLevel};
5+
use rusqlite::named_params;
6+
use std::io;
7+
use std::path::PathBuf;
8+
19
pub mod console;
2-
pub mod auth;
10+
pub mod auth;
11+
pub mod stats;
12+
13+
pub fn check_session_id(data: &AppData, session_id: &str) -> io::Result<bool> {
14+
let db = Database::new(PathBuf::from(data.db_path.clone())).unwrap();
15+
let sql_check_session: rusqlite::Result<bool> = db.connection.query_row("SELECT EXISTS(SELECT 1 FROM sessions WHERE session_id = :session_id)", named_params! {
16+
":session_id": session_id
17+
}, |row| row.get(0));
18+
19+
if sql_check_session.is_err() {
20+
let tx = data.jvm_command_tx.lock().unwrap();
21+
let jvm_command = JvmCommand::log(ConsoleLogItem::new(LogLevel::Warn,format!("An error occurred while verifying a session_id: {:?}", sql_check_session.err().unwrap()) ));
22+
tx.send(jvm_command).expect("An issue occurred while sending a JvmCommand");
23+
24+
return Err(io::Error::new(io::ErrorKind::Other, "An issue occurred while executing the database query"));
25+
}
26+
27+
Ok(sql_check_session.unwrap())
28+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
use crate::webserver::AppData;
2+
use crate::endpoints::check_session_id;
3+
use actix_web::{web, post, HttpResponse};
4+
use serde::{Serialize, Deserialize};
5+
use std::io;
6+
7+
#[cfg(windows)]
8+
use winapi::um::processthreadsapi::GetSystemTimes;
9+
#[cfg(windows)]
10+
use winapi::shared::minwindef::FILETIME;
11+
#[cfg(windows)]
12+
use std::time::Duration;
13+
14+
#[cfg(unix)]
15+
use libc::c_int;
16+
17+
#[cfg(windows)]
18+
const WINDOWS_SAMPLE_TIME_MILIS: u64 = 500;
19+
20+
#[derive(Serialize)]
21+
pub struct LoadAverage {
22+
one: f64,
23+
five: f64,
24+
fifteen: f64
25+
}
26+
27+
#[derive(Deserialize)]
28+
pub struct LoadAverageRequest {
29+
session_id: String
30+
}
31+
32+
#[derive(Serialize)]
33+
pub struct LoadAverageResponse {
34+
status: i16,
35+
load: Option<LoadAverage>
36+
}
37+
38+
#[cfg(unix)]
39+
#[link(name = "c")]
40+
#[allow(dead_code)] //Even though it's used, we still get a warning
41+
extern "C" {
42+
fn getloadavg(loadavg: *mut f64, nelem: c_int) -> c_int;
43+
}
44+
45+
#[cfg(windows)]
46+
#[inline(always)]
47+
#[allow(dead_code)] //Even though it's used, we still get a warning
48+
fn empty() -> FILETIME {
49+
FILETIME {
50+
dwLowDateTime: 0,
51+
dwHighDateTime: 0,
52+
}
53+
}
54+
55+
#[cfg(windows)]
56+
#[inline(always)]
57+
#[allow(dead_code)] //Even though it's used, we still get a warning
58+
fn filetime_to_u64(f: FILETIME) -> u64 {
59+
(f.dwHighDateTime as u64) << 32 | (f.dwLowDateTime as u64)
60+
61+
}
62+
63+
#[cfg(windows)]
64+
#[allow(dead_code)] //Even though it's used, we still get a warning
65+
fn win_poll_load_avg() -> io::Result<f64> {
66+
let get_system_times = || -> io::Result<(u64, u64)> {
67+
let mut idle_ticks = empty();
68+
let mut user_ticks = empty();
69+
let mut kernel_ticks = empty();
70+
71+
if unsafe { GetSystemTimes(&mut idle_ticks, &mut kernel_ticks, &mut user_ticks) }== 0 {
72+
return Err(io::Error::new(io::ErrorKind::Other, "GetSystemTimes() failed"));
73+
}
74+
75+
let idle_ticks = filetime_to_u64(idle_ticks);
76+
let total_ticks = filetime_to_u64(kernel_ticks) + filetime_to_u64(user_ticks);
77+
78+
Ok((idle_ticks, total_ticks))
79+
};
80+
81+
let (idle_ticks_before, total_ticks_before) = get_system_times()?;
82+
std::thread::sleep(Duration::from_millis(WINDOWS_SAMPLE_TIME_MILIS));
83+
84+
let (idle_ticks_after, total_ticks_after) = get_system_times()?;
85+
let delta_idle_ticks = idle_ticks_after - idle_ticks_before;
86+
let delta_total_ticks = total_ticks_after - total_ticks_before;
87+
88+
let div_ticks = if delta_total_ticks > 0u64 {
89+
//We need to cast these to f64, otherwise we get integer division and not floating point division
90+
(delta_idle_ticks as f64) / (delta_total_ticks as f64)
91+
} else {
92+
0f64
93+
};
94+
95+
let result = 1f64 - div_ticks;
96+
Ok(result)
97+
}
98+
99+
fn load_avg() -> io::Result<LoadAverage> {
100+
#[cfg(windows)]
101+
{
102+
let cpu_usage = win_poll_load_avg()?;
103+
104+
return Ok(LoadAverage {
105+
one: cpu_usage,
106+
five: -1f64, //Unsupported on Windows
107+
fifteen: -1f64 //Unsupported on Windows
108+
});
109+
}
110+
111+
# [cfg(unix)]
112+
{
113+
let mut loads: [f64; 3] = [0.0, 0.0, 0.0];
114+
if unsafe{ getloadavg(&mut loads[0], 3) } != 3 {
115+
return Err(io::Error::new(io::ErrorKind::Other, "getloadavg() failed"));
116+
}
117+
118+
return Ok(LoadAverage {
119+
one: loads[0],
120+
five: loads[1],
121+
fifteen: loads[2]
122+
});
123+
}
124+
}
125+
126+
#[post("/stats/cpu/load")]
127+
pub fn post_get_load_avg(data: web::Data<AppData>, form: web::Form<LoadAverageRequest>) -> HttpResponse {
128+
let session_valid = check_session_id(&data, &form.session_id);
129+
if session_valid.is_err() {
130+
return HttpResponse::InternalServerError().finish();
131+
}
132+
133+
if !session_valid.unwrap() {
134+
return HttpResponse::Ok().json(LoadAverageResponse { status: 401, load: None });
135+
}
136+
137+
let load = load_avg();
138+
if load.is_err() {
139+
return HttpResponse::InternalServerError().finish();
140+
}
141+
142+
HttpResponse::Ok().json(LoadAverageResponse {
143+
status: 200,
144+
load: Some(load.unwrap())
145+
})
146+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use actix_web::{post, web, HttpResponse};
2+
use serde::{Serialize, Deserialize};
3+
use crate::webserver::AppData;
4+
use crate::endpoints::check_session_id;
5+
use crate::jni::{JvmCommand, Method};
6+
use jni::sys::jlong;
7+
8+
#[derive(Deserialize)]
9+
pub struct GetMemRequest {
10+
session_id: String
11+
}
12+
13+
#[derive(Serialize)]
14+
pub struct GetMemResponse {
15+
status: i16,
16+
total_mem: Option<f64>,
17+
free_mem: Option<f64>,
18+
max_mem: Option<f64>
19+
}
20+
21+
#[post("/stats/mem")]
22+
pub fn post_get_mem(data: web::Data<AppData>, form: web::Form<GetMemRequest>) -> HttpResponse {
23+
let session_id_valid = check_session_id(&data, &form.session_id);
24+
if session_id_valid.is_err() {
25+
return HttpResponse::InternalServerError().finish();
26+
}
27+
28+
if !session_id_valid.unwrap() {
29+
return HttpResponse::Ok().json(GetMemResponse { status: 401, free_mem: None, total_mem: None, max_mem: None });
30+
}
31+
32+
let tx = data.jvm_command_tx.lock().unwrap();
33+
34+
let runtime_class_command = JvmCommand::get_class("java/lang/Runtime");
35+
tx.send(runtime_class_command.0).expect("An error occurred while sending the JvmCommand 'runtime_class_command'");
36+
let runtime_class = runtime_class_command.1.recv().unwrap();
37+
38+
let get_runtime_method = Method::static_method(runtime_class, "getRuntime", "()Ljava/lang/Runtime;", Vec::new());
39+
let get_runtime_command = JvmCommand::exec_method(get_runtime_method);
40+
tx.send(get_runtime_command.0).expect("An error occurred while sending the JvmCommand 'get_runtime_command'");
41+
let runtime_object = get_runtime_command.1.recv().unwrap();
42+
43+
let total_mem_method = Method::method(runtime_object, "totalMemory", "()J", Vec::new());
44+
let total_mem_command = JvmCommand::exec_method(total_mem_method);
45+
tx.send(total_mem_command.0).expect("An error occurred while sending the JvmCommand 'total_mem_command'");
46+
let total_mem_jobject = total_mem_command.1.recv().unwrap();
47+
48+
let free_mem_method = Method::method(runtime_object, "freeMemory", "()J", Vec::new());
49+
let free_mem_command = JvmCommand::exec_method(free_mem_method);
50+
tx.send(free_mem_command.0).expect("An error occurred while sending the JvmCommand 'free_mem_command'");
51+
let free_mem_jobject = free_mem_command.1.recv().unwrap();
52+
53+
let max_mem_method = Method::method(runtime_object, "maxMemory", "()J", Vec::new());
54+
let max_mem_command = JvmCommand::exec_method(max_mem_method);
55+
tx.send(max_mem_command.0).expect("An error occurred while sending the JvmCommand 'max_mem_command'");
56+
let max_mem_jobject = max_mem_command.1.recv().unwrap();
57+
58+
//the objects are of type *mut _jobject, so is jlong, so we can cast them directly
59+
//since we know the methods we called return a java long, i.e i64.
60+
//We then cast them to f64, so we can floating point division when we convert the
61+
//values from Bytes to Megabytes
62+
let total_mem = total_mem_jobject as jlong as f64;
63+
let free_mem = free_mem_jobject as jlong as f64;
64+
let max_mem = max_mem_jobject as jlong as f64;
65+
66+
const BYTE_TO_MB_FACTOR: f64 = 1_000_000f64;
67+
68+
//Some more information about the memory metrics, see: https://stackoverflow.com/a/18375641/10765090
69+
let response = GetMemResponse {
70+
status: 200,
71+
total_mem: Some(total_mem / BYTE_TO_MB_FACTOR),
72+
free_mem: Some(free_mem / BYTE_TO_MB_FACTOR),
73+
max_mem: Some(max_mem / BYTE_TO_MB_FACTOR)
74+
};
75+
76+
HttpResponse::Ok().json(response)
77+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod cpu;
2+
pub mod mem;

librconsole/src/jni/mod.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,12 @@ impl Method {
141141
args: The Arguments to be passed to the method. The order should be the same as provided in the method's signature. If no arguments are needed, an empty Vec should be provided
142142
*/
143143
#[allow(dead_code)]
144-
pub fn method(obj: jobject, name: String, sig: String, args: Vec<Argument>) -> Method {
144+
pub fn method(obj: jobject, name: &str, sig: &str, args: Vec<Argument>) -> Method {
145145
Method {
146146
class: None,
147147
obj: Some(obj),
148-
name,
149-
sig,
148+
name: name.to_string(),
149+
sig: sig.to_string(),
150150
args
151151
}
152152
}
@@ -166,7 +166,13 @@ pub enum Argument {
166166

167167
pub fn jvm_command_exec(env: JNIEnv, rx: Receiver<JvmCommand>) {
168168
loop {
169-
let received_cmd = rx.recv().unwrap();
169+
let received_cmd_wrapped = rx.recv();
170+
if received_cmd_wrapped.is_err() {
171+
eprintln!("An error occurred while receiving on the jvm_command_exec Receiver: {:?}", received_cmd_wrapped.err().unwrap());
172+
std::process::exit(1);
173+
}
174+
175+
let received_cmd = received_cmd_wrapped.unwrap();
170176
match received_cmd.intent {
171177
Intent::Log(log_item) => {
172178
match log_item.level {

librconsole/src/jni/web_server_native.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ pub extern "system" fn Java_nl_thedutchmc_rconsole_webserver_Native_startWebServ
8080

8181
//Start the HTTP server
8282
std::thread::spawn(move || {
83-
let _ = crate::webserver::start(config,database_file_path, static_files_path, jvm_command_tx);
83+
let webserver_start = crate::webserver::start(config,database_file_path, static_files_path, jvm_command_tx);
84+
if webserver_start.is_err() {
85+
panic!("An error occurred while running the Actix web server: {:?}", webserver_start.err().unwrap());
86+
}
8487
});
8588

8689
jvm_command_exec(env, jvm_command_rx);

librconsole/src/webserver.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ pub async fn start(config: Config, db_path: String, static_files_path: String, j
3232
.service(crate::endpoints::console::logs_new::post_logs_new)
3333
.service(crate::endpoints::console::execute_command::post_execute_command)
3434
.service(crate::endpoints::auth::login::post_login)
35+
.service(crate::endpoints::stats::cpu::post_get_load_avg)
36+
.service(crate::endpoints::stats::mem::post_get_mem)
3537
.service(Files::new("/", &static_files_path)
3638
.prefer_utf8(true)
3739
.index_file("index.html")
3840
.show_files_listing()
3941
)
4042
})
41-
.bind(format!("0.0.0.0:{}", port))?
4243
.bind(format!("[::]:{}", port))?
4344
.run()
4445
.await

0 commit comments

Comments
 (0)