removed languages.json and territories.json
Some checks failed
Build Zenyx ⚡ / 🧪 Run Cargo Tests (pull_request) Failing after 6m9s
Build Zenyx ⚡ / 🏗️ Build aarch64-apple-darwin (pull_request) Has been skipped
Build Zenyx ⚡ / 🏗️ Build aarch64-pc-windows-msvc (pull_request) Has been skipped
Build Zenyx ⚡ / 🏗️ Build aarch64-unknown-linux-gnu (pull_request) Has been skipped
Build Zenyx ⚡ / 🏗️ Build x86_64-apple-darwin (pull_request) Has been skipped
Build Zenyx ⚡ / 🏗️ Build x86_64-pc-windows-msvc (pull_request) Has been skipped
Build Zenyx ⚡ / 🏗️ Build x86_64-unknown-linux-gnu (pull_request) Has been skipped
Some checks failed
Build Zenyx ⚡ / 🧪 Run Cargo Tests (pull_request) Failing after 6m9s
Build Zenyx ⚡ / 🏗️ Build aarch64-apple-darwin (pull_request) Has been skipped
Build Zenyx ⚡ / 🏗️ Build aarch64-pc-windows-msvc (pull_request) Has been skipped
Build Zenyx ⚡ / 🏗️ Build aarch64-unknown-linux-gnu (pull_request) Has been skipped
Build Zenyx ⚡ / 🏗️ Build x86_64-apple-darwin (pull_request) Has been skipped
Build Zenyx ⚡ / 🏗️ Build x86_64-pc-windows-msvc (pull_request) Has been skipped
Build Zenyx ⚡ / 🏗️ Build x86_64-unknown-linux-gnu (pull_request) Has been skipped
lib.rs is separated into src/modules now for cleaner code optimized error handling to -> (storage.rs, uptime.rs, meta.rs, memory.rs, os.rs, network.rs) optimized struct types to -> (storage.rs, uptime.rs, meta.rs, memory.rs, os.rs, network.rs)
This commit is contained in:
parent
91c80d0f91
commit
528d4b03a3
28 changed files with 1261 additions and 1726 deletions
69
subcrates/telemetry/src/modules/cpu.rs
Normal file
69
subcrates/telemetry/src/modules/cpu.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use super::Defaults::Unknown;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::HashMap;
|
||||
use sysinfo::{CpuRefreshKind, RefreshKind, System};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ProcessorInfo {
|
||||
vendor: String,
|
||||
brand: String,
|
||||
total_system_cores: String,
|
||||
threads: u16,
|
||||
architecture: String,
|
||||
byte_order: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CPUInfo {
|
||||
cpus: Vec<ProcessorInfo>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_struct() -> CPUInfo {
|
||||
let system =
|
||||
System::new_with_specifics(RefreshKind::nothing().with_cpu(CpuRefreshKind::everything()));
|
||||
|
||||
let mut processors = Vec::new();
|
||||
let mut processor_count: HashMap<String, ProcessorInfo> = HashMap::new();
|
||||
|
||||
for cpu in system.cpus() {
|
||||
let brand = cpu.brand().trim();
|
||||
let total_cores = System::physical_core_count();
|
||||
|
||||
if processor_count.contains_key(brand) {
|
||||
processor_count.get_mut(brand).unwrap().threads += 1;
|
||||
continue;
|
||||
}
|
||||
processor_count.insert(
|
||||
brand.to_string(),
|
||||
ProcessorInfo {
|
||||
vendor: cpu.vendor_id().trim().to_string(),
|
||||
brand: brand.to_string(),
|
||||
total_system_cores: if let Some(cores) = total_cores {
|
||||
cores.to_string()
|
||||
} else {
|
||||
Unknown.into()
|
||||
},
|
||||
threads: 1,
|
||||
architecture: System::cpu_arch().trim().to_string(),
|
||||
byte_order: if cfg!(target_endian = "little") {
|
||||
String::from("little-endian")
|
||||
} else {
|
||||
String::from("big-endian")
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for (_brand, info) in processor_count {
|
||||
processors.push(info);
|
||||
}
|
||||
|
||||
CPUInfo { cpus: processors }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_json() -> Value {
|
||||
json!(get_struct())
|
||||
}
|
59
subcrates/telemetry/src/modules/devices.rs
Normal file
59
subcrates/telemetry/src/modules/devices.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
//use crate::custom_anyhow;
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
extern crate hidapi;
|
||||
use hidapi::HidApi;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DeviceInfo {
|
||||
manufacturer: String,
|
||||
products: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DevicesInfo {
|
||||
devices: Vec<DeviceInfo>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_struct() -> Result<DevicesInfo> {
|
||||
let api = HidApi::new().unwrap();
|
||||
let mut grouped_devices: HashMap<String, Vec<String>> = HashMap::new();
|
||||
|
||||
for device in api.device_list() {
|
||||
let manufacturer = device
|
||||
.manufacturer_string()
|
||||
.unwrap_or("Unknown")
|
||||
.to_string()
|
||||
.to_lowercase();
|
||||
let product = device.product_string().unwrap_or("Unknown").to_string();
|
||||
|
||||
if !manufacturer.trim().is_empty() && !product.trim().is_empty() {
|
||||
let entry = grouped_devices.entry(manufacturer).or_default();
|
||||
|
||||
if !entry.contains(&product) {
|
||||
entry.push(product);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut devices = Vec::new();
|
||||
let mut grouped_vec: Vec<_> = grouped_devices.into_iter().collect();
|
||||
grouped_vec.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
|
||||
|
||||
for (manufacturer, products) in grouped_vec {
|
||||
devices.push(DeviceInfo {
|
||||
manufacturer,
|
||||
products,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(DevicesInfo { devices })
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_json() -> Result<Value> {
|
||||
Ok(json!(get_struct()?))
|
||||
}
|
63
subcrates/telemetry/src/modules/external.rs
Normal file
63
subcrates/telemetry/src/modules/external.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use std::{cmp::Ordering::Equal, collections::HashMap};
|
||||
use sysinfo::{System, Users};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SoftwareInfo {
|
||||
name: String,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ExternalInfo {
|
||||
softwares: Vec<SoftwareInfo>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_struct() -> ExternalInfo {
|
||||
let mut system = System::new_all();
|
||||
system.refresh_all();
|
||||
|
||||
let users = &Users::new_with_refreshed_list();
|
||||
let mut grouped_processes: HashMap<String, Vec<i32>> = HashMap::new();
|
||||
|
||||
for process in system.processes().values() {
|
||||
let name = process.name().to_str().unwrap().to_string();
|
||||
let user_id = process.user_id();
|
||||
let is_user_owned = user_id.is_some_and(|uid| {
|
||||
users
|
||||
.list()
|
||||
.iter()
|
||||
.any(|u| u.id() == uid && u.name() != "root")
|
||||
});
|
||||
|
||||
if is_user_owned && !name.trim().is_empty() && !name.starts_with('[') {
|
||||
grouped_processes
|
||||
.entry(name)
|
||||
.or_default()
|
||||
.push(process.pid().as_u32() as i32);
|
||||
}
|
||||
}
|
||||
|
||||
let mut softwares = Vec::new();
|
||||
let mut grouped_vec: Vec<_> = grouped_processes.into_iter().collect();
|
||||
grouped_vec.sort_by(|a, b| match b.1.len().cmp(&a.1.len()) {
|
||||
Equal => a.0.to_lowercase().cmp(&b.0.to_lowercase()),
|
||||
other => other,
|
||||
});
|
||||
|
||||
for (name, pids) in grouped_vec {
|
||||
softwares.push(SoftwareInfo {
|
||||
name,
|
||||
count: pids.len(),
|
||||
});
|
||||
}
|
||||
|
||||
ExternalInfo { softwares }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_json() -> Value {
|
||||
json!(get_struct())
|
||||
}
|
55
subcrates/telemetry/src/modules/formatted/architecture.rs
Normal file
55
subcrates/telemetry/src/modules/formatted/architecture.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
use serde::Serialize;
|
||||
use std::{
|
||||
fmt::{Debug, Display, Formatter, Result as FmtResult},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize)]
|
||||
pub enum TargetPointerWidth {
|
||||
W32Bit,
|
||||
W64Bit,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct FmtOSArchitecture(pub TargetPointerWidth);
|
||||
|
||||
impl FmtOSArchitecture {
|
||||
pub const W32_BIT: Self = FmtOSArchitecture(TargetPointerWidth::W32Bit);
|
||||
pub const W64_BIT: Self = FmtOSArchitecture(TargetPointerWidth::W64Bit);
|
||||
}
|
||||
|
||||
impl Display for FmtOSArchitecture {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self.0 {
|
||||
TargetPointerWidth::W32Bit => "32 Bit",
|
||||
TargetPointerWidth::W64Bit => "64 Bit",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for FmtOSArchitecture {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for FmtOSArchitecture {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FmtOSArchitecture {
|
||||
type Target = TargetPointerWidth;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
38
subcrates/telemetry/src/modules/formatted/bytes.rs
Normal file
38
subcrates/telemetry/src/modules/formatted/bytes.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use serde::Serialize;
|
||||
use std::{
|
||||
fmt::{Debug, Display, Formatter, Result as FmtResult},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct FmtBytes(pub u64);
|
||||
|
||||
impl Display for FmtBytes {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
use humansize::{DECIMAL, format_size};
|
||||
write!(f, "{}", format_size(self.0, DECIMAL))
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for FmtBytes {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for FmtBytes {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FmtBytes {
|
||||
type Target = u64;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
125
subcrates/telemetry/src/modules/formatted/date_time.rs
Normal file
125
subcrates/telemetry/src/modules/formatted/date_time.rs
Normal file
|
@ -0,0 +1,125 @@
|
|||
use chrono::{DateTime, Local, TimeZone, Utc};
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
fmt::{Debug, Display, Formatter, Result as FMTResult},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DateTimeInfo {
|
||||
date: String,
|
||||
time: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FmtDateTime<Tz: TimeZone>(pub DateTime<Tz>);
|
||||
|
||||
impl<Tz: TimeZone> Display for FmtDateTime<Tz>
|
||||
where
|
||||
Tz: TimeZone,
|
||||
Tz::Offset: std::fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FMTResult {
|
||||
write!(
|
||||
f,
|
||||
"{:#?}",
|
||||
DateTimeInfo {
|
||||
date: self.0.format("%Y-%m-%d").to_string(),
|
||||
time: self.0.format("%H:%M:%S").to_string()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Tz: TimeZone> Debug for FmtDateTime<Tz>
|
||||
where
|
||||
Tz::Offset: std::fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FMTResult {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Tz: TimeZone> Serialize for FmtDateTime<Tz>
|
||||
where
|
||||
DateTime<Tz>: Display,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Tz: TimeZone> Deref for FmtDateTime<Tz> {
|
||||
type Target = DateTime<Tz>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<Tz: TimeZone> Eq for FmtDateTime<Tz> where DateTime<Tz>: Eq {}
|
||||
impl<Tz: TimeZone> PartialEq for FmtDateTime<Tz>
|
||||
where
|
||||
DateTime<Tz>: PartialEq,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<Tz: TimeZone> PartialOrd for FmtDateTime<Tz>
|
||||
where
|
||||
DateTime<Tz>: PartialOrd,
|
||||
{
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Tz: TimeZone> Ord for FmtDateTime<Tz>
|
||||
where
|
||||
DateTime<Tz>: Ord,
|
||||
{
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.0.cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FmtDateTime<Utc>> for FmtDateTime<Local> {
|
||||
fn from(dt: FmtDateTime<Utc>) -> Self {
|
||||
FmtDateTime(dt.0.with_timezone(&Local))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FmtDateTime<Local>> for FmtDateTime<Utc> {
|
||||
fn from(dt: FmtDateTime<Local>) -> Self {
|
||||
FmtDateTime(dt.0.with_timezone(&Utc))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub trait IntoDateTime {
|
||||
fn into_utc(self) -> FmtDateTime<Utc>;
|
||||
fn into_local(self) -> FmtDateTime<Local>;
|
||||
}
|
||||
|
||||
impl IntoDateTime for FmtDateTime<Local> {
|
||||
fn into_utc(self) -> FmtDateTime<Utc> {
|
||||
FmtDateTime(self.0.with_timezone(&Utc))
|
||||
}
|
||||
fn into_local(self) -> FmtDateTime<Local> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDateTime for FmtDateTime<Utc> {
|
||||
fn into_local(self) -> FmtDateTime<Local> {
|
||||
FmtDateTime(self.0.with_timezone(&Local))
|
||||
}
|
||||
fn into_utc(self) -> FmtDateTime<Utc> {
|
||||
self
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
use detect_desktop_environment::DesktopEnvironment;
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::{
|
||||
fmt::{Debug, Display, Formatter, Result as FmtResult},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct SerdeDE(pub DesktopEnvironment);
|
||||
|
||||
impl Serialize for SerdeDE {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(&format!("{:?}", self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl SerdeDE {
|
||||
pub fn detect() -> Option<Self> {
|
||||
DesktopEnvironment::detect().map(SerdeDE)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct FmtDE(pub SerdeDE);
|
||||
|
||||
impl FmtDE {
|
||||
pub fn detect() -> Option<Self> {
|
||||
SerdeDE::detect().map(FmtDE)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FmtDE {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
write!(f, "{:?}", self.0.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for FmtDE {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for FmtDE {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FmtDE {
|
||||
type Target = SerdeDE;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
7
subcrates/telemetry/src/modules/formatted/mod.rs
Normal file
7
subcrates/telemetry/src/modules/formatted/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
pub mod architecture;
|
||||
pub mod bytes;
|
||||
pub mod date_time;
|
||||
pub mod desktop_environment;
|
||||
pub mod offset_time;
|
||||
pub mod relative_time;
|
||||
pub mod version;
|
68
subcrates/telemetry/src/modules/formatted/offset_time.rs
Normal file
68
subcrates/telemetry/src/modules/formatted/offset_time.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use chrono::{DateTime, Local, Offset};
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt::{Debug, Display, Formatter, Result as FmtResult},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct FmtOffsetTime(pub DateTime<Local>);
|
||||
|
||||
impl Display for FmtOffsetTime {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
write!(f, "UTC{}", self.0.offset())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for FmtOffsetTime {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for FmtOffsetTime {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FmtOffsetTime {
|
||||
type Target = DateTime<Local>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for FmtOffsetTime {}
|
||||
impl PartialEq for FmtOffsetTime {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.offset().fix() == other.0.offset().fix()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for FmtOffsetTime {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(
|
||||
self.0
|
||||
.offset()
|
||||
.fix()
|
||||
.local_minus_utc()
|
||||
.cmp(&other.0.offset().fix().local_minus_utc()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for FmtOffsetTime {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0
|
||||
.offset()
|
||||
.fix()
|
||||
.local_minus_utc()
|
||||
.cmp(&other.0.offset().fix().local_minus_utc())
|
||||
}
|
||||
}
|
40
subcrates/telemetry/src/modules/formatted/relative_time.rs
Normal file
40
subcrates/telemetry/src/modules/formatted/relative_time.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use serde::Serialize;
|
||||
use std::{
|
||||
fmt::{Debug, Display, Formatter, Result as FmtResult},
|
||||
ops::Deref,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct FmtRelativeTime(pub Duration);
|
||||
|
||||
impl Display for FmtRelativeTime {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
let formatter = timeago::Formatter::new();
|
||||
|
||||
write!(f, "{}", formatter.convert(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for FmtRelativeTime {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for FmtRelativeTime {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FmtRelativeTime {
|
||||
type Target = Duration;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
38
subcrates/telemetry/src/modules/formatted/version.rs
Normal file
38
subcrates/telemetry/src/modules/formatted/version.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use serde::Serialize;
|
||||
use std::{
|
||||
fmt::{Debug, Display, Formatter, Result as FmtResult},
|
||||
ops::Deref,
|
||||
};
|
||||
use versions::Versioning;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct FmtVersion(pub Versioning);
|
||||
|
||||
impl Display for FmtVersion {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for FmtVersion {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for FmtVersion {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FmtVersion {
|
||||
type Target = Versioning;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
131
subcrates/telemetry/src/modules/gpu/mod.rs
Normal file
131
subcrates/telemetry/src/modules/gpu/mod.rs
Normal file
|
@ -0,0 +1,131 @@
|
|||
use super::Defaults::Unknown;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use wgpu;
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, EventLoop},
|
||||
window::WindowId,
|
||||
};
|
||||
|
||||
mod vram;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DriverInfo {
|
||||
version: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AdapterInfo {
|
||||
vendor: String,
|
||||
model: String,
|
||||
driver: DriverInfo,
|
||||
vram: String,
|
||||
display: DisplayInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct GPUInfo {
|
||||
supported_backends: Vec<String>,
|
||||
gpus: Vec<AdapterInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Default)]
|
||||
pub struct DisplayInfo {
|
||||
resolution: String,
|
||||
refresh_rate: String,
|
||||
}
|
||||
|
||||
impl ApplicationHandler for DisplayInfo {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if let Some(monitor) = event_loop.primary_monitor() {
|
||||
let size = monitor.size();
|
||||
let refresh_rate = monitor.refresh_rate_millihertz();
|
||||
|
||||
self.resolution = format!("{}x{}", size.width, size.height);
|
||||
self.refresh_rate = if let Some(refresh) = refresh_rate {
|
||||
format!("{} hz", refresh / 1000)
|
||||
} else {
|
||||
Unknown.into()
|
||||
}
|
||||
} else {
|
||||
self.resolution = Unknown.into();
|
||||
self.refresh_rate = Unknown.into();
|
||||
}
|
||||
|
||||
event_loop.exit();
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
_event_loop: &ActiveEventLoop,
|
||||
_window_id: WindowId,
|
||||
_event: WindowEvent,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
fn vendor_name(vendor: u32) -> &'static str {
|
||||
match vendor {
|
||||
0x10DE => "NVIDIA",
|
||||
0x1002 => "AMD(Advanced Micro Devices), Inc.",
|
||||
0x8086 => "Intel(integrated electronics)",
|
||||
0x13B5 => "ARM(Advanced RISC Machines)",
|
||||
0x5143 => "Qualcomm(Quality Communications)",
|
||||
0x1010 => "Apple Inc.",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_struct() -> GPUInfo {
|
||||
let mut gpu_data: Vec<AdapterInfo> = Vec::new();
|
||||
let mut backends: Vec<String> = Vec::new();
|
||||
let instance_descriptor = wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::all(),
|
||||
flags: wgpu::InstanceFlags::empty(),
|
||||
backend_options: Default::default(),
|
||||
};
|
||||
let instance = wgpu::Instance::new(&instance_descriptor);
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let mut app = DisplayInfo::default();
|
||||
|
||||
event_loop.run_app(&mut app).unwrap();
|
||||
|
||||
for adapter in instance.enumerate_adapters(wgpu::Backends::all()) {
|
||||
let info = adapter.get_info();
|
||||
|
||||
if !backends.contains(&info.backend.to_string()) {
|
||||
backends.push(info.backend.to_string());
|
||||
}
|
||||
if info.driver.is_empty() || info.driver_info.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
gpu_data.push(AdapterInfo {
|
||||
vendor: vendor_name(info.vendor).to_string(),
|
||||
model: info.name,
|
||||
driver: DriverInfo {
|
||||
version: info.driver_info,
|
||||
name: info.driver,
|
||||
},
|
||||
vram: vram::get(info.vendor, info.device),
|
||||
display: DisplayInfo {
|
||||
resolution: app.resolution.to_string(),
|
||||
refresh_rate: app.refresh_rate.to_string(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
GPUInfo {
|
||||
supported_backends: backends,
|
||||
gpus: gpu_data,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_json() -> Value {
|
||||
json!(get_struct())
|
||||
}
|
67
subcrates/telemetry/src/modules/gpu/vram.rs
Normal file
67
subcrates/telemetry/src/modules/gpu/vram.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use super::super::Defaults::Unknown;
|
||||
use ash::vk::{API_VERSION_1_2, ApplicationInfo, InstanceCreateInfo, MemoryHeapFlags};
|
||||
use humansize::{DECIMAL, make_format};
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn get_metal() -> u64 {
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_metal() -> u64 {
|
||||
use metal::Device as MetalDevice;
|
||||
|
||||
let device = MetalDevice::system_default().expect("No Metal-compatible GPU found");
|
||||
device.recommended_max_working_set_size()
|
||||
}
|
||||
|
||||
pub fn get_vulkan(device_id: u32) -> u64 {
|
||||
let entry = unsafe { ash::Entry::load().unwrap() };
|
||||
let app_info = ApplicationInfo {
|
||||
p_application_name: std::ptr::null(),
|
||||
application_version: 0,
|
||||
p_engine_name: std::ptr::null(),
|
||||
engine_version: 0,
|
||||
api_version: API_VERSION_1_2,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let create_info = InstanceCreateInfo {
|
||||
p_application_info: &app_info,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let instance = unsafe { entry.create_instance(&create_info, None).unwrap() };
|
||||
|
||||
let physical_devices = unsafe { instance.enumerate_physical_devices().unwrap() };
|
||||
let mut total_vram = 0;
|
||||
|
||||
for device in physical_devices {
|
||||
let memory_properties = unsafe { instance.get_physical_device_memory_properties(device) };
|
||||
let device_properties = unsafe { instance.get_physical_device_properties(device) };
|
||||
|
||||
if device_id != device_properties.device_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
for heap in memory_properties.memory_heaps {
|
||||
if heap.flags.contains(MemoryHeapFlags::DEVICE_LOCAL) {
|
||||
total_vram += heap.size;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
total_vram
|
||||
}
|
||||
|
||||
pub fn get(vendor: u32, device_id: u32) -> String {
|
||||
let formatter = make_format(DECIMAL);
|
||||
|
||||
match vendor {
|
||||
0x10DE | 0x1002 | 0x8086 | 0x5143 => formatter(get_vulkan(device_id)),
|
||||
0x1010 => formatter(get_metal()),
|
||||
_ => Unknown.into(),
|
||||
}
|
||||
}
|
11
subcrates/telemetry/src/modules/macros/custom_anyhow.rs
Normal file
11
subcrates/telemetry/src/modules/macros/custom_anyhow.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_export]
|
||||
macro_rules! custom_anyhow {
|
||||
($fmt:literal $(, $arg:expr)* $(,)?) => {
|
||||
::anyhow::anyhow!(
|
||||
concat!($fmt, " (file: {}, line: {})"),
|
||||
$($arg,)*
|
||||
file!(),
|
||||
line!()
|
||||
)
|
||||
};
|
||||
}
|
1
subcrates/telemetry/src/modules/macros/mod.rs
Normal file
1
subcrates/telemetry/src/modules/macros/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod custom_anyhow;
|
49
subcrates/telemetry/src/modules/memory.rs
Normal file
49
subcrates/telemetry/src/modules/memory.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use crate::modules::FmtBytes;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use sysinfo::System;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PhysicalInfo {
|
||||
total: FmtBytes,
|
||||
used: FmtBytes,
|
||||
free: FmtBytes,
|
||||
available: FmtBytes,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct VirtualInfo {
|
||||
total: FmtBytes,
|
||||
used: FmtBytes,
|
||||
free: FmtBytes,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MemoryInfo {
|
||||
physical: PhysicalInfo,
|
||||
virtual_swap: VirtualInfo,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_struct() -> MemoryInfo {
|
||||
let system = System::new_all();
|
||||
|
||||
MemoryInfo {
|
||||
physical: PhysicalInfo {
|
||||
total: FmtBytes(system.total_memory()),
|
||||
used: FmtBytes(system.used_memory()),
|
||||
free: FmtBytes(system.free_memory()),
|
||||
available: FmtBytes(system.available_memory()),
|
||||
},
|
||||
virtual_swap: VirtualInfo {
|
||||
total: FmtBytes(system.total_swap()),
|
||||
used: FmtBytes(system.used_swap()),
|
||||
free: FmtBytes(system.free_swap()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_json() -> Value {
|
||||
json!(get_struct())
|
||||
}
|
33
subcrates/telemetry/src/modules/meta.rs
Normal file
33
subcrates/telemetry/src/modules/meta.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use crate::{custom_anyhow, modules::FmtVersion};
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use sys_locale::get_locale;
|
||||
use versions::Versioning;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MetaInfo {
|
||||
version: FmtVersion,
|
||||
commit_hash: String,
|
||||
locale: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_struct() -> Result<MetaInfo> {
|
||||
let locale = get_locale().unwrap_or_else(|| String::from("en-US"));
|
||||
let version = Versioning::new(env!("CARGO_PKG_VERSION"))
|
||||
.ok_or(custom_anyhow!("Invalid version").context("Invalid Semver version"))?;
|
||||
|
||||
Ok(MetaInfo {
|
||||
version: FmtVersion(version),
|
||||
commit_hash: GIT_COMMIT_HASH.to_string(),
|
||||
locale,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_json() -> Result<Value> {
|
||||
Ok(json!(get_struct()?))
|
||||
}
|
42
subcrates/telemetry/src/modules/mod.rs
Normal file
42
subcrates/telemetry/src/modules/mod.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
|
||||
|
||||
pub mod cpu;
|
||||
pub mod devices;
|
||||
pub mod external;
|
||||
pub mod formatted;
|
||||
pub mod gpu;
|
||||
pub mod macros;
|
||||
pub mod memory;
|
||||
pub mod meta;
|
||||
pub mod network;
|
||||
pub mod os;
|
||||
pub mod storage;
|
||||
pub mod uptime;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use formatted::{
|
||||
architecture::FmtOSArchitecture,
|
||||
bytes::FmtBytes,
|
||||
date_time::{FmtDateTime, IntoDateTime},
|
||||
desktop_environment::FmtDE,
|
||||
offset_time::FmtOffsetTime,
|
||||
relative_time::FmtRelativeTime,
|
||||
version::FmtVersion,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Defaults {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Display for Defaults {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Defaults> for String {
|
||||
fn from(value: Defaults) -> Self {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
25
subcrates/telemetry/src/modules/network.rs
Normal file
25
subcrates/telemetry/src/modules/network.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use sysinfo::Networks;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NetworkInfo {
|
||||
adapters: Vec<String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_struct() -> NetworkInfo {
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
let mut adapters = Vec::new();
|
||||
|
||||
for (interface_name, _network_data) in &networks {
|
||||
adapters.push(interface_name.to_string());
|
||||
}
|
||||
|
||||
NetworkInfo { adapters }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_json() -> Value {
|
||||
json!(get_struct())
|
||||
}
|
54
subcrates/telemetry/src/modules/os.rs
Normal file
54
subcrates/telemetry/src/modules/os.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use super::Defaults::Unknown;
|
||||
use crate::{
|
||||
custom_anyhow,
|
||||
modules::{FmtDE, FmtOSArchitecture},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use sysinfo::System;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct OSInfo {
|
||||
name: String,
|
||||
edition: String,
|
||||
version: String,
|
||||
architecture: FmtOSArchitecture,
|
||||
kernel: Option<String>,
|
||||
desktop_environment: Option<FmtDE>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_struct() -> Result<OSInfo> {
|
||||
let architecture = if cfg!(target_pointer_width = "64") {
|
||||
Ok(FmtOSArchitecture::W64_BIT)
|
||||
} else if cfg!(target_pointer_width = "32") {
|
||||
Ok(FmtOSArchitecture::W32_BIT)
|
||||
} else {
|
||||
Err(custom_anyhow!("Unsupported pointer width").context("16 bit systems are not supported"))
|
||||
}?;
|
||||
let kernel = if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
|
||||
Some(System::kernel_long_version())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let desktop_environment = if cfg!(target_os = "linux") {
|
||||
FmtDE::detect()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(OSInfo {
|
||||
name: System::name().unwrap_or(Unknown.into()),
|
||||
edition: System::long_os_version().unwrap_or(Unknown.into()),
|
||||
version: System::os_version().unwrap_or(Unknown.into()),
|
||||
architecture,
|
||||
kernel,
|
||||
desktop_environment,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_json() -> Result<Value> {
|
||||
Ok(json!(get_struct()?))
|
||||
}
|
46
subcrates/telemetry/src/modules/storage.rs
Normal file
46
subcrates/telemetry/src/modules/storage.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
use crate::{custom_anyhow, modules::FmtBytes};
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
pub use sysinfo::DiskKind;
|
||||
use sysinfo::Disks;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct StorageInfo {
|
||||
name: String,
|
||||
mount_point: String,
|
||||
disk_kind: DiskKind,
|
||||
space_left: FmtBytes,
|
||||
total: FmtBytes,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_list() -> Result<Vec<StorageInfo>> {
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
let mut drive_data = Vec::new();
|
||||
|
||||
for disk in disks.list() {
|
||||
drive_data.push(StorageInfo {
|
||||
name: format!("{:?}", disk.name()).trim_matches('\"').to_string(),
|
||||
mount_point: format!("{:?}", disk.mount_point())
|
||||
.replace("\\", "")
|
||||
.trim_matches('\"')
|
||||
.to_string(),
|
||||
disk_kind: disk.kind(),
|
||||
space_left: FmtBytes(disk.available_space()),
|
||||
total: FmtBytes(disk.total_space()),
|
||||
});
|
||||
}
|
||||
|
||||
if drive_data.is_empty() {
|
||||
return Err(custom_anyhow!("No storage drives found")
|
||||
.context("Drive_data is empty, expected disks.list() to return non-zero at line 39"));
|
||||
}
|
||||
|
||||
Ok(drive_data)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_json() -> Result<Value> {
|
||||
Ok(json!(get_list()?))
|
||||
}
|
54
subcrates/telemetry/src/modules/uptime.rs
Normal file
54
subcrates/telemetry/src/modules/uptime.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use crate::{
|
||||
custom_anyhow,
|
||||
modules::{FmtDateTime, FmtOffsetTime, FmtRelativeTime},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Local, Utc};
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use std::time::Duration as StdDuration;
|
||||
use sysinfo::System;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct DateInfo {
|
||||
time_offset: FmtOffsetTime,
|
||||
local_date_time: FmtDateTime<Local>,
|
||||
utc_date_time: FmtDateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UptimeInfo {
|
||||
boot: DateInfo,
|
||||
now: DateInfo,
|
||||
relative: FmtRelativeTime,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_struct() -> Result<UptimeInfo> {
|
||||
let boot_time_utc = DateTime::<Utc>::from_timestamp(System::boot_time() as i64, 0).ok_or(
|
||||
custom_anyhow!("Invalid, or out of range timestamp")
|
||||
.context("Invalid timestamp: check seconds < 8_220_000_000_000"),
|
||||
)?;
|
||||
let boot_time_local: DateTime<Local> = boot_time_utc.with_timezone(&Local);
|
||||
let relative_time =
|
||||
StdDuration::from_secs((Local::now() - boot_time_local).num_seconds() as u64);
|
||||
|
||||
Ok(UptimeInfo {
|
||||
boot: DateInfo {
|
||||
time_offset: FmtOffsetTime(Local::now()),
|
||||
local_date_time: FmtDateTime(boot_time_local),
|
||||
utc_date_time: FmtDateTime(boot_time_utc),
|
||||
},
|
||||
now: DateInfo {
|
||||
time_offset: FmtOffsetTime(Local::now()),
|
||||
local_date_time: FmtDateTime(Local::now()),
|
||||
utc_date_time: FmtDateTime(Utc::now()),
|
||||
},
|
||||
relative: FmtRelativeTime(relative_time),
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_json() -> Result<Value> {
|
||||
Ok(json!(get_struct()?))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue