Versão 0.6.0: saúde do disco no estilo do CrystalDiskInfo e nova logo
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
SMART completo (estado com % de vida útil, temperatura, horas e vezes ligado, total lido/gravado, ficha do dispositivo e tabela de atributos ATA e NVMe), leitura pelo disco que contém a partição e comando `guara health`. Nova logo: a cabeça do guará recortada das trilhas de um disco, nas cores do app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f65e7ae615
commit
c3b5f79009
22 files changed
+638
-139
No files matched your search
@@ -32,6 +32,13 @@ enum Cmd {
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Saúde do disco (SMART): estado, temperatura, uso e tabela de atributos
|
||||
Health {
|
||||
/// Dispositivo ou partição (o SMART lido é o do disco que a contém)
|
||||
path: String,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Procura arquivos apagados/perdidos (metadados + assinatura)
|
||||
Scan(ScanArgs),
|
||||
/// Continua uma varredura pausada (Ctrl+C) a partir do arquivo de sessão
|
||||
@@ -206,6 +213,15 @@ fn run(cli: Cli) -> Result<(), String> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Cmd::Health { path, json } => {
|
||||
let v = eng.rpc("health", json!({ "path": path })).map_err(s)?;
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&v).unwrap());
|
||||
} else {
|
||||
print_health(&v);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Cmd::Scan(a) => {
|
||||
let opts = ScanOptions {
|
||||
mode: if a.quick {
|
||||
@@ -702,3 +718,98 @@ fn paint(code: &str, s: &str) -> String {
|
||||
fn s<E: std::fmt::Display>(e: E) -> String {
|
||||
e.to_string()
|
||||
}
|
||||
|
||||
fn print_health(v: &Value) {
|
||||
let st = v["status"].as_str().unwrap_or("unknown");
|
||||
if !v["available"].as_bool().unwrap_or(false) {
|
||||
println!("{}", v["summary"].as_str().unwrap_or(""));
|
||||
return;
|
||||
}
|
||||
let word = match st {
|
||||
"ok" => paint("32", "Bom"),
|
||||
"warning" => paint("33", "Atenção"),
|
||||
"failing" => paint("31", "Ruim"),
|
||||
_ => "?".into(),
|
||||
};
|
||||
let pct = v["health_pct"]
|
||||
.as_u64()
|
||||
.map(|p| format!(" ({p}%)"))
|
||||
.unwrap_or_default();
|
||||
println!("Disco: {}", v["disk"].as_str().unwrap_or(""));
|
||||
if let Some(p) = v["partition"].as_str() {
|
||||
println!(" (o SMART é do disco inteiro que contém {p})");
|
||||
}
|
||||
println!(
|
||||
"Estado: {word}{pct} {}",
|
||||
v["summary"].as_str().unwrap_or("")
|
||||
);
|
||||
let num = |k: &str| v[k].as_i64();
|
||||
let bytes = |k: &str| v[k].as_u64().map(human);
|
||||
let mut m = Vec::new();
|
||||
if let Some(t) = num("temperature_c") {
|
||||
m.push(format!("Temperatura: {t} °C"));
|
||||
}
|
||||
if let Some(h) = num("power_on_hours") {
|
||||
m.push(format!("Horas ligado: {h}"));
|
||||
}
|
||||
if let Some(c) = num("power_cycles") {
|
||||
m.push(format!("Vezes ligado: {c}"));
|
||||
}
|
||||
if let Some(r) = bytes("host_reads") {
|
||||
m.push(format!("Total lido: {r}"));
|
||||
}
|
||||
if let Some(w) = bytes("host_writes") {
|
||||
m.push(format!("Total gravado: {w}"));
|
||||
}
|
||||
if !m.is_empty() {
|
||||
println!("{}", m.join(" · "));
|
||||
}
|
||||
if st != "ok" {
|
||||
for kv in v["details"].as_array().into_iter().flatten() {
|
||||
println!(
|
||||
" ! {}: {}",
|
||||
kv[0].as_str().unwrap_or(""),
|
||||
kv[1].as_str().unwrap_or("")
|
||||
);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
for kv in v["info"].as_array().into_iter().flatten() {
|
||||
println!(
|
||||
" {:<24}{}",
|
||||
kv[0].as_str().unwrap_or(""),
|
||||
kv[1].as_str().unwrap_or("")
|
||||
);
|
||||
}
|
||||
let attrs = v["attributes"].as_array().cloned().unwrap_or_default();
|
||||
if !attrs.is_empty() {
|
||||
let o = |x: &Value| {
|
||||
x.as_i64()
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or_else(|| "-".into())
|
||||
};
|
||||
println!(
|
||||
"\n ID {:<34}{:>6}{:>6}{:>7} Bruto",
|
||||
"Atributo", "Atual", "Pior", "Limite"
|
||||
);
|
||||
for a in attrs {
|
||||
let dot = match a["status"].as_str() {
|
||||
Some("failing") => paint("31", "●"),
|
||||
Some("warning") => paint("33", "●"),
|
||||
_ => paint("32", "●"),
|
||||
};
|
||||
println!(
|
||||
" {dot} {:<3} {:<34}{:>6}{:>6}{:>7} {}",
|
||||
a["id"].as_str().unwrap_or(""),
|
||||
a["name"].as_str().unwrap_or(""),
|
||||
o(&a["value"]),
|
||||
o(&a["worst"]),
|
||||
o(&a["thresh"]),
|
||||
a["raw"].as_str().unwrap_or("")
|
||||
);
|
||||
}
|
||||
}
|
||||
for adv in v["advice"].as_array().into_iter().flatten() {
|
||||
println!("\n{}", adv.as_str().unwrap_or(""));
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,37 @@ pub struct Health {
|
||||
pub summary: String,
|
||||
pub details: Vec<(String, String)>,
|
||||
pub advice: Vec<String>,
|
||||
/// Ficha do dispositivo, no estilo do CrystalDiskInfo (modelo, firmware, interface...).
|
||||
pub info: Vec<(String, String)>,
|
||||
/// Saúde estimada em % (SSD: vida útil restante). `None` em HD, que não informa isso.
|
||||
pub health_pct: Option<u8>,
|
||||
pub temperature_c: Option<i64>,
|
||||
pub power_on_hours: Option<i64>,
|
||||
pub power_cycles: Option<i64>,
|
||||
/// Total lido e gravado pelo computador, em bytes, quando o disco informa.
|
||||
pub host_reads: Option<u64>,
|
||||
pub host_writes: Option<u64>,
|
||||
/// Tabela completa de atributos SMART.
|
||||
pub attributes: Vec<Attr>,
|
||||
/// Caminho avaliado de fato (o disco inteiro, quando `path` é uma partição).
|
||||
pub disk: String,
|
||||
/// Partição pedida, quando a avaliação foi feita no disco que a contém.
|
||||
pub partition: Option<String>,
|
||||
}
|
||||
|
||||
/// Uma linha da tabela de atributos SMART.
|
||||
#[derive(Debug, Clone, Serialize, Default)]
|
||||
pub struct Attr {
|
||||
/// Identificador em hexadecimal, como o CrystalDiskInfo mostra ("05", "C5").
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
/// Valores normalizados (ATA): atual, pior já visto e limite de falha.
|
||||
pub value: Option<i64>,
|
||||
pub worst: Option<i64>,
|
||||
pub thresh: Option<i64>,
|
||||
pub raw: String,
|
||||
/// "ok", "warning" ou "failing".
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl Health {
|
||||
@@ -44,14 +75,47 @@ pub fn check(path: &str) -> Health {
|
||||
if !is_dev {
|
||||
return Health::unknown("Imagem de disco: não há SMART para avaliar.");
|
||||
}
|
||||
let mut h = match run_smartctl(path) {
|
||||
let disk = parent_disk(path);
|
||||
let mut h = match run_smartctl(&disk) {
|
||||
Some(v) => parse_smartctl(&v),
|
||||
None => Health::unknown("O SMART não pôde ser lido. Instale o pacote smartmontools (comando smartctl) e rode como administrador para ver a saúde do disco."),
|
||||
};
|
||||
kernel_errors(path, &mut h);
|
||||
if disk != path {
|
||||
h.partition = Some(path.to_string());
|
||||
}
|
||||
h.disk = disk;
|
||||
h
|
||||
}
|
||||
|
||||
/// O SMART é do disco, não da partição: `/dev/sda1` vira `/dev/sda`,
|
||||
/// `/dev/nvme0n1p2` vira `/dev/nvme0n1` e `/dev/disk2s1` vira `/dev/disk2`.
|
||||
pub fn parent_disk(path: &str) -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let name = path.trim_start_matches("/dev/");
|
||||
let sys = format!("/sys/class/block/{name}");
|
||||
if std::path::Path::new(&format!("{sys}/partition")).exists() {
|
||||
if let Ok(real) = std::fs::canonicalize(&sys) {
|
||||
if let Some(p) = real.parent().and_then(|p| p.file_name()) {
|
||||
return format!("/dev/{}", p.to_string_lossy());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
for pre in ["/dev/rdisk", "/dev/disk"] {
|
||||
if let Some(rest) = path.strip_prefix(pre) {
|
||||
if let Some(i) = rest.find('s') {
|
||||
return format!("{pre}{}", &rest[..i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
path.to_string()
|
||||
}
|
||||
|
||||
fn smartctl_name(path: &str) -> String {
|
||||
// Windows: \\.\PhysicalDriveN -> /dev/sdX (convenção do smartctl)
|
||||
if let Some(n) = path
|
||||
@@ -70,7 +134,7 @@ fn smartctl_name(path: &str) -> String {
|
||||
fn run_smartctl(path: &str) -> Option<Value> {
|
||||
let dev = smartctl_name(path);
|
||||
let out = Command::new("smartctl")
|
||||
.args(["--json=o", "-H", "-A", "-i", "-l", "error", &dev])
|
||||
.args(["--json=o", "-a", &dev])
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.output()
|
||||
@@ -111,6 +175,8 @@ pub fn parse_smartctl(v: &Value) -> Health {
|
||||
bad = bad.max(sev);
|
||||
h.details.push((k.to_string(), val));
|
||||
};
|
||||
fill_info(v, &mut h);
|
||||
fill_attributes(v, &mut h);
|
||||
if let Some(m) = v["model_name"].as_str() {
|
||||
h.details.push(("Modelo".into(), m.to_string()));
|
||||
}
|
||||
@@ -225,6 +291,9 @@ pub fn parse_smartctl(v: &Value) -> Health {
|
||||
}
|
||||
}
|
||||
}
|
||||
if h.attributes.iter().any(|a| a.status == "failing") {
|
||||
bad = 2;
|
||||
}
|
||||
match bad {
|
||||
2 => {
|
||||
h.status = "failing".into();
|
||||
@@ -241,6 +310,225 @@ pub fn parse_smartctl(v: &Value) -> Health {
|
||||
h
|
||||
}
|
||||
|
||||
fn s(v: &Value, p: &str) -> Option<String> {
|
||||
v.pointer(p)
|
||||
.and_then(|x| x.as_str())
|
||||
.map(|x| x.trim().to_string())
|
||||
.filter(|x| !x.is_empty())
|
||||
}
|
||||
fn n(v: &Value, p: &str) -> Option<i64> {
|
||||
v.pointer(p).and_then(|x| x.as_i64())
|
||||
}
|
||||
|
||||
/// Ficha do dispositivo e métricas gerais (temperatura, horas, total lido/gravado, saúde %).
|
||||
fn fill_info(v: &Value, h: &mut Health) {
|
||||
let mut put = |k: &str, val: Option<String>| {
|
||||
if let Some(val) = val {
|
||||
h.info.push((k.to_string(), val));
|
||||
}
|
||||
};
|
||||
put(
|
||||
"Modelo",
|
||||
s(v, "/model_name").or_else(|| {
|
||||
let a = s(v, "/scsi_vendor")?;
|
||||
Some(format!("{a} {}", s(v, "/scsi_product").unwrap_or_default()))
|
||||
}),
|
||||
);
|
||||
put("Firmware", s(v, "/firmware_version"));
|
||||
put("Nº de série", s(v, "/serial_number"));
|
||||
put("Família", s(v, "/model_family"));
|
||||
let proto = s(v, "/device/protocol");
|
||||
let iface = s(v, "/sata_version/string").or_else(|| {
|
||||
let sp = v
|
||||
.pointer("/nvme_pci_vendor")
|
||||
.map(|_| "NVM Express".to_string());
|
||||
sp.or_else(|| proto.clone())
|
||||
});
|
||||
put("Interface", iface);
|
||||
let speed = |p: &str| s(v, &format!("/interface_speed/{p}/string"));
|
||||
put(
|
||||
"Modo de transferência",
|
||||
match (speed("current"), speed("max")) {
|
||||
(Some(c), Some(m)) if c != m => Some(format!("{c} (máx. {m})")),
|
||||
(Some(c), _) => Some(c),
|
||||
(None, Some(m)) => Some(m),
|
||||
_ => None,
|
||||
},
|
||||
);
|
||||
put(
|
||||
"Capacidade",
|
||||
n(v, "/user_capacity/bytes")
|
||||
.or_else(|| n(v, "/nvme_total_capacity"))
|
||||
.map(|b| decimal_size(b as u64)),
|
||||
);
|
||||
put(
|
||||
"Rotação",
|
||||
n(v, "/rotation_rate").map(|r| {
|
||||
if r == 0 {
|
||||
"SSD (sem rotação)".into()
|
||||
} else {
|
||||
format!("{r} RPM")
|
||||
}
|
||||
}),
|
||||
);
|
||||
put("Formato", s(v, "/form_factor/name"));
|
||||
let mut feats = Vec::new();
|
||||
if v.pointer("/smart_support/enabled") == Some(&Value::Bool(true))
|
||||
|| v.get("nvme_smart_health_information_log").is_some()
|
||||
{
|
||||
feats.push("S.M.A.R.T.");
|
||||
}
|
||||
if v.pointer("/trim/supported") == Some(&Value::Bool(true)) {
|
||||
feats.push("TRIM");
|
||||
}
|
||||
if v.pointer("/write_cache/enabled") == Some(&Value::Bool(true)) {
|
||||
feats.push("Cache de gravação");
|
||||
}
|
||||
if v.get("ata_security").and_then(|x| x.get("enabled")) == Some(&Value::Bool(true)) {
|
||||
feats.push("Segurança ATA");
|
||||
}
|
||||
if !feats.is_empty() {
|
||||
put("Recursos", Some(feats.join(", ")));
|
||||
}
|
||||
|
||||
h.temperature_c = n(v, "/temperature/current");
|
||||
h.power_on_hours = n(v, "/power_on_time/hours");
|
||||
h.power_cycles = n(v, "/power_cycle_count");
|
||||
if let Some(l) = v.get("nvme_smart_health_information_log") {
|
||||
// a unidade NVMe vale 1000 setores de 512 bytes
|
||||
h.host_reads = l["data_units_read"].as_u64().map(|u| u * 512_000);
|
||||
h.host_writes = l["data_units_written"].as_u64().map(|u| u * 512_000);
|
||||
h.health_pct = l["percentage_used"]
|
||||
.as_i64()
|
||||
.map(|u| (100 - u.clamp(0, 100)) as u8);
|
||||
h.power_cycles = h.power_cycles.or(l["power_cycles"].as_i64());
|
||||
} else {
|
||||
let lba = n(v, "/logical_block_size").unwrap_or(512) as u64;
|
||||
h.host_reads = attr(v, 242).map(|x| x as u64 * lba);
|
||||
h.host_writes = attr(v, 241).map(|x| x as u64 * lba);
|
||||
// SSD SATA: vida útil restante no valor normalizado de um destes atributos
|
||||
// (231 SSD Life Left, 233 Media Wearout, 177 Wear Leveling, 202 Percent Lifetime Remain)
|
||||
h.health_pct = [231u64, 233, 177, 202, 169]
|
||||
.iter()
|
||||
.find_map(|&id| attr_field(v, id, "value"))
|
||||
.filter(|_| n(v, "/rotation_rate") == Some(0))
|
||||
.map(|x| x.clamp(0, 100) as u8);
|
||||
}
|
||||
}
|
||||
|
||||
/// Capacidade como o fabricante anuncia (base 1000): "500,1 GB".
|
||||
fn decimal_size(b: u64) -> String {
|
||||
let units = ["B", "kB", "MB", "GB", "TB", "PB"];
|
||||
let mut x = b as f64;
|
||||
let mut i = 0;
|
||||
while x >= 1000.0 && i < units.len() - 1 {
|
||||
x /= 1000.0;
|
||||
i += 1;
|
||||
}
|
||||
format!("{x:.1} {}", units[i]).replace('.', ",")
|
||||
}
|
||||
|
||||
fn attr_field(v: &Value, id: u64, f: &str) -> Option<i64> {
|
||||
v.pointer("/ata_smart_attributes/table")?
|
||||
.as_array()?
|
||||
.iter()
|
||||
.find(|a| a["id"] == id)?
|
||||
.get(f)?
|
||||
.as_i64()
|
||||
}
|
||||
|
||||
/// Atributos cujo valor bruto acima de zero já é motivo de atenção.
|
||||
const RAW_WARN: [u64; 5] = [5, 196, 197, 198, 187];
|
||||
|
||||
fn fill_attributes(v: &Value, h: &mut Health) {
|
||||
if let Some(t) = v
|
||||
.pointer("/ata_smart_attributes/table")
|
||||
.and_then(|t| t.as_array())
|
||||
{
|
||||
for a in t {
|
||||
let id = a["id"].as_u64().unwrap_or(0);
|
||||
let value = a["value"].as_i64();
|
||||
let thresh = a["thresh"].as_i64();
|
||||
let raw_n = a
|
||||
.pointer("/raw/value")
|
||||
.and_then(|x| x.as_i64())
|
||||
.unwrap_or(0);
|
||||
let failing = a
|
||||
.pointer("/when_failed")
|
||||
.and_then(|x| x.as_str())
|
||||
.is_some_and(|w| w == "now")
|
||||
|| matches!((value, thresh), (Some(v), Some(t)) if t > 0 && v <= t);
|
||||
let status = if failing {
|
||||
"failing"
|
||||
} else if RAW_WARN.contains(&id) && raw_n > 0 {
|
||||
"warning"
|
||||
} else {
|
||||
"ok"
|
||||
};
|
||||
h.attributes.push(Attr {
|
||||
id: format!("{id:02X}"),
|
||||
name: a["name"]
|
||||
.as_str()
|
||||
.unwrap_or("Unknown_Attribute")
|
||||
.replace('_', " "),
|
||||
value,
|
||||
worst: a["worst"].as_i64(),
|
||||
thresh,
|
||||
raw: a
|
||||
.pointer("/raw/string")
|
||||
.and_then(|x| x.as_str())
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| raw_n.to_string()),
|
||||
status: status.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(l) = v.get("nvme_smart_health_information_log") {
|
||||
// mesma ordem e nomes que o CrystalDiskInfo usa para NVMe
|
||||
let rows: [(u8, &str, &str); 15] = [
|
||||
(1, "Critical Warning", "critical_warning"),
|
||||
(2, "Composite Temperature", "temperature"),
|
||||
(3, "Available Spare", "available_spare"),
|
||||
(4, "Available Spare Threshold", "available_spare_threshold"),
|
||||
(5, "Percentage Used", "percentage_used"),
|
||||
(6, "Data Units Read", "data_units_read"),
|
||||
(7, "Data Units Written", "data_units_written"),
|
||||
(8, "Host Read Commands", "host_reads"),
|
||||
(9, "Host Write Commands", "host_writes"),
|
||||
(10, "Controller Busy Time", "controller_busy_time"),
|
||||
(11, "Power Cycles", "power_cycles"),
|
||||
(12, "Power On Hours", "power_on_hours"),
|
||||
(13, "Unsafe Shutdowns", "unsafe_shutdowns"),
|
||||
(14, "Media and Data Integrity Errors", "media_errors"),
|
||||
(
|
||||
15,
|
||||
"Number of Error Information Log Entries",
|
||||
"num_err_log_entries",
|
||||
),
|
||||
];
|
||||
let spare_low = matches!((l["available_spare"].as_i64(), l["available_spare_threshold"].as_i64()), (Some(s), Some(t)) if s < t);
|
||||
for (id, name, key) in rows {
|
||||
let Some(x) = l.get(key).and_then(|x| x.as_i64()) else {
|
||||
continue;
|
||||
};
|
||||
let status = match key {
|
||||
"critical_warning" | "media_errors" if x > 0 => "failing",
|
||||
"available_spare" if spare_low => "failing",
|
||||
"percentage_used" if x >= 100 => "failing",
|
||||
"percentage_used" if x >= 90 => "warning",
|
||||
_ => "ok",
|
||||
};
|
||||
h.attributes.push(Attr {
|
||||
id: format!("{id:02X}"),
|
||||
name: name.into(),
|
||||
raw: x.to_string(),
|
||||
status: status.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contadores de erro de E/S do kernel (Linux): reforçam o aviso do SMART.
|
||||
fn kernel_errors(path: &str, h: &mut Health) {
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -51,6 +51,66 @@ fn smart_saudavel_nvme_desgastado_e_nvme_critico() {
|
||||
assert_eq!(health::parse_smartctl(&media).status, "failing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smart_ssd_sata_traz_ficha_metricas_e_tabela() {
|
||||
let v = json!({
|
||||
"device": {"protocol": "ATA"},
|
||||
"model_name": "Samsung SSD 860 EVO 500GB", "firmware_version": "RVT04B6Q",
|
||||
"serial_number": "S3Z1NB0K", "sata_version": {"string": "SATA 3.2"},
|
||||
"interface_speed": {"max": {"string": "6.0 Gb/s"}, "current": {"string": "6.0 Gb/s"}},
|
||||
"user_capacity": {"bytes": 500107862016u64}, "logical_block_size": 512, "rotation_rate": 0,
|
||||
"smart_support": {"enabled": true}, "trim": {"supported": true},
|
||||
"smart_status": {"passed": true}, "temperature": {"current": 31},
|
||||
"power_on_time": {"hours": 12034}, "power_cycle_count": 811,
|
||||
"ata_smart_attributes": {"table": [
|
||||
{"id": 5, "name": "Reallocated_Sector_Ct", "value": 100, "worst": 100, "thresh": 10, "raw": {"value": 0, "string": "0"}},
|
||||
{"id": 177, "name": "Wear_Leveling_Count", "value": 92, "worst": 92, "thresh": 0, "raw": {"value": 41, "string": "41"}},
|
||||
{"id": 241, "name": "Total_LBAs_Written", "value": 99, "worst": 99, "thresh": 0, "raw": {"value": 2000000000u64, "string": "2000000000"}}
|
||||
]}
|
||||
});
|
||||
let h = health::parse_smartctl(&v);
|
||||
assert_eq!(h.status, "ok");
|
||||
assert_eq!(h.health_pct, Some(92));
|
||||
assert_eq!(h.temperature_c, Some(31));
|
||||
assert_eq!(h.power_cycles, Some(811));
|
||||
assert_eq!(h.host_writes, Some(2_000_000_000 * 512));
|
||||
assert!(h
|
||||
.info
|
||||
.iter()
|
||||
.any(|(k, v)| k == "Capacidade" && v == "500,1 GB"));
|
||||
assert!(h
|
||||
.info
|
||||
.iter()
|
||||
.any(|(k, v)| k == "Recursos" && v.contains("TRIM")));
|
||||
assert_eq!(h.attributes.len(), 3);
|
||||
assert_eq!(h.attributes[0].id, "05");
|
||||
assert_eq!(h.attributes[0].name, "Reallocated Sector Ct");
|
||||
assert_eq!(h.attributes[1].id, "B1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smart_atributo_abaixo_do_limite_reprova_e_nvme_vira_tabela() {
|
||||
let v = json!({"smart_status": {"passed": true}, "ata_smart_attributes": {"table": [
|
||||
{"id": 1, "name": "Raw_Read_Error_Rate", "value": 40, "worst": 40, "thresh": 51, "raw": {"value": 9}}
|
||||
]}});
|
||||
let h = health::parse_smartctl(&v);
|
||||
assert_eq!(h.attributes[0].status, "failing");
|
||||
assert_eq!(h.status, "failing");
|
||||
|
||||
let nv = json!({"smart_status": {"passed": true}, "nvme_smart_health_information_log": {
|
||||
"critical_warning": 0, "temperature": 40, "available_spare": 100, "available_spare_threshold": 10,
|
||||
"percentage_used": 3, "data_units_read": 1000, "data_units_written": 2000,
|
||||
"power_cycles": 50, "power_on_hours": 900, "unsafe_shutdowns": 4, "media_errors": 0}});
|
||||
let h = health::parse_smartctl(&nv);
|
||||
assert_eq!(h.health_pct, Some(97));
|
||||
assert_eq!(h.host_writes, Some(2000 * 512_000));
|
||||
assert_eq!(h.power_cycles, Some(50));
|
||||
assert!(h
|
||||
.attributes
|
||||
.iter()
|
||||
.any(|a| a.id == "0D" && a.name == "Unsafe Shutdowns" && a.raw == "4"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imagem_de_disco_nao_tem_smart() {
|
||||
let f = tempfile::NamedTempFile::new().unwrap();
|
||||
|
||||
Reference in new issue
Block a user