feat: initial commit of diagd - unified Linux/BSD/macOS/Windows diagnostic tool (TUI + GUI)

This commit is contained in:
2026-09-13 18:29:47 +03:00
commit c3a9569918
165 changed files with 31940 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Rust / Cargo
/target/
**/*.rs.bk
*.pdb
# Ephemeral Flutter folders
linux/flutter/ephemeral/
windows/flutter/ephemeral/
macos/Flutter/ephemeral/
.flutter-plugins
.flutter-plugins-dependencies
# Debug archives
*.tar.gz
*.zip
# Widget Preview related
.widget_preview/
+33
View File
@@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "e8113bf45620cbeb8aff64947ee4c93e16adb4cf"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: e8113bf45620cbeb8aff64947ee4c93e16adb4cf
base_revision: e8113bf45620cbeb8aff64947ee4c93e16adb4cf
- platform: macos
create_revision: e8113bf45620cbeb8aff64947ee4c93e16adb4cf
base_revision: e8113bf45620cbeb8aff64947ee4c93e16adb4cf
- platform: windows
create_revision: e8113bf45620cbeb8aff64947ee4c93e16adb4cf
base_revision: e8113bf45620cbeb8aff64947ee4c93e16adb4cf
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
Generated
+2496
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
[workspace]
resolver = "2"
members = [
"crates/core",
"rust",
]
+30
View File
@@ -0,0 +1,30 @@
.PHONY: all tui gui clean help
all: tui gui
help:
@echo "Использование: make [цель]"
@echo ""
@echo "Цели:"
@echo " tui Собрать консольный интерактивный TUI (Linux / BSD, без Flutter/GTK)"
@echo " gui Собрать графический Flutter Desktop GUI (Linux GTK)"
@echo " all Собрать оба интерфейса"
@echo " clean Очистить артефакты сборки Cargo и Flutter"
@echo ""
@echo "Примеры:"
@echo " make tui -> target/release/diagd"
@echo " make gui -> build/linux/x64/release/bundle/diagd"
tui:
@echo "==> Сборка автономного TUI (Ratatui, Linux + BSD)..."
cargo build --release -p rust_lib_diagd --no-default-features --features tui --bin diagd
@echo "✓ Готово: target/release/diagd"
gui:
@echo "==> Сборка графического интерфейса GUI (Flutter Desktop Linux)..."
flutter build linux --release
@echo "✓ Готово: build/linux/x64/release/bundle/diagd"
clean:
cargo clean
flutter clean
+17
View File
@@ -0,0 +1,17 @@
# diagd
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+36
View File
@@ -0,0 +1,36 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- build/**
- linux/**
- rust_builder/cargokit/**
- windows/**
- macos/**
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+48
View File
@@ -0,0 +1,48 @@
@echo off
setlocal
set TARGET=%1
if "%TARGET%"=="" set TARGET=help
if "%TARGET%"=="tui" goto build_tui
if "%TARGET%"=="gui" goto build_gui
if "%TARGET%"=="all" goto build_all
if "%TARGET%"=="help" goto show_help
:show_help
echo Использование: build.bat {tui^|gui^|all}
echo.
echo tui - Собрать автономный консольный TUI (diagd.exe)
echo gui - Собрать графический интерфейс Flutter Desktop (Windows)
echo all - Собрать оба варианта
exit /b 0
:build_tui
echo ==^> Сборка автономного консольного TUI под Windows...
cargo build --release -p rust_lib_diagd --no-default-features --features tui --bin diagd
if %ERRORLEVEL% equ 0 (
echo [OK] Успешно собрано: target\release\diagd.exe
) else (
echo [ERROR] Ошибка сборки TUI
exit /b %ERRORLEVEL%
)
exit /b 0
:build_gui
echo ==^> Сборка графического GUI (Flutter Desktop Windows)...
flutter build windows --release
if %ERRORLEVEL% equ 0 (
echo [OK] Успешно собрано: build\windows\x64\runner\Release\diagd.exe
) else (
echo [ERROR] Ошибка сборки GUI
exit /b %ERRORLEVEL%
)
exit /b 0
:build_all
call :build_tui
if %ERRORLEVEL% neq 0 exit /b %ERRORLEVEL%
call :build_gui
if %ERRORLEVEL% neq 0 exit /b %ERRORLEVEL%
echo [OK] Все компоненты успешно собраны!
exit /b 0
Executable
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
cd "$DIR"
TARGET="${1:-help}"
case "$TARGET" in
tui)
echo "==> Сборка автономного консольного TUI (Linux / BSD)..."
cargo build --release -p rust_lib_diagd --no-default-features --features tui --bin diagd
echo "✓ Успешно собрано: $DIR/target/release/diagd"
;;
gui)
echo "==> Сборка графического GUI (Flutter Desktop Linux)..."
flutter build linux --release
echo "✓ Успешно собрано: $DIR/build/linux/x64/release/bundle/diagd"
;;
all)
echo "==> Сборка TUI и GUI..."
cargo build --release -p rust_lib_diagd --no-default-features --features tui --bin diagd
flutter build linux --release
echo "✓ Все интерфейсы собраны!"
;;
*)
echo "Использование: $0 {tui|gui|all}"
echo ""
echo " tui - собрать консольный TUI (на базе Ratatui, работает на Linux и BSD)"
echo " gui - собрать графический интерфейс (на базе Flutter Desktop для Linux)"
echo " all - собрать оба варианта"
exit 1
;;
esac
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "diagd-core"
version = "0.1.0"
edition = "2021"
description = "Core diagnostic engine for Linux and BSD systems"
[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
regex = "1.10"
reqwest = { version = "0.12", features = ["json"] }
chrono = { version = "0.4", features = ["serde"] }
libc = "0.2"
flate2 = "1.0"
tar = "0.4"
+810
View File
@@ -0,0 +1,810 @@
use crate::models::{
DiagnosticIssue, FixSuggestion, IssueItem, Severity, Subsystem, SubsystemStatus, SystemReport,
};
use crate::sanitize::DataSanitizer;
use crate::scanner::UnifiedRawData;
use serde::Serialize;
#[derive(Serialize)]
pub struct LlmDiagnosticPrompt {
pub role: String,
pub system_instruction: String,
pub os_family: String,
pub init_system: String,
pub is_root: bool,
pub sanitized_context: SanitizedContextPayload,
}
#[derive(Serialize)]
pub struct SanitizedContextPayload {
pub failed_services: Vec<FailedServiceSummary>,
pub kernel_summary: KernelSummaryPayload,
pub system_limits: LimitsSummaryPayload,
pub storage_alerts: Vec<String>,
pub hardware_summary: HardwareSummaryPayload,
pub network_summary: NetworkSummaryPayload,
pub raw_sanitized_logs: String,
}
#[derive(Serialize)]
pub struct HardwareSummaryPayload {
pub max_cpu_temp_c: f32,
pub thermal_throttled_cores: usize,
pub total_throttle_events: u64,
pub is_actively_throttling: bool,
pub battery_degraded: bool,
pub edac_memory_errors: u64,
}
#[derive(Serialize)]
pub struct NetworkSummaryPayload {
pub dns_broken_symlink: bool,
pub dns_resolution_ok: bool,
pub dns_latency_ms: Option<u128>,
pub has_default_gateway: bool,
pub firewall_conflict: Option<String>,
pub wifi_drops_count: usize,
}
#[derive(Serialize)]
pub struct FailedServiceSummary {
pub name: String,
pub init_system: String,
pub state: String,
pub error_snippet: String,
}
#[derive(Serialize)]
pub struct KernelSummaryPayload {
pub tainted_flags: Vec<String>,
pub oom_events_count: usize,
pub gpu_hangs_count: usize,
pub io_errors_count: usize,
pub csum_errors_count: usize,
pub permission_denied: bool,
}
#[derive(Serialize)]
pub struct LimitsSummaryPayload {
pub file_descriptors_exhausted: bool,
pub recent_coredumps: Vec<String>,
pub memory_pressure_avg10: f32,
pub io_pressure_avg10: f32,
}
impl LlmDiagnosticPrompt {
pub fn build(data: &UnifiedRawData) -> (String, Self) {
let failed_summary: Vec<FailedServiceSummary> = data
.failed_services
.iter()
.map(|s| FailedServiceSummary {
name: s.name.clone(),
init_system: s.init_system.clone(),
state: s.state.clone(),
error_snippet: DataSanitizer::sanitize(&s.logs)
.lines()
.take(6)
.collect::<Vec<_>>()
.join("\n"),
})
.collect();
let kernel_summary = KernelSummaryPayload {
tainted_flags: data.kernel_scan.tainted_flags.clone(),
oom_events_count: data.kernel_scan.oom_events.len(),
gpu_hangs_count: data.kernel_scan.gpu_hangs.len(),
io_errors_count: data.kernel_scan.io_errors.len(),
csum_errors_count: data.kernel_scan.csum_errors.len(),
permission_denied: data.kernel_scan.permission_denied,
};
let limits_summary = LimitsSummaryPayload {
file_descriptors_exhausted: data.limits_scan.file_descriptors_exhausted,
recent_coredumps: data.limits_scan.recent_coredumps.clone(),
memory_pressure_avg10: data.pressure_scan.mem_some_avg10,
io_pressure_avg10: data.pressure_scan.io_some_avg10,
};
let storage_alerts: Vec<String> = data
.storage_mounts
.iter()
.filter(|m| m.is_critical)
.map(|m| {
format!(
"Mount {}: space used {:.1}%, inode used {:.1}%",
m.mount_point, m.used_percent, m.inode_used_percent
)
})
.collect();
let hardware_summary = HardwareSummaryPayload {
max_cpu_temp_c: data.hardware_scan.max_cpu_temp_c,
thermal_throttled_cores: data.hardware_scan.thermal_throttled_cores,
total_throttle_events: data.hardware_scan.total_throttle_events,
is_actively_throttling: data.hardware_scan.is_actively_throttling,
battery_degraded: data.hardware_scan.battery_health.as_ref().map(|b| b.is_degraded).unwrap_or(false),
edac_memory_errors: data.hardware_scan.edac_ce_count + data.hardware_scan.edac_ue_count,
};
let network_summary = NetworkSummaryPayload {
dns_broken_symlink: data.network_deep_scan.dns_broken_symlink,
dns_resolution_ok: data.network_deep_scan.dns_resolution_ok,
dns_latency_ms: data.network_deep_scan.dns_latency_ms,
has_default_gateway: data.network_deep_scan.has_default_gateway,
firewall_conflict: data.network_deep_scan.firewall_conflict.clone(),
wifi_drops_count: data.network_deep_scan.wifi_drop_events.len(),
};
let mut combined_logs = String::new();
combined_logs.push_str(&DataSanitizer::sanitize(&data.kernel_scan.raw_logs));
combined_logs.push_str("\n");
combined_logs.push_str(&DataSanitizer::sanitize(&data.service_logs));
let payload = SanitizedContextPayload {
failed_services: failed_summary,
kernel_summary,
system_limits: limits_summary,
storage_alerts,
hardware_summary,
network_summary,
raw_sanitized_logs: combined_logs,
};
let prompt_obj = Self {
role: "Unix/Linux Systems Reliability Engineer & AI Diagnostic Analyst".to_string(),
system_instruction: format!(
"Analyze the sanitized {} ({}) system diagnostics. Identify all anomalies, assign severity (Critical/Warning/Info), determine exact root cause, and provide precise bash/sh commands specifically tailored for init system '{}' and this OS family.",
data.os_name, data.init_name, data.init_name
),
os_family: data.os_name.clone(),
init_system: data.init_name.clone(),
is_root: data.is_root,
sanitized_context: payload,
};
let json_preview = serde_json::to_string_pretty(&prompt_obj).unwrap_or_default();
(json_preview, prompt_obj)
}
}
pub async fn try_query_llm(prompt: &str) -> Option<String> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(4))
.build()
.ok()?;
let body = serde_json::json!({
"model": "llama3:latest",
"prompt": prompt,
"stream": false
});
let res = client
.post("http://127.0.0.1:11434/api/generate")
.json(&body)
.send()
.await
.ok()?;
if res.status().is_success() {
let json_res: serde_json::Value = res.json().await.ok()?;
json_res.get("response").and_then(|r| r.as_str()).map(|s| s.to_string())
} else {
None
}
}
pub fn heuristic_ai_analyze(data: &UnifiedRawData) -> (Vec<IssueItem>, Vec<SubsystemStatus>, String) {
let mut issues = Vec::new();
// 1. Анализ сбойных служб Init (systemd, OpenRC, runit, etc.)
let mut init_status = "Healthy".to_string();
let mut init_issue_count = 0;
for (idx, svc) in data.failed_services.iter().enumerate() {
init_issue_count += 1;
init_status = "Failed".to_string();
let clean_log = DataSanitizer::sanitize(&svc.logs);
let snippet = clean_log
.lines()
.take(10)
.collect::<Vec<_>>()
.join("\n");
issues.push(DiagnosticIssue {
id: format!("init-{}", idx + 1),
title: format!("Служба '{}' в состоянии сбоя ({})", svc.name, svc.init_system),
subsystem: Subsystem::Systemd,
severity: Severity::Critical,
root_cause: format!("Процесс службы {} аварийно остановился под управлением {}.", svc.name, svc.init_system),
explanation: svc.description.clone(),
fix: Some(FixSuggestion {
title: format!("Перезапуск службы {}", svc.name),
command: svc.restart_command.clone(),
explanation: format!("Команда перезапуска, адаптированная под систему инициализации {}.", svc.init_system),
}),
raw_log: if snippet.is_empty() { clean_log } else { snippet },
sanitized: true,
permission_denied: false,
});
}
// 2. Анализ Kernel Space
let mut kernel_status = "Healthy".to_string();
let mut kernel_issue_count = 0;
if data.kernel_scan.permission_denied {
issues.push(DiagnosticIssue {
id: "kern-perm".to_string(),
title: "Доступ к кольцевому буферу ядра (dmesg) ограничен".to_string(),
subsystem: Subsystem::Kernel,
severity: Severity::Warning,
root_cause: "В системе включен dmesg_restrict, чтение системных логов ядра требует прав суперпользователя.".to_string(),
explanation: "Сканирование продолжено в unprivileged-режиме. Для доступа к полному логу dmesg запустите приложение с правами sudo/pkexec.".to_string(),
fix: Some(FixSuggestion {
title: "Запуск диагностического сканирования с sudo".to_string(),
command: "sudo diagd-tui".to_string(),
explanation: "Предоставляет приложению временные привилегии для глубокого аудита оборудования.".to_string(),
}),
raw_log: "Operation not permitted reading dmesg/kmsg".to_string(),
sanitized: true,
permission_denied: true,
});
}
if !data.kernel_scan.oom_events.is_empty() {
kernel_status = "Failed".to_string();
kernel_issue_count += 1;
issues.push(DiagnosticIssue {
id: "kern-oom".to_string(),
title: "Сработал OOM-Killer: принудительное завершение процессов из-за нехватки памяти".to_string(),
subsystem: Subsystem::Kernel,
severity: Severity::Critical,
root_cause: "Система исчерпала физическую RAM и swap-пространство, ядро завершило наиболее прожорливые процессы.".to_string(),
explanation: "Необходимо проанализировать потребление памяти приложениями или увеличить объем файла подкачки (swap).".to_string(),
fix: Some(FixSuggestion {
title: "Проверка текущего распределения памяти и процессов".to_string(),
command: "free -h && ps aux --sort=-%mem | head -n 10".to_string(),
explanation: "Выводит доступную память и топ-10 процессов по потреблению RAM.".to_string(),
}),
raw_log: data.kernel_scan.oom_events.join("\n"),
sanitized: true,
permission_denied: false,
});
}
if !data.kernel_scan.gpu_hangs.is_empty() {
kernel_status = "Failed".to_string();
kernel_issue_count += 1;
issues.push(DiagnosticIssue {
id: "kern-gpu".to_string(),
title: "Зафиксировано зависание или аварийный сброс видеодрайвера (GPU Hang)".to_string(),
subsystem: Subsystem::Kernel,
severity: Severity::Critical,
root_cause: "Драйвер графического ускорителя сообщил о таймауте выполнения команд (Xid / amdgpu reset / ring hang).".to_string(),
explanation: "Возможен перегрев видеокарты, нестабильность драйвера или аппаратная деградация GPU.".to_string(),
fix: Some(FixSuggestion {
title: "Проверка статуса видеодрайвера".to_string(),
command: "nvidia-smi || lspci -k | grep -EA3 'VGA|3D'".to_string(),
explanation: "Запрашивает состояние видеоускорителя и используемый драйвер ядра.".to_string(),
}),
raw_log: data.kernel_scan.gpu_hangs.join("\n"),
sanitized: true,
permission_denied: false,
});
}
if !data.kernel_scan.csum_errors.is_empty() {
kernel_status = "Failed".to_string();
kernel_issue_count += 1;
issues.push(DiagnosticIssue {
id: "kern-csum".to_string(),
title: "Нарушение контрольных сумм данных (Btrfs/ZFS Checksum Error)".to_string(),
subsystem: Subsystem::Storage,
severity: Severity::Critical,
root_cause: "Файловая система обнаружила несовпадение csum при чтении блоков. Возможен 'silent data corruption'.".to_string(),
explanation: "Проверьте стабильность модулей оперативной памяти (memtest) и SMART-статус накопителя.".to_string(),
fix: Some(FixSuggestion {
title: "Запуск scrub для верификации и восстановления данных".to_string(),
command: "sudo btrfs scrub start / || sudo zpool scrub $(zpool list -H -o name)".to_string(),
explanation: "Инициирует полную проверку целостности данных файловой системы.".to_string(),
}),
raw_log: data.kernel_scan.csum_errors.join("\n"),
sanitized: true,
permission_denied: false,
});
}
// 3. Анализ давления PSI и системных лимитов
if data.pressure_scan.mem_some_avg10 > 25.0 {
issues.push(DiagnosticIssue {
id: "psi-mem".to_string(),
title: format!("Критическое давление на память (PSI Memory avg10: {:.1}%)", data.pressure_scan.mem_some_avg10),
subsystem: Subsystem::Kernel,
severity: Severity::Warning,
root_cause: "Процессы проводят значительное время в ожидании выделения страниц оперативной памяти.".to_string(),
explanation: "Высокий показатель PSI Memory указывает на активный трэшинг подсистемы подкачки (swap trashing).".to_string(),
fix: None,
raw_log: format!("PSI: mem_some_avg10={:.2}%, mem_full_avg10={:.2}%", data.pressure_scan.mem_some_avg10, data.pressure_scan.mem_full_avg10),
sanitized: true,
permission_denied: false,
});
}
if data.limits_scan.file_descriptors_exhausted {
issues.push(DiagnosticIssue {
id: "limit-fd".to_string(),
title: "Исчерпание системного лимита файловых дескрипторов (file-nr)".to_string(),
subsystem: Subsystem::Kernel,
severity: Severity::Critical,
root_cause: format!("Использовано {} из {} файловых дескрипторов (>95%).", data.limits_scan.file_nr_allocated, data.limits_scan.file_nr_max),
explanation: "Новые процессы и сетевые сокеты не смогут открываться, вызывая ошибку 'Too many open files'.".to_string(),
fix: Some(FixSuggestion {
title: "Увеличение лимита файловых дескрипторов fs.file-max".to_string(),
command: "sudo sysctl -w fs.file-max=2097152".to_string(),
explanation: "Увеличивает максимальное количество одновременно открытых файлов в системе.".to_string(),
}),
raw_log: format!("file-nr allocated: {}, max: {}", data.limits_scan.file_nr_allocated, data.limits_scan.file_nr_max),
sanitized: true,
permission_denied: false,
});
}
if !data.limits_scan.recent_coredumps.is_empty() {
issues.push(DiagnosticIssue {
id: "app-crash".to_string(),
title: "Зафиксированы недавние аварийные сбои приложений (Coredumps)".to_string(),
subsystem: Subsystem::Systemd,
severity: Severity::Warning,
root_cause: "Пользовательские или системные программы завершились с сигналом SIGSEGV, SIGABRT или SIGBUS.".to_string(),
explanation: "Служба coredumpctl сохранила дампы памяти упавших приложений для отладки.".to_string(),
fix: Some(FixSuggestion {
title: "Просмотр информации о последнем дампе памяти".to_string(),
command: "coredumpctl info".to_string(),
explanation: "Выводит стек вызовов и причину падения последнего сбойного процесса.".to_string(),
}),
raw_log: data.limits_scan.recent_coredumps.join("\n"),
sanitized: true,
permission_denied: false,
});
}
// 4. Анализ Storage Mounts (дисковое пространство и inode)
let mut storage_status = "Healthy".to_string();
let mut storage_issue_count = 0;
for mount in &data.storage_mounts {
if mount.is_critical {
storage_status = "Failed".to_string();
storage_issue_count += 1;
let reason = if mount.used_percent >= 90.0 {
format!("Заполнено {:.1}% дискового пространства на точке монтирования '{}'", mount.used_percent, mount.mount_point)
} else {
format!("Исчерпано {:.1}% inode на точке монтирования '{}'", mount.inode_used_percent, mount.mount_point)
};
issues.push(DiagnosticIssue {
id: format!("stor-{}", mount.mount_point.replace('/', "-")),
title: reason.clone(),
subsystem: Subsystem::Storage,
severity: Severity::Critical,
root_cause: reason,
explanation: format!("Переполнение раздела {} ({}) может заблокировать запись журналов и работу системных баз данных.", mount.mount_point, mount.filesystem),
fix: Some(FixSuggestion {
title: format!("Поиск 10 самых объемных директорий на {}", mount.mount_point),
command: format!("sudo du -xh --max-depth=1 {} | sort -hr | head -n 10", mount.mount_point),
explanation: "Локализует каталоги, занимающие максимальное пространство на проблемном разделе.".to_string(),
}),
raw_log: format!("Mount: {}, FSType: {}, Used: {:.1}%, Free: {} MB, Inodes Free: {}", mount.mount_point, mount.filesystem, mount.used_percent, mount.free_bytes / (1024 * 1024), mount.inodes_free),
sanitized: true,
permission_denied: false,
});
}
}
// 5. Анализ аппаратного обеспечения и перегрева (Hardware & Thermals)
if data.hardware_scan.total_throttle_events > 0 || data.hardware_scan.max_cpu_temp_c >= 90.0 {
kernel_issue_count += 1;
let is_crit = data.hardware_scan.max_cpu_temp_c >= 90.0;
issues.push(DiagnosticIssue {
id: "hw-throttling".to_string(),
title: format!(
"Зафиксирован Thermal Throttling CPU (макс. темп: {:.1}°C, событий: {})",
data.hardware_scan.max_cpu_temp_c, data.hardware_scan.total_throttle_events
),
subsystem: Subsystem::Kernel,
severity: if is_crit { Severity::Critical } else { Severity::Warning },
root_cause: format!(
"Температура ядер процессора достигла лимита троттлинга. Ядро зафиксировало {} событий сброса частоты на {} ядрах.",
data.hardware_scan.total_throttle_events, data.hardware_scan.thermal_throttled_cores
),
explanation: "Длительный перегрев приводит к деградации кремния и просадкам производительности. Проверьте кулеры и термопасту.".to_string(),
fix: Some(FixSuggestion {
title: "Мониторинг температур датчиков в реальном времени".to_string(),
command: "sensors || cat /sys/class/thermal/thermal_zone*/temp".to_string(),
explanation: "Отображает текущую температуру ядер и обороты вентиляторов охлаждения.".to_string(),
}),
raw_log: format!(
"CPU Temp: {:.1} C, Throttled Cores: {}, Total Events: {}",
data.hardware_scan.max_cpu_temp_c,
data.hardware_scan.thermal_throttled_cores,
data.hardware_scan.total_throttle_events
),
sanitized: true,
permission_denied: false,
});
}
if let Some(ref batt) = data.hardware_scan.battery_health {
if batt.is_degraded {
issues.push(DiagnosticIssue {
id: "hw-battery".to_string(),
title: format!("Деградация аккумулятора {} (остаток: {:.1}%, циклов: {})", batt.name, batt.health_percent, batt.cycle_count),
subsystem: Subsystem::Kernel,
severity: Severity::Warning,
root_cause: format!("Здоровье батареи снизилось до {:.1}% от проектной емкости при {} циклах перезарядки.", batt.health_percent, batt.cycle_count),
explanation: "Износ батареи может приводить к внезапным отключениям ноутбука под пиковой нагрузкой.".to_string(),
fix: Some(FixSuggestion {
title: "Подробный отчет энергопотребления".to_string(),
command: "upower -i $(upower -e | grep 'BAT')".to_string(),
explanation: "Показывает детальную статистику батареи и контроллера питания.".to_string(),
}),
raw_log: format!("Battery: {}, Capacity: {}%, Health: {:.1}%, Cycles: {}", batt.name, batt.capacity_percent, batt.health_percent, batt.cycle_count),
sanitized: true,
permission_denied: false,
});
}
}
if data.hardware_scan.edac_ue_count > 0 {
kernel_status = "Failed".to_string();
kernel_issue_count += 1;
issues.push(DiagnosticIssue {
id: "hw-edac-ue".to_string(),
title: format!("Критические неисправимые ошибки памяти (EDAC UE: {})", data.hardware_scan.edac_ue_count),
subsystem: Subsystem::Kernel,
severity: Severity::Critical,
root_cause: "Контроллер памяти зафиксировал неисправимые (Uncorrected) аппаратные сбои DRAM.".to_string(),
explanation: "Возможен дефект планки оперативной памяти. Рекомендуется немедленная замена модуля RAM.".to_string(),
fix: Some(FixSuggestion {
title: "Просмотр журналов EDAC".to_string(),
command: "edac-util -v || dmesg | grep -i edac".to_string(),
explanation: "Локализует проблемный слот памяти (DIMM / Channel).".to_string(),
}),
raw_log: format!("EDAC Uncorrected Errors: {}, Corrected Errors: {}", data.hardware_scan.edac_ue_count, data.hardware_scan.edac_ce_count),
sanitized: true,
permission_denied: false,
});
}
// 6. Анализ сети, DNS и брандмауэра (Network & DNS)
let mut network_status = "Healthy".to_string();
let mut network_issue_count = 0;
if data.network_deep_scan.dns_broken_symlink {
network_status = "Failed".to_string();
network_issue_count += 1;
issues.push(DiagnosticIssue {
id: "net-resolv-symlink".to_string(),
title: "Сломанная ссылка /etc/resolv.conf (битый симлинк DNS)".to_string(),
subsystem: Subsystem::Network,
severity: Severity::Critical,
root_cause: "Файл /etc/resolv.conf указывает на несуществующий целевой путь (systemd-resolved или сетевой менеджер не инициализирован).".to_string(),
explanation: "Программы не могут преобразовать доменные имена в IP-адреса.".to_string(),
fix: Some(FixSuggestion {
title: "Восстановление конфигурации resolv.conf".to_string(),
command: "sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf || echo 'nameserver 1.1.1.1' | sudo tee /etc/resolv.conf".to_string(),
explanation: "Пересоздает симлинк на stub-резолвер systemd или задает публичный DNS.".to_string(),
}),
raw_log: "Symlink /etc/resolv.conf is dangling".to_string(),
sanitized: true,
permission_denied: false,
});
}
if !data.network_deep_scan.dns_resolution_ok {
network_status = "Failed".to_string();
network_issue_count += 1;
issues.push(DiagnosticIssue {
id: "net-dns-fail".to_string(),
title: "Сбой разрешения DNS-имен (DNS Lookup Failed)".to_string(),
subsystem: Subsystem::Network,
severity: Severity::Critical,
root_cause: format!("Ни один из настроенных DNS-серверов {:?} не отвечает на запросы разрешения имен.", data.network_deep_scan.dns_servers),
explanation: "Интернет-соединение может быть физически доступно, но браузеры и сервисы не открывают сайты по доменам.".to_string(),
fix: Some(FixSuggestion {
title: "Проверка статуса DNS резолвера".to_string(),
command: "resolvectl status || nslookup cloudflare.com".to_string(),
explanation: "Диагностирует опрос апстрим DNS-серверов.".to_string(),
}),
raw_log: format!("DNS servers: {:?}, resolution_ok: false", data.network_deep_scan.dns_servers),
sanitized: true,
permission_denied: false,
});
}
if !data.network_deep_scan.has_default_gateway {
network_status = "Failed".to_string();
network_issue_count += 1;
issues.push(DiagnosticIssue {
id: "net-no-gateway".to_string(),
title: "Отсутствует маршрут по умолчанию (Default Gateway Missing)".to_string(),
subsystem: Subsystem::Network,
severity: Severity::Critical,
root_cause: "В таблице маршрутизации ядра нет шлюза 0.0.0.0. Пакеты во внешнюю сеть не могут быть смаршрутизированы.".to_string(),
explanation: "Сетевой адаптер не получил адрес шлюза по DHCP или интерфейс отключен.".to_string(),
fix: Some(FixSuggestion {
title: "Проверка сетевых интерфейсов и получение аренды DHCP".to_string(),
command: "ip route show && ip link".to_string(),
explanation: "Выводит текущую таблицу маршрутизации и состояние линков.".to_string(),
}),
raw_log: "No default gateway (00000000) found in /proc/net/route".to_string(),
sanitized: true,
permission_denied: false,
});
}
if let Some(ref conflict) = data.network_deep_scan.firewall_conflict {
network_issue_count += 1;
issues.push(DiagnosticIssue {
id: "net-firewall-conflict".to_string(),
title: "Конфликт нескольких брандмауэров (Firewall Conflict)".to_string(),
subsystem: Subsystem::Network,
severity: Severity::Warning,
root_cause: conflict.clone(),
explanation: "Одновременная работа нескольких фаерволов (например, UFW и Firewalld) приводит к гонкам в nftables/iptables и внезапным обрывам соединений.".to_string(),
fix: Some(FixSuggestion {
title: "Отключение конфликтующей службы firewalld".to_string(),
command: "sudo systemctl disable --now firewalld".to_string(),
explanation: "Оставляет активным только один брандмауэр.".to_string(),
}),
raw_log: conflict.clone(),
sanitized: true,
permission_denied: false,
});
}
if !data.network_deep_scan.wifi_drop_events.is_empty() {
network_issue_count += 1;
issues.push(DiagnosticIssue {
id: "net-wifi-drop".to_string(),
title: format!("Обнаружены сбросы Wi-Fi сессий (событий: {})", data.network_deep_scan.wifi_drop_events.len()),
subsystem: Subsystem::Network,
severity: Severity::Warning,
root_cause: "Беспроводной адаптер зафиксировал деаутентификацию или потерю маяков точки доступа (beacon loss).".to_string(),
explanation: "Возможен слабый сигнал, помехи на частоте или агрессивный режим энергосбережения Wi-Fi адаптера.".to_string(),
fix: Some(FixSuggestion {
title: "Просмотр детальных логов Wi-Fi демона".to_string(),
command: "journalctl -u wpa_supplicant -u iwd -e".to_string(),
explanation: "Показывает причины деаутентификации Wi-Fi модуля.".to_string(),
}),
raw_log: data.network_deep_scan.wifi_drop_events.join("\n"),
sanitized: true,
permission_denied: false,
});
}
// Если проблем нет вообще, сформируем Info
if issues.is_empty() {
issues.push(DiagnosticIssue {
id: "ok-1".to_string(),
title: format!("Все подсистемы {} ({}) функционируют стабильно", data.os_name, data.init_name),
subsystem: Subsystem::Systemd,
severity: Severity::Info,
root_cause: "Сбоев служб, ошибок ядра dmesg, переполнения дисков и превышения PSI не обнаружено.".to_string(),
explanation: format!("Глубокий аудит проверил супервизор {}, кольцевой буфер ядра, файловые системы и лимиты ОС. Система в норме.", data.init_name),
fix: None,
raw_log: format!("OS: {}, Init: {}, Root: {}. All checks HEALTHY.", data.os_name, data.init_name, data.is_root),
sanitized: true,
permission_denied: false,
});
}
// 7. Анализ аварийных сбоев (Crash Dumps)
let mut crashes_status = "Healthy".to_string();
let mut crashes_issue_count = 0;
for (idx, crash) in data.crashes.iter().enumerate() {
crashes_issue_count += 1;
crashes_status = "Warning".to_string();
issues.push(DiagnosticIssue {
id: format!("crash-{}", idx + 1),
title: format!("Аварийный сбой процесса '{}' ({})", crash.process_name, crash.signal_or_code),
subsystem: Subsystem::Crashes,
severity: if crash.signal_or_code.contains("SIGSEGV") || crash.signal_or_code.contains("Access Violation") {
Severity::Critical
} else {
Severity::Warning
},
root_cause: format!("Процесс завершился аварийно с кодом/сигналом {}.", crash.signal_or_code),
explanation: crash.stack_trace_snippet.clone(),
fix: crash.dump_path.as_ref().map(|p| FixSuggestion {
title: format!("Анализ дампа {}", crash.process_name),
command: format!("gdb {} {} || coredumpctl gdb {}", crash.process_name, p, crash.pid.unwrap_or(0)),
explanation: "Запуск отладчика для исследования стека вызовов и локализации сбойной инструкции.".to_string(),
}),
raw_log: crash.stack_trace_snippet.clone(),
sanitized: true,
permission_denied: false,
});
}
// 8. Анализ безопасности и открытых портов
let mut sec_status = "Healthy".to_string();
let mut sec_issue_count = 0;
for (idx, port) in data.security.exposed_ports.iter().filter(|p| p.is_public && p.risk_level != Severity::Info).enumerate() {
sec_issue_count += 1;
if port.risk_level == Severity::Critical {
sec_status = "Critical".to_string();
} else if sec_status != "Critical" {
sec_status = "Warning".to_string();
}
issues.push(DiagnosticIssue {
id: format!("sec-port-{}", idx + 1),
title: format!("Опасный порт {}: {} слушает на 0.0.0.0", port.port, port.process_name),
subsystem: Subsystem::Security,
severity: port.risk_level.clone(),
root_cause: format!("Служба {} привязана к публичному адресу {} без сетевой изоляции.", port.process_name, port.local_address),
explanation: port.recommendation.clone(),
fix: Some(FixSuggestion {
title: format!("Изоляция порта {}", port.port),
command: format!("sudo ufw deny {}/{} || iptables -A INPUT -p {} --dport {} -j DROP", port.port, port.protocol.to_lowercase(), port.protocol.to_lowercase(), port.port),
explanation: "Блокировка внешних подключений к порту через системный брандмауэр.".to_string(),
}),
raw_log: format!("{} {} pid={:?}", port.protocol, port.local_address, port.pid),
sanitized: true,
permission_denied: false,
});
}
for (idx, suid) in data.security.suid_anomalies.iter().enumerate() {
sec_issue_count += 1;
sec_status = "Critical".to_string();
issues.push(DiagnosticIssue {
id: format!("sec-suid-{}", idx + 1),
title: format!("Подозрительный SUID файл: {}", suid.path),
subsystem: Subsystem::Security,
severity: Severity::Critical,
root_cause: suid.risk_reason.clone(),
explanation: format!("Файл {} имеет права {} и установлен бит SUID/SGID.", suid.path, suid.permissions),
fix: Some(FixSuggestion {
title: format!("Снятие SUID-бита с {}", suid.path),
command: format!("sudo chmod -s {}", suid.path),
explanation: "Удаление флага setuid/setgid для предотвращения эскалации привилегий.".to_string(),
}),
raw_log: format!("path={} perms={}", suid.path, suid.permissions),
sanitized: true,
permission_denied: false,
});
}
for check in &data.security.hardening_checks {
if !check.is_secure && check.remediation.is_some() {
sec_issue_count += 1;
if sec_status == "Healthy" {
sec_status = "Warning".to_string();
}
issues.push(DiagnosticIssue {
id: format!("sec-check-{}", check.name.chars().filter(|c| c.is_ascii_alphanumeric()).take(8).collect::<String>()),
title: format!("{}: {}", check.name, check.status),
subsystem: Subsystem::Security,
severity: Severity::Warning,
root_cause: check.description.clone(),
explanation: format!("Параметр безопасности '{}' в состоянии '{}'.", check.name, check.status),
fix: check.remediation.as_ref().map(|rem| FixSuggestion {
title: format!("Усиление защиты: {}", check.name),
command: rem.clone(),
explanation: "Применение рекомендуемого безопасного значения конфигурации.".to_string(),
}),
raw_log: format!("{}: {}", check.name, check.status),
sanitized: true,
permission_denied: false,
});
}
}
let statuses = vec![
SubsystemStatus {
name: Subsystem::Systemd,
status: init_status,
issue_count: init_issue_count,
details: format!("{}: сбойных служб — {}", data.init_name, data.failed_services.len()),
},
SubsystemStatus {
name: Subsystem::Kernel,
status: kernel_status,
issue_count: kernel_issue_count,
details: if data.kernel_scan.tainted_value > 0 {
format!("Tainted: {}", data.kernel_scan.tainted_flags.join(", "))
} else if data.hardware_scan.total_throttle_events > 0 {
format!("Thermal throttled: {} раз", data.hardware_scan.total_throttle_events)
} else {
"Буфер ядра чист, tainted=0".to_string()
},
},
SubsystemStatus {
name: Subsystem::Storage,
status: storage_status,
issue_count: storage_issue_count,
details: format!("Проверено точек монтирования: {}", data.storage_mounts.len()),
},
SubsystemStatus {
name: Subsystem::Network,
status: network_status,
issue_count: network_issue_count,
details: if let Some(ms) = data.network_deep_scan.dns_latency_ms {
format!("DNS: {} ms | Шлюз: {}", ms, data.network_deep_scan.default_gateway_ip.as_deref().unwrap_or("нет"))
} else if data.network_deep_scan.has_default_gateway {
format!("Шлюз: {}", data.network_deep_scan.default_gateway_ip.as_deref().unwrap_or("OK"))
} else {
"Маршрут по умолчанию отсутствует".to_string()
},
},
SubsystemStatus {
name: Subsystem::Security,
status: sec_status,
issue_count: sec_issue_count,
details: format!("Защищенность: {}% | Открытых портов: {}", data.security.security_score, data.security.exposed_ports.len()),
},
SubsystemStatus {
name: Subsystem::Crashes,
status: crashes_status,
issue_count: crashes_issue_count,
details: format!("Зафиксировано сбоев: {}", data.crashes.len()),
},
SubsystemStatus {
name: Subsystem::Audio,
status: "Healthy".to_string(),
issue_count: 0,
details: "Аудио подсистема в норме".to_string(),
},
];
let summary = format!(
"Аудит {} ({}): инцидентов — {}, сбоев — {}, безопасность — {}%, root: {}.",
data.os_name,
data.init_name,
issues.len(),
data.crashes.len(),
data.security.security_score,
if data.is_root { "да" } else { "нет" }
);
(issues, statuses, summary)
}
pub async fn run_scan() -> Result<SystemReport, String> {
run_scan_with_mode(crate::auth::AuthMode::Tui).await
}
pub async fn run_scan_with_mode(mode: crate::auth::AuthMode) -> Result<SystemReport, String> {
let raw_data = tokio::task::spawn_blocking(move || UnifiedRawData::collect(Some(mode)))
.await
.map_err(|e| e.to_string())?;
let (prompt_preview, _) = LlmDiagnosticPrompt::build(&raw_data);
let llm_res = try_query_llm(&prompt_preview).await;
let llm_analyzed = llm_res.is_some();
let (issues, subsystems, summary) = heuristic_ai_analyze(&raw_data);
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
Ok(SystemReport {
timestamp,
os_family: raw_data.os_name,
init_system: raw_data.init_name,
is_root: raw_data.is_root,
subsystems,
issues,
failed_services: raw_data.failed_services,
hardware: raw_data.hardware_info,
crashes: raw_data.crashes,
security: raw_data.security,
llm_analyzed,
summary,
prompt_preview,
})
}
+224
View File
@@ -0,0 +1,224 @@
use std::io::Write;
use std::process::{Command, Stdio};
use std::sync::RwLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthMode {
Tui,
Gui,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PrivilegeStatus {
AlreadyRoot,
Elevated,
Unprivileged { reason: String },
}
#[allow(dead_code)]
static CACHED_PRIV_STATUS: RwLock<Option<PrivilegeStatus>> = RwLock::new(None);
pub struct PrivilegeManager;
impl PrivilegeManager {
/// Проверяет, запущен ли текущий процесс с UID 0 (root)
pub fn is_current_user_root() -> bool {
#[cfg(unix)]
{
unsafe { libc::geteuid() == 0 }
}
#[cfg(not(unix))]
{
false
}
}
/// Возвращает статус привилегий или запрашивает их однократно за сессию
pub fn ensure_privileges(mode: AuthMode) -> PrivilegeStatus {
#[cfg(test)]
{
let _ = mode;
if Self::is_current_user_root() {
return PrivilegeStatus::AlreadyRoot;
}
if let Ok(status) = Command::new("sudo")
.args(["-n", "true"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
{
if status.success() {
return PrivilegeStatus::Elevated;
}
}
return PrivilegeStatus::Unprivileged {
reason: "Non-interactive test mode".to_string(),
};
}
#[cfg(not(test))]
{
if let Ok(guard) = CACHED_PRIV_STATUS.read() {
if let Some(ref status) = *guard {
return status.clone();
}
}
let status = Self::request_privileges(mode);
if let Ok(mut guard) = CACHED_PRIV_STATUS.write() {
*guard = Some(status.clone());
}
status
}
}
/// Однократный запрос повышения привилегий:
/// - Если уже root, не запрашивает повторно.
/// - В TUI-режиме: интерактивный `sudo -v` с кэшированием временного токена sudoers.
/// - В GUI-режиме: графический диалог (Zenity / Polkit pkexec).
/// - При отказе или отмене возвращает `PrivilegeStatus::Unprivileged`.
pub fn request_privileges(mode: AuthMode) -> PrivilegeStatus {
if Self::is_current_user_root() {
return PrivilegeStatus::AlreadyRoot;
}
// Проверяем, есть ли уже действующий кэш sudo без ввода пароля (sudo -n true)
if let Ok(status) = Command::new("sudo")
.args(["-n", "true"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
{
if status.success() {
return PrivilegeStatus::Elevated;
}
}
match mode {
AuthMode::Tui => {
println!("\n\x1b[1;36m[diagd]\x1b[0m Для полного аудита ядра (dmesg) и системных журналов требуются права суперпользователя.");
println!("\x1b[1;33m[diagd]\x1b[0m Запрос авторизации sudo:");
match Command::new("sudo").arg("-v").status() {
Ok(status) if status.success() => {
println!("\x1b[1;32m[diagd]\x1b[0m Привилегии суперпользователя успешно получены.\n");
PrivilegeStatus::Elevated
}
Ok(_) => {
println!("\x1b[1;33m[diagd]\x1b[0m Пароль не введен или отклонен. Сканирование продолжится в unprivileged-режиме.\n");
PrivilegeStatus::Unprivileged {
reason: "Пользователь отменил ввод пароля sudo".to_string(),
}
}
Err(e) => PrivilegeStatus::Unprivileged {
reason: format!("Утилита sudo недоступна: {}", e),
},
}
}
AuthMode::Gui => {
#[cfg(target_os = "macos")]
{
if let Ok(status) = Command::new("osascript")
.args(["-e", "do shell script \"sudo -v\" with administrator privileges"])
.status()
{
if status.success() {
return PrivilegeStatus::Elevated;
}
}
}
#[cfg(target_os = "windows")]
{
let ps_check = "([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)";
if let Ok(output) = Command::new("powershell").args(["-NoProfile", "-NonInteractive", "-Command", ps_check]).output() {
if String::from_utf8_lossy(&output.stdout).trim() == "True" {
return PrivilegeStatus::Elevated;
}
}
}
// В Linux GUI пробуем графический ввод через zenity или pkexec
if let Ok(output) = Command::new("zenity")
.args(["--password", "--title=diagd • Запрос прав суперпользователя"])
.output()
{
if output.status.success() {
let pass = String::from_utf8_lossy(&output.stdout);
let trimmed_pass = pass.trim_end_matches(&['\r', '\n'][..]);
if let Ok(mut child) = Command::new("sudo")
.args(["-S", "-v"])
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(format!("{}\n", trimmed_pass).as_bytes());
}
if let Ok(exit) = child.wait() {
if exit.success() {
return PrivilegeStatus::Elevated;
}
}
}
}
}
// Fallback: pkexec
match Command::new("pkexec").args(["true"]).status() {
Ok(status) if status.success() => PrivilegeStatus::Elevated,
Ok(_) => PrivilegeStatus::Unprivileged {
reason: "Авторизация Polkit отклонена пользователем".to_string(),
},
Err(e) => PrivilegeStatus::Unprivileged {
reason: format!("Polkit (pkexec) недоступен: {}", e),
},
}
}
}
}
/// Выполняет команду с правами суперпользователя (через sudo -n, если доступно),
/// либо делает откат к обычному вызову
pub fn exec_privileged(cmd: &str, args: &[&str]) -> std::io::Result<std::process::Output> {
if Self::is_current_user_root() {
return Command::new(cmd).args(args).output();
}
// Если активен sudo кэш, выполняем через sudo -n
if let Ok(privileged_cmd) = Command::new("sudo")
.arg("-n")
.arg(cmd)
.args(args)
.output()
{
if privileged_cmd.status.success() {
return Ok(privileged_cmd);
} else {
let err = String::from_utf8_lossy(&privileged_cmd.stderr);
// Если sudo -n отклонен, пробуем обычный вызов
if !err.contains("a password is required") {
return Ok(privileged_cmd);
}
}
}
// Обычный вызов без sudo
Command::new(cmd).args(args).output()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_privilege_check() {
let is_root = PrivilegeManager::is_current_user_root();
assert_eq!(is_root, is_root);
}
}
+466
View File
@@ -0,0 +1,466 @@
use crate::models::{Severity, SystemReport};
use flate2::write::GzEncoder;
use flate2::Compression;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
pub struct BundleExporter;
impl BundleExporter {
/// Экспортирует полный архив Debug Bundle:
/// - `report.html` (интерактивный dark HTML)
/// - `report.md` (Markdown для баг-трекера)
/// - `system_report.json` (структурированный дамп)
/// Упаковывает всё в tar.gz и возвращает путь к файлу архива.
pub fn export_bundle(report: &SystemReport) -> Result<String, String> {
let timestamp_safe = report.timestamp.replace([':', ' '], "-");
let filename = format!("diagd-bundle-{}.tar.gz", timestamp_safe);
// Определяем каталог для сохранения: $HOME или /tmp
let output_dir = std::env::var("HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| std::env::temp_dir());
let bundle_path = output_dir.join(&filename);
let tar_gz_file = File::create(&bundle_path)
.map_err(|e| format!("Не удалось создать файл {}: {}", bundle_path.display(), e))?;
let enc = GzEncoder::new(tar_gz_file, Compression::default());
let mut tar = tar::Builder::new(enc);
// 1. Создаем HTML отчет
let html_content = Self::generate_html(report);
let mut html_header = tar::Header::new_gnu();
html_header.set_size(html_content.len() as u64);
html_header.set_mode(0o644);
html_header.set_cksum();
tar.append_data(&mut html_header, "report.html", html_content.as_bytes())
.map_err(|e| format!("Ошибка добавления report.html в архив: {}", e))?;
// 2. Создаем Markdown отчет
let md_content = Self::generate_markdown(report);
let mut md_header = tar::Header::new_gnu();
md_header.set_size(md_content.len() as u64);
md_header.set_mode(0o644);
md_header.set_cksum();
tar.append_data(&mut md_header, "report.md", md_content.as_bytes())
.map_err(|e| format!("Ошибка добавления report.md в архив: {}", e))?;
// 3. Создаем system_report.json
let json_content = serde_json::to_string_pretty(report).unwrap_or_default();
let mut json_header = tar::Header::new_gnu();
json_header.set_size(json_content.len() as u64);
json_header.set_mode(0o644);
json_header.set_cksum();
tar.append_data(&mut json_header, "system_report.json", json_content.as_bytes())
.map_err(|e| format!("Ошибка добавления system_report.json в архив: {}", e))?;
tar.finish()
.map_err(|e| format!("Ошибка финализации архива tar.gz: {}", e))?;
// Также сохраняем отдельную копию HTML для моментального открытия в браузере
let standalone_html = output_dir.join(format!("diagd-report-{}.html", timestamp_safe));
if let Ok(mut f) = File::create(&standalone_html) {
let _ = f.write_all(html_content.as_bytes());
}
Ok(bundle_path.to_string_lossy().to_string())
}
pub fn generate_markdown(report: &SystemReport) -> String {
let mut md = String::new();
md.push_str(&format!("# Отчет диагностики diagd • {}\n\n", report.timestamp));
md.push_str(&format!("- **ОС**: {}\n", report.os_family));
md.push_str(&format!("- **Init System**: {}\n", report.init_system));
md.push_str(&format!("- **Root**: {}\n", if report.is_root { "Да" } else { "Нет (unprivileged)" }));
md.push_str(&format!("- **Сводка**: {}\n\n", report.summary));
md.push_str("## Статус подсистем\n\n");
md.push_str("| Подсистема | Статус | Ошибок | Детали |\n");
md.push_str("|------------|--------|--------|--------|\n");
for sub in &report.subsystems {
md.push_str(&format!(
"| {:?} | {} | {} | {} |\n",
sub.name, sub.status, sub.issue_count, sub.details
));
}
md.push_str("\n## Обнаруженные проблемы\n\n");
for issue in &report.issues {
let badge = match issue.severity {
Severity::Critical => "🔴 CRITICAL",
Severity::Warning => "🟡 WARNING",
Severity::Info => "🔵 INFO",
};
md.push_str(&format!("### {} {}\n\n", badge, issue.title));
md.push_str(&format!("- **Подсистема**: `{:?}`\n", issue.subsystem));
md.push_str(&format!("- **Корень проблемы**: {}\n", issue.root_cause));
md.push_str(&format!("- **Объяснение**: {}\n", issue.explanation));
if let Some(ref fix) = issue.fix {
md.push_str(&format!("\n**Рекомендуемое решение:** {}\n", fix.title));
md.push_str("```bash\n");
md.push_str(&fix.command);
md.push_str("\n```\n");
}
if !issue.raw_log.trim().is_empty() {
md.push_str("\n<details><summary>Сырой лог</summary>\n\n```text\n");
md.push_str(&issue.raw_log);
md.push_str("\n```\n</details>\n");
}
md.push_str("\n---\n\n");
}
let hw = &report.hardware;
md.push_str("## Спецификация оборудования (Hardware Profile)\n\n");
md.push_str(&format!("- **Процессор (CPU)**: {} ({} ядер / {} потоков, arch: {})\n", hw.cpu.model_name, hw.cpu.physical_cores, hw.cpu.logical_threads, hw.cpu.architecture));
if let Some(freq) = hw.cpu.current_frequency_mhz {
md.push_str(&format!(" - Частота: {:.0} MHz | Кэш: {}\n", freq, hw.cpu.l1_cache));
}
if let Some(ref virt) = hw.cpu.virtualization {
md.push_str(&format!(" - Виртуализация: {}\n", virt));
}
md.push_str(&format!("- **Материнская плата**: {} {} (BIOS: {} {})\n", hw.motherboard.vendor, hw.motherboard.product_name, hw.motherboard.bios_vendor, hw.motherboard.bios_version));
let ram_total_gb = hw.memory.total_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let ram_used_gb = hw.memory.used_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
md.push_str(&format!("- **Память (RAM)**: Всего: {:.1} GB | Занято: {:.1} GB | Ошибки ECC (CE: {}, UE: {})\n", ram_total_gb, ram_used_gb, hw.memory.edac_ce_count, hw.memory.edac_ue_count));
if !hw.gpus.is_empty() {
md.push_str("- **Графика (GPU)**:\n");
for gpu in &hw.gpus {
md.push_str(&format!(" - {} (драйвер: {}, слот: {})\n", gpu.model, gpu.driver, gpu.pci_slot));
}
}
if !hw.disks.is_empty() {
md.push_str("- **Накопители (Disks)**:\n");
for disk in &hw.disks {
let size_gb = disk.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let smart_str = if !disk.smart_status.is_empty() { format!(" | SMART: {}", disk.smart_status) } else { "".to_string() };
let health_str = disk.health_percent.map(|h| format!(" | Здоровье: {}%", h)).unwrap_or_default();
let wear_str = disk.wear_percent.map(|w| format!(" | Износ: {}%", w)).unwrap_or_default();
let temp_str = disk.temperature_c.map(|t| format!(" | Темп: {}°C", t)).unwrap_or_default();
let pwr_str = disk.power_on_hours.map(|p| format!(" | Наработка: {} ч.", p)).unwrap_or_default();
let sn_str = if !disk.serial.is_empty() { format!(" | S/N: {}", disk.serial) } else { "".to_string() };
md.push_str(&format!(" - `{}` — {} ({:.1} GB, {}{}{}{}{}{}{})\n", disk.name, disk.model, size_gb, disk.disk_type, smart_str, health_str, wear_str, temp_str, pwr_str, sn_str));
for part in &disk.partitions {
let part_gb = part.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let mount = part.mount_point.as_deref().unwrap_or("не смонтирован");
md.push_str(&format!(" - `{}` ({:.1} GB, {}) -> {}\n", part.name, part_gb, part.fs_type, mount));
}
}
}
if let Some(ref batt) = hw.battery {
md.push_str(&format!("- **Батарея**: {} (емкость: {}%, здоровье: {:.1}%, циклов: {}, статус: {})\n", batt.name, batt.capacity_percent, batt.health_percent, batt.cycle_count, batt.status));
}
md.push_str(&format!("- **Температура / Охлаждение**: Макс. темп CPU: {:.1}°C | Троттлинг: {} событий\n\n", hw.thermals.max_cpu_temp_c, hw.thermals.total_throttle_events));
if !report.crashes.is_empty() {
md.push_str("## Зафиксированные падения и аварийные дампы (Crashes)\n\n");
md.push_str("| Время | Процесс | Сигнал / Код | Сбойный модуль | Дамп |\n");
md.push_str("|---|---|---|---|---|\n");
for c in &report.crashes {
md.push_str(&format!(
"| {} | `{}` | `{}` | {} | {} |\n",
c.timestamp,
c.process_name,
c.signal_or_code,
c.fault_module.as_deref().unwrap_or("—"),
c.dump_path.as_deref().unwrap_or("—")
));
}
md.push_str("\n");
}
md.push_str(&format!("## Аудит безопасности (Оценка защищенности: {}%)\n\n", report.security.security_score));
if !report.security.exposed_ports.is_empty() {
md.push_str("### Открытые порты и сетевые службы\n\n");
md.push_str("| Протокол | Адрес:Порт | Процесс | Доступность | Уровень риска |\n");
md.push_str("|---|---|---|---|---|\n");
for p in &report.security.exposed_ports {
let pub_badge = if p.is_public { "🌐 Публичный" } else { "🔒 Локальный" };
let risk_badge = match p.risk_level {
Severity::Critical => "🔴 КРИТИЧЕСКИЙ",
Severity::Warning => "🟡 ВНИМАНИЕ",
Severity::Info => "🔵 ИНФО",
};
md.push_str(&format!(
"| {} | `{}` | `{}` | {} | {} |\n",
p.protocol, p.local_address, p.process_name, pub_badge, risk_badge
));
}
md.push_str("\n");
}
md
}
pub fn generate_html(report: &SystemReport) -> String {
let mut html = String::new();
html.push_str(r#"<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>diagd • Отчет диагностики системы</title>
<style>
:root {
--bg: #0d1117;
--card: #161b22;
--border: #30363d;
--text: #c9d1d9;
--text-bright: #f0f6fc;
--accent: #58a6ff;
--green: #3fb950;
--red: #f85149;
--yellow: #d29922;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
background-color: var(--bg);
color: var(--text);
margin: 0;
padding: 30px;
line-height: 1.5;
}
.container { max-width: 1050px; margin: 0 auto; }
.header {
background: var(--card);
border: 1px solid var(--border);
padding: 24px;
border-radius: 12px;
margin-bottom: 24px;
}
h1 { margin: 0 0 8px 0; color: var(--text-bright); font-size: 24px; }
.meta { color: #8b949e; font-size: 14px; margin-bottom: 12px; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 28px;
}
.sub-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
}
.sub-name { font-weight: bold; color: var(--text-bright); }
.issue-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 20px;
margin-bottom: 16px;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 6px;
font-weight: bold;
font-size: 11px;
letter-spacing: 0.5px;
margin-right: 8px;
}
.badge-crit { background: rgba(248, 81, 73, 0.2); color: var(--red); border: 1px solid var(--red); }
.badge-warn { background: rgba(210, 153, 34, 0.2); color: var(--yellow); border: 1px solid var(--yellow); }
.badge-info { background: rgba(88, 166, 255, 0.2); color: var(--accent); border: 1px solid var(--accent); }
.cmd-box {
background: #090d13;
border: 1px solid #21262d;
padding: 12px;
border-radius: 6px;
font-family: monospace;
color: #79c0ff;
margin-top: 10px;
overflow-x: auto;
}
details { margin-top: 12px; }
pre {
background: #090d13;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
font-size: 12px;
color: #8b949e;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>diagd • Linux / BSD AI Diagnostic Report</h1>
<div class="meta">
Дата: <b>"#);
html.push_str(&report.timestamp);
html.push_str(r#"</b> | ОС: <b>"#);
html.push_str(&report.os_family);
html.push_str(r#"</b> | Init: <b>"#);
html.push_str(&report.init_system);
html.push_str(r#"</b> | Права root: <b>"#);
html.push_str(if report.is_root { "Да" } else { "Нет (unprivileged)" });
html.push_str(r#"</b>
</div>
<div>"#);
html.push_str(&report.summary);
html.push_str(r#"</div>
</div>
<h2>Состояние подсистем</h2>
<div class="grid">"#);
for sub in &report.subsystems {
let color_class = if sub.status == "Healthy" { "color: var(--green);" } else { "color: var(--red);" };
html.push_str(&format!(
r#"<div class="sub-card">
<div class="sub-name">{:?}</div>
<div style="{} font-weight: bold; font-size: 12px;">{}</div>
<div style="font-size: 12px; color: #8b949e;">Ошибок: {}</div>
</div>"#,
sub.name, color_class, sub.status, sub.issue_count
));
}
html.push_str(r#"</div>
<h2>Обнаруженные инциденты</h2>"#);
for issue in &report.issues {
let (badge_cls, badge_txt) = match issue.severity {
Severity::Critical => ("badge-crit", "CRITICAL"),
Severity::Warning => ("badge-warn", "WARNING"),
Severity::Info => ("badge-info", "INFO"),
};
html.push_str(&format!(
r#"<div class="issue-card">
<span class="badge {}">{}</span>
<span style="font-size: 12px; color: #8b949e;">{:?}</span>
<h3 style="color: var(--text-bright); margin: 8px 0;">{}</h3>
<div style="margin-bottom: 6px;"><b>Корень проблемы:</b> {}</div>
<div style="color: #8b949e; font-size: 13px;">{}</div>"#,
badge_cls, badge_txt, issue.subsystem, issue.title, issue.root_cause, issue.explanation
));
if let Some(ref fix) = issue.fix {
html.push_str(&format!(
r#"<div class="cmd-box">
<div style="color: var(--green); font-size: 11px; margin-bottom: 4px;">✓ {}</div>
$ {}
</div>"#,
fix.title, fix.command
));
}
if !issue.raw_log.trim().is_empty() {
html.push_str(&format!(
r#"<details>
<summary style="cursor: pointer; color: var(--accent); font-size: 13px;">Показать сырой лог</summary>
<pre>{}</pre>
</details>"#,
issue.raw_log
));
}
html.push_str("</div>");
}
let hw = &report.hardware;
let ram_total_gb = hw.memory.total_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let ram_used_gb = hw.memory.used_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
html.push_str(r#"
<h2>Спецификация оборудования (Hardware Profile)</h2>
<div class="grid">
<div class="sub-card">
<div class="sub-name">Процессор (CPU)</div>
<div style="font-size: 13px; color: var(--text-bright); margin-top: 4px;">"#);
html.push_str(&hw.cpu.model_name);
html.push_str(&format!(r#"</div>
<div style="font-size: 12px; color: #8b949e; margin-top: 4px;">Ядер: {} | Потоков: {} | Кэш: {}</div>
</div>
<div class="sub-card">
<div class="sub-name">Материнская плата & BIOS</div>
<div style="font-size: 13px; color: var(--text-bright); margin-top: 4px;">{} {}</div>
<div style="font-size: 12px; color: #8b949e; margin-top: 4px;">BIOS: {} {} ({})</div>
</div>
<div class="sub-card">
<div class="sub-name">Оперативная память (RAM)</div>
<div style="font-size: 13px; color: var(--text-bright); margin-top: 4px;">{:.1} GB (занято {:.1} GB)</div>
<div style="font-size: 12px; color: #8b949e; margin-top: 4px;">ECC сбои: CE={}, UE={}</div>
</div>
<div class="sub-card">
<div class="sub-name">Термометрия & Батарея</div>
<div style="font-size: 13px; color: var(--text-bright); margin-top: 4px;">Макс. темп: {:.1}°C</div>
<div style="font-size: 12px; color: #8b949e; margin-top: 4px;">Троттлинг: {} событий</div>
</div>
</div>
"#,
hw.cpu.physical_cores, hw.cpu.logical_threads, hw.cpu.l1_cache,
hw.motherboard.vendor, hw.motherboard.product_name,
hw.motherboard.bios_vendor, hw.motherboard.bios_version, hw.motherboard.bios_date,
ram_total_gb, ram_used_gb,
hw.memory.edac_ce_count, hw.memory.edac_ue_count,
hw.thermals.max_cpu_temp_c,
hw.thermals.total_throttle_events
));
html.push_str("<h3>Накопители данных (Disks)</h3>\n<div style=\"display: flex; flex-direction: column; gap: 10px;\">");
for disk in &hw.disks {
let size_gb = disk.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let health_val = disk.health_percent.unwrap_or(100);
let wear_val = disk.wear_percent.unwrap_or(0);
let bar_color = if health_val >= 80 { "var(--green)" } else if health_val >= 50 { "var(--warn)" } else { "var(--red)" };
html.push_str(&format!(
r#"<div class="sub-card" style="padding: 12px 16px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<b style="color: var(--accent); font-family: monospace;">{}</b>
<span style="color: var(--text-bright); margin-left: 8px;">{}</span>
<span style="font-size: 11px; background: #21262d; border: 1px solid #30363d; border-radius: 4px; padding: 2px 6px; margin-left: 8px;">{}</span>
</div>
<div style="font-size: 12px; color: var(--green);">SMART: {}</div>
</div>
<div style="margin-top: 8px; font-size: 12px; color: #8b949e;">
Здоровье: <b>{}%</b> (Износ: {}%) • Емкость: <b>{:.1} GB</b>
{} {}
</div>
<div style="background: #21262d; border-radius: 4px; height: 6px; margin-top: 6px; overflow: hidden;">
<div style="background: {}; height: 100%; width: {}%;"></div>
</div>
"#,
disk.name, disk.model, disk.disk_type, disk.smart_status,
health_val, wear_val, size_gb,
disk.temperature_c.map(|t| format!("• Температура: <b>{}°C</b>", t)).unwrap_or_default(),
disk.power_on_hours.map(|p| format!("• Наработка: <b>{} ч.</b>", p)).unwrap_or_default(),
bar_color, health_val
));
if !disk.partitions.is_empty() {
html.push_str("<div style=\"margin-top: 8px; font-size: 11px; color: #8b949e;\">Разделы: ");
for (idx, part) in disk.partitions.iter().enumerate() {
let p_gb = part.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let mount = part.mount_point.as_deref().unwrap_or("не смонтирован");
if idx > 0 { html.push_str(" | "); }
html.push_str(&format!("<code>{}</code> ({:.1} GB, {}) ➔ <code>{}</code>", part.name, p_gb, part.fs_type, mount));
}
html.push_str("</div>");
}
html.push_str("</div>");
}
html.push_str("</div>");
html.push_str(r#"
</div>
</body>
</html>"#);
html
}
}
+38
View File
@@ -0,0 +1,38 @@
pub mod auth;
pub mod models;
pub mod sanitize;
pub mod scanner;
pub mod analyst;
pub mod export;
pub use auth::{AuthMode, PrivilegeManager, PrivilegeStatus};
pub use models::*;
pub use sanitize::DataSanitizer;
pub use analyst::{run_scan, run_scan_with_mode};
pub use export::BundleExporter;
pub use scanner::init::{detect_init_system, InitSystemProvider, ScanError};
pub use scanner::live::LiveMetricsScanner;
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_core_deep_scan() {
let report = run_scan().await;
assert!(report.is_ok());
let rep = report.unwrap();
println!("=== DISKS SCANNED: {} ===", rep.hardware.disks.len());
for d in &rep.hardware.disks {
println!("Disk: {} ({}) | SMART: {} | Health: {:?}% | Wear: {:?}% | Temp: {:?}°C | Hours: {:?} | TBW: {:?} TB | Partitions: {}",
d.name, d.model, d.smart_status, d.health_percent, d.wear_percent, d.temperature_c, d.power_on_hours, d.total_written_tb, d.partitions.len());
for p in &d.partitions {
println!(" -> Part: {} ({}) mount: {:?}", p.name, p.fs_type, p.mount_point);
}
}
assert!(!rep.subsystems.is_empty());
assert!(!rep.issues.is_empty());
assert!(!rep.init_system.is_empty());
assert!(!rep.hardware.disks.is_empty());
}
}
+270
View File
@@ -0,0 +1,270 @@
use serde::{Deserialize, Serialize};
/// Уровень критичности обнаруженной проблемы
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum Severity {
Critical,
Warning,
#[default]
Info,
}
/// Исследуемая подсистема ОС (Linux/BSD)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Subsystem {
Systemd, // Службы Init (systemd, openrc, runit, etc.)
Kernel,
Storage,
Audio,
Network,
Security,
Crashes,
}
/// Статус состояния подсистемы для сводного дашборда
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SubsystemStatus {
pub name: Subsystem,
pub status: String, // "Healthy", "Degraded", "Failed"
pub issue_count: u32,
pub details: String,
}
/// Предложение по устранению ошибки с готовой CLI-командой
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FixSuggestion {
pub title: String,
pub command: String,
pub explanation: String,
}
/// Информация о сбойной системной службе (в независимости от Init: systemd, OpenRC, runit...)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FailedService {
pub name: String,
pub init_system: String,
pub state: String,
pub description: String,
pub logs: String,
pub restart_command: String,
pub status_command: String,
}
/// Элемент диагностического отчета (проблема)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DiagnosticIssue {
pub id: String,
pub title: String,
pub subsystem: Subsystem,
pub severity: Severity,
pub root_cause: String,
pub explanation: String,
pub fix: Option<FixSuggestion>,
pub raw_log: String,
pub sanitized: bool,
pub permission_denied: bool,
}
pub type IssueItem = DiagnosticIssue;
/// Подробный отчет об аппаратном обеспечении системы
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct HardwareInfo {
pub cpu: CpuInfo,
pub memory: MemoryInfo,
pub motherboard: MotherboardInfo,
pub gpus: Vec<GpuInfo>,
pub disks: Vec<DiskInfo>,
pub network_adapters: Vec<NetworkAdapterInfo>,
pub battery: Option<BatteryInfo>,
pub thermals: ThermalsInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct CpuInfo {
pub model_name: String,
pub architecture: String,
pub physical_cores: usize,
pub logical_threads: usize,
pub base_frequency_mhz: Option<f32>,
pub current_frequency_mhz: Option<f32>,
pub l1_cache: String,
pub l2_cache: String,
pub l3_cache: String,
pub virtualization: Option<String>,
pub flags: Vec<String>,
pub microcode: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct MemoryInfo {
pub total_bytes: u64,
pub available_bytes: u64,
pub used_bytes: u64,
pub swap_total_bytes: u64,
pub swap_used_bytes: u64,
pub edac_ce_count: u64,
pub edac_ue_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct MotherboardInfo {
pub vendor: String,
pub product_name: String,
pub version: String,
pub bios_vendor: String,
pub bios_version: String,
pub bios_date: String,
pub chassis_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct GpuInfo {
pub vendor: String,
pub model: String,
pub driver: String,
pub pci_slot: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct PartitionInfo {
pub name: String,
pub path: String,
pub mount_point: Option<String>,
pub fs_type: String,
pub size_bytes: u64,
pub used_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct DiskInfo {
pub name: String,
pub model: String,
pub serial: String,
pub size_bytes: u64,
pub disk_type: String,
pub rotational_speed_rpm: Option<u32>,
pub smart_status: String,
pub health_percent: Option<u8>,
pub wear_percent: Option<u8>,
pub nvme_wear_percent: Option<u8>,
pub temperature_c: Option<i32>,
pub power_on_hours: Option<u64>,
pub power_cycle_count: Option<u64>,
pub bad_sectors_count: Option<u64>,
pub total_written_tb: Option<f64>,
pub partitions: Vec<PartitionInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NetworkAdapterInfo {
pub interface_name: String,
pub if_type: String,
pub state: String,
pub speed_mbps: Option<u32>,
pub mac_address: String,
pub driver: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct BatteryInfo {
pub name: String,
pub capacity_percent: u8,
pub health_percent: f32,
pub cycle_count: u32,
pub status: String,
pub is_degraded: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct ThermalsInfo {
pub max_cpu_temp_c: f32,
pub thermal_throttled_cores: usize,
pub total_throttle_events: u64,
pub is_actively_throttling: bool,
}
// --- LIVE METRICS ---
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct LiveMetricsSnapshot {
pub cpu_usage_percent: f32,
pub ram_used_bytes: u64,
pub ram_total_bytes: u64,
pub swap_used_bytes: u64,
pub swap_total_bytes: u64,
pub max_cpu_temp_c: f32,
pub is_throttling: bool,
pub disk_read_kbps: u64,
pub disk_write_kbps: u64,
pub net_rx_kbps: u64,
pub net_tx_kbps: u64,
}
// --- CRASH DUMPS ---
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct CrashReportInfo {
pub timestamp: String,
pub process_name: String,
pub pid: Option<u32>,
pub signal_or_code: String,
pub fault_module: Option<String>,
pub stack_trace_snippet: String,
pub dump_path: Option<String>,
}
// --- SECURITY AUDIT ---
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct ListeningPortInfo {
pub protocol: String,
pub local_address: String,
pub port: u16,
pub process_name: String,
pub pid: Option<u32>,
pub is_public: bool,
pub risk_level: Severity,
pub recommendation: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct SuidAnomalyInfo {
pub path: String,
pub owner: String,
pub permissions: String,
pub risk_reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct SecurityCheckItem {
pub category: String,
pub name: String,
pub status: String,
pub is_secure: bool,
pub description: String,
pub remediation: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct SecurityAuditReport {
pub exposed_ports: Vec<ListeningPortInfo>,
pub suid_anomalies: Vec<SuidAnomalyInfo>,
pub hardening_checks: Vec<SecurityCheckItem>,
pub security_score: u8,
}
/// Общий итоговый отчет диагностики
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct SystemReport {
pub timestamp: String,
pub os_family: String, // "Linux", "FreeBSD", "OpenBSD", etc.
pub init_system: String, // "systemd", "openrc", "runit", "s6", "dinit", "bsd-rc"
pub is_root: bool,
pub subsystems: Vec<SubsystemStatus>,
pub issues: Vec<IssueItem>,
pub failed_services: Vec<FailedService>,
pub hardware: HardwareInfo,
pub crashes: Vec<CrashReportInfo>,
pub security: SecurityAuditReport,
pub llm_analyzed: bool,
pub summary: String,
pub prompt_preview: String,
}
+75
View File
@@ -0,0 +1,75 @@
use regex::Regex;
use std::sync::OnceLock;
pub struct DataSanitizer;
static RE_HOME_PATH: OnceLock<Regex> = OnceLock::new();
static RE_USR_HOME_PATH: OnceLock<Regex> = OnceLock::new();
static RE_IPV4: OnceLock<Regex> = OnceLock::new();
static RE_MAC_ADDR: OnceLock<Regex> = OnceLock::new();
static RE_AUTH_TOKEN: OnceLock<Regex> = OnceLock::new();
static RE_UUID: OnceLock<Regex> = OnceLock::new();
impl DataSanitizer {
/// Очищает системные логи от персональных данных:
/// - Домашние пути (/home/<user>/ и /usr/home/<user>/ на BSD -> /home/[USER]/)
/// - IPv4 адреса (192.168.x.x, внешние IP)
/// - MAC-адреса
/// - Токены/секреты/пароли
/// - Аппаратные UUID
pub fn sanitize(raw: &str) -> String {
let home_re = RE_HOME_PATH.get_or_init(|| {
Regex::new(r"/home/[a-zA-Z0-9_\-]+").expect("Invalid home regex")
});
let usr_home_re = RE_USR_HOME_PATH.get_or_init(|| {
Regex::new(r"/usr/home/[a-zA-Z0-9_\-]+").expect("Invalid BSD usr home regex")
});
let ipv4_re = RE_IPV4.get_or_init(|| {
Regex::new(r"\b(?:\d{1,3}\.){3}\d{1,3}\b").expect("Invalid IPv4 regex")
});
let mac_re = RE_MAC_ADDR.get_or_init(|| {
Regex::new(r"(?i)\b([0-9a-f]{2}[:-]){5}([0-9a-f]{2})\b").expect("Invalid MAC regex")
});
let auth_re = RE_AUTH_TOKEN.get_or_init(|| {
Regex::new(r"(?i)(bearer|token|password|secret|api[_-]?key)[=:\s]+[A-Za-z0-9_\-\.\/]{8,}").expect("Invalid secret regex")
});
let uuid_re = RE_UUID.get_or_init(|| {
Regex::new(r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b").expect("Invalid UUID regex")
});
let mut clean = home_re.replace_all(raw, "/home/[USER]").to_string();
clean = usr_home_re.replace_all(&clean, "/home/[USER]").to_string();
clean = auth_re.replace_all(&clean, "$1=[REDACTED_SECRET]").to_string();
clean = mac_re.replace_all(&clean, "[MAC_ADDR]").to_string();
clean = uuid_re.replace_all(&clean, "[UUID]").to_string();
clean = ipv4_re.replace_all(&clean, |caps: &regex::Captures| {
let ip = &caps[0];
if ip.starts_with("127.") {
ip.to_string()
} else {
"[IP_ADDR]".to_string()
}
}).to_string();
clean
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitizer() {
let text = "Error for /usr/home/bsduser/file.txt and /home/linuxuser/app.rs at 10.0.0.4 with token=xyzSecret12345";
let clean = DataSanitizer::sanitize(text);
assert!(!clean.contains("bsduser"));
assert!(!clean.contains("linuxuser"));
assert!(!clean.contains("10.0.0.4"));
assert!(!clean.contains("xyzSecret12345"));
assert!(clean.contains("/home/[USER]"));
assert!(clean.contains("[IP_ADDR]"));
assert!(clean.contains("[REDACTED_SECRET]"));
}
}
+95
View File
@@ -0,0 +1,95 @@
use std::process::Command;
pub struct BsdRawData {
pub failed_services: Vec<String>,
pub service_logs: String,
pub kernel_logs: String,
pub storage_logs: String,
pub network_logs: String,
pub audio_logs: String,
}
impl BsdRawData {
pub fn collect() -> Self {
// 1. Опрос сервисов: в FreeBSD/OpenBSD проверяем service / rcctl
let mut failed_services = Vec::new();
let mut service_logs = String::new();
// Проверка OpenBSD rcctl ls failed
if let Ok(output) = Command::new("rcctl").args(["ls", "failed"]).output() {
let str_out = String::from_utf8_lossy(&output.stdout);
for line in str_out.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
failed_services.push(trimmed.to_string());
}
}
}
// 2. Чтение логов ядра dmesg
let kernel_logs = Command::new("dmesg")
.output()
.map(|o| {
let text = String::from_utf8_lossy(&o.stdout);
// Фильтруем последние 40 строк dmesg
text.lines()
.rev()
.take(40)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>()
.join("\n")
})
.unwrap_or_default();
// 3. Дисковая подсистема: zpool status (ZFS популярен на FreeBSD) или dmesg
let storage_logs = Command::new("zpool")
.args(["status", "-x"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_else(|_| {
// Ищем ошибки дисков в dmesg
kernel_logs
.lines()
.filter(|l| l.contains("error") || l.contains("CAM") || l.contains("GEOM"))
.collect::<Vec<_>>()
.join("\n")
});
// 4. Сеть: ifconfig или /var/log/messages
let network_logs = Command::new("ifconfig")
.args(["-a"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default();
// 5. Звук: cat /dev/sndstat (FreeBSD)
let audio_logs = std::fs::read_to_string("/dev/sndstat").unwrap_or_else(|_| {
"Audio driver: default BSD sound subsystem".to_string()
});
// Логи из /var/log/messages, если есть
if let Ok(messages) = std::fs::read_to_string("/var/log/messages") {
let errors = messages
.lines()
.rev()
.filter(|l| l.contains("error") || l.contains("fail") || l.contains("crit"))
.take(25)
.collect::<Vec<_>>()
.join("\n");
if !errors.is_empty() {
service_logs.push_str(&format!("--- Recent /var/log/messages ---\n{}\n", errors));
}
}
Self {
failed_services,
service_logs,
kernel_logs,
storage_logs,
network_logs,
audio_logs,
}
}
}
+213
View File
@@ -0,0 +1,213 @@
use crate::models::CrashReportInfo;
use std::path::Path;
use std::process::Command;
pub struct CrashScanner;
impl CrashScanner {
pub fn scan() -> Vec<CrashReportInfo> {
let mut crashes = Vec::new();
#[cfg(target_os = "linux")]
{
Self::scan_linux_coredumps(&mut crashes);
Self::scan_linux_var_crash(&mut crashes);
Self::scan_linux_dmesg_segfaults(&mut crashes);
}
#[cfg(target_os = "windows")]
{
Self::scan_windows_minidumps(&mut crashes);
Self::scan_windows_event_crashes(&mut crashes);
}
#[cfg(target_os = "macos")]
{
Self::scan_macos_diagnostic_reports(&mut crashes);
}
// Generic fallback if empty (e.g. BSD or systems without coredumpctl)
if crashes.is_empty() {
Self::scan_generic_system_logs(&mut crashes);
}
// Limit to 20 most recent crashes
crashes.truncate(20);
crashes
}
#[cfg(target_os = "linux")]
fn scan_linux_coredumps(crashes: &mut Vec<CrashReportInfo>) {
if let Ok(output) = Command::new("coredumpctl")
.args(["list", "--no-legend", "-n", "10"])
.output()
{
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 5 {
let time = parts[0..parts.len().saturating_sub(6)].join(" ");
let pid = parts.iter().find_map(|p| p.parse::<u32>().ok());
let signal = parts.iter().find(|p| p.starts_with("SIG") || **p == "11" || **p == "6").copied().unwrap_or("SIGSEGV");
let exe = parts.last().unwrap_or(&"unknown");
crashes.push(CrashReportInfo {
timestamp: if time.is_empty() { "Недавний".to_string() } else { time },
process_name: exe.to_string(),
pid,
signal_or_code: signal.to_string(),
fault_module: None,
stack_trace_snippet: format!("Core dump сохранен systemd-coredump для исполняемого файла {}", exe),
dump_path: Some(format!("coredumpctl info {}", pid.unwrap_or(0))),
});
}
}
}
}
#[cfg(target_os = "linux")]
fn scan_linux_var_crash(crashes: &mut Vec<CrashReportInfo>) {
let crash_dir = Path::new("/var/crash");
if let Ok(entries) = std::fs::read_dir(crash_dir) {
for entry in entries.flatten() {
let path = entry.path();
let filename = path.file_name().unwrap_or_default().to_string_lossy().to_string();
if filename.ends_with(".crash") {
let proc_name = filename.trim_end_matches(".crash").to_string();
let metadata = std::fs::metadata(&path).ok();
let time_str = metadata
.and_then(|m| m.modified().ok())
.map(|t| format!("{:?}", t))
.unwrap_or_else(|| "Архивный".to_string());
crashes.push(CrashReportInfo {
timestamp: time_str,
process_name: proc_name,
pid: None,
signal_or_code: "Crash Dump".to_string(),
fault_module: None,
stack_trace_snippet: format!("Дамп аварийного завершения в /var/crash/{}", filename),
dump_path: Some(path.to_string_lossy().to_string()),
});
}
}
}
}
#[cfg(target_os = "linux")]
fn scan_linux_dmesg_segfaults(crashes: &mut Vec<CrashReportInfo>) {
// Поиск segfaults в dmesg через неинтерактивный вызов
if let Ok(output) = Command::new("dmesg").args(["-T", "-l", "err,warn"]).output() {
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
if line.contains("segfault at") || line.contains("general protection fault") {
let mut proc = "kernel".to_string();
let mut fault_mod = None;
if let Some(idx) = line.find("[") {
if let Some(_end) = line[idx..].find("]") {
let candidate = line[..idx].trim().split_whitespace().last().unwrap_or("");
if !candidate.is_empty() {
proc = candidate.to_string();
}
}
}
if let Some(in_idx) = line.find(" in ") {
let after_in = &line[in_idx + 4..];
let mod_name = after_in.split_whitespace().next().unwrap_or("");
if !mod_name.is_empty() {
fault_mod = Some(mod_name.to_string());
}
}
crashes.push(CrashReportInfo {
timestamp: "Из dmesg".to_string(),
process_name: proc,
pid: None,
signal_or_code: "SIGSEGV (Segmentation Fault)".to_string(),
fault_module: fault_mod,
stack_trace_snippet: line.trim().to_string(),
dump_path: None,
});
}
}
}
}
#[cfg(target_os = "windows")]
fn scan_windows_minidumps(crashes: &mut Vec<CrashReportInfo>) {
let minidump_dir = Path::new(r"C:\Windows\Minidump");
if let Ok(entries) = std::fs::read_dir(minidump_dir) {
for entry in entries.flatten() {
let path = entry.path();
let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
if name.ends_with(".dmp") {
crashes.push(CrashReportInfo {
timestamp: "BSOD Crash Dump".to_string(),
process_name: "ntoskrnl.exe (Kernel BSOD)".to_string(),
pid: Some(4),
signal_or_code: "BUGCHECK_DUMP".to_string(),
fault_module: Some("Kernel".to_string()),
stack_trace_snippet: format!("Дамп аварийного синего экрана смерти: {}", path.display()),
dump_path: Some(path.to_string_lossy().to_string()),
});
}
}
}
}
#[cfg(target_os = "windows")]
fn scan_windows_event_crashes(crashes: &mut Vec<CrashReportInfo>) {
let ps_cmd = "Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='Application Error'} -MaxEvents 5 -ErrorAction SilentlyContinue | Select-Object TimeCreated, Message | ConvertTo-Json -Compress";
if let Ok(output) = Command::new("powershell").args(["-NoProfile", "-NonInteractive", "-Command", ps_cmd]).output() {
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&String::from_utf8_lossy(&output.stdout)) {
let items = match val {
serde_json::Value::Array(a) => a,
serde_json::Value::Object(_) => vec![val],
_ => vec![],
};
for item in items {
let msg = item["Message"].as_str().unwrap_or("");
let time = item["TimeCreated"].as_str().unwrap_or("Недавно");
crashes.push(CrashReportInfo {
timestamp: time.to_string(),
process_name: "Application Error".to_string(),
pid: None,
signal_or_code: "0xC0000005 (Access Violation)".to_string(),
fault_module: None,
stack_trace_snippet: msg.trim().to_string(),
dump_path: None,
});
}
}
}
}
#[cfg(target_os = "macos")]
fn scan_macos_diagnostic_reports(crashes: &mut Vec<CrashReportInfo>) {
let reports_dir = Path::new("/Library/Logs/DiagnosticReports");
if let Ok(entries) = std::fs::read_dir(reports_dir) {
for entry in entries.flatten() {
let path = entry.path();
let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
if name.ends_with(".ips") || name.ends_with(".crash") {
let proc = name.split(['_', '-']).next().unwrap_or(&name).to_string();
crashes.push(CrashReportInfo {
timestamp: "Diagnostic Report".to_string(),
process_name: proc,
pid: None,
signal_or_code: "EXC_BAD_ACCESS / Crash".to_string(),
fault_module: None,
stack_trace_snippet: format!("macOS Crash Report: {}", path.display()),
dump_path: Some(path.to_string_lossy().to_string()),
});
}
}
}
}
fn scan_generic_system_logs(_crashes: &mut Vec<CrashReportInfo>) {
// Fallback placeholder
}
}
File diff suppressed because it is too large Load Diff
+59
View File
@@ -0,0 +1,59 @@
use super::provider::{InitSystemProvider, ScanError};
use crate::models::FailedService;
use std::process::Command;
pub struct BsdRcProvider;
impl BsdRcProvider {
pub fn new() -> Self {
Self
}
}
impl Default for BsdRcProvider {
fn default() -> Self {
Self::new()
}
}
impl InitSystemProvider for BsdRcProvider {
fn name(&self) -> &'static str {
"bsd-rc"
}
fn is_active(&self) -> bool {
#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))]
{
true
}
#[cfg(not(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd")))]
{
false
}
}
fn get_failed_services(&self) -> Result<Vec<FailedService>, ScanError> {
let mut failed = Vec::new();
// 1. OpenBSD rcctl
if let Ok(output) = Command::new("rcctl").args(["ls", "failed"]).output() {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
failed.push(FailedService {
name: trimmed.to_string(),
init_system: "bsd-rc".to_string(),
state: "failed".to_string(),
description: format!("Daemon {} failed in OpenBSD rcctl", trimmed),
logs: "Check /var/log/messages or /var/log/daemon".to_string(),
restart_command: format!("sudo rcctl restart {0} && rcctl check {0}", trimmed),
status_command: format!("rcctl check {0}", trimmed),
});
}
}
}
Ok(failed)
}
}
+91
View File
@@ -0,0 +1,91 @@
use super::provider::{InitSystemProvider, ScanError};
use crate::models::FailedService;
use std::process::Command;
pub struct LaunchdProvider;
impl LaunchdProvider {
pub fn new() -> Self {
Self
}
}
impl Default for LaunchdProvider {
fn default() -> Self {
Self::new()
}
}
impl InitSystemProvider for LaunchdProvider {
fn name(&self) -> &'static str {
"launchd"
}
fn is_active(&self) -> bool {
#[cfg(target_os = "macos")]
{
true
}
#[cfg(not(target_os = "macos"))]
{
false
}
}
fn get_failed_services(&self) -> Result<Vec<FailedService>, ScanError> {
let mut failed = Vec::new();
// 1. Опрос списка служб launchctl list
if let Ok(output) = Command::new("launchctl").arg("list").output() {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines().skip(1) {
// Строка имеет вид: PID \t Status \t Label
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let pid_str = parts[0];
let status_str = parts[1];
let label = parts[2];
// Игнорируем стандартные успешные статусы (0)
if let Ok(exit_code) = status_str.parse::<i32>() {
if exit_code != 0 && exit_code != -9 && exit_code != -15 {
// Получаем короткий журнал сбоя через `log show`
let logs = Self::collect_logs(label);
failed.push(FailedService {
name: label.to_string(),
init_system: "launchd".to_string(),
state: format!("exit({}) PID: {}", exit_code, pid_str),
description: format!("Служба launchd завершилась с кодом ошибки {}", exit_code),
logs,
restart_command: format!("launchctl kickstart -k gui/$(id -u)/{0} || sudo launchctl kickstart -k system/{0}", label),
status_command: format!("launchctl print gui/$(id -u)/{0} || launchctl print system/{0}", label),
});
}
}
}
}
}
Ok(failed)
}
}
impl LaunchdProvider {
fn collect_logs(label: &str) -> String {
let predicate = format!("subsystem contains \"{}\" OR process contains \"{}\"", label, label);
if let Ok(output) = Command::new("log")
.args(["show", "--predicate", &predicate, "--last", "15m", "--style", "compact"])
.output()
{
let text = String::from_utf8_lossy(&output.stdout);
if !text.trim().is_empty() {
let lines: Vec<&str> = text.lines().rev().take(30).collect();
let mut rev_lines = lines;
rev_lines.reverse();
return rev_lines.join("\n");
}
}
format!("Записи о падении {} в unified log отсутствуют.", label)
}
}
+73
View File
@@ -0,0 +1,73 @@
pub mod provider;
pub mod systemd;
pub mod openrc;
pub mod runit;
pub mod bsd_rc;
pub mod launchd;
pub mod windows_scm;
pub use provider::{InitSystemProvider, ScanError};
pub use systemd::SystemdProvider;
pub use openrc::OpenRcProvider;
pub use runit::RunitProvider;
pub use bsd_rc::BsdRcProvider;
pub use launchd::LaunchdProvider;
pub use windows_scm::WindowsScmProvider;
/// Автоматически определяет активную систему инициализации в текущем окружении
pub fn detect_init_system() -> Box<dyn InitSystemProvider> {
// 1. macOS launchd
let launchd = LaunchdProvider::new();
if launchd.is_active() {
return Box::new(launchd);
}
// 2. Windows SCM
let win_scm = WindowsScmProvider::new();
if win_scm.is_active() {
return Box::new(win_scm);
}
// 3. systemd
let systemd = SystemdProvider::new();
if systemd.is_active() {
return Box::new(systemd);
}
// 4. OpenRC
let openrc = OpenRcProvider::new();
if openrc.is_active() {
return Box::new(openrc);
}
// 5. runit
let runit = RunitProvider::new();
if runit.is_active() {
return Box::new(runit);
}
// 6. BSD rc
let bsd = BsdRcProvider::new();
if bsd.is_active() {
return Box::new(bsd);
}
// Fallback: если ничего явно не определилось
#[cfg(target_os = "windows")]
return Box::new(WindowsScmProvider::new());
#[cfg(target_os = "macos")]
return Box::new(LaunchdProvider::new());
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
Box::new(SystemdProvider::new())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_init_system() {
let init = detect_init_system();
assert!(!init.name().is_empty());
}
}
+83
View File
@@ -0,0 +1,83 @@
use super::provider::{InitSystemProvider, ScanError};
use crate::models::FailedService;
use std::path::Path;
use std::process::Command;
pub struct OpenRcProvider;
impl OpenRcProvider {
pub fn new() -> Self {
Self
}
}
impl Default for OpenRcProvider {
fn default() -> Self {
Self::new()
}
}
impl InitSystemProvider for OpenRcProvider {
fn name(&self) -> &'static str {
"openrc"
}
fn is_active(&self) -> bool {
if Path::new("/run/openrc").exists() || Path::new("/etc/init.d").exists() && Path::new("/sbin/rc-status").exists() {
return true;
}
if let Ok(comm) = std::fs::read_to_string("/proc/1/comm") {
if comm.trim() == "openrc-init" {
return true;
}
}
false
}
fn get_failed_services(&self) -> Result<Vec<FailedService>, ScanError> {
// Опрос crashed сервисов через rc-status -c
let output = Command::new("rc-status")
.args(["-c"])
.output()
.or_else(|_| Command::new("rc-status").args(["-s"]).output())
.map_err(|e| ScanError::CommandFailed(e.to_string()))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut failed = Vec::new();
for line in stdout.lines() {
let line_trimmed = line.trim();
if line_trimmed.contains("crashed") || line_trimmed.contains("failed") || line_trimmed.contains("broken") {
let parts: Vec<&str> = line_trimmed.split_whitespace().collect();
if let Some(service) = parts.first() {
let svc_name = service.to_string();
// Читаем логи из /var/log/rc.log или syslog
let logs = std::fs::read_to_string("/var/log/rc.log")
.unwrap_or_default()
.lines()
.rev()
.filter(|l| l.contains(&svc_name))
.take(30)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>()
.join("\n");
failed.push(FailedService {
name: svc_name.clone(),
init_system: "openrc".to_string(),
state: "crashed".to_string(),
description: format!("Service {} crashed under OpenRC supervision", svc_name),
logs: if logs.is_empty() { "No entries in /var/log/rc.log".to_string() } else { logs },
restart_command: format!("sudo rc-service {0} restart && sudo rc-service {0} status", svc_name),
status_command: format!("rc-service {0} status", svc_name),
});
}
}
}
Ok(failed)
}
}
+35
View File
@@ -0,0 +1,35 @@
use crate::models::FailedService;
use std::fmt;
#[derive(Debug, Clone)]
pub enum ScanError {
CommandFailed(String),
PermissionDenied(String),
Io(String),
NotSupported(String),
}
impl fmt::Display for ScanError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ScanError::CommandFailed(e) => write!(f, "Init command error: {}", e),
ScanError::PermissionDenied(e) => write!(f, "Permission denied: {}", e),
ScanError::Io(e) => write!(f, "I/O error: {}", e),
ScanError::NotSupported(e) => write!(f, "Init system not supported: {}", e),
}
}
}
impl std::error::Error for ScanError {}
/// Единый трейт для взаимодействия с системами инициализации ОС
pub trait InitSystemProvider: Send + Sync {
/// Имя системы инициализации (systemd, openrc, runit, s6, dinit, bsd-rc)
fn name(&self) -> &'static str;
/// Проверка, активна ли данная система инициализации в текущем окружении
fn is_active(&self) -> bool;
/// Получение списка упавших или аварийно завершенных служб
fn get_failed_services(&self) -> Result<Vec<FailedService>, ScanError>;
}
+87
View File
@@ -0,0 +1,87 @@
use super::provider::{InitSystemProvider, ScanError};
use crate::models::FailedService;
use std::path::Path;
use std::process::Command;
pub struct RunitProvider;
impl RunitProvider {
pub fn new() -> Self {
Self
}
}
impl Default for RunitProvider {
fn default() -> Self {
Self::new()
}
}
impl InitSystemProvider for RunitProvider {
fn name(&self) -> &'static str {
"runit"
}
fn is_active(&self) -> bool {
if Path::new("/run/runit").exists() || Path::new("/var/service").exists() && Path::new("/usr/bin/sv").exists() {
return true;
}
if let Ok(comm) = std::fs::read_to_string("/proc/1/comm") {
if comm.trim() == "runit" || comm.trim() == "runit-init" {
return true;
}
}
false
}
fn get_failed_services(&self) -> Result<Vec<FailedService>, ScanError> {
let output = Command::new("sh")
.args(["-c", "sv status /var/service/* 2>&1"])
.output()
.map_err(|e| ScanError::CommandFailed(e.to_string()))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut failed = Vec::new();
for line in stdout.lines() {
let line_trimmed = line.trim();
// runit выводит: "down: /var/service/sshd: 0s, normally up, want up" или "fail: ..."
if line_trimmed.starts_with("down:") || line_trimmed.starts_with("fail:") {
let parts: Vec<&str> = line_trimmed.split_whitespace().collect();
if parts.len() >= 2 {
let full_path = parts[1].trim_end_matches(':');
let svc_name = Path::new(full_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(full_path)
.to_string();
// Читаем логи svlogd: /var/log/<service>/current
let log_path = format!("/var/log/{}/current", svc_name);
let logs = std::fs::read_to_string(&log_path)
.unwrap_or_default()
.lines()
.rev()
.take(30)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>()
.join("\n");
failed.push(FailedService {
name: svc_name.clone(),
init_system: "runit".to_string(),
state: "down/failed".to_string(),
description: format!("Service {} is down or failed in runit", svc_name),
logs: if logs.is_empty() { format!("No svlogd logs at {}", log_path) } else { logs },
restart_command: format!("sudo sv restart {0} && sv status {0}", svc_name),
status_command: format!("sv status {0}", svc_name),
});
}
}
}
Ok(failed)
}
}
+84
View File
@@ -0,0 +1,84 @@
use super::provider::{InitSystemProvider, ScanError};
use crate::models::FailedService;
use std::path::Path;
use std::process::Command;
pub struct SystemdProvider;
impl SystemdProvider {
pub fn new() -> Self {
Self
}
}
impl Default for SystemdProvider {
fn default() -> Self {
Self::new()
}
}
impl InitSystemProvider for SystemdProvider {
fn name(&self) -> &'static str {
"systemd"
}
fn is_active(&self) -> bool {
// 1. Проверка /run/systemd/system
if Path::new("/run/systemd/system").exists() {
return true;
}
// 2. Проверка /proc/1/comm == "systemd"
if let Ok(comm) = std::fs::read_to_string("/proc/1/comm") {
if comm.trim() == "systemd" {
return true;
}
}
false
}
fn get_failed_services(&self) -> Result<Vec<FailedService>, ScanError> {
let output = Command::new("systemctl")
.args(["--failed", "--all", "--no-legend", "--plain"])
.output()
.map_err(|e| ScanError::CommandFailed(e.to_string()))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("Access denied") || stderr.contains("Permission denied") {
return Err(ScanError::PermissionDenied(stderr.to_string()));
}
return Err(ScanError::CommandFailed(stderr.to_string()));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut failed = Vec::new();
for line in stdout.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
continue;
}
let unit = parts[0].to_string();
// Читаем журнал юнита: journalctl -u <unit> -b -n 40 --no-pager
let logs = crate::auth::PrivilegeManager::exec_privileged(
"journalctl",
&["-u", &unit, "-b", "-n", "40", "--no-pager"],
)
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default();
failed.push(FailedService {
name: unit.clone(),
init_system: "systemd".to_string(),
state: "failed".to_string(),
description: format!("Unit {} is in failed state in systemd", unit),
logs,
restart_command: format!("sudo systemctl restart {0} && systemctl status {0}", unit),
status_command: format!("systemctl status {0} -l", unit),
});
}
Ok(failed)
}
}
@@ -0,0 +1,95 @@
use super::provider::{InitSystemProvider, ScanError};
use crate::models::FailedService;
use std::process::Command;
pub struct WindowsScmProvider;
impl WindowsScmProvider {
pub fn new() -> Self {
Self
}
}
impl Default for WindowsScmProvider {
fn default() -> Self {
Self::new()
}
}
impl InitSystemProvider for WindowsScmProvider {
fn name(&self) -> &'static str {
"windows-scm"
}
fn is_active(&self) -> bool {
#[cfg(target_os = "windows")]
{
true
}
#[cfg(not(target_os = "windows"))]
{
false
}
}
fn get_failed_services(&self) -> Result<Vec<FailedService>, ScanError> {
let mut failed = Vec::new();
// 1. Поиск служб Windows с автоматическим запуском, находящихся в остановленном состоянии
let ps_cmd = "Get-CimInstance Win32_Service | Where-Object { $_.StartMode -eq 'Auto' -and $_.State -ne 'Running' } | Select-Object Name, DisplayName, State, ExitCode | ConvertTo-Json -Compress";
if let Ok(output) = Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", ps_cmd])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&stdout) {
let items = match val {
serde_json::Value::Array(arr) => arr,
serde_json::Value::Object(_) => vec![val],
_ => vec![],
};
for item in items {
let name = item["Name"].as_str().unwrap_or("Unknown").to_string();
let display_name = item["DisplayName"].as_str().unwrap_or(&name).to_string();
let state = item["State"].as_str().unwrap_or("Stopped").to_string();
let exit_code = item["ExitCode"].as_i64().unwrap_or(0);
let logs = Self::collect_event_logs(&name);
failed.push(FailedService {
name: name.clone(),
init_system: "windows-scm".to_string(),
state: format!("{} (ExitCode: {})", state, exit_code),
description: format!("Служба Windows '{}' ({}) не запущена", display_name, name),
logs,
restart_command: format!("Start-Service -Name '{}'", name),
status_command: format!("Get-Service -Name '{}'", name),
});
}
}
}
Ok(failed)
}
}
impl WindowsScmProvider {
fn collect_event_logs(service_name: &str) -> String {
let ps_cmd = format!(
"Get-WinEvent -FilterHashtable @{{LogName='System'; ProviderName='Service Control Manager'}} -MaxEvents 5 -ErrorAction SilentlyContinue | Where-Object {{ $_.Message -like '*{}*' }} | ForEach-Object {{ $_.TimeCreated.ToString('s') + ' ' + $_.Message }}",
service_name
);
if let Ok(output) = Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", &ps_cmd])
.output()
{
let text = String::from_utf8_lossy(&output.stdout);
if !text.trim().is_empty() {
return text.trim().to_string();
}
}
format!("События сбоя службы {} в журнале System Event Log не найдены.", service_name)
}
}
+111
View File
@@ -0,0 +1,111 @@
#[derive(Debug, Clone, Default)]
pub struct KernelScanResult {
pub raw_logs: String,
pub oom_events: Vec<String>,
pub gpu_hangs: Vec<String>,
pub io_errors: Vec<String>,
pub csum_errors: Vec<String>,
pub tainted_value: u64,
pub tainted_flags: Vec<String>,
pub permission_denied: bool,
}
pub struct KernelScanner;
impl KernelScanner {
pub fn scan() -> KernelScanResult {
let mut result = KernelScanResult::default();
// 1. Читаем /proc/sys/kernel/tainted
if let Ok(tainted_str) = std::fs::read_to_string("/proc/sys/kernel/tainted") {
if let Ok(val) = tainted_str.trim().parse::<u64>() {
result.tainted_value = val;
result.tainted_flags = Self::decode_tainted(val);
}
}
// 2. Читаем dmesg / /dev/kmsg
let dmesg_out = crate::auth::PrivilegeManager::exec_privileged("dmesg", &["--level=err,crit,alert,emerg"])
.or_else(|_| crate::auth::PrivilegeManager::exec_privileged("dmesg", &[]));
let logs = match dmesg_out {
Ok(output) => {
if !output.status.success() {
let err = String::from_utf8_lossy(&output.stderr);
if err.contains("Operation not permitted") || err.contains("Permission denied") {
result.permission_denied = true;
}
String::new()
} else {
String::from_utf8_lossy(&output.stdout).to_string()
}
}
Err(_) => {
// Попытка прямого чтения /dev/kmsg
match std::fs::read_to_string("/dev/kmsg") {
Ok(kmsg) => kmsg,
Err(e) => {
if e.kind() == std::io::ErrorKind::PermissionDenied {
result.permission_denied = true;
}
String::new()
}
}
}
};
result.raw_logs = logs.clone();
// 3. Парсинг критических паттернов
for line in logs.lines() {
let lower = line.to_lowercase();
// OOM-Killer
if lower.contains("out of memory") || lower.contains("oom-killer") || lower.contains("invoked oom-killer") {
result.oom_events.push(line.to_string());
}
// GPU Hangs (NVIDIA Xid, AMDGPU reset, Intel i915 hang)
if line.contains("NVRM: Xid") || lower.contains("amdgpu reset") || lower.contains("gpu hang") || lower.contains("drm/i915: reset") {
result.gpu_hangs.push(line.to_string());
}
// I/O Errors
if lower.contains("i/o error") || lower.contains("blk_update_request") || lower.contains("ext4-fs error") || lower.contains("btrfs: error") {
result.io_errors.push(line.to_string());
}
// Checksum errors (Btrfs / ZFS csum)
if lower.contains("checksum error") || lower.contains("csum failed") || lower.contains("checksum mismatch") {
result.csum_errors.push(line.to_string());
}
}
result
}
/// Декодирует флаги /proc/sys/kernel/tainted
pub fn decode_tainted(val: u64) -> Vec<String> {
let mut flags = Vec::new();
if val & (1 << 0) != 0 { flags.push("P: Proprietary module loaded".to_string()); }
if val & (1 << 1) != 0 { flags.push("F: Module force loaded".to_string()); }
if val & (1 << 2) != 0 { flags.push("S: SMP with CPUs not designed for SMP".to_string()); }
if val & (1 << 3) != 0 { flags.push("R: Module force unloaded".to_string()); }
if val & (1 << 4) != 0 { flags.push("M: Machine Check Exception occurred".to_string()); }
if val & (1 << 5) != 0 { flags.push("B: Bad page referenced".to_string()); }
if val & (1 << 6) != 0 { flags.push("U: User requested taint".to_string()); }
if val & (1 << 7) != 0 { flags.push("D: Kernel died / oops".to_string()); }
if val & (1 << 8) != 0 { flags.push("A: ACPI table overridden".to_string()); }
if val & (1 << 9) != 0 { flags.push("W: Kernel warning issued".to_string()); }
if val & (1 << 10) != 0 { flags.push("C: Staging driver loaded".to_string()); }
if val & (1 << 11) != 0 { flags.push("I: Firmware workaround active".to_string()); }
if val & (1 << 12) != 0 { flags.push("O: External out-of-tree module loaded".to_string()); }
if val & (1 << 13) != 0 { flags.push("E: Unsigned module loaded".to_string()); }
if val & (1 << 14) != 0 { flags.push("L: Soft lockup occurred".to_string()); }
if val & (1 << 15) != 0 { flags.push("K: Kernel livepatched".to_string()); }
if val & (1 << 16) != 0 { flags.push("X: Auxiliary taint".to_string()); }
if val & (1 << 17) != 0 { flags.push("T: Built with struct layout randomization".to_string()); }
flags
}
}
+59
View File
@@ -0,0 +1,59 @@
use std::process::Command;
#[derive(Debug, Clone, Default)]
pub struct SystemLimitsResult {
pub max_user_watches: u64,
pub file_nr_allocated: u64,
pub file_nr_max: u64,
pub pid_max: u64,
pub file_descriptors_exhausted: bool,
pub recent_coredumps: Vec<String>,
}
pub struct LimitsScanner;
impl LimitsScanner {
pub fn scan() -> SystemLimitsResult {
let mut result = SystemLimitsResult::default();
// 1. inotify max_user_watches
if let Ok(val) = std::fs::read_to_string("/proc/sys/fs/inotify/max_user_watches") {
result.max_user_watches = val.trim().parse().unwrap_or(0);
}
// 2. file-nr: allocated, unused, max
if let Ok(val) = std::fs::read_to_string("/proc/sys/fs/file-nr") {
let parts: Vec<&str> = val.split_whitespace().collect();
if parts.len() >= 3 {
result.file_nr_allocated = parts[0].parse().unwrap_or(0);
result.file_nr_max = parts[2].parse().unwrap_or(0);
if result.file_nr_max > 0 {
let ratio = result.file_nr_allocated as f64 / result.file_nr_max as f64;
if ratio >= 0.95 {
result.file_descriptors_exhausted = true;
}
}
}
}
// 3. pid_max
if let Ok(val) = std::fs::read_to_string("/proc/sys/kernel/pid_max") {
result.pid_max = val.trim().parse().unwrap_or(0);
}
// 4. coredumpctl
if let Ok(output) = Command::new("coredumpctl").args(["list", "--no-legend", "-n", "5"]).output() {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
result.recent_coredumps.push(trimmed.to_string());
}
}
}
}
result
}
}
+86
View File
@@ -0,0 +1,86 @@
use std::process::Command;
pub struct LinuxRawData {
pub failed_units: Vec<String>,
pub systemd_logs: String,
pub kernel_logs: String,
pub storage_logs: String,
pub network_logs: String,
pub audio_logs: String,
}
impl LinuxRawData {
pub fn collect() -> Self {
// 1. Опрос systemd на упавшие юниты
let failed_output = Command::new("systemctl")
.args(["--failed", "--no-legend", "--plain"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default();
let mut failed_units = Vec::new();
for line in failed_output.lines() {
if let Some(unit) = line.split_whitespace().next() {
if !unit.is_empty() {
failed_units.push(unit.to_string());
}
}
}
// Логи упавших сервисов
let mut systemd_logs = String::new();
if !failed_units.is_empty() {
for unit in failed_units.iter().take(3) {
let status = Command::new("journalctl")
.args(["-u", unit, "-n", "20", "--no-pager"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default();
systemd_logs.push_str(&format!("--- Logs for {} ---\n{}\n", unit, status));
}
}
// 2. Критические логи ядра
let kernel_logs = Command::new("journalctl")
.args(["-k", "-p", "3", "-n", "30", "--no-pager"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_else(|_| {
Command::new("dmesg")
.args(["--level=err,crit,alert,emerg"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default()
});
// 3. Дисковые ошибки
let storage_logs = Command::new("journalctl")
.args(["-g", "(I/O error|EXT4-fs error|BTRFS error|blk_update_request)", "-n", "20", "--no-pager"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default();
// 4. Сеть
let network_logs = Command::new("journalctl")
.args(["-u", "NetworkManager", "-p", "4", "-n", "20", "--no-pager"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default();
// 5. Звук
let audio_logs = Command::new("journalctl")
.args(["--user-unit", "pipewire", "-p", "4", "-n", "20", "--no-pager"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default();
Self {
failed_units,
systemd_logs,
kernel_logs,
storage_logs,
network_logs,
audio_logs,
}
}
}
+202
View File
@@ -0,0 +1,202 @@
use crate::models::LiveMetricsSnapshot;
use std::fs;
use std::sync::Mutex;
use std::time::Instant;
static PREV_CPU_SAMPLE: Mutex<Option<(u64, u64, Instant)>> = Mutex::new(None);
static PREV_NET_SAMPLE: Mutex<Option<(u64, u64, Instant)>> = Mutex::new(None);
static PREV_DISK_SAMPLE: Mutex<Option<(u64, u64, Instant)>> = Mutex::new(None);
pub struct LiveMetricsScanner;
impl LiveMetricsScanner {
pub fn sample() -> LiveMetricsSnapshot {
let mut snap = LiveMetricsSnapshot::default();
snap.cpu_usage_percent = Self::sample_cpu_usage();
Self::sample_memory(&mut snap);
Self::sample_thermals(&mut snap);
Self::sample_network_io(&mut snap);
Self::sample_disk_io(&mut snap);
snap
}
fn sample_cpu_usage() -> f32 {
#[cfg(target_os = "linux")]
if let Ok(content) = fs::read_to_string("/proc/stat") {
if let Some(first_line) = content.lines().next() {
let parts: Vec<&str> = first_line.split_whitespace().skip(1).collect();
if parts.len() >= 4 {
let user: u64 = parts[0].parse().unwrap_or(0);
let nice: u64 = parts[1].parse().unwrap_or(0);
let system: u64 = parts[2].parse().unwrap_or(0);
let idle: u64 = parts[3].parse().unwrap_or(0);
let iowait: u64 = parts.get(4).and_then(|v| v.parse().ok()).unwrap_or(0);
let irq: u64 = parts.get(5).and_then(|v| v.parse().ok()).unwrap_or(0);
let softirq: u64 = parts.get(6).and_then(|v| v.parse().ok()).unwrap_or(0);
let total_idle = idle + iowait;
let total_active = user + nice + system + irq + softirq;
let total = total_idle + total_active;
let now = Instant::now();
if let Ok(mut lock) = PREV_CPU_SAMPLE.lock() {
if let Some((prev_total, prev_idle, _)) = *lock {
let diff_total = total.saturating_sub(prev_total);
let diff_idle = total_idle.saturating_sub(prev_idle);
*lock = Some((total, total_idle, now));
if diff_total > 0 {
let usage = ((diff_total.saturating_sub(diff_idle)) as f32 / diff_total as f32) * 100.0;
return usage.clamp(0.0, 100.0);
}
} else {
*lock = Some((total, total_idle, now));
return 15.0; // Значение по умолчанию на 1-м тике
}
}
}
}
}
20.0
}
fn sample_memory(snap: &mut LiveMetricsSnapshot) {
#[cfg(target_os = "linux")]
if let Ok(meminfo) = fs::read_to_string("/proc/meminfo") {
for line in meminfo.lines() {
let mut parts = line.split(':');
let key = parts.next().unwrap_or("").trim();
let val_str = parts.next().unwrap_or("").trim();
let kb = val_str.split_whitespace().next().and_then(|v| v.parse::<u64>().ok()).unwrap_or(0);
let bytes = kb * 1024;
match key {
"MemTotal" => snap.ram_total_bytes = bytes,
"MemAvailable" => {
let avail = bytes;
if snap.ram_total_bytes >= avail {
snap.ram_used_bytes = snap.ram_total_bytes - avail;
}
}
"SwapTotal" => snap.swap_total_bytes = bytes,
"SwapFree" => {
let free = bytes;
if snap.swap_total_bytes >= free {
snap.swap_used_bytes = snap.swap_total_bytes - free;
}
}
_ => {}
}
}
}
}
fn sample_thermals(snap: &mut LiveMetricsSnapshot) {
let mut max_temp = 0.0f32;
#[cfg(target_os = "linux")]
if let Ok(entries) = fs::read_dir("/sys/class/thermal") {
for entry in entries.flatten() {
let p = entry.path();
if p.file_name().map(|n| n.to_string_lossy().starts_with("thermal_zone")).unwrap_or(false) {
if let Ok(content) = fs::read_to_string(p.join("temp")) {
if let Ok(milli) = content.trim().parse::<i64>() {
let c = milli as f32 / 1000.0;
if c > max_temp && c < 150.0 {
max_temp = c;
}
}
}
}
}
}
// Детекция активного троттлинга
let mut throttle = max_temp >= 90.0;
#[cfg(target_os = "linux")]
if let Ok(entries) = fs::read_dir("/sys/devices/system/cpu") {
for entry in entries.flatten() {
let p = entry.path().join("thermal_throttle/core_throttle_count");
if let Ok(cnt) = fs::read_to_string(p) {
if let Ok(num) = cnt.trim().parse::<u64>() {
if num > 0 {
throttle = true;
}
}
}
}
}
snap.max_cpu_temp_c = if max_temp > 0.0 { max_temp } else { 38.0 };
snap.is_throttling = throttle;
}
fn sample_network_io(snap: &mut LiveMetricsSnapshot) {
#[cfg(target_os = "linux")]
if let Ok(content) = fs::read_to_string("/proc/net/dev") {
let mut total_rx = 0u64;
let mut total_tx = 0u64;
for line in content.lines().skip(2) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 10 {
let iface = parts[0].trim_end_matches(':');
if iface != "lo" {
let rx: u64 = parts[1].parse().unwrap_or(0);
let tx: u64 = parts[9].parse().unwrap_or(0);
total_rx += rx;
total_tx += tx;
}
}
}
let now = Instant::now();
if let Ok(mut lock) = PREV_NET_SAMPLE.lock() {
if let Some((prev_rx, prev_tx, prev_t)) = *lock {
let dt = now.duration_since(prev_t).as_secs_f32().max(0.1);
snap.net_rx_kbps = ((total_rx.saturating_sub(prev_rx)) as f32 / (dt * 1024.0)) as u64;
snap.net_tx_kbps = ((total_tx.saturating_sub(prev_tx)) as f32 / (dt * 1024.0)) as u64;
*lock = Some((total_rx, total_tx, now));
} else {
*lock = Some((total_rx, total_tx, now));
}
}
}
}
fn sample_disk_io(snap: &mut LiveMetricsSnapshot) {
#[cfg(target_os = "linux")]
if let Ok(content) = fs::read_to_string("/proc/diskstats") {
let mut read_sectors = 0u64;
let mut write_sectors = 0u64;
for line in content.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 14 {
let dev_name = parts[2];
if dev_name.starts_with("sd") || dev_name.starts_with("nvme") {
if !dev_name.chars().last().unwrap_or(' ').is_ascii_digit() || dev_name.contains('n') {
let rs: u64 = parts[5].parse().unwrap_or(0);
let ws: u64 = parts[9].parse().unwrap_or(0);
read_sectors += rs;
write_sectors += ws;
}
}
}
}
let now = Instant::now();
if let Ok(mut lock) = PREV_DISK_SAMPLE.lock() {
if let Some((prev_r, prev_w, prev_t)) = *lock {
let dt = now.duration_since(prev_t).as_secs_f32().max(0.1);
// 1 sector = 512 bytes = 0.5 KB
snap.disk_read_kbps = ((read_sectors.saturating_sub(prev_r) * 512) as f32 / (dt * 1024.0)) as u64;
snap.disk_write_kbps = ((write_sectors.saturating_sub(prev_w) * 512) as f32 / (dt * 1024.0)) as u64;
*lock = Some((read_sectors, write_sectors, now));
} else {
*lock = Some((read_sectors, write_sectors, now));
}
}
}
}
}
+114
View File
@@ -0,0 +1,114 @@
pub mod init;
pub mod kernel;
pub mod limits;
pub mod pressure;
pub mod storage;
pub mod hardware;
pub mod network_deep;
pub mod crashes;
pub mod security;
pub mod live;
pub mod linux;
pub mod bsd;
use crate::auth::PrivilegeManager;
use crate::models::{CrashReportInfo, FailedService, SecurityAuditReport};
pub use init::{detect_init_system, InitSystemProvider};
pub use kernel::{KernelScanner, KernelScanResult};
pub use limits::{LimitsScanner, SystemLimitsResult};
pub use pressure::{PressureScanner, PsiMetrics};
pub use storage::{StorageScanner, MountHealth};
pub use hardware::{HardwareScanner, HardwareAuditResult};
pub use network_deep::{NetworkDeepScanner, NetworkDeepScanResult};
pub use crashes::CrashScanner;
pub use security::SecurityScanner;
pub use live::LiveMetricsScanner;
pub struct UnifiedRawData {
pub os_name: String,
pub init_name: String,
pub is_root: bool,
pub failed_services: Vec<FailedService>,
pub kernel_scan: KernelScanResult,
pub limits_scan: SystemLimitsResult,
pub pressure_scan: PsiMetrics,
pub storage_mounts: Vec<MountHealth>,
pub hardware_scan: HardwareAuditResult,
pub hardware_info: crate::models::HardwareInfo,
pub network_deep_scan: NetworkDeepScanResult,
pub crashes: Vec<CrashReportInfo>,
pub security: SecurityAuditReport,
pub service_logs: String,
pub network_logs: String,
pub audio_logs: String,
}
impl UnifiedRawData {
pub fn collect(mode: Option<crate::auth::AuthMode>) -> Self {
if let Some(m) = mode {
let _ = PrivilegeManager::ensure_privileges(m);
}
let is_root = PrivilegeManager::is_current_user_root()
|| matches!(
PrivilegeManager::ensure_privileges(mode.unwrap_or(crate::auth::AuthMode::Tui)),
crate::auth::PrivilegeStatus::Elevated
);
let init_provider = detect_init_system();
let init_name = init_provider.name().to_string();
let failed_services = init_provider.get_failed_services().unwrap_or_default();
let kernel_scan = KernelScanner::scan();
let limits_scan = LimitsScanner::scan();
let pressure_scan = PressureScanner::scan();
let storage_mounts = StorageScanner::scan();
let (hardware_scan, hardware_info) = HardwareScanner::scan();
let network_deep_scan = NetworkDeepScanner::scan();
let crashes = CrashScanner::scan();
let security = SecurityScanner::scan();
#[cfg(target_os = "linux")]
let (service_logs, network_logs, audio_logs) = {
let data = linux::LinuxRawData::collect();
(data.systemd_logs, data.network_logs, data.audio_logs)
};
#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))]
let (service_logs, network_logs, audio_logs) = {
let data = bsd::BsdRawData::collect();
(data.service_logs, data.network_logs, data.audio_logs)
};
#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd")))]
let (service_logs, network_logs, audio_logs) = (String::new(), String::new(), String::new());
let os_name = if cfg!(target_os = "linux") {
"Linux".to_string()
} else if cfg!(target_os = "freebsd") {
"FreeBSD".to_string()
} else if cfg!(target_os = "openbsd") {
"OpenBSD".to_string()
} else {
std::env::consts::OS.to_string()
};
UnifiedRawData {
os_name,
init_name,
is_root,
failed_services,
kernel_scan,
limits_scan,
pressure_scan,
storage_mounts,
hardware_scan,
hardware_info,
network_deep_scan,
crashes,
security,
service_logs,
network_logs,
audio_logs,
}
}
}
+199
View File
@@ -0,0 +1,199 @@
use std::net::ToSocketAddrs;
use std::path::Path;
use std::time::Instant;
#[derive(Debug, Clone, Default)]
pub struct NetworkDeepScanResult {
pub dns_broken_symlink: bool,
pub dns_servers: Vec<String>,
pub dns_resolution_ok: bool,
pub dns_latency_ms: Option<u128>,
pub has_default_gateway: bool,
pub default_gateway_ip: Option<String>,
pub firewall_conflict: Option<String>,
pub wifi_drop_events: Vec<String>,
}
pub struct NetworkDeepScanner;
impl NetworkDeepScanner {
pub fn scan() -> NetworkDeepScanResult {
let mut res = NetworkDeepScanResult::default();
// 1. Проверка /etc/resolv.conf
Self::check_resolv_conf(&mut res);
// 2. Замер DNS задержки
Self::measure_dns(&mut res);
// 3. Проверка шлюза по умолчанию
Self::check_default_gateway(&mut res);
// 4. Конфликты брандмауэров
res.firewall_conflict = Self::check_firewall_conflicts();
// 5. Поиск сбросов Wi-Fi
res.wifi_drop_events = Self::scan_wifi_drops();
result_post_process(res)
}
fn check_resolv_conf(res: &mut NetworkDeepScanResult) {
let resolv = Path::new("/etc/resolv.conf");
if resolv.is_symlink() {
if let Ok(target) = std::fs::read_link(resolv) {
if !target.exists() && !resolv.exists() {
res.dns_broken_symlink = true;
}
}
}
if let Ok(content) = std::fs::read_to_string(resolv) {
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("nameserver") {
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() >= 2 {
res.dns_servers.push(parts[1].to_string());
}
}
}
}
#[cfg(target_os = "macos")]
if res.dns_servers.is_empty() {
if let Ok(output) = std::process::Command::new("scutil").arg("--dns").output() {
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
let trimmed = line.trim();
if trimmed.starts_with("nameserver[0] :") || trimmed.starts_with("nameserver[1] :") {
if let Some(ip) = trimmed.split(':').nth(1) {
let ip_clean = ip.trim().to_string();
if !res.dns_servers.contains(&ip_clean) {
res.dns_servers.push(ip_clean);
}
}
}
}
}
}
#[cfg(target_os = "windows")]
if res.dns_servers.is_empty() {
let ps_cmd = "Get-DnsClientServerAddress -AddressFamily IPv4 | Select-Object -ExpandProperty ServerAddresses";
if let Ok(output) = std::process::Command::new("powershell").args(["-NoProfile", "-NonInteractive", "-Command", ps_cmd]).output() {
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
let ip = line.trim().to_string();
if !ip.is_empty() && !res.dns_servers.contains(&ip) {
res.dns_servers.push(ip);
}
}
}
}
}
fn measure_dns(res: &mut NetworkDeepScanResult) {
let start = Instant::now();
// Пытаемся зарезолвить надежный домен
match ("cloudflare.com", 80).to_socket_addrs() {
Ok(_) => {
res.dns_resolution_ok = true;
res.dns_latency_ms = Some(start.elapsed().as_millis());
}
Err(_) => {
// Вторая попытка
match ("1.1.1.1", 80).to_socket_addrs() {
Ok(_) => {
res.dns_resolution_ok = false; // IP работает, а доменное имя нет -> DNS сломан!
}
Err(_) => {
res.dns_resolution_ok = false;
}
}
}
}
}
fn check_default_gateway(res: &mut NetworkDeepScanResult) {
if let Ok(content) = std::fs::read_to_string("/proc/net/route") {
for line in content.lines().skip(1) {
let parts: Vec<&str> = line.split_whitespace().collect();
// If destination is 00000000, it's default gateway
if parts.len() >= 3 && parts[1] == "00000000" {
res.has_default_gateway = true;
if let Ok(hex_ip) = u32::from_str_radix(parts[2], 16) {
let ip = std::net::Ipv4Addr::from(hex_ip.to_be());
res.default_gateway_ip = Some(ip.to_string());
}
break;
}
}
}
#[cfg(target_os = "macos")]
if !res.has_default_gateway {
if let Ok(output) = std::process::Command::new("route").args(["-n", "get", "default"]).output() {
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
let trimmed = line.trim();
if trimmed.starts_with("gateway:") {
if let Some(gw) = trimmed.split(':').nth(1) {
res.has_default_gateway = true;
res.default_gateway_ip = Some(gw.trim().to_string());
break;
}
}
}
}
}
#[cfg(target_os = "windows")]
if !res.has_default_gateway {
let ps_cmd = "Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Select-Object -ExpandProperty NextHop";
if let Ok(output) = std::process::Command::new("powershell").args(["-NoProfile", "-NonInteractive", "-Command", ps_cmd]).output() {
let gw = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !gw.is_empty() {
res.has_default_gateway = true;
res.default_gateway_ip = Some(gw);
}
}
}
}
fn check_firewall_conflicts() -> Option<String> {
let ufw_active = Path::new("/run/ufw").exists() || Path::new("/etc/ufw").exists();
let firewalld_active = Path::new("/run/firewalld").exists();
if ufw_active && firewalld_active {
return Some("Обнаружены одновременно активные службы UFW и firewalld. Это может вызывать сброс соединений.".to_string());
}
None
}
fn scan_wifi_drops() -> Vec<String> {
let mut drops = Vec::new();
// Ищем в journalctl события деаутентификации wpa_supplicant или iwd
if let Ok(output) = crate::auth::PrivilegeManager::exec_privileged(
"journalctl",
&["-u", "wpa_supplicant", "-u", "iwd", "-n", "30", "--no-pager"],
) {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let lower = line.to_lowercase();
if lower.contains("deauthenticated") || lower.contains("connection lost") || lower.contains("beacon loss") {
drops.push(line.to_string());
}
}
}
drops
}
}
fn result_post_process(mut res: NetworkDeepScanResult) -> NetworkDeepScanResult {
if res.dns_servers.is_empty() {
res.dns_resolution_ok = false;
}
res
}
+52
View File
@@ -0,0 +1,52 @@
#[derive(Debug, Clone, Default)]
pub struct PsiMetrics {
pub cpu_some_avg10: f32,
pub mem_some_avg10: f32,
pub mem_full_avg10: f32,
pub io_some_avg10: f32,
pub io_full_avg10: f32,
pub is_supported: bool,
}
pub struct PressureScanner;
impl PressureScanner {
pub fn scan() -> PsiMetrics {
let mut psi = PsiMetrics::default();
// 1. /proc/pressure/cpu
if let Ok(content) = std::fs::read_to_string("/proc/pressure/cpu") {
psi.is_supported = true;
psi.cpu_some_avg10 = Self::extract_avg10(&content, "some");
}
// 2. /proc/pressure/memory
if let Ok(content) = std::fs::read_to_string("/proc/pressure/memory") {
psi.is_supported = true;
psi.mem_some_avg10 = Self::extract_avg10(&content, "some");
psi.mem_full_avg10 = Self::extract_avg10(&content, "full");
}
// 3. /proc/pressure/io
if let Ok(content) = std::fs::read_to_string("/proc/pressure/io") {
psi.is_supported = true;
psi.io_some_avg10 = Self::extract_avg10(&content, "some");
psi.io_full_avg10 = Self::extract_avg10(&content, "full");
}
psi
}
fn extract_avg10(content: &str, line_prefix: &str) -> f32 {
for line in content.lines() {
if line.starts_with(line_prefix) {
for part in line.split_whitespace() {
if let Some(val_str) = part.strip_prefix("avg10=") {
return val_str.parse::<f32>().unwrap_or(0.0);
}
}
}
}
0.0
}
}
+364
View File
@@ -0,0 +1,364 @@
use crate::models::{
ListeningPortInfo, SecurityAuditReport, SecurityCheckItem, Severity, SuidAnomalyInfo,
};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::Command;
pub struct SecurityScanner;
impl SecurityScanner {
pub fn scan() -> SecurityAuditReport {
let mut report = SecurityAuditReport::default();
Self::scan_listening_ports(&mut report);
Self::scan_suid_anomalies(&mut report);
Self::scan_hardening_checks(&mut report);
Self::calculate_score(&mut report);
report
}
fn scan_listening_ports(report: &mut SecurityAuditReport) {
// 1. Используем утилиту ss -tulpn (или netstat)
if let Ok(output) = Command::new("ss").args(["-tulpn", "-H"]).output() {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 5 {
let proto = parts[0].to_ascii_uppercase();
let local_addr = parts[4];
// Разбираем IP и порт (например: 0.0.0.0:22 или [::]:80 или 127.0.0.1:6379)
if let Some(colon_idx) = local_addr.rfind(':') {
let ip = &local_addr[..colon_idx];
let port_str = &local_addr[colon_idx + 1..];
if let Ok(port) = port_str.parse::<u16>() {
let is_public = ip == "0.0.0.0" || ip == "*" || ip == "::" || ip == "[::]";
// Извлекаем имя процесса и PID из последней колонки: users:(("sshd",pid=123,fd=3))
let proc_info = parts.get(6).or_else(|| parts.get(5)).unwrap_or(&"");
let (proc_name, pid) = parse_process_info(proc_info);
let (risk_level, rec) = assess_port_risk(port, is_public, &proc_name);
report.exposed_ports.push(ListeningPortInfo {
protocol: proto,
local_address: local_addr.to_string(),
port,
process_name: proc_name,
pid,
is_public,
risk_level,
recommendation: rec,
});
}
}
}
}
} else {
// Fallback через чтение /proc/net/tcp
Self::scan_proc_net_tcp(report);
}
// Сортировка: сначала критические и публичные порты
report.exposed_ports.sort_by(|a, b| {
let rank = |sev: &Severity| match sev {
Severity::Critical => 0,
Severity::Warning => 1,
Severity::Info => 2,
};
rank(&a.risk_level).cmp(&rank(&b.risk_level)).then(b.is_public.cmp(&a.is_public))
});
}
fn scan_proc_net_tcp(report: &mut SecurityAuditReport) {
if let Ok(content) = fs::read_to_string("/proc/net/tcp") {
for line in content.lines().skip(1) {
let parts: Vec<&str> = line.split_whitespace().collect();
// State 0A = TCP_LISTEN
if parts.len() >= 4 && parts[3] == "0A" {
if let Some(local) = parts.get(1) {
if let Some((_, port)) = parse_hex_socket(local) {
let is_public = local.starts_with("00000000:");
let (risk_level, rec) = assess_port_risk(port, is_public, "tcp-service");
report.exposed_ports.push(ListeningPortInfo {
protocol: "TCP".to_string(),
local_address: format!("*:{}", port),
port,
process_name: "unknown".to_string(),
pid: None,
is_public,
risk_level,
recommendation: rec,
});
}
}
}
}
}
}
fn scan_suid_anomalies(report: &mut SecurityAuditReport) {
// Проверяем директории, где SUID-бинарников быть не должно: /tmp, /var/tmp, /dev/shm
let suspect_dirs = ["/tmp", "/var/tmp", "/dev/shm"];
for dir in suspect_dirs {
let path = Path::new(dir);
if path.exists() {
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
let p = entry.path();
if let Ok(meta) = p.metadata() {
let mode = meta.permissions().mode();
// Бит SUID: 0o4000, SGID: 0o2000
if mode & 0o4000 != 0 || mode & 0o2000 != 0 {
report.suid_anomalies.push(SuidAnomalyInfo {
path: p.to_string_lossy().to_string(),
owner: "root".to_string(),
permissions: format!("{:o}", mode & 0o7777),
risk_reason: format!("SUID/SGID файл обнаружен во временном каталоге {}. Потенциальный вектор эскалации привилегий.", dir),
});
}
}
}
}
}
}
}
fn scan_hardening_checks(report: &mut SecurityAuditReport) {
// 1. ASLR (randomize_va_space)
let aslr = fs::read_to_string("/proc/sys/kernel/randomize_va_space")
.unwrap_or_default()
.trim()
.to_string();
let aslr_ok = aslr == "2";
report.hardening_checks.push(SecurityCheckItem {
category: "Ядро".to_string(),
name: "ASLR (Address Space Layout Randomization)".to_string(),
status: if aslr_ok { "Активен (Full)" } else { "Уязвим / Отключен" }.to_string(),
is_secure: aslr_ok,
description: "Случайное распределение адресного пространства защищает от переполнения буфера.".to_string(),
remediation: if aslr_ok { None } else { Some("sysctl -w kernel.randomize_va_space=2".to_string()) },
});
// 2. Ptrace Scope (Yama LSM)
let ptrace = fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope")
.unwrap_or_default()
.trim()
.to_string();
let ptrace_ok = ptrace == "1" || ptrace == "2" || ptrace == "3";
report.hardening_checks.push(SecurityCheckItem {
category: "Ядро".to_string(),
name: "Yama ptrace_scope (Инъекция кода в процессы)".to_string(),
status: if ptrace_ok { "Защищен" } else { "Разрешена инъекция (0)" }.to_string(),
is_secure: ptrace_ok,
description: "Ограничивает возможность нерутовых процессов внедрять код в память других процессов через ptrace.".to_string(),
remediation: if ptrace_ok { None } else { Some("sysctl -w kernel.yama.ptrace_scope=1".to_string()) },
});
// 3. Dmesg Restrict
let dmesg_res = fs::read_to_string("/proc/sys/kernel/dmesg_restrict")
.unwrap_or_default()
.trim()
.to_string();
let dmesg_ok = dmesg_res == "1";
report.hardening_checks.push(SecurityCheckItem {
category: "Ядро".to_string(),
name: "Защита буфера dmesg_restrict".to_string(),
status: if dmesg_ok { "Защищен" } else { "Открыт всем" }.to_string(),
is_secure: dmesg_ok,
description: "Запрещает непривилегированным пользователям читать адреса ядра из dmesg.".to_string(),
remediation: if dmesg_ok { None } else { Some("sysctl -w kernel.dmesg_restrict=1".to_string()) },
});
// 4. Unprivileged BPF
let bpf = fs::read_to_string("/proc/sys/kernel/unprivileged_bpf_disabled")
.unwrap_or_default()
.trim()
.to_string();
let bpf_ok = bpf == "1" || bpf == "2";
report.hardening_checks.push(SecurityCheckItem {
category: "Ядро".to_string(),
name: "Unprivileged eBPF".to_string(),
status: if bpf_ok { "Заблокирован" } else { "Разрешен" }.to_string(),
is_secure: bpf_ok,
description: "Защищает ядро от Spectre-подобных атак через запуск eBPF обычными пользователями.".to_string(),
remediation: if bpf_ok { None } else { Some("sysctl -w kernel.unprivileged_bpf_disabled=1".to_string()) },
});
// 5. AppArmor / SELinux
let apparmor_active = Path::new("/sys/kernel/security/apparmor").exists();
let selinux_active = Path::new("/sys/fs/selinux").exists();
let lsm_ok = apparmor_active || selinux_active;
report.hardening_checks.push(SecurityCheckItem {
category: "LSM".to_string(),
name: "Мандатный контроль доступа (LSM)".to_string(),
status: if apparmor_active { "AppArmor активен" } else if selinux_active { "SELinux активен" } else { "LSM отключен" }.to_string(),
is_secure: lsm_ok,
description: "Ограничивает права демонов даже при компрометации root прав.".to_string(),
remediation: if lsm_ok { None } else { Some("Включите AppArmor или SELinux в параметрах загрузки ядра.".to_string()) },
});
// 6. Secure Boot
let sb_active = Path::new("/sys/firmware/efi/efivars").exists();
report.hardening_checks.push(SecurityCheckItem {
category: "Прошивка".to_string(),
name: "UEFI Secure Boot".to_string(),
status: if sb_active { "UEFI режим" } else { "Legacy BIOS" }.to_string(),
is_secure: true,
description: "Проверяет криптографическую подпись модулей ядра при загрузке.".to_string(),
remediation: None,
});
// 7. SSH Configuration
Self::check_ssh_hardening(report);
}
fn check_ssh_hardening(report: &mut SecurityAuditReport) {
let ssh_paths = ["/etc/ssh/sshd_config", "/etc/sshd_config"];
for path_str in ssh_paths {
let path = Path::new(path_str);
if path.exists() {
if let Ok(content) = fs::read_to_string(path) {
let mut root_login_ok = true;
let mut empty_pass_ok = true;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('#') {
continue;
}
let lower = trimmed.to_lowercase();
if lower.starts_with("permitrootlogin") && lower.contains("yes") {
root_login_ok = false;
}
if lower.starts_with("permitemptypasswords") && lower.contains("yes") {
empty_pass_ok = false;
}
}
report.hardening_checks.push(SecurityCheckItem {
category: "SSH".to_string(),
name: "SSH PermitRootLogin".to_string(),
status: if root_login_ok { "Защищен" } else { "Разрешен вход root" }.to_string(),
is_secure: root_login_ok,
description: "Прямой вход суперпользователя по SSH без sudo.".to_string(),
remediation: if root_login_ok { None } else { Some("Установите 'PermitRootLogin prohibit-password' в /etc/ssh/sshd_config".to_string()) },
});
report.hardening_checks.push(SecurityCheckItem {
category: "SSH".to_string(),
name: "SSH PermitEmptyPasswords".to_string(),
status: if empty_pass_ok { "Запрещены" } else { "Разрешены пустые пароли!" }.to_string(),
is_secure: empty_pass_ok,
description: "Разрешение авторизации учетных записей без паролей.".to_string(),
remediation: if empty_pass_ok { None } else { Some("Установите 'PermitEmptyPasswords no' в /etc/ssh/sshd_config".to_string()) },
});
return;
}
}
}
}
fn calculate_score(report: &mut SecurityAuditReport) {
let mut score: i32 = 100;
for port in &report.exposed_ports {
if port.is_public {
match port.risk_level {
Severity::Critical => score -= 20,
Severity::Warning => score -= 8,
Severity::Info => score -= 2,
}
}
}
for _suid in &report.suid_anomalies {
score -= 25;
}
for check in &report.hardening_checks {
if !check.is_secure {
score -= 10;
}
}
report.security_score = score.clamp(10, 100) as u8;
}
}
fn parse_process_info(text: &str) -> (String, Option<u32>) {
if let Some(start) = text.find('"') {
if let Some(end) = text[start + 1..].find('"') {
let name = &text[start + 1..start + 1 + end];
let pid = text.find("pid=").and_then(|idx| {
let rest = &text[idx + 4..];
let pid_str: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
pid_str.parse::<u32>().ok()
});
return (name.to_string(), pid);
}
}
("unknown".to_string(), None)
}
fn assess_port_risk(port: u16, is_public: bool, proc_name: &str) -> (Severity, String) {
let lower_proc = proc_name.to_lowercase();
match port {
6379 if is_public => (
Severity::Critical,
"Redis открыт на 0.0.0.0! Риск неавторизованного RCE. Привяжите к 127.0.0.1 (bind 127.0.0.1).".to_string(),
),
27017 if is_public => (
Severity::Critical,
"MongoDB слушает на публичном интерфейсе. Ограничьте bindIp 127.0.0.1.".to_string(),
),
2375 | 2376 if is_public => (
Severity::Critical,
"Docker Daemon API открыт в публичную сеть без TLS. Любой клиент имеет root-доступ к хосту!".to_string(),
),
9200 if is_public => (
Severity::Critical,
"Elasticsearch HTTP порт доступен публично. Настройте аутентификацию и фаервол.".to_string(),
),
23 if is_public => (
Severity::Critical,
"Telnet передает пароли в открытом виде. Отключите службу и перейдите на SSH.".to_string(),
),
5432 if is_public => (
Severity::Warning,
"PostgreSQL слушает на всех интерфейсах. Убедитесь в надежности pg_hba.conf.".to_string(),
),
3306 if is_public => (
Severity::Warning,
"MySQL/MariaDB порт открыт публично. Привяжите к bind-address = 127.0.0.1 при локальной работе.".to_string(),
),
5900 if is_public => (
Severity::Warning,
"VNC сервер доступен публично. Рекомендуется доступ исключительно через SSH туннель.".to_string(),
),
22 if is_public => (
Severity::Info,
"SSH порт 22 открыт наружу. Рекомендуется защита fail2ban/sshguard и вход по ключам.".to_string(),
),
_ if is_public && (lower_proc.contains("redis") || lower_proc.contains("mongo")) => (
Severity::Critical,
"База данных слушает на публичном интерфейсе!".to_string(),
),
_ => (
Severity::Info,
"Порт слушает входящие соединения.".to_string(),
),
}
}
fn parse_hex_socket(hex: &str) -> Option<(std::net::Ipv4Addr, u16)> {
let mut parts = hex.split(':');
let ip_hex = parts.next()?;
let port_hex = parts.next()?;
let ip_num = u32::from_str_radix(ip_hex, 16).ok()?;
let port = u16::from_str_radix(port_hex, 16).ok()?;
Some((std::net::Ipv4Addr::from(ip_num.to_be()), port))
}
+91
View File
@@ -0,0 +1,91 @@
use std::ffi::CString;
use std::mem::MaybeUninit;
#[derive(Debug, Clone)]
pub struct MountHealth {
pub mount_point: String,
pub filesystem: String,
pub total_bytes: u64,
pub free_bytes: u64,
pub used_percent: f32,
pub inodes_total: u64,
pub inodes_free: u64,
pub inode_used_percent: f32,
pub is_critical: bool,
}
pub struct StorageScanner;
impl StorageScanner {
pub fn scan() -> Vec<MountHealth> {
let mut results = Vec::new();
if let Ok(mounts) = std::fs::read_to_string("/proc/mounts") {
for line in mounts.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 3 {
continue;
}
let device = parts[0];
let mount_point = parts[1];
let fstype = parts[2];
// Фильтруем виртуальные ФС, оставляем реальные блочные устройства / ZFS / BTRFS
if !device.starts_with("/dev/") && fstype != "zfs" && fstype != "btrfs" {
continue;
}
if let Some(health) = Self::check_mount(mount_point, fstype) {
results.push(health);
}
}
}
results
}
fn check_mount(mount_point: &str, fstype: &str) -> Option<MountHealth> {
let c_path = CString::new(mount_point).ok()?;
unsafe {
let mut stat: MaybeUninit<libc::statvfs> = MaybeUninit::uninit();
if libc::statvfs(c_path.as_ptr(), stat.as_mut_ptr()) == 0 {
let stat = stat.assume_init();
let bsize = if stat.f_frsize > 0 { stat.f_frsize } else { stat.f_bsize } as u64;
let total_bytes = stat.f_blocks * bsize;
let free_bytes = stat.f_bavail * bsize;
let used_bytes = total_bytes.saturating_sub(free_bytes);
let used_percent = if total_bytes > 0 {
(used_bytes as f32 / total_bytes as f32) * 100.0
} else {
0.0
};
let inodes_total = stat.f_files;
let inodes_free = stat.f_ffree;
let inodes_used = inodes_total.saturating_sub(inodes_free);
let inode_used_percent = if inodes_total > 0 {
(inodes_used as f32 / inodes_total as f32) * 100.0
} else {
0.0
};
let is_critical = used_percent >= 90.0 || inode_used_percent >= 95.0;
Some(MountHealth {
mount_point: mount_point.to_string(),
filesystem: fstype.to_string(),
total_bytes,
free_bytes,
used_percent,
inodes_total,
inodes_free,
inode_used_percent,
is_critical,
})
} else {
None
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
rust_input: crate::api
rust_root: rust/
dart_output: lib/src/rust
+35
View File
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:diagd/screens/dashboard_screen.dart';
import 'package:diagd/src/rust/frb_generated.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await RustLib.init();
runApp(const DiagdApp());
}
class DiagdApp extends StatelessWidget {
const DiagdApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'diagd • Linux AI Diagnostic Analyst',
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.dark,
darkTheme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF0D1117),
colorScheme: const ColorScheme.dark(
primary: Color(0xFF58A6FF),
secondary: Color(0xFF238636),
surface: Color(0xFF161B22),
error: Color(0xFFF85149),
),
fontFamily: 'Inter',
),
home: const DashboardScreen(),
);
}
}
File diff suppressed because it is too large Load Diff
+973
View File
@@ -0,0 +1,973 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.13.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
/// Запрашивает права суперпользователя в графическом режиме через Polkit (pkexec)
Future<bool> requestGuiPrivileges() =>
RustLib.instance.api.crateApiDiagnosticsRequestGuiPrivileges();
/// Сканирует состояние системы, очищает логи от персональных данных,
/// генерирует структурированный промпт для LLM и формирует итоговый аналитический отчет.
Future<SystemReport> scanSystemAndAnalyze() =>
RustLib.instance.api.crateApiDiagnosticsScanSystemAndAnalyze();
/// Возвращает мгновенный снимок телеметрии системы в реальном времени (CPU %, RAM, частоты, температуры, IO)
Future<LiveMetricsSnapshot> getLiveMetrics() =>
RustLib.instance.api.crateApiDiagnosticsGetLiveMetrics();
/// Экспортирует полный архив Debug Bundle (.tar.gz, standalone HTML и markdown).
/// Возвращает путь к сохраненному архиву.
Future<String> exportDebugBundle({required SystemReport report}) =>
RustLib.instance.api.crateApiDiagnosticsExportDebugBundle(report: report);
class BatteryInfo {
final String name;
final int capacityPercent;
final double healthPercent;
final int cycleCount;
final String status;
final bool isDegraded;
const BatteryInfo({
required this.name,
required this.capacityPercent,
required this.healthPercent,
required this.cycleCount,
required this.status,
required this.isDegraded,
});
@override
int get hashCode =>
name.hashCode ^
capacityPercent.hashCode ^
healthPercent.hashCode ^
cycleCount.hashCode ^
status.hashCode ^
isDegraded.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BatteryInfo &&
runtimeType == other.runtimeType &&
name == other.name &&
capacityPercent == other.capacityPercent &&
healthPercent == other.healthPercent &&
cycleCount == other.cycleCount &&
status == other.status &&
isDegraded == other.isDegraded;
}
class CpuInfo {
final String modelName;
final String architecture;
final BigInt physicalCores;
final BigInt logicalThreads;
final double? baseFrequencyMhz;
final double? currentFrequencyMhz;
final String l1Cache;
final String l2Cache;
final String l3Cache;
final String? virtualization;
final List<String> flags;
final String microcode;
const CpuInfo({
required this.modelName,
required this.architecture,
required this.physicalCores,
required this.logicalThreads,
this.baseFrequencyMhz,
this.currentFrequencyMhz,
required this.l1Cache,
required this.l2Cache,
required this.l3Cache,
this.virtualization,
required this.flags,
required this.microcode,
});
@override
int get hashCode =>
modelName.hashCode ^
architecture.hashCode ^
physicalCores.hashCode ^
logicalThreads.hashCode ^
baseFrequencyMhz.hashCode ^
currentFrequencyMhz.hashCode ^
l1Cache.hashCode ^
l2Cache.hashCode ^
l3Cache.hashCode ^
virtualization.hashCode ^
flags.hashCode ^
microcode.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CpuInfo &&
runtimeType == other.runtimeType &&
modelName == other.modelName &&
architecture == other.architecture &&
physicalCores == other.physicalCores &&
logicalThreads == other.logicalThreads &&
baseFrequencyMhz == other.baseFrequencyMhz &&
currentFrequencyMhz == other.currentFrequencyMhz &&
l1Cache == other.l1Cache &&
l2Cache == other.l2Cache &&
l3Cache == other.l3Cache &&
virtualization == other.virtualization &&
flags == other.flags &&
microcode == other.microcode;
}
class CrashReportInfo {
final String timestamp;
final String processName;
final int? pid;
final String signalOrCode;
final String? faultModule;
final String stackTraceSnippet;
final String? dumpPath;
const CrashReportInfo({
required this.timestamp,
required this.processName,
this.pid,
required this.signalOrCode,
this.faultModule,
required this.stackTraceSnippet,
this.dumpPath,
});
static Future<CrashReportInfo> default_() =>
RustLib.instance.api.crateApiDiagnosticsCrashReportInfoDefault();
@override
int get hashCode =>
timestamp.hashCode ^
processName.hashCode ^
pid.hashCode ^
signalOrCode.hashCode ^
faultModule.hashCode ^
stackTraceSnippet.hashCode ^
dumpPath.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CrashReportInfo &&
runtimeType == other.runtimeType &&
timestamp == other.timestamp &&
processName == other.processName &&
pid == other.pid &&
signalOrCode == other.signalOrCode &&
faultModule == other.faultModule &&
stackTraceSnippet == other.stackTraceSnippet &&
dumpPath == other.dumpPath;
}
class DiskInfo {
final String name;
final String model;
final String serial;
final BigInt sizeBytes;
final String diskType;
final int? rotationalSpeedRpm;
final String smartStatus;
final int? healthPercent;
final int? wearPercent;
final int? nvmeWearPercent;
final int? temperatureC;
final BigInt? powerOnHours;
final BigInt? powerCycleCount;
final BigInt? badSectorsCount;
final double? totalWrittenTb;
final List<PartitionInfo> partitions;
const DiskInfo({
required this.name,
required this.model,
required this.serial,
required this.sizeBytes,
required this.diskType,
this.rotationalSpeedRpm,
required this.smartStatus,
this.healthPercent,
this.wearPercent,
this.nvmeWearPercent,
this.temperatureC,
this.powerOnHours,
this.powerCycleCount,
this.badSectorsCount,
this.totalWrittenTb,
required this.partitions,
});
@override
int get hashCode =>
name.hashCode ^
model.hashCode ^
serial.hashCode ^
sizeBytes.hashCode ^
diskType.hashCode ^
rotationalSpeedRpm.hashCode ^
smartStatus.hashCode ^
healthPercent.hashCode ^
wearPercent.hashCode ^
nvmeWearPercent.hashCode ^
temperatureC.hashCode ^
powerOnHours.hashCode ^
powerCycleCount.hashCode ^
badSectorsCount.hashCode ^
totalWrittenTb.hashCode ^
partitions.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is DiskInfo &&
runtimeType == other.runtimeType &&
name == other.name &&
model == other.model &&
serial == other.serial &&
sizeBytes == other.sizeBytes &&
diskType == other.diskType &&
rotationalSpeedRpm == other.rotationalSpeedRpm &&
smartStatus == other.smartStatus &&
healthPercent == other.healthPercent &&
wearPercent == other.wearPercent &&
nvmeWearPercent == other.nvmeWearPercent &&
temperatureC == other.temperatureC &&
powerOnHours == other.powerOnHours &&
powerCycleCount == other.powerCycleCount &&
badSectorsCount == other.badSectorsCount &&
totalWrittenTb == other.totalWrittenTb &&
partitions == other.partitions;
}
/// Информация о сбойной системной службе
class FailedService {
final String name;
final String initSystem;
final String state;
final String description;
final String logs;
final String restartCommand;
final String statusCommand;
const FailedService({
required this.name,
required this.initSystem,
required this.state,
required this.description,
required this.logs,
required this.restartCommand,
required this.statusCommand,
});
@override
int get hashCode =>
name.hashCode ^
initSystem.hashCode ^
state.hashCode ^
description.hashCode ^
logs.hashCode ^
restartCommand.hashCode ^
statusCommand.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FailedService &&
runtimeType == other.runtimeType &&
name == other.name &&
initSystem == other.initSystem &&
state == other.state &&
description == other.description &&
logs == other.logs &&
restartCommand == other.restartCommand &&
statusCommand == other.statusCommand;
}
/// Предложение по устранению ошибки с готовой CLI-командой
class FixSuggestion {
final String title;
final String command;
final String explanation;
const FixSuggestion({
required this.title,
required this.command,
required this.explanation,
});
@override
int get hashCode => title.hashCode ^ command.hashCode ^ explanation.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FixSuggestion &&
runtimeType == other.runtimeType &&
title == other.title &&
command == other.command &&
explanation == other.explanation;
}
class GpuInfo {
final String vendor;
final String model;
final String driver;
final String pciSlot;
const GpuInfo({
required this.vendor,
required this.model,
required this.driver,
required this.pciSlot,
});
@override
int get hashCode =>
vendor.hashCode ^ model.hashCode ^ driver.hashCode ^ pciSlot.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GpuInfo &&
runtimeType == other.runtimeType &&
vendor == other.vendor &&
model == other.model &&
driver == other.driver &&
pciSlot == other.pciSlot;
}
/// Подробный отчет об аппаратном обеспечении системы
class HardwareInfo {
final CpuInfo cpu;
final MemoryInfo memory;
final MotherboardInfo motherboard;
final List<GpuInfo> gpus;
final List<DiskInfo> disks;
final List<NetworkAdapterInfo> networkAdapters;
final BatteryInfo? battery;
final ThermalsInfo thermals;
const HardwareInfo({
required this.cpu,
required this.memory,
required this.motherboard,
required this.gpus,
required this.disks,
required this.networkAdapters,
this.battery,
required this.thermals,
});
@override
int get hashCode =>
cpu.hashCode ^
memory.hashCode ^
motherboard.hashCode ^
gpus.hashCode ^
disks.hashCode ^
networkAdapters.hashCode ^
battery.hashCode ^
thermals.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is HardwareInfo &&
runtimeType == other.runtimeType &&
cpu == other.cpu &&
memory == other.memory &&
motherboard == other.motherboard &&
gpus == other.gpus &&
disks == other.disks &&
networkAdapters == other.networkAdapters &&
battery == other.battery &&
thermals == other.thermals;
}
/// Элемент диагностического отчета (проблема)
class IssueItem {
final String id;
final String title;
final Subsystem subsystem;
final Severity severity;
final String rootCause;
final String explanation;
final FixSuggestion? fix;
final String rawLog;
final bool sanitized;
final bool permissionDenied;
const IssueItem({
required this.id,
required this.title,
required this.subsystem,
required this.severity,
required this.rootCause,
required this.explanation,
this.fix,
required this.rawLog,
required this.sanitized,
required this.permissionDenied,
});
@override
int get hashCode =>
id.hashCode ^
title.hashCode ^
subsystem.hashCode ^
severity.hashCode ^
rootCause.hashCode ^
explanation.hashCode ^
fix.hashCode ^
rawLog.hashCode ^
sanitized.hashCode ^
permissionDenied.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is IssueItem &&
runtimeType == other.runtimeType &&
id == other.id &&
title == other.title &&
subsystem == other.subsystem &&
severity == other.severity &&
rootCause == other.rootCause &&
explanation == other.explanation &&
fix == other.fix &&
rawLog == other.rawLog &&
sanitized == other.sanitized &&
permissionDenied == other.permissionDenied;
}
class ListeningPortInfo {
final String protocol;
final String localAddress;
final int port;
final String processName;
final int? pid;
final bool isPublic;
final Severity riskLevel;
final String recommendation;
const ListeningPortInfo({
required this.protocol,
required this.localAddress,
required this.port,
required this.processName,
this.pid,
required this.isPublic,
required this.riskLevel,
required this.recommendation,
});
@override
int get hashCode =>
protocol.hashCode ^
localAddress.hashCode ^
port.hashCode ^
processName.hashCode ^
pid.hashCode ^
isPublic.hashCode ^
riskLevel.hashCode ^
recommendation.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ListeningPortInfo &&
runtimeType == other.runtimeType &&
protocol == other.protocol &&
localAddress == other.localAddress &&
port == other.port &&
processName == other.processName &&
pid == other.pid &&
isPublic == other.isPublic &&
riskLevel == other.riskLevel &&
recommendation == other.recommendation;
}
class LiveMetricsSnapshot {
final double cpuUsagePercent;
final BigInt ramUsedBytes;
final BigInt ramTotalBytes;
final BigInt swapUsedBytes;
final BigInt swapTotalBytes;
final double maxCpuTempC;
final bool isThrottling;
final BigInt diskReadKbps;
final BigInt diskWriteKbps;
final BigInt netRxKbps;
final BigInt netTxKbps;
const LiveMetricsSnapshot({
required this.cpuUsagePercent,
required this.ramUsedBytes,
required this.ramTotalBytes,
required this.swapUsedBytes,
required this.swapTotalBytes,
required this.maxCpuTempC,
required this.isThrottling,
required this.diskReadKbps,
required this.diskWriteKbps,
required this.netRxKbps,
required this.netTxKbps,
});
static Future<LiveMetricsSnapshot> default_() =>
RustLib.instance.api.crateApiDiagnosticsLiveMetricsSnapshotDefault();
@override
int get hashCode =>
cpuUsagePercent.hashCode ^
ramUsedBytes.hashCode ^
ramTotalBytes.hashCode ^
swapUsedBytes.hashCode ^
swapTotalBytes.hashCode ^
maxCpuTempC.hashCode ^
isThrottling.hashCode ^
diskReadKbps.hashCode ^
diskWriteKbps.hashCode ^
netRxKbps.hashCode ^
netTxKbps.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LiveMetricsSnapshot &&
runtimeType == other.runtimeType &&
cpuUsagePercent == other.cpuUsagePercent &&
ramUsedBytes == other.ramUsedBytes &&
ramTotalBytes == other.ramTotalBytes &&
swapUsedBytes == other.swapUsedBytes &&
swapTotalBytes == other.swapTotalBytes &&
maxCpuTempC == other.maxCpuTempC &&
isThrottling == other.isThrottling &&
diskReadKbps == other.diskReadKbps &&
diskWriteKbps == other.diskWriteKbps &&
netRxKbps == other.netRxKbps &&
netTxKbps == other.netTxKbps;
}
class MemoryInfo {
final BigInt totalBytes;
final BigInt availableBytes;
final BigInt usedBytes;
final BigInt swapTotalBytes;
final BigInt swapUsedBytes;
final BigInt edacCeCount;
final BigInt edacUeCount;
const MemoryInfo({
required this.totalBytes,
required this.availableBytes,
required this.usedBytes,
required this.swapTotalBytes,
required this.swapUsedBytes,
required this.edacCeCount,
required this.edacUeCount,
});
@override
int get hashCode =>
totalBytes.hashCode ^
availableBytes.hashCode ^
usedBytes.hashCode ^
swapTotalBytes.hashCode ^
swapUsedBytes.hashCode ^
edacCeCount.hashCode ^
edacUeCount.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MemoryInfo &&
runtimeType == other.runtimeType &&
totalBytes == other.totalBytes &&
availableBytes == other.availableBytes &&
usedBytes == other.usedBytes &&
swapTotalBytes == other.swapTotalBytes &&
swapUsedBytes == other.swapUsedBytes &&
edacCeCount == other.edacCeCount &&
edacUeCount == other.edacUeCount;
}
class MotherboardInfo {
final String vendor;
final String productName;
final String version;
final String biosVendor;
final String biosVersion;
final String biosDate;
final String chassisType;
const MotherboardInfo({
required this.vendor,
required this.productName,
required this.version,
required this.biosVendor,
required this.biosVersion,
required this.biosDate,
required this.chassisType,
});
@override
int get hashCode =>
vendor.hashCode ^
productName.hashCode ^
version.hashCode ^
biosVendor.hashCode ^
biosVersion.hashCode ^
biosDate.hashCode ^
chassisType.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MotherboardInfo &&
runtimeType == other.runtimeType &&
vendor == other.vendor &&
productName == other.productName &&
version == other.version &&
biosVendor == other.biosVendor &&
biosVersion == other.biosVersion &&
biosDate == other.biosDate &&
chassisType == other.chassisType;
}
class NetworkAdapterInfo {
final String interfaceName;
final String ifType;
final String state;
final int? speedMbps;
final String macAddress;
final String driver;
const NetworkAdapterInfo({
required this.interfaceName,
required this.ifType,
required this.state,
this.speedMbps,
required this.macAddress,
required this.driver,
});
@override
int get hashCode =>
interfaceName.hashCode ^
ifType.hashCode ^
state.hashCode ^
speedMbps.hashCode ^
macAddress.hashCode ^
driver.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NetworkAdapterInfo &&
runtimeType == other.runtimeType &&
interfaceName == other.interfaceName &&
ifType == other.ifType &&
state == other.state &&
speedMbps == other.speedMbps &&
macAddress == other.macAddress &&
driver == other.driver;
}
class PartitionInfo {
final String name;
final String path;
final String? mountPoint;
final String fsType;
final BigInt sizeBytes;
final BigInt usedBytes;
const PartitionInfo({
required this.name,
required this.path,
this.mountPoint,
required this.fsType,
required this.sizeBytes,
required this.usedBytes,
});
@override
int get hashCode =>
name.hashCode ^
path.hashCode ^
mountPoint.hashCode ^
fsType.hashCode ^
sizeBytes.hashCode ^
usedBytes.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PartitionInfo &&
runtimeType == other.runtimeType &&
name == other.name &&
path == other.path &&
mountPoint == other.mountPoint &&
fsType == other.fsType &&
sizeBytes == other.sizeBytes &&
usedBytes == other.usedBytes;
}
class SecurityAuditReport {
final List<ListeningPortInfo> exposedPorts;
final List<SuidAnomalyInfo> suidAnomalies;
final List<SecurityCheckItem> hardeningChecks;
final int securityScore;
const SecurityAuditReport({
required this.exposedPorts,
required this.suidAnomalies,
required this.hardeningChecks,
required this.securityScore,
});
static Future<SecurityAuditReport> default_() =>
RustLib.instance.api.crateApiDiagnosticsSecurityAuditReportDefault();
@override
int get hashCode =>
exposedPorts.hashCode ^
suidAnomalies.hashCode ^
hardeningChecks.hashCode ^
securityScore.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SecurityAuditReport &&
runtimeType == other.runtimeType &&
exposedPorts == other.exposedPorts &&
suidAnomalies == other.suidAnomalies &&
hardeningChecks == other.hardeningChecks &&
securityScore == other.securityScore;
}
class SecurityCheckItem {
final String category;
final String name;
final String status;
final bool isSecure;
final String description;
final String? remediation;
const SecurityCheckItem({
required this.category,
required this.name,
required this.status,
required this.isSecure,
required this.description,
this.remediation,
});
static Future<SecurityCheckItem> default_() =>
RustLib.instance.api.crateApiDiagnosticsSecurityCheckItemDefault();
@override
int get hashCode =>
category.hashCode ^
name.hashCode ^
status.hashCode ^
isSecure.hashCode ^
description.hashCode ^
remediation.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SecurityCheckItem &&
runtimeType == other.runtimeType &&
category == other.category &&
name == other.name &&
status == other.status &&
isSecure == other.isSecure &&
description == other.description &&
remediation == other.remediation;
}
/// Уровень критичности обнаруженной проблемы
enum Severity { critical, warning, info }
/// Исследуемая подсистема ОС Linux / BSD
enum Subsystem { systemd, kernel, storage, audio, network, security, crashes }
/// Статус состояния подсистемы для сводного дашборда
class SubsystemStatus {
final Subsystem name;
final String status;
final int issueCount;
final String details;
const SubsystemStatus({
required this.name,
required this.status,
required this.issueCount,
required this.details,
});
@override
int get hashCode =>
name.hashCode ^ status.hashCode ^ issueCount.hashCode ^ details.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SubsystemStatus &&
runtimeType == other.runtimeType &&
name == other.name &&
status == other.status &&
issueCount == other.issueCount &&
details == other.details;
}
class SuidAnomalyInfo {
final String path;
final String owner;
final String permissions;
final String riskReason;
const SuidAnomalyInfo({
required this.path,
required this.owner,
required this.permissions,
required this.riskReason,
});
static Future<SuidAnomalyInfo> default_() =>
RustLib.instance.api.crateApiDiagnosticsSuidAnomalyInfoDefault();
@override
int get hashCode =>
path.hashCode ^
owner.hashCode ^
permissions.hashCode ^
riskReason.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SuidAnomalyInfo &&
runtimeType == other.runtimeType &&
path == other.path &&
owner == other.owner &&
permissions == other.permissions &&
riskReason == other.riskReason;
}
/// Общий итоговый отчет диагностики
class SystemReport {
final String timestamp;
final String osFamily;
final String initSystem;
final bool isRoot;
final List<SubsystemStatus> subsystems;
final List<IssueItem> issues;
final List<FailedService> failedServices;
final HardwareInfo hardware;
final List<CrashReportInfo> crashes;
final SecurityAuditReport security;
final bool llmAnalyzed;
final String summary;
final String promptPreview;
const SystemReport({
required this.timestamp,
required this.osFamily,
required this.initSystem,
required this.isRoot,
required this.subsystems,
required this.issues,
required this.failedServices,
required this.hardware,
required this.crashes,
required this.security,
required this.llmAnalyzed,
required this.summary,
required this.promptPreview,
});
@override
int get hashCode =>
timestamp.hashCode ^
osFamily.hashCode ^
initSystem.hashCode ^
isRoot.hashCode ^
subsystems.hashCode ^
issues.hashCode ^
failedServices.hashCode ^
hardware.hashCode ^
crashes.hashCode ^
security.hashCode ^
llmAnalyzed.hashCode ^
summary.hashCode ^
promptPreview.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SystemReport &&
runtimeType == other.runtimeType &&
timestamp == other.timestamp &&
osFamily == other.osFamily &&
initSystem == other.initSystem &&
isRoot == other.isRoot &&
subsystems == other.subsystems &&
issues == other.issues &&
failedServices == other.failedServices &&
hardware == other.hardware &&
crashes == other.crashes &&
security == other.security &&
llmAnalyzed == other.llmAnalyzed &&
summary == other.summary &&
promptPreview == other.promptPreview;
}
class ThermalsInfo {
final double maxCpuTempC;
final BigInt thermalThrottledCores;
final BigInt totalThrottleEvents;
final bool isActivelyThrottling;
const ThermalsInfo({
required this.maxCpuTempC,
required this.thermalThrottledCores,
required this.totalThrottleEvents,
required this.isActivelyThrottling,
});
@override
int get hashCode =>
maxCpuTempC.hashCode ^
thermalThrottledCores.hashCode ^
totalThrottleEvents.hashCode ^
isActivelyThrottling.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ThermalsInfo &&
runtimeType == other.runtimeType &&
maxCpuTempC == other.maxCpuTempC &&
thermalThrottledCores == other.thermalThrottledCores &&
totalThrottleEvents == other.totalThrottleEvents &&
isActivelyThrottling == other.isActivelyThrottling;
}
+11
View File
@@ -0,0 +1,11 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.13.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
String greet({required String name}) =>
RustLib.instance.api.crateApiSimpleGreet(name: name);
File diff suppressed because it is too large Load Diff
+730
View File
@@ -0,0 +1,730 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.13.0.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
import 'api/diagnostics.dart';
import 'api/simple.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:ffi' as ffi;
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({
required super.handler,
required super.wire,
required super.generalizedFrbRustBinding,
required super.portManager,
});
@protected
String dco_decode_String(dynamic raw);
@protected
BatteryInfo dco_decode_battery_info(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
BatteryInfo dco_decode_box_autoadd_battery_info(dynamic raw);
@protected
double dco_decode_box_autoadd_f_32(dynamic raw);
@protected
double dco_decode_box_autoadd_f_64(dynamic raw);
@protected
FixSuggestion dco_decode_box_autoadd_fix_suggestion(dynamic raw);
@protected
int dco_decode_box_autoadd_i_32(dynamic raw);
@protected
SystemReport dco_decode_box_autoadd_system_report(dynamic raw);
@protected
int dco_decode_box_autoadd_u_32(dynamic raw);
@protected
BigInt dco_decode_box_autoadd_u_64(dynamic raw);
@protected
int dco_decode_box_autoadd_u_8(dynamic raw);
@protected
CpuInfo dco_decode_cpu_info(dynamic raw);
@protected
CrashReportInfo dco_decode_crash_report_info(dynamic raw);
@protected
DiskInfo dco_decode_disk_info(dynamic raw);
@protected
double dco_decode_f_32(dynamic raw);
@protected
double dco_decode_f_64(dynamic raw);
@protected
FailedService dco_decode_failed_service(dynamic raw);
@protected
FixSuggestion dco_decode_fix_suggestion(dynamic raw);
@protected
GpuInfo dco_decode_gpu_info(dynamic raw);
@protected
HardwareInfo dco_decode_hardware_info(dynamic raw);
@protected
int dco_decode_i_32(dynamic raw);
@protected
IssueItem dco_decode_issue_item(dynamic raw);
@protected
List<String> dco_decode_list_String(dynamic raw);
@protected
List<CrashReportInfo> dco_decode_list_crash_report_info(dynamic raw);
@protected
List<DiskInfo> dco_decode_list_disk_info(dynamic raw);
@protected
List<FailedService> dco_decode_list_failed_service(dynamic raw);
@protected
List<GpuInfo> dco_decode_list_gpu_info(dynamic raw);
@protected
List<IssueItem> dco_decode_list_issue_item(dynamic raw);
@protected
List<ListeningPortInfo> dco_decode_list_listening_port_info(dynamic raw);
@protected
List<NetworkAdapterInfo> dco_decode_list_network_adapter_info(dynamic raw);
@protected
List<PartitionInfo> dco_decode_list_partition_info(dynamic raw);
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<SecurityCheckItem> dco_decode_list_security_check_item(dynamic raw);
@protected
List<SubsystemStatus> dco_decode_list_subsystem_status(dynamic raw);
@protected
List<SuidAnomalyInfo> dco_decode_list_suid_anomaly_info(dynamic raw);
@protected
ListeningPortInfo dco_decode_listening_port_info(dynamic raw);
@protected
LiveMetricsSnapshot dco_decode_live_metrics_snapshot(dynamic raw);
@protected
MemoryInfo dco_decode_memory_info(dynamic raw);
@protected
MotherboardInfo dco_decode_motherboard_info(dynamic raw);
@protected
NetworkAdapterInfo dco_decode_network_adapter_info(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@protected
BatteryInfo? dco_decode_opt_box_autoadd_battery_info(dynamic raw);
@protected
double? dco_decode_opt_box_autoadd_f_32(dynamic raw);
@protected
double? dco_decode_opt_box_autoadd_f_64(dynamic raw);
@protected
FixSuggestion? dco_decode_opt_box_autoadd_fix_suggestion(dynamic raw);
@protected
int? dco_decode_opt_box_autoadd_i_32(dynamic raw);
@protected
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
@protected
BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw);
@protected
int? dco_decode_opt_box_autoadd_u_8(dynamic raw);
@protected
PartitionInfo dco_decode_partition_info(dynamic raw);
@protected
SecurityAuditReport dco_decode_security_audit_report(dynamic raw);
@protected
SecurityCheckItem dco_decode_security_check_item(dynamic raw);
@protected
Severity dco_decode_severity(dynamic raw);
@protected
Subsystem dco_decode_subsystem(dynamic raw);
@protected
SubsystemStatus dco_decode_subsystem_status(dynamic raw);
@protected
SuidAnomalyInfo dco_decode_suid_anomaly_info(dynamic raw);
@protected
SystemReport dco_decode_system_report(dynamic raw);
@protected
ThermalsInfo dco_decode_thermals_info(dynamic raw);
@protected
int dco_decode_u_16(dynamic raw);
@protected
int dco_decode_u_32(dynamic raw);
@protected
BigInt dco_decode_u_64(dynamic raw);
@protected
int dco_decode_u_8(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@protected
BigInt dco_decode_usize(dynamic raw);
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
BatteryInfo sse_decode_battery_info(SseDeserializer deserializer);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
BatteryInfo sse_decode_box_autoadd_battery_info(SseDeserializer deserializer);
@protected
double sse_decode_box_autoadd_f_32(SseDeserializer deserializer);
@protected
double sse_decode_box_autoadd_f_64(SseDeserializer deserializer);
@protected
FixSuggestion sse_decode_box_autoadd_fix_suggestion(
SseDeserializer deserializer,
);
@protected
int sse_decode_box_autoadd_i_32(SseDeserializer deserializer);
@protected
SystemReport sse_decode_box_autoadd_system_report(
SseDeserializer deserializer,
);
@protected
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
@protected
BigInt sse_decode_box_autoadd_u_64(SseDeserializer deserializer);
@protected
int sse_decode_box_autoadd_u_8(SseDeserializer deserializer);
@protected
CpuInfo sse_decode_cpu_info(SseDeserializer deserializer);
@protected
CrashReportInfo sse_decode_crash_report_info(SseDeserializer deserializer);
@protected
DiskInfo sse_decode_disk_info(SseDeserializer deserializer);
@protected
double sse_decode_f_32(SseDeserializer deserializer);
@protected
double sse_decode_f_64(SseDeserializer deserializer);
@protected
FailedService sse_decode_failed_service(SseDeserializer deserializer);
@protected
FixSuggestion sse_decode_fix_suggestion(SseDeserializer deserializer);
@protected
GpuInfo sse_decode_gpu_info(SseDeserializer deserializer);
@protected
HardwareInfo sse_decode_hardware_info(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
IssueItem sse_decode_issue_item(SseDeserializer deserializer);
@protected
List<String> sse_decode_list_String(SseDeserializer deserializer);
@protected
List<CrashReportInfo> sse_decode_list_crash_report_info(
SseDeserializer deserializer,
);
@protected
List<DiskInfo> sse_decode_list_disk_info(SseDeserializer deserializer);
@protected
List<FailedService> sse_decode_list_failed_service(
SseDeserializer deserializer,
);
@protected
List<GpuInfo> sse_decode_list_gpu_info(SseDeserializer deserializer);
@protected
List<IssueItem> sse_decode_list_issue_item(SseDeserializer deserializer);
@protected
List<ListeningPortInfo> sse_decode_list_listening_port_info(
SseDeserializer deserializer,
);
@protected
List<NetworkAdapterInfo> sse_decode_list_network_adapter_info(
SseDeserializer deserializer,
);
@protected
List<PartitionInfo> sse_decode_list_partition_info(
SseDeserializer deserializer,
);
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<SecurityCheckItem> sse_decode_list_security_check_item(
SseDeserializer deserializer,
);
@protected
List<SubsystemStatus> sse_decode_list_subsystem_status(
SseDeserializer deserializer,
);
@protected
List<SuidAnomalyInfo> sse_decode_list_suid_anomaly_info(
SseDeserializer deserializer,
);
@protected
ListeningPortInfo sse_decode_listening_port_info(
SseDeserializer deserializer,
);
@protected
LiveMetricsSnapshot sse_decode_live_metrics_snapshot(
SseDeserializer deserializer,
);
@protected
MemoryInfo sse_decode_memory_info(SseDeserializer deserializer);
@protected
MotherboardInfo sse_decode_motherboard_info(SseDeserializer deserializer);
@protected
NetworkAdapterInfo sse_decode_network_adapter_info(
SseDeserializer deserializer,
);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
BatteryInfo? sse_decode_opt_box_autoadd_battery_info(
SseDeserializer deserializer,
);
@protected
double? sse_decode_opt_box_autoadd_f_32(SseDeserializer deserializer);
@protected
double? sse_decode_opt_box_autoadd_f_64(SseDeserializer deserializer);
@protected
FixSuggestion? sse_decode_opt_box_autoadd_fix_suggestion(
SseDeserializer deserializer,
);
@protected
int? sse_decode_opt_box_autoadd_i_32(SseDeserializer deserializer);
@protected
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
@protected
BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer);
@protected
int? sse_decode_opt_box_autoadd_u_8(SseDeserializer deserializer);
@protected
PartitionInfo sse_decode_partition_info(SseDeserializer deserializer);
@protected
SecurityAuditReport sse_decode_security_audit_report(
SseDeserializer deserializer,
);
@protected
SecurityCheckItem sse_decode_security_check_item(
SseDeserializer deserializer,
);
@protected
Severity sse_decode_severity(SseDeserializer deserializer);
@protected
Subsystem sse_decode_subsystem(SseDeserializer deserializer);
@protected
SubsystemStatus sse_decode_subsystem_status(SseDeserializer deserializer);
@protected
SuidAnomalyInfo sse_decode_suid_anomaly_info(SseDeserializer deserializer);
@protected
SystemReport sse_decode_system_report(SseDeserializer deserializer);
@protected
ThermalsInfo sse_decode_thermals_info(SseDeserializer deserializer);
@protected
int sse_decode_u_16(SseDeserializer deserializer);
@protected
int sse_decode_u_32(SseDeserializer deserializer);
@protected
BigInt sse_decode_u_64(SseDeserializer deserializer);
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@protected
BigInt sse_decode_usize(SseDeserializer deserializer);
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_battery_info(BatteryInfo self, SseSerializer serializer);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_battery_info(
BatteryInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_f_32(double self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_f_64(double self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_fix_suggestion(
FixSuggestion self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_32(int self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_system_report(
SystemReport self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_u_64(BigInt self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_cpu_info(CpuInfo self, SseSerializer serializer);
@protected
void sse_encode_crash_report_info(
CrashReportInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_disk_info(DiskInfo self, SseSerializer serializer);
@protected
void sse_encode_f_32(double self, SseSerializer serializer);
@protected
void sse_encode_f_64(double self, SseSerializer serializer);
@protected
void sse_encode_failed_service(FailedService self, SseSerializer serializer);
@protected
void sse_encode_fix_suggestion(FixSuggestion self, SseSerializer serializer);
@protected
void sse_encode_gpu_info(GpuInfo self, SseSerializer serializer);
@protected
void sse_encode_hardware_info(HardwareInfo self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
@protected
void sse_encode_issue_item(IssueItem self, SseSerializer serializer);
@protected
void sse_encode_list_String(List<String> self, SseSerializer serializer);
@protected
void sse_encode_list_crash_report_info(
List<CrashReportInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_disk_info(List<DiskInfo> self, SseSerializer serializer);
@protected
void sse_encode_list_failed_service(
List<FailedService> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_gpu_info(List<GpuInfo> self, SseSerializer serializer);
@protected
void sse_encode_list_issue_item(
List<IssueItem> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_listening_port_info(
List<ListeningPortInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_network_adapter_info(
List<NetworkAdapterInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_partition_info(
List<PartitionInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self,
SseSerializer serializer,
);
@protected
void sse_encode_list_security_check_item(
List<SecurityCheckItem> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_subsystem_status(
List<SubsystemStatus> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_suid_anomaly_info(
List<SuidAnomalyInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_listening_port_info(
ListeningPortInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_live_metrics_snapshot(
LiveMetricsSnapshot self,
SseSerializer serializer,
);
@protected
void sse_encode_memory_info(MemoryInfo self, SseSerializer serializer);
@protected
void sse_encode_motherboard_info(
MotherboardInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_network_adapter_info(
NetworkAdapterInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_battery_info(
BatteryInfo? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_f_32(double? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_f_64(double? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_fix_suggestion(
FixSuggestion? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_i_32(int? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_u_8(int? self, SseSerializer serializer);
@protected
void sse_encode_partition_info(PartitionInfo self, SseSerializer serializer);
@protected
void sse_encode_security_audit_report(
SecurityAuditReport self,
SseSerializer serializer,
);
@protected
void sse_encode_security_check_item(
SecurityCheckItem self,
SseSerializer serializer,
);
@protected
void sse_encode_severity(Severity self, SseSerializer serializer);
@protected
void sse_encode_subsystem(Subsystem self, SseSerializer serializer);
@protected
void sse_encode_subsystem_status(
SubsystemStatus self,
SseSerializer serializer,
);
@protected
void sse_encode_suid_anomaly_info(
SuidAnomalyInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_system_report(SystemReport self, SseSerializer serializer);
@protected
void sse_encode_thermals_info(ThermalsInfo self, SseSerializer serializer);
@protected
void sse_encode_u_16(int self, SseSerializer serializer);
@protected
void sse_encode_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_u_64(BigInt self, SseSerializer serializer);
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_usize(BigInt self, SseSerializer serializer);
}
// Section: wire_class
class RustLibWire implements BaseWire {
factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) =>
RustLibWire(lib.ffiDynamicLibrary);
/// Holds the symbol lookup function.
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
_lookup;
/// The symbols are looked up in [dynamicLibrary].
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;
}
+730
View File
@@ -0,0 +1,730 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.13.0.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
// Static analysis wrongly picks the IO variant, thus ignore this
// ignore_for_file: argument_type_not_assignable
import 'api/diagnostics.dart';
import 'api/simple.dart';
import 'dart:async';
import 'dart:convert';
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({
required super.handler,
required super.wire,
required super.generalizedFrbRustBinding,
required super.portManager,
});
@protected
String dco_decode_String(dynamic raw);
@protected
BatteryInfo dco_decode_battery_info(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
BatteryInfo dco_decode_box_autoadd_battery_info(dynamic raw);
@protected
double dco_decode_box_autoadd_f_32(dynamic raw);
@protected
double dco_decode_box_autoadd_f_64(dynamic raw);
@protected
FixSuggestion dco_decode_box_autoadd_fix_suggestion(dynamic raw);
@protected
int dco_decode_box_autoadd_i_32(dynamic raw);
@protected
SystemReport dco_decode_box_autoadd_system_report(dynamic raw);
@protected
int dco_decode_box_autoadd_u_32(dynamic raw);
@protected
BigInt dco_decode_box_autoadd_u_64(dynamic raw);
@protected
int dco_decode_box_autoadd_u_8(dynamic raw);
@protected
CpuInfo dco_decode_cpu_info(dynamic raw);
@protected
CrashReportInfo dco_decode_crash_report_info(dynamic raw);
@protected
DiskInfo dco_decode_disk_info(dynamic raw);
@protected
double dco_decode_f_32(dynamic raw);
@protected
double dco_decode_f_64(dynamic raw);
@protected
FailedService dco_decode_failed_service(dynamic raw);
@protected
FixSuggestion dco_decode_fix_suggestion(dynamic raw);
@protected
GpuInfo dco_decode_gpu_info(dynamic raw);
@protected
HardwareInfo dco_decode_hardware_info(dynamic raw);
@protected
int dco_decode_i_32(dynamic raw);
@protected
IssueItem dco_decode_issue_item(dynamic raw);
@protected
List<String> dco_decode_list_String(dynamic raw);
@protected
List<CrashReportInfo> dco_decode_list_crash_report_info(dynamic raw);
@protected
List<DiskInfo> dco_decode_list_disk_info(dynamic raw);
@protected
List<FailedService> dco_decode_list_failed_service(dynamic raw);
@protected
List<GpuInfo> dco_decode_list_gpu_info(dynamic raw);
@protected
List<IssueItem> dco_decode_list_issue_item(dynamic raw);
@protected
List<ListeningPortInfo> dco_decode_list_listening_port_info(dynamic raw);
@protected
List<NetworkAdapterInfo> dco_decode_list_network_adapter_info(dynamic raw);
@protected
List<PartitionInfo> dco_decode_list_partition_info(dynamic raw);
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<SecurityCheckItem> dco_decode_list_security_check_item(dynamic raw);
@protected
List<SubsystemStatus> dco_decode_list_subsystem_status(dynamic raw);
@protected
List<SuidAnomalyInfo> dco_decode_list_suid_anomaly_info(dynamic raw);
@protected
ListeningPortInfo dco_decode_listening_port_info(dynamic raw);
@protected
LiveMetricsSnapshot dco_decode_live_metrics_snapshot(dynamic raw);
@protected
MemoryInfo dco_decode_memory_info(dynamic raw);
@protected
MotherboardInfo dco_decode_motherboard_info(dynamic raw);
@protected
NetworkAdapterInfo dco_decode_network_adapter_info(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@protected
BatteryInfo? dco_decode_opt_box_autoadd_battery_info(dynamic raw);
@protected
double? dco_decode_opt_box_autoadd_f_32(dynamic raw);
@protected
double? dco_decode_opt_box_autoadd_f_64(dynamic raw);
@protected
FixSuggestion? dco_decode_opt_box_autoadd_fix_suggestion(dynamic raw);
@protected
int? dco_decode_opt_box_autoadd_i_32(dynamic raw);
@protected
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
@protected
BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw);
@protected
int? dco_decode_opt_box_autoadd_u_8(dynamic raw);
@protected
PartitionInfo dco_decode_partition_info(dynamic raw);
@protected
SecurityAuditReport dco_decode_security_audit_report(dynamic raw);
@protected
SecurityCheckItem dco_decode_security_check_item(dynamic raw);
@protected
Severity dco_decode_severity(dynamic raw);
@protected
Subsystem dco_decode_subsystem(dynamic raw);
@protected
SubsystemStatus dco_decode_subsystem_status(dynamic raw);
@protected
SuidAnomalyInfo dco_decode_suid_anomaly_info(dynamic raw);
@protected
SystemReport dco_decode_system_report(dynamic raw);
@protected
ThermalsInfo dco_decode_thermals_info(dynamic raw);
@protected
int dco_decode_u_16(dynamic raw);
@protected
int dco_decode_u_32(dynamic raw);
@protected
BigInt dco_decode_u_64(dynamic raw);
@protected
int dco_decode_u_8(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@protected
BigInt dco_decode_usize(dynamic raw);
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
BatteryInfo sse_decode_battery_info(SseDeserializer deserializer);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
BatteryInfo sse_decode_box_autoadd_battery_info(SseDeserializer deserializer);
@protected
double sse_decode_box_autoadd_f_32(SseDeserializer deserializer);
@protected
double sse_decode_box_autoadd_f_64(SseDeserializer deserializer);
@protected
FixSuggestion sse_decode_box_autoadd_fix_suggestion(
SseDeserializer deserializer,
);
@protected
int sse_decode_box_autoadd_i_32(SseDeserializer deserializer);
@protected
SystemReport sse_decode_box_autoadd_system_report(
SseDeserializer deserializer,
);
@protected
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
@protected
BigInt sse_decode_box_autoadd_u_64(SseDeserializer deserializer);
@protected
int sse_decode_box_autoadd_u_8(SseDeserializer deserializer);
@protected
CpuInfo sse_decode_cpu_info(SseDeserializer deserializer);
@protected
CrashReportInfo sse_decode_crash_report_info(SseDeserializer deserializer);
@protected
DiskInfo sse_decode_disk_info(SseDeserializer deserializer);
@protected
double sse_decode_f_32(SseDeserializer deserializer);
@protected
double sse_decode_f_64(SseDeserializer deserializer);
@protected
FailedService sse_decode_failed_service(SseDeserializer deserializer);
@protected
FixSuggestion sse_decode_fix_suggestion(SseDeserializer deserializer);
@protected
GpuInfo sse_decode_gpu_info(SseDeserializer deserializer);
@protected
HardwareInfo sse_decode_hardware_info(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
IssueItem sse_decode_issue_item(SseDeserializer deserializer);
@protected
List<String> sse_decode_list_String(SseDeserializer deserializer);
@protected
List<CrashReportInfo> sse_decode_list_crash_report_info(
SseDeserializer deserializer,
);
@protected
List<DiskInfo> sse_decode_list_disk_info(SseDeserializer deserializer);
@protected
List<FailedService> sse_decode_list_failed_service(
SseDeserializer deserializer,
);
@protected
List<GpuInfo> sse_decode_list_gpu_info(SseDeserializer deserializer);
@protected
List<IssueItem> sse_decode_list_issue_item(SseDeserializer deserializer);
@protected
List<ListeningPortInfo> sse_decode_list_listening_port_info(
SseDeserializer deserializer,
);
@protected
List<NetworkAdapterInfo> sse_decode_list_network_adapter_info(
SseDeserializer deserializer,
);
@protected
List<PartitionInfo> sse_decode_list_partition_info(
SseDeserializer deserializer,
);
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<SecurityCheckItem> sse_decode_list_security_check_item(
SseDeserializer deserializer,
);
@protected
List<SubsystemStatus> sse_decode_list_subsystem_status(
SseDeserializer deserializer,
);
@protected
List<SuidAnomalyInfo> sse_decode_list_suid_anomaly_info(
SseDeserializer deserializer,
);
@protected
ListeningPortInfo sse_decode_listening_port_info(
SseDeserializer deserializer,
);
@protected
LiveMetricsSnapshot sse_decode_live_metrics_snapshot(
SseDeserializer deserializer,
);
@protected
MemoryInfo sse_decode_memory_info(SseDeserializer deserializer);
@protected
MotherboardInfo sse_decode_motherboard_info(SseDeserializer deserializer);
@protected
NetworkAdapterInfo sse_decode_network_adapter_info(
SseDeserializer deserializer,
);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
BatteryInfo? sse_decode_opt_box_autoadd_battery_info(
SseDeserializer deserializer,
);
@protected
double? sse_decode_opt_box_autoadd_f_32(SseDeserializer deserializer);
@protected
double? sse_decode_opt_box_autoadd_f_64(SseDeserializer deserializer);
@protected
FixSuggestion? sse_decode_opt_box_autoadd_fix_suggestion(
SseDeserializer deserializer,
);
@protected
int? sse_decode_opt_box_autoadd_i_32(SseDeserializer deserializer);
@protected
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
@protected
BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer);
@protected
int? sse_decode_opt_box_autoadd_u_8(SseDeserializer deserializer);
@protected
PartitionInfo sse_decode_partition_info(SseDeserializer deserializer);
@protected
SecurityAuditReport sse_decode_security_audit_report(
SseDeserializer deserializer,
);
@protected
SecurityCheckItem sse_decode_security_check_item(
SseDeserializer deserializer,
);
@protected
Severity sse_decode_severity(SseDeserializer deserializer);
@protected
Subsystem sse_decode_subsystem(SseDeserializer deserializer);
@protected
SubsystemStatus sse_decode_subsystem_status(SseDeserializer deserializer);
@protected
SuidAnomalyInfo sse_decode_suid_anomaly_info(SseDeserializer deserializer);
@protected
SystemReport sse_decode_system_report(SseDeserializer deserializer);
@protected
ThermalsInfo sse_decode_thermals_info(SseDeserializer deserializer);
@protected
int sse_decode_u_16(SseDeserializer deserializer);
@protected
int sse_decode_u_32(SseDeserializer deserializer);
@protected
BigInt sse_decode_u_64(SseDeserializer deserializer);
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@protected
BigInt sse_decode_usize(SseDeserializer deserializer);
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_battery_info(BatteryInfo self, SseSerializer serializer);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_battery_info(
BatteryInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_f_32(double self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_f_64(double self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_fix_suggestion(
FixSuggestion self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_32(int self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_system_report(
SystemReport self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_u_64(BigInt self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_cpu_info(CpuInfo self, SseSerializer serializer);
@protected
void sse_encode_crash_report_info(
CrashReportInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_disk_info(DiskInfo self, SseSerializer serializer);
@protected
void sse_encode_f_32(double self, SseSerializer serializer);
@protected
void sse_encode_f_64(double self, SseSerializer serializer);
@protected
void sse_encode_failed_service(FailedService self, SseSerializer serializer);
@protected
void sse_encode_fix_suggestion(FixSuggestion self, SseSerializer serializer);
@protected
void sse_encode_gpu_info(GpuInfo self, SseSerializer serializer);
@protected
void sse_encode_hardware_info(HardwareInfo self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
@protected
void sse_encode_issue_item(IssueItem self, SseSerializer serializer);
@protected
void sse_encode_list_String(List<String> self, SseSerializer serializer);
@protected
void sse_encode_list_crash_report_info(
List<CrashReportInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_disk_info(List<DiskInfo> self, SseSerializer serializer);
@protected
void sse_encode_list_failed_service(
List<FailedService> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_gpu_info(List<GpuInfo> self, SseSerializer serializer);
@protected
void sse_encode_list_issue_item(
List<IssueItem> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_listening_port_info(
List<ListeningPortInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_network_adapter_info(
List<NetworkAdapterInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_partition_info(
List<PartitionInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self,
SseSerializer serializer,
);
@protected
void sse_encode_list_security_check_item(
List<SecurityCheckItem> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_subsystem_status(
List<SubsystemStatus> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_suid_anomaly_info(
List<SuidAnomalyInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_listening_port_info(
ListeningPortInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_live_metrics_snapshot(
LiveMetricsSnapshot self,
SseSerializer serializer,
);
@protected
void sse_encode_memory_info(MemoryInfo self, SseSerializer serializer);
@protected
void sse_encode_motherboard_info(
MotherboardInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_network_adapter_info(
NetworkAdapterInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_battery_info(
BatteryInfo? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_f_32(double? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_f_64(double? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_fix_suggestion(
FixSuggestion? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_i_32(int? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_u_8(int? self, SseSerializer serializer);
@protected
void sse_encode_partition_info(PartitionInfo self, SseSerializer serializer);
@protected
void sse_encode_security_audit_report(
SecurityAuditReport self,
SseSerializer serializer,
);
@protected
void sse_encode_security_check_item(
SecurityCheckItem self,
SseSerializer serializer,
);
@protected
void sse_encode_severity(Severity self, SseSerializer serializer);
@protected
void sse_encode_subsystem(Subsystem self, SseSerializer serializer);
@protected
void sse_encode_subsystem_status(
SubsystemStatus self,
SseSerializer serializer,
);
@protected
void sse_encode_suid_anomaly_info(
SuidAnomalyInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_system_report(SystemReport self, SseSerializer serializer);
@protected
void sse_encode_thermals_info(ThermalsInfo self, SseSerializer serializer);
@protected
void sse_encode_u_16(int self, SseSerializer serializer);
@protected
void sse_encode_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_u_64(BigInt self, SseSerializer serializer);
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_usize(BigInt self, SseSerializer serializer);
}
// Section: wire_class
class RustLibWire implements BaseWire {
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
}
@JS('wasm_bindgen')
external RustLibWasmModule get wasmModule;
@JS()
@anonymous
extension type RustLibWasmModule._(JSObject _) implements JSObject {}
+486
View File
@@ -0,0 +1,486 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:diagd/src/rust/api/diagnostics.dart';
class CrashesTab extends StatefulWidget {
final List<CrashReportInfo> crashes;
const CrashesTab({
super.key,
required this.crashes,
});
@override
State<CrashesTab> createState() => _CrashesTabState();
}
class _CrashesTabState extends State<CrashesTab> {
int _selectedCrashIndex = 0;
@override
Widget build(BuildContext context) {
if (widget.crashes.isEmpty) {
return Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 550),
margin: const EdgeInsets.all(24),
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF238636).withAlpha(80)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF238636).withAlpha(30),
shape: BoxShape.circle,
),
child: const Icon(
Icons.verified_rounded,
color: Color(0xFF3FB950),
size: 44,
),
),
const SizedBox(height: 18),
const Text(
'Аварийных падений не зафиксировано',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
const SizedBox(height: 8),
const Text(
'В системных журналах coredumpctl, /var/crash, dmesg и crash reporter отсутствуют записи о падениях процессов по сигналам SIGSEGV, SIGABRT или сбоям ядра.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13, color: Color(0xFF8B949E), height: 1.4),
),
],
),
),
);
}
final selectedCrash = widget.crashes[_selectedCrashIndex.clamp(0, widget.crashes.length - 1)];
return LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 850;
if (isWide) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Column: Crash list
SizedBox(
width: 340,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildListHeader(),
const SizedBox(height: 10),
Expanded(
child: ListView.separated(
itemCount: widget.crashes.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final crash = widget.crashes[index];
final isSelected = index == _selectedCrashIndex;
return _buildCrashListItem(crash, isSelected, () {
setState(() {
_selectedCrashIndex = index;
});
});
},
),
),
],
),
),
const SizedBox(width: 20),
// Right Column: Crash details
Expanded(
child: SingleChildScrollView(
child: _buildCrashDetails(context, selectedCrash),
),
),
],
),
);
} else {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildListHeader(),
const SizedBox(height: 10),
SizedBox(
height: 180,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: widget.crashes.length,
separatorBuilder: (context, index) => const SizedBox(width: 10),
itemBuilder: (context, index) {
final crash = widget.crashes[index];
final isSelected = index == _selectedCrashIndex;
return SizedBox(
width: 260,
child: _buildCrashListItem(crash, isSelected, () {
setState(() {
_selectedCrashIndex = index;
});
}),
);
},
),
),
const SizedBox(height: 20),
_buildCrashDetails(context, selectedCrash),
],
),
);
}
},
);
}
Widget _buildListHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'ЖУРНАЛ АВАРИЙНЫХ СБОЕВ',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Color(0xFF8B949E),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF85149).withAlpha(25),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFF85149).withAlpha(70)),
),
child: Text(
'${widget.crashes.length}',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Color(0xFFF85149),
),
),
),
],
);
}
Widget _buildCrashListItem(CrashReportInfo crash, bool isSelected, VoidCallback onTap) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF21262D) : const Color(0xFF161B22),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? const Color(0xFFF85149) : const Color(0xFF30363D),
width: isSelected ? 1.5 : 1.0,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF85149).withAlpha(30),
borderRadius: BorderRadius.circular(4),
),
child: Text(
crash.signalOrCode,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Color(0xFFF85149),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
crash.processName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
),
],
),
const SizedBox(height: 6),
Text(
crash.timestamp,
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E)),
),
if (crash.pid != null) ...[
const SizedBox(height: 2),
Text(
'PID: ${crash.pid}',
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E), fontFamily: 'monospace'),
),
],
],
),
),
);
}
Widget _buildCrashDetails(BuildContext context, CrashReportInfo crash) {
final debugCmd = crash.dumpPath != null && crash.dumpPath!.isNotEmpty
? 'gdb ${crash.processName} ${crash.dumpPath!}'
: (crash.pid != null ? 'coredumpctl debug ${crash.pid}' : 'coredumpctl info ${crash.processName}');
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF30363D)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(Icons.bug_report_rounded, color: Color(0xFFF85149), size: 24),
const SizedBox(width: 10),
Text(
crash.processName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFFF85149).withAlpha(25),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFF85149).withAlpha(70)),
),
child: Text(
crash.signalOrCode,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xFFF85149),
),
),
),
],
),
if (crash.pid != null)
Text(
'PID: ${crash.pid}',
style: const TextStyle(
fontSize: 13,
color: Color(0xFF8B949E),
fontFamily: 'monospace',
),
),
],
),
const SizedBox(height: 16),
// Metadata Grid
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF21262D)),
),
child: Column(
children: [
_buildMetaRow('Время падения:', crash.timestamp),
if (crash.faultModule != null && crash.faultModule!.isNotEmpty)
_buildMetaRow('Сбойный модуль:', crash.faultModule!),
if (crash.dumpPath != null && crash.dumpPath!.isNotEmpty) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
width: 130,
child: Text(
'Файл Core Dump:',
style: TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
),
Expanded(
child: Text(
crash.dumpPath!,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF58A6FF),
fontFamily: 'monospace',
),
),
),
IconButton(
icon: const Icon(Icons.copy_rounded, size: 14, color: Color(0xFF8B949E)),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
tooltip: 'Копировать путь к дампу',
onPressed: () {
Clipboard.setData(ClipboardData(text: crash.dumpPath!));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Путь к Core Dump скопирован'), duration: Duration(seconds: 2)),
);
},
),
],
),
],
],
),
),
const SizedBox(height: 20),
// Debug CLI Command
const Text(
'КОМАНДА ОТЛАДКИ (GDB / COREDUMPCTL)',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1.0,
color: Color(0xFF8B949E),
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF30363D)),
),
child: Row(
children: [
const Text(
'\$ ',
style: TextStyle(color: Color(0xFF3FB950), fontWeight: FontWeight.bold),
),
Expanded(
child: Text(
debugCmd,
style: const TextStyle(
color: Color(0xFFE6EDF3),
fontFamily: 'monospace',
fontSize: 13,
),
),
),
IconButton(
icon: const Icon(Icons.copy_rounded, size: 16, color: Color(0xFF8B949E)),
tooltip: 'Копировать команду',
onPressed: () {
Clipboard.setData(ClipboardData(text: debugCmd));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Команда отладки скопирована'), duration: Duration(seconds: 2)),
);
},
),
],
),
),
const SizedBox(height: 20),
// Stack Trace / Backtrace
const Text(
'СТЕК ВЫЗОВОВ (BACKTRACE / CRASH TRACE)',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1.0,
color: Color(0xFF8B949E),
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF21262D)),
),
child: crash.stackTraceSnippet.isNotEmpty
? SelectableText(
crash.stackTraceSnippet,
style: const TextStyle(
color: Color(0xFFE6EDF3),
fontFamily: 'monospace',
fontSize: 12,
height: 1.4,
),
)
: const Text(
'Трассировка стека недоступна в данном дампе памяти.',
style: TextStyle(color: Color(0xFF8B949E), fontSize: 12),
),
),
],
),
);
}
Widget _buildMetaRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 130,
child: Text(
label,
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFFE6EDF3),
),
),
),
],
),
);
}
}
File diff suppressed because it is too large Load Diff
+545
View File
@@ -0,0 +1,545 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:diagd/src/rust/api/diagnostics.dart';
class LiveMonitorTab extends StatefulWidget {
const LiveMonitorTab({super.key});
@override
State<LiveMonitorTab> createState() => _LiveMonitorTabState();
}
class _LiveMonitorTabState extends State<LiveMonitorTab> {
Timer? _ticker;
LiveMetricsSnapshot? _snapshot;
final List<double> _cpuHistory = [];
final List<double> _ramHistory = [];
final List<double> _tempHistory = [];
final List<double> _netHistory = [];
final int _maxHistory = 60;
@override
void initState() {
super.initState();
_fetchMetrics();
_ticker = Timer.periodic(const Duration(seconds: 1), (_) => _fetchMetrics());
}
@override
void dispose() {
_ticker?.cancel();
super.dispose();
}
Future<void> _fetchMetrics() async {
try {
final snap = await getLiveMetrics();
if (!mounted) return;
setState(() {
_snapshot = snap;
_cpuHistory.add(snap.cpuUsagePercent.clamp(0.0, 100.0));
if (_cpuHistory.length > _maxHistory) _cpuHistory.removeAt(0);
final ramPct = snap.ramTotalBytes > BigInt.zero
? (snap.ramUsedBytes.toDouble() / snap.ramTotalBytes.toDouble()) * 100.0
: 0.0;
_ramHistory.add(ramPct.clamp(0.0, 100.0));
if (_ramHistory.length > _maxHistory) _ramHistory.removeAt(0);
_tempHistory.add(snap.maxCpuTempC.clamp(0.0, 120.0));
if (_tempHistory.length > _maxHistory) _tempHistory.removeAt(0);
final netKb = (snap.netRxKbps + snap.netTxKbps).toDouble();
_netHistory.add(netKb);
if (_netHistory.length > _maxHistory) _netHistory.removeAt(0);
});
} catch (_) {}
}
@override
Widget build(BuildContext context) {
if (_snapshot == null) {
return const Center(
child: CircularProgressIndicator(color: Color(0xFF58A6FF)),
);
}
final snap = _snapshot!;
final ramUsedGb = snap.ramUsedBytes.toDouble() / (1024 * 1024 * 1024);
final ramTotalGb = snap.ramTotalBytes.toDouble() / (1024 * 1024 * 1024);
final ramPct = ramTotalGb > 0 ? (ramUsedGb / ramTotalGb) * 100.0 : 0.0;
final swapUsedGb = snap.swapUsedBytes.toDouble() / (1024 * 1024 * 1024);
final swapTotalGb = snap.swapTotalBytes.toDouble() / (1024 * 1024 * 1024);
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Throttling Alert Banner
if (snap.isThrottling) ...[
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: const Color(0xFFF85149).withAlpha(30),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFF85149)),
),
child: const Row(
children: [
Icon(Icons.warning_rounded, color: Color(0xFFF85149), size: 28),
SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'КРИТИЧЕСКИЙ ПЕРЕГРЕВ: АКТИВЕН THERMAL THROTTLING!',
style: TextStyle(
color: Color(0xFFF85149),
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
SizedBox(height: 4),
Text(
'Процессор сбросил тактовые частоты из-за превышения температурного порога. Проверьте систему охлаждения и термопасту.',
style: TextStyle(color: Color(0xFFE6EDF3), fontSize: 13),
),
],
),
),
],
),
),
],
// Top Metric Cards Grid
LayoutBuilder(
builder: (context, constraints) {
final cols = constraints.maxWidth > 900 ? 4 : (constraints.maxWidth > 550 ? 2 : 1);
return GridView.count(
crossAxisCount: cols,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 14,
mainAxisSpacing: 14,
childAspectRatio: constraints.maxWidth > 900 ? 1.6 : 2.0,
children: [
_buildMetricCard(
title: 'Загрузка CPU',
value: '${snap.cpuUsagePercent.toStringAsFixed(1)}%',
subvalue: 'Общая нагрузка ядер',
icon: Icons.memory_rounded,
color: snap.cpuUsagePercent > 85
? const Color(0xFFF85149)
: (snap.cpuUsagePercent > 60 ? const Color(0xFFD29922) : const Color(0xFF3FB950)),
progress: snap.cpuUsagePercent / 100.0,
),
_buildMetricCard(
title: 'Оперативная память (RAM)',
value: '${ramUsedGb.toStringAsFixed(1)} / ${ramTotalGb.toStringAsFixed(1)} GB',
subvalue: '${ramPct.toStringAsFixed(0)}% использовано',
icon: Icons.storage_rounded,
color: ramPct > 90
? const Color(0xFFF85149)
: (ramPct > 75 ? const Color(0xFFD29922) : const Color(0xFF58A6FF)),
progress: ramPct / 100.0,
),
_buildMetricCard(
title: 'Температура CPU',
value: '${snap.maxCpuTempC.toStringAsFixed(1)} °C',
subvalue: snap.isThrottling ? 'Троттлинг АКТИВЕН' : 'Охлаждение в норме',
icon: Icons.thermostat_rounded,
color: snap.maxCpuTempC > 85
? const Color(0xFFF85149)
: (snap.maxCpuTempC > 70 ? const Color(0xFFD29922) : const Color(0xFF3FB950)),
progress: (snap.maxCpuTempC.clamp(20.0, 105.0) - 20.0) / 85.0,
),
_buildMetricCard(
title: 'Дисковый и Сетевой I/O',
value: '${((snap.diskReadKbps + snap.diskWriteKbps).toDouble() / 1024.0).toStringAsFixed(1)} MB/s',
subvalue: 'Сеть: ${snap.netRxKbps + snap.netTxKbps} KB/s (RX+TX)',
icon: Icons.swap_vert_rounded,
color: const Color(0xFFA371F7),
progress: null,
),
],
);
},
),
const SizedBox(height: 24),
// Realtime Sparklines / Charts
const Text(
'ДИНАМИКА НАГРУЗКИ В РЕАЛЬНОМ ВРЕМЕНИ (ПОСЛЕДНИЕ 60 СЕК)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Color(0xFF8B949E),
),
),
const SizedBox(height: 12),
LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 800;
if (isWide) {
return Row(
children: [
Expanded(
child: _buildChartContainer(
title: 'Нагрузка CPU (%)',
color: const Color(0xFF58A6FF),
history: _cpuHistory,
maxVal: 100.0,
currentVal: '${snap.cpuUsagePercent.toStringAsFixed(1)}%',
),
),
const SizedBox(width: 14),
Expanded(
child: _buildChartContainer(
title: 'Использование RAM (%)',
color: const Color(0xFF3FB950),
history: _ramHistory,
maxVal: 100.0,
currentVal: '${ramPct.toStringAsFixed(1)}%',
),
),
],
);
} else {
return Column(
children: [
_buildChartContainer(
title: 'Нагрузка CPU (%)',
color: const Color(0xFF58A6FF),
history: _cpuHistory,
maxVal: 100.0,
currentVal: '${snap.cpuUsagePercent.toStringAsFixed(1)}%',
),
const SizedBox(height: 14),
_buildChartContainer(
title: 'Использование RAM (%)',
color: const Color(0xFF3FB950),
history: _ramHistory,
maxVal: 100.0,
currentVal: '${ramPct.toStringAsFixed(1)}%',
),
],
);
}
},
),
const SizedBox(height: 14),
LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 800;
if (isWide) {
return Row(
children: [
Expanded(
child: _buildChartContainer(
title: 'Температура CPU (°C)',
color: const Color(0xFFD29922),
history: _tempHistory,
maxVal: 105.0,
currentVal: '${snap.maxCpuTempC.toStringAsFixed(1)}°C',
),
),
const SizedBox(width: 14),
Expanded(
child: _buildChartContainer(
title: 'Сетевой трафик (KB/s)',
color: const Color(0xFFA371F7),
history: _netHistory,
maxVal: _netHistory.isEmpty ? 100.0 : max(100.0, _netHistory.reduce(max)),
currentVal: '${snap.netRxKbps + snap.netTxKbps} KB/s',
),
),
],
);
} else {
return Column(
children: [
_buildChartContainer(
title: 'Температура CPU (°C)',
color: const Color(0xFFD29922),
history: _tempHistory,
maxVal: 105.0,
currentVal: '${snap.maxCpuTempC.toStringAsFixed(1)}°C',
),
const SizedBox(height: 14),
_buildChartContainer(
title: 'Сетевой трафик (KB/s)',
color: const Color(0xFFA371F7),
history: _netHistory,
maxVal: _netHistory.isEmpty ? 100.0 : max(100.0, _netHistory.reduce(max)),
currentVal: '${snap.netRxKbps + snap.netTxKbps} KB/s',
),
],
);
}
},
),
const SizedBox(height: 24),
// Detailed Memory & Storage Row
Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF30363D)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.info_outline_rounded, color: Color(0xFF58A6FF), size: 18),
SizedBox(width: 8),
Text(
'Детализация виртуальной памяти и I/O накопителей',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
fontSize: 14,
),
),
],
),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: _buildDetailRow(
'Swap память:',
'${swapUsedGb.toStringAsFixed(2)} GB из ${swapTotalGb.toStringAsFixed(2)} GB',
),
),
Expanded(
child: _buildDetailRow(
'Диск (Read / Write):',
'${(snap.diskReadKbps.toDouble() / 1024.0).toStringAsFixed(1)} MB/s / ${(snap.diskWriteKbps.toDouble() / 1024.0).toStringAsFixed(1)} MB/s',
),
),
Expanded(
child: _buildDetailRow(
'Сеть (RX / TX):',
'${snap.netRxKbps} KB/s / ${snap.netTxKbps} KB/s',
),
),
],
),
],
),
),
],
),
);
}
Widget _buildMetricCard({
required String title,
required String value,
required String subvalue,
required IconData icon,
required Color color,
double? progress,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: color.withAlpha(50)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF8B949E),
),
),
Icon(icon, color: color, size: 20),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 2),
Text(
subvalue,
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E)),
),
],
),
if (progress != null)
ClipRRect(
borderRadius: BorderRadius.circular(3),
child: LinearProgressIndicator(
value: progress.clamp(0.0, 1.0),
backgroundColor: const Color(0xFF21262D),
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 4,
),
)
else
const SizedBox(height: 4),
],
),
);
}
Widget _buildChartContainer({
required String title,
required Color color,
required List<double> history,
required double maxVal,
required String currentVal,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF30363D)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFFE6EDF3),
),
),
Text(
currentVal,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
const SizedBox(height: 12),
SizedBox(
height: 90,
width: double.infinity,
child: CustomPaint(
painter: _SparklinePainter(
data: history,
color: color,
maxVal: maxVal,
),
),
),
],
),
);
}
Widget _buildDetailRow(String label, String val) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E))),
const SizedBox(height: 3),
Text(val, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFFE6EDF3))),
],
);
}
}
class _SparklinePainter extends CustomPainter {
final List<double> data;
final Color color;
final double maxVal;
_SparklinePainter({
required this.data,
required this.color,
required this.maxVal,
});
@override
void paint(Canvas canvas, Size size) {
if (data.isEmpty) return;
final paintLine = Paint()
..color = color
..strokeWidth = 2.0
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final paintFill = Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
color.withAlpha(80),
color.withAlpha(0),
],
).createShader(Rect.fromLTWH(0, 0, size.width, size.height))
..style = PaintingStyle.fill;
final path = Path();
final fillPath = Path();
final stepX = size.width / max(1, (60 - 1));
final startIdx = 60 - data.length;
for (int i = 0; i < data.length; i++) {
final x = (startIdx + i) * stepX;
final valNorm = (data[i] / (maxVal > 0 ? maxVal : 1.0)).clamp(0.0, 1.0);
final y = size.height - (valNorm * size.height);
if (i == 0) {
path.moveTo(x, y);
fillPath.moveTo(x, size.height);
fillPath.lineTo(x, y);
} else {
path.lineTo(x, y);
fillPath.lineTo(x, y);
}
}
fillPath.lineTo(size.width, size.height);
fillPath.close();
canvas.drawPath(fillPath, paintFill);
canvas.drawPath(path, paintLine);
}
@override
bool shouldRepaint(covariant _SparklinePainter oldDelegate) => true;
}
+497
View File
@@ -0,0 +1,497 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:diagd/src/rust/api/diagnostics.dart';
class SecurityTab extends StatelessWidget {
final SecurityAuditReport security;
const SecurityTab({
super.key,
required this.security,
});
@override
Widget build(BuildContext context) {
final score = security.securityScore;
final scoreColor = score >= 80
? const Color(0xFF3FB950)
: (score >= 50 ? const Color(0xFFD29922) : const Color(0xFFF85149));
final failedChecks = security.hardeningChecks.where((c) => !c.isSecure).length;
final passedChecks = security.hardeningChecks.where((c) => c.isSecure).length;
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Security Score Hero Card
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: scoreColor.withAlpha(70)),
),
child: Row(
children: [
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 80,
height: 80,
child: CircularProgressIndicator(
value: score / 100.0,
strokeWidth: 8,
backgroundColor: const Color(0xFF21262D),
valueColor: AlwaysStoppedAnimation<Color>(scoreColor),
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$score',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: scoreColor,
),
),
const Text(
'/100',
style: TextStyle(fontSize: 10, color: Color(0xFF8B949E)),
),
],
),
],
),
const SizedBox(width: 24),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'Индекс защищенности системы (Security Score)',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: scoreColor.withAlpha(30),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: scoreColor.withAlpha(80)),
),
child: Text(
score >= 80 ? 'ОТЛИЧНО' : (score >= 50 ? 'ТРЕБУЕТ ВНИМАНИЯ' : 'УЯЗВИМО'),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: scoreColor,
),
),
),
],
),
const SizedBox(height: 6),
Text(
'Аудит сетевого периметра, прав суперпользователя SUID/SGID, параметров защиты ядра (ASLR, ptrace, eBPF) и конфигурации SSH.',
style: const TextStyle(fontSize: 13, color: Color(0xFF8B949E)),
),
const SizedBox(height: 12),
Row(
children: [
_buildMiniBadge('Открытых портов (0.0.0.0): ${security.exposedPorts.length}',
security.exposedPorts.isNotEmpty ? const Color(0xFFD29922) : const Color(0xFF3FB950)),
const SizedBox(width: 10),
_buildMiniBadge('SUID аномалий: ${security.suidAnomalies.length}',
security.suidAnomalies.isNotEmpty ? const Color(0xFFF85149) : const Color(0xFF3FB950)),
const SizedBox(width: 10),
_buildMiniBadge('Пройдено тестов: $passedChecks / ${security.hardeningChecks.length}',
failedChecks > 0 ? const Color(0xFFD29922) : const Color(0xFF3FB950)),
],
),
],
),
),
],
),
),
const SizedBox(height: 24),
// 1. Exposed Listening Ports
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'СЕТЕВОЙ ПЕРИМЕТР (ПОРТЫ, СЛУШАЮЩИЕ 0.0.0.0 И ::)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Color(0xFF8B949E),
),
),
Text(
'Всего: ${security.exposedPorts.length}',
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
],
),
const SizedBox(height: 10),
if (security.exposedPorts.isEmpty)
_buildEmptyState('Открытых вовне портов не обнаружено. Локальный сетевой стек защищен.', Icons.verified_user_rounded)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: security.exposedPorts.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final port = security.exposedPorts[index];
return _buildPortCard(context, port);
},
),
const SizedBox(height: 28),
// 2. SUID / SGID Anomalies
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'ПОДОЗРИТЕЛЬНЫЕ SUID / SGID БИНАРНЫЕ ФАЙЛЫ',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Color(0xFF8B949E),
),
),
Text(
'Аномалий: ${security.suidAnomalies.length}',
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
],
),
const SizedBox(height: 10),
if (security.suidAnomalies.isEmpty)
_buildEmptyState('Аномальных или небезопасных SUID/SGID файлов в общедоступных каталогах (/tmp, /dev/shm) не обнаружено.', Icons.check_circle_outline_rounded)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: security.suidAnomalies.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final suid = security.suidAnomalies[index];
return _buildSuidCard(suid);
},
),
const SizedBox(height: 28),
// 3. Kernel & OS Hardening Checklist
const Text(
'ЧЕК-ЛИСТ ЗАЩИТЫ ЯДРА И ОПЕРАЦИОННОЙ СИСТЕМЫ (HARDENING)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Color(0xFF8B949E),
),
),
const SizedBox(height: 10),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: security.hardeningChecks.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final check = security.hardeningChecks[index];
return _buildHardeningCard(context, check);
},
),
],
),
);
}
Widget _buildMiniBadge(String text, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withAlpha(20),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withAlpha(60)),
),
child: Text(
text,
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: color),
),
);
}
Widget _buildEmptyState(String text, IconData icon) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF238636).withAlpha(50)),
),
child: Row(
children: [
Icon(icon, color: const Color(0xFF3FB950), size: 22),
const SizedBox(width: 12),
Expanded(
child: Text(
text,
style: const TextStyle(fontSize: 13, color: Color(0xFF8B949E)),
),
),
],
),
);
}
Widget _buildPortCard(BuildContext context, ListeningPortInfo port) {
final (Color badgeColor, String badgeText) = switch (port.riskLevel) {
Severity.critical => (const Color(0xFFF85149), 'CRITICAL'),
Severity.warning => (const Color(0xFFD29922), 'WARNING'),
Severity.info => (const Color(0xFF3FB950), 'INFO'),
};
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: badgeColor.withAlpha(60)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: badgeColor.withAlpha(25),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: badgeColor.withAlpha(80)),
),
child: Text(
badgeText,
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: badgeColor),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'${port.protocol.toUpperCase()} ${port.port}',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: const Color(0xFF21262D),
borderRadius: BorderRadius.circular(4),
),
child: Text(
port.processName.isNotEmpty ? port.processName : 'unknown process',
style: const TextStyle(fontSize: 12, color: Color(0xFF58A6FF), fontFamily: 'monospace'),
),
),
if (port.pid != null) ...[
const SizedBox(width: 6),
Text('(PID: ${port.pid})', style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E))),
],
],
),
const SizedBox(height: 4),
Text(
'Адрес прослушивания: ${port.localAddress}',
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
if (port.recommendation.isNotEmpty) ...[
const SizedBox(height: 6),
Row(
children: [
const Icon(Icons.arrow_right_rounded, size: 18, color: Color(0xFFD29922)),
Expanded(
child: Text(
port.recommendation,
style: TextStyle(fontSize: 12, color: badgeColor),
),
),
],
),
],
],
),
),
],
),
);
}
Widget _buildSuidCard(SuidAnomalyInfo suid) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFF85149).withAlpha(60)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.warning_amber_rounded, color: Color(0xFFF85149), size: 22),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
suid.path,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
fontFamily: 'monospace',
),
),
const SizedBox(height: 4),
Text(
'Владелец: ${suid.owner} | Права: ${suid.permissions}',
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
const SizedBox(height: 4),
Text(
suid.riskReason,
style: const TextStyle(fontSize: 12, color: Color(0xFFF85149)),
),
],
),
),
],
),
);
}
Widget _buildHardeningCard(BuildContext context, SecurityCheckItem check) {
final isPass = check.isSecure;
final color = isPass ? const Color(0xFF3FB950) : const Color(0xFFF85149);
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withAlpha(50)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
isPass ? Icons.check_circle_rounded : Icons.cancel_rounded,
color: color,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
check.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color.withAlpha(20),
borderRadius: BorderRadius.circular(6),
),
child: Text(
check.status,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: color,
),
),
),
],
),
const SizedBox(height: 4),
Text(
check.description,
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
if (!isPass && check.remediation != null && check.remediation!.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFF30363D)),
),
child: Row(
children: [
const Icon(Icons.build_rounded, size: 14, color: Color(0xFFD29922)),
const SizedBox(width: 8),
Expanded(
child: Text(
check.remediation!,
style: const TextStyle(fontSize: 12, color: Color(0xFFE6EDF3), fontFamily: 'monospace'),
),
),
IconButton(
icon: const Icon(Icons.copy_rounded, size: 16, color: Color(0xFF8B949E)),
tooltip: 'Скопировать команду',
onPressed: () {
Clipboard.setData(ClipboardData(text: check.remediation!));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Команда устранения скопирована в буфер'),
duration: Duration(seconds: 2),
),
);
},
),
],
),
),
],
],
),
),
],
),
);
}
}
+1
View File
@@ -0,0 +1 @@
flutter/ephemeral
+128
View File
@@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "diagd")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.diagd.diagd")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
+88
View File
@@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)
@@ -0,0 +1,11 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}
@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_
+24
View File
@@ -0,0 +1,24 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
rust_lib_diagd
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
+26
View File
@@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+30
View File
@@ -0,0 +1,30 @@
#include "my_application.h"
#include <string.h>
#include <dlfcn.h>
#include <stdio.h>
int main(int argc, char** argv) {
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--tui") == 0 || strcmp(argv[i], "-t") == 0) {
// Загрузка функции diagd_run_tui из bundled librust_lib_diagd.so
void* handle = dlopen("lib/librust_lib_diagd.so", RTLD_LAZY);
if (!handle) {
handle = dlopen("librust_lib_diagd.so", RTLD_LAZY);
}
if (handle) {
typedef int (*run_tui_fn)();
run_tui_fn run_tui = (run_tui_fn)dlsym(handle, "diagd_run_tui");
if (run_tui) {
int ret = run_tui();
dlclose(handle);
return ret;
}
}
fprintf(stderr, "Не удалось загрузить модуль TUI из librust_lib_diagd.so: %s\n", dlerror());
return 1;
}
}
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}
+148
View File
@@ -0,0 +1,148 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "diagd");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "diagd");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication,
my_application,
MY,
APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_
+7
View File
@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/
+1
View File
@@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -0,0 +1,10 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
}
+729
View File
@@ -0,0 +1,729 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
remoteInfo = Runner;
};
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* diagd.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "diagd.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
331C80D2294CF70F00263BE5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* diagd.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* diagd.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 33CC10EC2044A3C60003C045;
};
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
331C80D4294CF70F00263BE5 /* RunnerTests */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C80D3294CF70F00263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C80D1294CF70F00263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC10EC2044A3C60003C045 /* Runner */;
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
};
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.diagd.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/diagd.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/diagd";
};
name = Debug;
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.diagd.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/diagd.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/diagd";
};
name = Release;
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.diagd.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/diagd.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/diagd";
};
name = Profile;
};
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C80DB294CF71000263BE5 /* Debug */,
331C80DC294CF71000263BE5 /* Release */,
331C80DD294CF71000263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "&quot;$FLUTTER_ROOT&quot;/packages/flutter_tools/bin/macos_assemble.sh prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "diagd.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "diagd.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "diagd.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "diagd.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "diagd.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
+13
View File
@@ -0,0 +1,13 @@
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
@@ -0,0 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

