feat: initial commit of diagd - unified Linux/BSD/macOS/Windows diagnostic tool (TUI + GUI)
@@ -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/
|
||||
@@ -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'
|
||||
@@ -0,0 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/core",
|
||||
"rust",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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: ®ex::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]"));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
rust_input: crate::api
|
||||
rust_root: rust/
|
||||
dart_output: lib/src/rust
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
flutter/ephemeral
|
||||
@@ -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()
|
||||
@@ -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_
|
||||
@@ -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)
|
||||
@@ -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}")
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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_
|
||||
@@ -0,0 +1,7 @@
|
||||
# Flutter-related
|
||||
**/Flutter/ephemeral/
|
||||
**/Pods/
|
||||
|
||||
# Xcode-related
|
||||
**/dgph
|
||||
**/xcuserdata/
|
||||
@@ -0,0 +1 @@
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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 = ""$FLUTTER_ROOT"/packages/flutter_tools/bin/macos_assemble.sh prepare ">
|
||||
<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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 520 B |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
@@ -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>
|
||||
@@ -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.
|
||||
@@ -0,0 +1,2 @@
|
||||
#include "../../Flutter/Flutter-Debug.xcconfig"
|
||||
#include "Warnings.xcconfig"
|
||||
@@ -0,0 +1,2 @@
|
||||
#include "../../Flutter/Flutter-Release.xcconfig"
|
||||
#include "Warnings.xcconfig"
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
@@ -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)'] }
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod simple;
|
||||
pub mod diagnostics;
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
rust_lib_diagd::tui::run_tui()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod api;
|
||||
#[cfg(feature = "gui")]
|
||||
mod frb_generated;
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
pub mod tui;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||