+343
View File
@@ -0,0 +1,343 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
<connections>
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="EPT-qC-fAb">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
</menuItem>
</items>
<point key="canvasLocation" x="142" y="-258"/>
</menu>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</window>
</objects>
</document>
+14
View File
@@ -0,0 +1,14 @@
// Application-level settings for the Runner target.
//
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
// future. If not, the values below would default to using the project name when this becomes a
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = diagd
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.example.diagd
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved.
+2
View File
@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"
+2
View File
@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "Warnings.xcconfig"
+13
View File
@@ -0,0 +1,13 @@
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
GCC_WARN_UNDECLARED_SELECTOR = YES
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
CLANG_WARN_PRAGMA_PACK = YES
CLANG_WARN_STRICT_PROTOTYPES = YES
CLANG_WARN_COMMA = YES
GCC_WARN_STRICT_SELECTOR_MATCH = YES
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
GCC_WARN_SHADOW = YES
CLANG_WARN_UNREACHABLE_CODE = YES
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
+15
View File
@@ -0,0 +1,15 @@
import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>
+12
View File
@@ -0,0 +1,12 @@
import Cocoa
import FlutterMacOS
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
+268
View File
@@ -0,0 +1,268 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
build_cli_annotations:
dependency: transitive
description:
name: build_cli_annotations
sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95
url: "https://pub.dev"
source: hosted
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e
url: "https://pub.dev"
source: hosted
version: "1.1.3"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_rust_bridge:
dependency: "direct main"
description:
name: flutter_rust_bridge
sha256: ec69331e2335ab4d456c0a9fc700892c2c85b72b9c0cb39df3ecfe8833d5d5c8
url: "https://pub.dev"
source: hosted
version: "2.13.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
version: "0.12.20"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d
url: "https://pub.dev"
source: hosted
version: "1.18.3"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
rust_lib_diagd:
dependency: "direct main"
description:
path: rust_builder
relative: true
source: path
version: "0.0.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490"
url: "https://pub.dev"
source: hosted
version: "1.12.2"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.dev"
source: hosted
version: "0.7.12"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "1d774bbdf6b72a0b12122fc1560c9c2d2a67db5a4a4cc2bd8a5c990ab20e3188"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0"
url: "https://pub.dev"
source: hosted
version: "15.3.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
sdks:
dart: ">=3.13.3 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
+92
View File
@@ -0,0 +1,92 @@
name: diagd
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.13.3
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
flutter_rust_bridge: 2.13.0
rust_lib_diagd:
path: rust_builder
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
+1
View File
@@ -0,0 +1 @@
/target
+2090
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
[package]
name = "rust_lib_diagd"
version = "0.1.0"
edition = "2021"
[lib]
name = "rust_lib_diagd"
crate-type = ["cdylib", "staticlib", "rlib"]
[[bin]]
name = "diagd"
path = "src/bin/diagd.rs"
required-features = ["tui"]
[features]
default = ["tui", "gui"]
tui = ["dep:ratatui", "dep:crossterm"]
gui = ["dep:flutter_rust_bridge"]
[dependencies]
diagd-core = { path = "../crates/core" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
regex = "1.10"
reqwest = { version = "0.12", features = ["json"] }
chrono = { version = "0.4", features = ["serde"] }
# GUI-specific
flutter_rust_bridge = { version = "=2.13.0", optional = true }
# TUI-specific
ratatui = { version = "0.29", optional = true }
crossterm = { version = "0.28", optional = true }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
+846
View File
@@ -0,0 +1,846 @@
use serde::{Deserialize, Serialize};
/// Уровень критичности обнаруженной проблемы
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Severity {
Critical,
Warning,
Info,
}
/// Исследуемая подсистема ОС Linux / BSD
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Subsystem {
Systemd,
Kernel,
Storage,
Audio,
Network,
Security,
Crashes,
}
/// Статус состояния подсистемы для сводного дашборда
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubsystemStatus {
pub name: Subsystem,
pub status: String,
pub issue_count: u32,
pub details: String,
}
/// Предложение по устранению ошибки с готовой CLI-командой
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixSuggestion {
pub title: String,
pub command: String,
pub explanation: String,
}
/// Информация о сбойной системной службе
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FailedService {
pub name: String,
pub init_system: String,
pub state: String,
pub description: String,
pub logs: String,
pub restart_command: String,
pub status_command: String,
}
/// Элемент диагностического отчета (проблема)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IssueItem {
pub id: String,
pub title: String,
pub subsystem: Subsystem,
pub severity: Severity,
pub root_cause: String,
pub explanation: String,
pub fix: Option<FixSuggestion>,
pub raw_log: String,
pub sanitized: bool,
pub permission_denied: bool,
}
/// Подробный отчет об аппаратном обеспечении системы
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HardwareInfo {
pub cpu: CpuInfo,
pub memory: MemoryInfo,
pub motherboard: MotherboardInfo,
pub gpus: Vec<GpuInfo>,
pub disks: Vec<DiskInfo>,
pub network_adapters: Vec<NetworkAdapterInfo>,
pub battery: Option<BatteryInfo>,
pub thermals: ThermalsInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CpuInfo {
pub model_name: String,
pub architecture: String,
pub physical_cores: usize,
pub logical_threads: usize,
pub base_frequency_mhz: Option<f32>,
pub current_frequency_mhz: Option<f32>,
pub l1_cache: String,
pub l2_cache: String,
pub l3_cache: String,
pub virtualization: Option<String>,
pub flags: Vec<String>,
pub microcode: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryInfo {
pub total_bytes: u64,
pub available_bytes: u64,
pub used_bytes: u64,
pub swap_total_bytes: u64,
pub swap_used_bytes: u64,
pub edac_ce_count: u64,
pub edac_ue_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MotherboardInfo {
pub vendor: String,
pub product_name: String,
pub version: String,
pub bios_vendor: String,
pub bios_version: String,
pub bios_date: String,
pub chassis_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GpuInfo {
pub vendor: String,
pub model: String,
pub driver: String,
pub pci_slot: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartitionInfo {
pub name: String,
pub path: String,
pub mount_point: Option<String>,
pub fs_type: String,
pub size_bytes: u64,
pub used_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiskInfo {
pub name: String,
pub model: String,
pub serial: String,
pub size_bytes: u64,
pub disk_type: String,
pub rotational_speed_rpm: Option<u32>,
pub smart_status: String,
pub health_percent: Option<u8>,
pub wear_percent: Option<u8>,
pub nvme_wear_percent: Option<u8>,
pub temperature_c: Option<i32>,
pub power_on_hours: Option<u64>,
pub power_cycle_count: Option<u64>,
pub bad_sectors_count: Option<u64>,
pub total_written_tb: Option<f64>,
pub partitions: Vec<PartitionInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkAdapterInfo {
pub interface_name: String,
pub if_type: String,
pub state: String,
pub speed_mbps: Option<u32>,
pub mac_address: String,
pub driver: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatteryInfo {
pub name: String,
pub capacity_percent: u8,
pub health_percent: f32,
pub cycle_count: u32,
pub status: String,
pub is_degraded: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThermalsInfo {
pub max_cpu_temp_c: f32,
pub thermal_throttled_cores: usize,
pub total_throttle_events: u64,
pub is_actively_throttling: bool,
}
// --- LIVE METRICS ---
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct LiveMetricsSnapshot {
pub cpu_usage_percent: f32,
pub ram_used_bytes: u64,
pub ram_total_bytes: u64,
pub swap_used_bytes: u64,
pub swap_total_bytes: u64,
pub max_cpu_temp_c: f32,
pub is_throttling: bool,
pub disk_read_kbps: u64,
pub disk_write_kbps: u64,
pub net_rx_kbps: u64,
pub net_tx_kbps: u64,
}
// --- CRASH DUMPS ---
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct CrashReportInfo {
pub timestamp: String,
pub process_name: String,
pub pid: Option<u32>,
pub signal_or_code: String,
pub fault_module: Option<String>,
pub stack_trace_snippet: String,
pub dump_path: Option<String>,
}
// --- SECURITY AUDIT ---
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ListeningPortInfo {
pub protocol: String,
pub local_address: String,
pub port: u16,
pub process_name: String,
pub pid: Option<u32>,
pub is_public: bool,
pub risk_level: Severity,
pub recommendation: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct SuidAnomalyInfo {
pub path: String,
pub owner: String,
pub permissions: String,
pub risk_reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct SecurityCheckItem {
pub category: String,
pub name: String,
pub status: String,
pub is_secure: bool,
pub description: String,
pub remediation: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct SecurityAuditReport {
pub exposed_ports: Vec<ListeningPortInfo>,
pub suid_anomalies: Vec<SuidAnomalyInfo>,
pub hardening_checks: Vec<SecurityCheckItem>,
pub security_score: u8,
}
/// Общий итоговый отчет диагностики
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemReport {
pub timestamp: String,
pub os_family: String,
pub init_system: String,
pub is_root: bool,
pub subsystems: Vec<SubsystemStatus>,
pub issues: Vec<IssueItem>,
pub failed_services: Vec<FailedService>,
pub hardware: HardwareInfo,
pub crashes: Vec<CrashReportInfo>,
pub security: SecurityAuditReport,
pub llm_analyzed: bool,
pub summary: String,
pub prompt_preview: String,
}
// Конвертеры из diagd-core в FRB структуры
impl From<diagd_core::models::Severity> for Severity {
fn from(s: diagd_core::models::Severity) -> Self {
match s {
diagd_core::models::Severity::Critical => Severity::Critical,
diagd_core::models::Severity::Warning => Severity::Warning,
diagd_core::models::Severity::Info => Severity::Info,
}
}
}
impl From<Severity> for diagd_core::models::Severity {
fn from(s: Severity) -> Self {
match s {
Severity::Critical => diagd_core::models::Severity::Critical,
Severity::Warning => diagd_core::models::Severity::Warning,
Severity::Info => diagd_core::models::Severity::Info,
}
}
}
impl From<diagd_core::models::Subsystem> for Subsystem {
fn from(s: diagd_core::models::Subsystem) -> Self {
match s {
diagd_core::models::Subsystem::Systemd => Subsystem::Systemd,
diagd_core::models::Subsystem::Kernel => Subsystem::Kernel,
diagd_core::models::Subsystem::Storage => Subsystem::Storage,
diagd_core::models::Subsystem::Audio => Subsystem::Audio,
diagd_core::models::Subsystem::Network => Subsystem::Network,
diagd_core::models::Subsystem::Security => Subsystem::Security,
diagd_core::models::Subsystem::Crashes => Subsystem::Crashes,
}
}
}
impl From<diagd_core::models::SubsystemStatus> for SubsystemStatus {
fn from(s: diagd_core::models::SubsystemStatus) -> Self {
SubsystemStatus {
name: s.name.into(),
status: s.status,
issue_count: s.issue_count,
details: s.details,
}
}
}
impl From<diagd_core::models::FixSuggestion> for FixSuggestion {
fn from(f: diagd_core::models::FixSuggestion) -> Self {
FixSuggestion {
title: f.title,
command: f.command,
explanation: f.explanation,
}
}
}
impl From<diagd_core::models::FailedService> for FailedService {
fn from(f: diagd_core::models::FailedService) -> Self {
FailedService {
name: f.name,
init_system: f.init_system,
state: f.state,
description: f.description,
logs: f.logs,
restart_command: f.restart_command,
status_command: f.status_command,
}
}
}
impl From<diagd_core::models::IssueItem> for IssueItem {
fn from(i: diagd_core::models::IssueItem) -> Self {
IssueItem {
id: i.id,
title: i.title,
subsystem: i.subsystem.into(),
severity: i.severity.into(),
root_cause: i.root_cause,
explanation: i.explanation,
fix: i.fix.map(Into::into),
raw_log: i.raw_log,
sanitized: i.sanitized,
permission_denied: i.permission_denied,
}
}
}
impl From<diagd_core::models::CpuInfo> for CpuInfo {
fn from(c: diagd_core::models::CpuInfo) -> Self {
Self {
model_name: c.model_name,
architecture: c.architecture,
physical_cores: c.physical_cores,
logical_threads: c.logical_threads,
base_frequency_mhz: c.base_frequency_mhz,
current_frequency_mhz: c.current_frequency_mhz,
l1_cache: c.l1_cache,
l2_cache: c.l2_cache,
l3_cache: c.l3_cache,
virtualization: c.virtualization,
flags: c.flags,
microcode: c.microcode,
}
}
}
impl From<diagd_core::models::MemoryInfo> for MemoryInfo {
fn from(m: diagd_core::models::MemoryInfo) -> Self {
Self {
total_bytes: m.total_bytes,
available_bytes: m.available_bytes,
used_bytes: m.used_bytes,
swap_total_bytes: m.swap_total_bytes,
swap_used_bytes: m.swap_used_bytes,
edac_ce_count: m.edac_ce_count,
edac_ue_count: m.edac_ue_count,
}
}
}
impl From<diagd_core::models::MotherboardInfo> for MotherboardInfo {
fn from(m: diagd_core::models::MotherboardInfo) -> Self {
Self {
vendor: m.vendor,
product_name: m.product_name,
version: m.version,
bios_vendor: m.bios_vendor,
bios_version: m.bios_version,
bios_date: m.bios_date,
chassis_type: m.chassis_type,
}
}
}
impl From<diagd_core::models::GpuInfo> for GpuInfo {
fn from(g: diagd_core::models::GpuInfo) -> Self {
Self {
vendor: g.vendor,
model: g.model,
driver: g.driver,
pci_slot: g.pci_slot,
}
}
}
impl From<diagd_core::models::PartitionInfo> for PartitionInfo {
fn from(p: diagd_core::models::PartitionInfo) -> Self {
Self {
name: p.name,
path: p.path,
mount_point: p.mount_point,
fs_type: p.fs_type,
size_bytes: p.size_bytes,
used_bytes: p.used_bytes,
}
}
}
impl From<diagd_core::models::DiskInfo> for DiskInfo {
fn from(d: diagd_core::models::DiskInfo) -> Self {
Self {
name: d.name,
model: d.model,
serial: d.serial,
size_bytes: d.size_bytes,
disk_type: d.disk_type,
rotational_speed_rpm: d.rotational_speed_rpm,
smart_status: d.smart_status,
health_percent: d.health_percent,
wear_percent: d.wear_percent,
nvme_wear_percent: d.nvme_wear_percent,
temperature_c: d.temperature_c,
power_on_hours: d.power_on_hours,
power_cycle_count: d.power_cycle_count,
bad_sectors_count: d.bad_sectors_count,
total_written_tb: d.total_written_tb,
partitions: d.partitions.into_iter().map(Into::into).collect(),
}
}
}
impl From<diagd_core::models::NetworkAdapterInfo> for NetworkAdapterInfo {
fn from(n: diagd_core::models::NetworkAdapterInfo) -> Self {
Self {
interface_name: n.interface_name,
if_type: n.if_type,
state: n.state,
speed_mbps: n.speed_mbps,
mac_address: n.mac_address,
driver: n.driver,
}
}
}
impl From<diagd_core::models::BatteryInfo> for BatteryInfo {
fn from(b: diagd_core::models::BatteryInfo) -> Self {
Self {
name: b.name,
capacity_percent: b.capacity_percent,
health_percent: b.health_percent,
cycle_count: b.cycle_count,
status: b.status,
is_degraded: b.is_degraded,
}
}
}
impl From<diagd_core::models::ThermalsInfo> for ThermalsInfo {
fn from(t: diagd_core::models::ThermalsInfo) -> Self {
Self {
max_cpu_temp_c: t.max_cpu_temp_c,
thermal_throttled_cores: t.thermal_throttled_cores,
total_throttle_events: t.total_throttle_events,
is_actively_throttling: t.is_actively_throttling,
}
}
}
impl From<diagd_core::models::HardwareInfo> for HardwareInfo {
fn from(h: diagd_core::models::HardwareInfo) -> Self {
Self {
cpu: h.cpu.into(),
memory: h.memory.into(),
motherboard: h.motherboard.into(),
gpus: h.gpus.into_iter().map(Into::into).collect(),
disks: h.disks.into_iter().map(Into::into).collect(),
network_adapters: h.network_adapters.into_iter().map(Into::into).collect(),
battery: h.battery.map(Into::into),
thermals: h.thermals.into(),
}
}
}
impl From<HardwareInfo> for diagd_core::models::HardwareInfo {
fn from(h: HardwareInfo) -> Self {
diagd_core::models::HardwareInfo {
cpu: diagd_core::models::CpuInfo {
model_name: h.cpu.model_name,
architecture: h.cpu.architecture,
physical_cores: h.cpu.physical_cores,
logical_threads: h.cpu.logical_threads,
base_frequency_mhz: h.cpu.base_frequency_mhz,
current_frequency_mhz: h.cpu.current_frequency_mhz,
l1_cache: h.cpu.l1_cache,
l2_cache: h.cpu.l2_cache,
l3_cache: h.cpu.l3_cache,
virtualization: h.cpu.virtualization,
flags: h.cpu.flags,
microcode: h.cpu.microcode,
},
memory: diagd_core::models::MemoryInfo {
total_bytes: h.memory.total_bytes,
available_bytes: h.memory.available_bytes,
used_bytes: h.memory.used_bytes,
swap_total_bytes: h.memory.swap_total_bytes,
swap_used_bytes: h.memory.swap_used_bytes,
edac_ce_count: h.memory.edac_ce_count,
edac_ue_count: h.memory.edac_ue_count,
},
motherboard: diagd_core::models::MotherboardInfo {
vendor: h.motherboard.vendor,
product_name: h.motherboard.product_name,
version: h.motherboard.version,
bios_vendor: h.motherboard.bios_vendor,
bios_version: h.motherboard.bios_version,
bios_date: h.motherboard.bios_date,
chassis_type: h.motherboard.chassis_type,
},
gpus: h.gpus.into_iter().map(|g| diagd_core::models::GpuInfo {
vendor: g.vendor,
model: g.model,
driver: g.driver,
pci_slot: g.pci_slot,
}).collect(),
disks: h.disks.into_iter().map(|d| diagd_core::models::DiskInfo {
name: d.name,
model: d.model,
serial: d.serial,
size_bytes: d.size_bytes,
disk_type: d.disk_type,
rotational_speed_rpm: d.rotational_speed_rpm,
smart_status: d.smart_status,
health_percent: d.health_percent,
wear_percent: d.wear_percent,
nvme_wear_percent: d.nvme_wear_percent,
temperature_c: d.temperature_c,
power_on_hours: d.power_on_hours,
power_cycle_count: d.power_cycle_count,
bad_sectors_count: d.bad_sectors_count,
total_written_tb: d.total_written_tb,
partitions: d.partitions.into_iter().map(|p| diagd_core::models::PartitionInfo {
name: p.name,
path: p.path,
mount_point: p.mount_point,
fs_type: p.fs_type,
size_bytes: p.size_bytes,
used_bytes: p.used_bytes,
}).collect(),
}).collect(),
network_adapters: h.network_adapters.into_iter().map(|n| diagd_core::models::NetworkAdapterInfo {
interface_name: n.interface_name,
if_type: n.if_type,
state: n.state,
speed_mbps: n.speed_mbps,
mac_address: n.mac_address,
driver: n.driver,
}).collect(),
battery: h.battery.map(|b| diagd_core::models::BatteryInfo {
name: b.name,
capacity_percent: b.capacity_percent,
health_percent: b.health_percent,
cycle_count: b.cycle_count,
status: b.status,
is_degraded: b.is_degraded,
}),
thermals: diagd_core::models::ThermalsInfo {
max_cpu_temp_c: h.thermals.max_cpu_temp_c,
thermal_throttled_cores: h.thermals.thermal_throttled_cores,
total_throttle_events: h.thermals.total_throttle_events,
is_actively_throttling: h.thermals.is_actively_throttling,
},
}
}
}
impl From<diagd_core::models::LiveMetricsSnapshot> for LiveMetricsSnapshot {
fn from(s: diagd_core::models::LiveMetricsSnapshot) -> Self {
LiveMetricsSnapshot {
cpu_usage_percent: s.cpu_usage_percent,
ram_used_bytes: s.ram_used_bytes,
ram_total_bytes: s.ram_total_bytes,
swap_used_bytes: s.swap_used_bytes,
swap_total_bytes: s.swap_total_bytes,
max_cpu_temp_c: s.max_cpu_temp_c,
is_throttling: s.is_throttling,
disk_read_kbps: s.disk_read_kbps,
disk_write_kbps: s.disk_write_kbps,
net_rx_kbps: s.net_rx_kbps,
net_tx_kbps: s.net_tx_kbps,
}
}
}
impl From<diagd_core::models::CrashReportInfo> for CrashReportInfo {
fn from(c: diagd_core::models::CrashReportInfo) -> Self {
CrashReportInfo {
timestamp: c.timestamp,
process_name: c.process_name,
pid: c.pid,
signal_or_code: c.signal_or_code,
fault_module: c.fault_module,
stack_trace_snippet: c.stack_trace_snippet,
dump_path: c.dump_path,
}
}
}
impl From<CrashReportInfo> for diagd_core::models::CrashReportInfo {
fn from(c: CrashReportInfo) -> Self {
diagd_core::models::CrashReportInfo {
timestamp: c.timestamp,
process_name: c.process_name,
pid: c.pid,
signal_or_code: c.signal_or_code,
fault_module: c.fault_module,
stack_trace_snippet: c.stack_trace_snippet,
dump_path: c.dump_path,
}
}
}
impl From<diagd_core::models::ListeningPortInfo> for ListeningPortInfo {
fn from(p: diagd_core::models::ListeningPortInfo) -> Self {
ListeningPortInfo {
protocol: p.protocol,
local_address: p.local_address,
port: p.port,
process_name: p.process_name,
pid: p.pid,
is_public: p.is_public,
risk_level: p.risk_level.into(),
recommendation: p.recommendation,
}
}
}
impl From<ListeningPortInfo> for diagd_core::models::ListeningPortInfo {
fn from(p: ListeningPortInfo) -> Self {
diagd_core::models::ListeningPortInfo {
protocol: p.protocol,
local_address: p.local_address,
port: p.port,
process_name: p.process_name,
pid: p.pid,
is_public: p.is_public,
risk_level: p.risk_level.into(),
recommendation: p.recommendation,
}
}
}
impl From<diagd_core::models::SuidAnomalyInfo> for SuidAnomalyInfo {
fn from(s: diagd_core::models::SuidAnomalyInfo) -> Self {
SuidAnomalyInfo {
path: s.path,
owner: s.owner,
permissions: s.permissions,
risk_reason: s.risk_reason,
}
}
}
impl From<SuidAnomalyInfo> for diagd_core::models::SuidAnomalyInfo {
fn from(s: SuidAnomalyInfo) -> Self {
diagd_core::models::SuidAnomalyInfo {
path: s.path,
owner: s.owner,
permissions: s.permissions,
risk_reason: s.risk_reason,
}
}
}
impl From<diagd_core::models::SecurityCheckItem> for SecurityCheckItem {
fn from(c: diagd_core::models::SecurityCheckItem) -> Self {
SecurityCheckItem {
category: c.category,
name: c.name,
status: c.status,
is_secure: c.is_secure,
description: c.description,
remediation: c.remediation,
}
}
}
impl From<SecurityCheckItem> for diagd_core::models::SecurityCheckItem {
fn from(c: SecurityCheckItem) -> Self {
diagd_core::models::SecurityCheckItem {
category: c.category,
name: c.name,
status: c.status,
is_secure: c.is_secure,
description: c.description,
remediation: c.remediation,
}
}
}
impl From<diagd_core::models::SecurityAuditReport> for SecurityAuditReport {
fn from(s: diagd_core::models::SecurityAuditReport) -> Self {
SecurityAuditReport {
exposed_ports: s.exposed_ports.into_iter().map(Into::into).collect(),
suid_anomalies: s.suid_anomalies.into_iter().map(Into::into).collect(),
hardening_checks: s.hardening_checks.into_iter().map(Into::into).collect(),
security_score: s.security_score,
}
}
}
impl From<SecurityAuditReport> for diagd_core::models::SecurityAuditReport {
fn from(s: SecurityAuditReport) -> Self {
diagd_core::models::SecurityAuditReport {
exposed_ports: s.exposed_ports.into_iter().map(Into::into).collect(),
suid_anomalies: s.suid_anomalies.into_iter().map(Into::into).collect(),
hardening_checks: s.hardening_checks.into_iter().map(Into::into).collect(),
security_score: s.security_score,
}
}
}
impl From<diagd_core::models::SystemReport> for SystemReport {
fn from(r: diagd_core::models::SystemReport) -> Self {
SystemReport {
timestamp: r.timestamp,
os_family: r.os_family,
init_system: r.init_system,
is_root: r.is_root,
subsystems: r.subsystems.into_iter().map(Into::into).collect(),
issues: r.issues.into_iter().map(Into::into).collect(),
failed_services: r.failed_services.into_iter().map(Into::into).collect(),
hardware: r.hardware.into(),
crashes: r.crashes.into_iter().map(Into::into).collect(),
security: r.security.into(),
llm_analyzed: r.llm_analyzed,
summary: r.summary,
prompt_preview: r.prompt_preview,
}
}
}
/// Запрашивает права суперпользователя в графическом режиме через Polkit (pkexec)
pub fn request_gui_privileges() -> bool {
let status = diagd_core::auth::PrivilegeManager::request_privileges(diagd_core::auth::AuthMode::Gui);
matches!(status, diagd_core::auth::PrivilegeStatus::AlreadyRoot | diagd_core::auth::PrivilegeStatus::Elevated)
}
/// Сканирует состояние системы, очищает логи от персональных данных,
/// генерирует структурированный промпт для LLM и формирует итоговый аналитический отчет.
pub async fn scan_system_and_analyze() -> Result<SystemReport, String> {
let core_report = diagd_core::analyst::run_scan_with_mode(diagd_core::auth::AuthMode::Gui).await?;
Ok(core_report.into())
}
/// Возвращает мгновенный снимок телеметрии системы в реальном времени (CPU %, RAM, частоты, температуры, IO)
pub fn get_live_metrics() -> LiveMetricsSnapshot {
diagd_core::LiveMetricsScanner::sample().into()
}
/// Экспортирует полный архив Debug Bundle (.tar.gz, standalone HTML и markdown).
/// Возвращает путь к сохраненному архиву.
pub fn export_debug_bundle(report: SystemReport) -> Result<String, String> {
let core_report = diagd_core::models::SystemReport {
timestamp: report.timestamp,
os_family: report.os_family,
init_system: report.init_system,
is_root: report.is_root,
subsystems: report.subsystems.into_iter().map(|s| diagd_core::models::SubsystemStatus {
name: match s.name {
Subsystem::Systemd => diagd_core::models::Subsystem::Systemd,
Subsystem::Kernel => diagd_core::models::Subsystem::Kernel,
Subsystem::Storage => diagd_core::models::Subsystem::Storage,
Subsystem::Audio => diagd_core::models::Subsystem::Audio,
Subsystem::Network => diagd_core::models::Subsystem::Network,
Subsystem::Security => diagd_core::models::Subsystem::Security,
Subsystem::Crashes => diagd_core::models::Subsystem::Crashes,
},
status: s.status,
issue_count: s.issue_count,
details: s.details,
}).collect(),
issues: report.issues.into_iter().map(|i| diagd_core::models::IssueItem {
id: i.id,
title: i.title,
subsystem: match i.subsystem {
Subsystem::Systemd => diagd_core::models::Subsystem::Systemd,
Subsystem::Kernel => diagd_core::models::Subsystem::Kernel,
Subsystem::Storage => diagd_core::models::Subsystem::Storage,
Subsystem::Audio => diagd_core::models::Subsystem::Audio,
Subsystem::Network => diagd_core::models::Subsystem::Network,
Subsystem::Security => diagd_core::models::Subsystem::Security,
Subsystem::Crashes => diagd_core::models::Subsystem::Crashes,
},
severity: match i.severity {
Severity::Critical => diagd_core::models::Severity::Critical,
Severity::Warning => diagd_core::models::Severity::Warning,
Severity::Info => diagd_core::models::Severity::Info,
},
root_cause: i.root_cause,
explanation: i.explanation,
fix: i.fix.map(|f| diagd_core::models::FixSuggestion {
title: f.title,
command: f.command,
explanation: f.explanation,
}),
raw_log: i.raw_log,
sanitized: i.sanitized,
permission_denied: i.permission_denied,
}).collect(),
failed_services: report.failed_services.into_iter().map(|f| diagd_core::models::FailedService {
name: f.name,
init_system: f.init_system,
state: f.state,
description: f.description,
logs: f.logs,
restart_command: f.restart_command,
status_command: f.status_command,
}).collect(),
hardware: report.hardware.into(),
crashes: report.crashes.into_iter().map(Into::into).collect(),
security: report.security.into(),
llm_analyzed: report.llm_analyzed,
summary: report.summary,
prompt_preview: report.prompt_preview,
};
diagd_core::BundleExporter::export_bundle(&core_report)
}
+2
View File
@@ -0,0 +1,2 @@
pub mod simple;
pub mod diagnostics;
+10
View File
@@ -0,0 +1,10 @@
#[flutter_rust_bridge::frb(sync)] // Synchronous mode for simplicity of the demo
pub fn greet(name: String) -> String {
format!("Hello, {name}!")
}
#[flutter_rust_bridge::frb(init)]
pub fn init_app() {
// Default utilities - feel free to customize
flutter_rust_bridge::setup_default_user_utils();
}
+3
View File
@@ -0,0 +1,3 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
rust_lib_diagd::tui::run_tui()
}
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
#[cfg(feature = "gui")]
pub mod api;
#[cfg(feature = "gui")]
mod frb_generated;
#[cfg(feature = "tui")]
pub mod tui;
+399
View File
@@ -0,0 +1,399 @@
use diagd_core::models::SystemReport;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ActiveView {
Issues,
Hardware,
LiveMonitor,
Security,
Crashes,
RawLog,
LlmPrompt,
}
pub struct App {
pub report: Option<SystemReport>,
pub selected_issue_idx: usize,
pub selected_crash_idx: usize,
pub log_scroll: u16,
pub active_view: ActiveView,
pub status_message: String,
pub should_quit: bool,
pub live_snapshot: diagd_core::models::LiveMetricsSnapshot,
pub cpu_history: Vec<u64>,
pub ram_history: Vec<u64>,
pub temp_history: Vec<u64>,
pub net_history: Vec<u64>,
pub disk_history: Vec<u64>,
}
impl App {
pub fn new(initial_report: Option<SystemReport>) -> Self {
Self {
report: initial_report,
selected_issue_idx: 0,
selected_crash_idx: 0,
log_scroll: 0,
active_view: ActiveView::Issues,
status_message: "Готов к работе. Поддерживается клик и скролл мышкой. [s] Сканировать, [q] Выход.".to_string(),
should_quit: false,
live_snapshot: diagd_core::models::LiveMetricsSnapshot::default(),
cpu_history: Vec::with_capacity(60),
ram_history: Vec::with_capacity(60),
temp_history: Vec::with_capacity(60),
net_history: Vec::with_capacity(60),
disk_history: Vec::with_capacity(60),
}
}
pub fn tick_live_metrics(&mut self) {
let snap = diagd_core::LiveMetricsScanner::sample();
let cpu_val = snap.cpu_usage_percent.round() as u64;
self.cpu_history.push(cpu_val);
if self.cpu_history.len() > 60 {
self.cpu_history.remove(0);
}
let ram_pct = if snap.ram_total_bytes > 0 {
((snap.ram_used_bytes as f64 / snap.ram_total_bytes as f64) * 100.0).round() as u64
} else {
0
};
self.ram_history.push(ram_pct);
if self.ram_history.len() > 60 {
self.ram_history.remove(0);
}
let temp_val = snap.max_cpu_temp_c.max(0.0).round() as u64;
self.temp_history.push(temp_val);
if self.temp_history.len() > 60 {
self.temp_history.remove(0);
}
let net_kb = snap.net_rx_kbps + snap.net_tx_kbps;
self.net_history.push(net_kb);
if self.net_history.len() > 60 {
self.net_history.remove(0);
}
let disk_kb = snap.disk_read_kbps + snap.disk_write_kbps;
self.disk_history.push(disk_kb);
if self.disk_history.len() > 60 {
self.disk_history.remove(0);
}
self.live_snapshot = snap;
}
pub fn next_crash(&mut self) {
if let Some(ref report) = self.report {
if !report.crashes.is_empty() {
if self.selected_crash_idx + 1 < report.crashes.len() {
self.selected_crash_idx += 1;
} else {
self.selected_crash_idx = 0;
}
self.log_scroll = 0;
}
}
}
pub fn previous_crash(&mut self) {
if let Some(ref report) = self.report {
if !report.crashes.is_empty() {
if self.selected_crash_idx > 0 {
self.selected_crash_idx -= 1;
} else {
self.selected_crash_idx = report.crashes.len() - 1;
}
self.log_scroll = 0;
}
}
}
pub fn next_issue(&mut self) {
if let Some(ref report) = self.report {
if !report.issues.is_empty() {
if self.selected_issue_idx + 1 < report.issues.len() {
self.selected_issue_idx += 1;
} else {
self.selected_issue_idx = 0;
}
self.log_scroll = 0;
}
}
}
pub fn previous_issue(&mut self) {
if let Some(ref report) = self.report {
if !report.issues.is_empty() {
if self.selected_issue_idx > 0 {
self.selected_issue_idx -= 1;
} else {
self.selected_issue_idx = report.issues.len() - 1;
}
self.log_scroll = 0;
}
}
}
pub fn scroll_log_down(&mut self) {
self.log_scroll = self.log_scroll.saturating_add(3);
}
pub fn scroll_log_up(&mut self) {
self.log_scroll = self.log_scroll.saturating_sub(3);
}
pub fn toggle_raw_log(&mut self) {
self.active_view = match self.active_view {
ActiveView::RawLog => ActiveView::Issues,
_ => {
self.log_scroll = 0;
ActiveView::RawLog
}
};
}
pub fn toggle_prompt(&mut self) {
self.active_view = match self.active_view {
ActiveView::LlmPrompt => ActiveView::Issues,
_ => {
self.log_scroll = 0;
ActiveView::LlmPrompt
}
};
}
pub fn toggle_hardware(&mut self) {
self.active_view = match self.active_view {
ActiveView::Hardware => ActiveView::Issues,
_ => {
self.log_scroll = 0;
ActiveView::Hardware
}
};
}
pub fn toggle_live_monitor(&mut self) {
self.active_view = match self.active_view {
ActiveView::LiveMonitor => ActiveView::Issues,
_ => {
self.log_scroll = 0;
ActiveView::LiveMonitor
}
};
}
pub fn toggle_security(&mut self) {
self.active_view = match self.active_view {
ActiveView::Security => ActiveView::Issues,
_ => {
self.log_scroll = 0;
ActiveView::Security
}
};
}
pub fn toggle_crashes(&mut self) {
self.active_view = match self.active_view {
ActiveView::Crashes => ActiveView::Issues,
_ => {
self.log_scroll = 0;
ActiveView::Crashes
}
};
}
pub fn copy_fix_command(&mut self) {
if let Some(ref report) = self.report {
if let Some(issue) = report.issues.get(self.selected_issue_idx) {
if let Some(ref fix) = issue.fix {
copy_to_clipboard(&fix.command);
self.status_message = format!("✓ Скопировано: {}", fix.command);
return;
}
}
}
self.status_message = "Нет команды для копирования.".to_string();
}
pub fn export_bundle(&mut self) {
if let Some(ref report) = self.report {
match diagd_core::BundleExporter::export_bundle(report) {
Ok(path) => {
self.status_message = format!("✓ Отчет и Debug Bundle сохранены: {}", path);
}
Err(err) => {
self.status_message = format!("Ошибка экспорта: {}", err);
}
}
} else {
self.status_message = "Нет данных для экспорта. Сначала выполните сканирование.".to_string();
}
}
/// Интерактивная обработка клика мыши
pub fn handle_mouse_click(&mut self, col: u16, row: u16, screen_width: u16, screen_height: u16) {
// 0. Клик в верхний Tab Bar (строки 0 или 1)
if row <= 2 {
if col < 35 {
self.active_view = ActiveView::Issues;
self.log_scroll = 0;
} else if col < 53 {
self.active_view = ActiveView::Hardware;
self.log_scroll = 0;
} else if col < 67 {
self.active_view = ActiveView::LiveMonitor;
self.log_scroll = 0;
} else if col < 84 {
self.active_view = ActiveView::Security;
self.log_scroll = 0;
} else if col < 95 {
self.active_view = ActiveView::Crashes;
self.log_scroll = 0;
} else if col < 105 {
self.toggle_raw_log();
} else if col < 120 {
self.toggle_prompt();
}
return;
}
// 1. Клик в футер (горячие клавиши)
if row >= screen_height.saturating_sub(2) {
if col < 12 {
self.should_quit = true;
} else if col < 26 {
// [s] Сканировать
self.status_message = "Запрос повторного сканирования...".to_string();
} else if col < 40 {
// [h] Железо
self.toggle_hardware();
} else if col < 55 {
// Копировать
self.copy_fix_command();
} else if col < 66 {
// Лог
self.toggle_raw_log();
} else if col < 80 {
// Промпт
self.toggle_prompt();
} else if col < 96 {
// Экспорт
self.export_bundle();
}
return;
}
// 2. Клик вне модального окна закрывает его
if self.active_view == ActiveView::RawLog || self.active_view == ActiveView::LlmPrompt {
if row <= 7 || row >= screen_height.saturating_sub(2) {
self.active_view = ActiveView::Issues;
}
return;
}
// 3. Клик в список инцидентов (левая колонка: 0..width*38%)
let list_width = (screen_width as f32 * 0.38) as u16;
if self.active_view == ActiveView::Issues && col <= list_width && row >= 8 {
let item_index = (row - 8) as usize;
if let Some(ref report) = self.report {
if item_index < report.issues.len() {
self.selected_issue_idx = item_index;
self.log_scroll = 0;
self.status_message = format!("Выбрана проблема #{}", item_index + 1);
}
}
} else if self.active_view == ActiveView::Crashes && col <= list_width && row >= 8 {
let item_index = (row - 8) as usize;
if let Some(ref report) = self.report {
if item_index < report.crashes.len() {
self.selected_crash_idx = item_index;
self.log_scroll = 0;
self.status_message = format!("Выбран сбой #{}", item_index + 1);
}
}
}
}
}
fn copy_to_clipboard(text: &str) {
use std::io::Write;
use std::process::{Command, Stdio};
let _ = Command::new("wl-copy")
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(text.as_bytes())?;
}
child.wait()
})
.or_else(|_| {
Command::new("xclip")
.args(["-selection", "clipboard"])
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(text.as_bytes())?;
}
child.wait()
})
})
.or_else(|_| {
// macOS pbcopy
Command::new("pbcopy")
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(text.as_bytes())?;
}
child.wait()
})
})
.or_else(|_| {
// Windows clip
Command::new("clip")
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(text.as_bytes())?;
}
child.wait()
})
});
let b64 = base64_simple_encode(text.as_bytes());
print!("\x1b]52;c;{}\x07", b64);
let _ = std::io::stdout().flush();
}
fn base64_simple_encode(input: &[u8]) -> String {
const CHARSET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::new();
for chunk in input.chunks(3) {
let b0 = chunk[0];
let b1 = if chunk.len() > 1 { chunk[1] } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] } else { 0 };
let n = ((b0 as u32) << 16) | ((b1 as u32) << 8) | (b2 as u32);
out.push(CHARSET[((n >> 18) & 63) as usize] as char);
out.push(CHARSET[((n >> 12) & 63) as usize] as char);
if chunk.len() > 1 {
out.push(CHARSET[((n >> 6) & 63) as usize] as char);
} else {
out.push('=');
}
if chunk.len() > 2 {
out.push(CHARSET[(n & 63) as usize] as char);
} else {
out.push('=');
}
}
out
}
+295
View File
@@ -0,0 +1,295 @@
pub mod app;
pub mod ui;
use app::{ActiveView, App};
use crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers,
MouseButton, MouseEventKind,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use diagd_core::auth::{AuthMode, PrivilegeManager, PrivilegeStatus};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
use std::time::Duration;
pub async fn run_tui_async() -> Result<(), Box<dyn std::error::Error>> {
// 1. Однократная проверка/запрос привилегий перед входом в fullscreen TUI
let priv_status = PrivilegeManager::request_privileges(AuthMode::Tui);
// 2. Инициализация терминала (Raw Mode + Alternate Screen + Mouse Capture)
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new(None);
let auth_msg = match priv_status {
PrivilegeStatus::AlreadyRoot => "Запущено от root".to_string(),
PrivilegeStatus::Elevated => "Привилегии sudo получены".to_string(),
PrivilegeStatus::Unprivileged { reason } => format!("Unprivileged режим: {}", reason),
};
app.status_message = format!("{} | Сканирование системы...", auth_msg);
terminal.draw(|f| ui::draw(f, &app))?;
// 3. Запуск глубокого аудита
if let Ok(report) = diagd_core::run_scan().await {
app.status_message = format!(
"{} ({}) | Найдено проблем: {} | Кликайте мышкой по списку",
report.os_family,
report.init_system,
report.issues.len()
);
app.report = Some(report);
} else {
app.status_message = "Ошибка при первичном сканировании системы.".to_string();
}
// 4. Основной цикл событий с поддержкой клавиатуры и мыши
let res = run_app(&mut terminal, &mut app).await;
// 5. Корректное восстановление терминала (отключение мыши, возврат экрана)
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
eprintln!("Ошибка работы TUI: {:?}", err);
}
Ok(())
}
pub fn run_tui() -> Result<(), Box<dyn std::error::Error>> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(run_tui_async())
}
#[no_mangle]
pub extern "C" fn diagd_run_tui() -> i32 {
match run_tui() {
Ok(_) => 0,
Err(e) => {
eprintln!("Ошибка запуска TUI: {}", e);
1
}
}
}
async fn run_app<B: ratatui::backend::Backend>(
terminal: &mut Terminal<B>,
app: &mut App,
) -> io::Result<()> {
let mut last_live_tick = std::time::Instant::now();
app.tick_live_metrics();
loop {
terminal.draw(|f| ui::draw(f, app))?;
if app.should_quit {
return Ok(());
}
if last_live_tick.elapsed() >= Duration::from_millis(1000) {
app.tick_live_metrics();
last_live_tick = std::time::Instant::now();
}
let size = terminal.size()?;
if event::poll(Duration::from_millis(100))? {
match event::read()? {
// Обработка МЫШИ (клик и колесико скролла)
Event::Mouse(mouse) => match mouse.kind {
MouseEventKind::Down(MouseButton::Left) => {
app.handle_mouse_click(mouse.column, mouse.row, size.width, size.height);
}
MouseEventKind::ScrollDown => {
app.scroll_log_down();
}
MouseEventKind::ScrollUp => {
app.scroll_log_up();
}
_ => {}
},
// Обработка КЛАВИАТУРЫ
Event::Key(key) => {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
return Ok(());
}
// Глобальные быстрые переключения вкладок
match key.code {
KeyCode::Char('1') => {
app.active_view = ActiveView::Issues;
continue;
}
KeyCode::Char('2') => {
app.active_view = ActiveView::Hardware;
continue;
}
KeyCode::Char('3') => {
app.active_view = ActiveView::LiveMonitor;
continue;
}
KeyCode::Char('4') => {
app.active_view = ActiveView::Security;
continue;
}
KeyCode::Char('5') => {
app.active_view = ActiveView::Crashes;
continue;
}
KeyCode::Char('6') => {
app.toggle_raw_log();
continue;
}
KeyCode::Char('7') => {
app.toggle_prompt();
continue;
}
_ => {}
}
match app.active_view {
ActiveView::Issues => match key.code {
KeyCode::Char('q') => {
app.should_quit = true;
}
KeyCode::Char('s') => {
app.status_message = "Глубокое сканирование системы...".to_string();
terminal.draw(|f| ui::draw(f, app))?;
if let Ok(report) = diagd_core::run_scan().await {
app.status_message = format!(
"{} ({}) | Проблем: {} | Клик мышью для выбора",
report.os_family,
report.init_system,
report.issues.len()
);
app.report = Some(report);
app.selected_issue_idx = 0;
} else {
app.status_message = "Ошибка при выполнении сканирования!".to_string();
}
}
KeyCode::Down | KeyCode::Char('j') => {
app.next_issue();
}
KeyCode::Up | KeyCode::Char('k') => {
app.previous_issue();
}
KeyCode::Char('c') => {
app.copy_fix_command();
}
KeyCode::Char('h') => {
app.toggle_hardware();
}
KeyCode::Char('m') => {
app.toggle_live_monitor();
}
KeyCode::Char('b') => {
app.toggle_security();
}
KeyCode::Char('x') => {
app.toggle_crashes();
}
KeyCode::Char('l') | KeyCode::Enter => {
app.toggle_raw_log();
}
KeyCode::Char('p') => {
app.toggle_prompt();
}
KeyCode::Char('e') => {
app.export_bundle();
}
_ => {}
},
ActiveView::Hardware | ActiveView::Security => match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
app.active_view = ActiveView::Issues;
}
KeyCode::Char('e') => {
app.export_bundle();
}
KeyCode::Down | KeyCode::Char('j') => {
app.scroll_log_down();
}
KeyCode::Up | KeyCode::Char('k') => {
app.scroll_log_up();
}
KeyCode::PageDown => {
app.log_scroll = app.log_scroll.saturating_add(10);
}
KeyCode::PageUp => {
app.log_scroll = app.log_scroll.saturating_sub(10);
}
_ => {}
},
ActiveView::LiveMonitor => match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
app.active_view = ActiveView::Issues;
}
KeyCode::Char('e') => {
app.export_bundle();
}
_ => {}
},
ActiveView::Crashes => match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
app.active_view = ActiveView::Issues;
}
KeyCode::Down | KeyCode::Char('j') => {
app.next_crash();
}
KeyCode::Up | KeyCode::Char('k') => {
app.previous_crash();
}
KeyCode::PageDown => {
app.log_scroll = app.log_scroll.saturating_add(10);
}
KeyCode::PageUp => {
app.log_scroll = app.log_scroll.saturating_sub(10);
}
KeyCode::Char('e') => {
app.export_bundle();
}
_ => {}
},
ActiveView::RawLog | ActiveView::LlmPrompt => match key.code {
KeyCode::Esc
| KeyCode::Char('q')
| KeyCode::Char('l')
| KeyCode::Char('p') => {
app.active_view = ActiveView::Issues;
}
KeyCode::Down | KeyCode::Char('j') => {
app.scroll_log_down();
}
KeyCode::Up | KeyCode::Char('k') => {
app.scroll_log_up();
}
KeyCode::PageDown => {
app.log_scroll = app.log_scroll.saturating_add(10);
}
KeyCode::PageUp => {
app.log_scroll = app.log_scroll.saturating_sub(10);
}
_ => {}
},
}
}
_ => {}
}
}
}
}
+1003
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More