feat(diagnostics): add OS / distro / install method / deps section
New v2_get_system_info command and matching UI section in the System Diagnostics panel. What it reports: - os (linux/macos/windows) - arch (x86_64, aarch64) - kernel_version from /proc/sys/kernel/osrelease - Linux distro: id, version_id, pretty_name from /etc/os-release (falls back to /run/host/os-release so it works inside Flatpak) - install_method: flatpak / snap / appimage / dev / native, detected via FLATPAK_ID + /.flatpak-info, SNAP, APPIMAGE env vars, and debug_assertions as the last fallback - flatpak_runtime + version: parsed from /.flatpak-info - loaded library versions (from /proc/self/maps, symlink-resolved): webkit2gtk-4.1, gtk-3 (or 4), glibc, alsa, pipewire, pulseaudio All info is derived from local files/env; no sensitive data, no IDs, no hashes. Included in the clipboard export. cc
This commit is contained in:
8 files changed
+248
-2
No files matched your search
@@ -166,3 +166,166 @@ pub fn v2_get_runtime_diagnostics(
|
||||
app_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== System Info ====================
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SystemInfo {
|
||||
pub os: String,
|
||||
pub arch: String,
|
||||
pub kernel_version: Option<String>,
|
||||
pub distro_id: Option<String>,
|
||||
pub distro_version_id: Option<String>,
|
||||
pub distro_pretty_name: Option<String>,
|
||||
pub install_method: String,
|
||||
pub flatpak_runtime: Option<String>,
|
||||
pub flatpak_runtime_version: Option<String>,
|
||||
pub webkit2gtk_version: Option<String>,
|
||||
pub gtk_version: Option<String>,
|
||||
pub glibc_version: Option<String>,
|
||||
pub alsa_version: Option<String>,
|
||||
pub pipewire_version: Option<String>,
|
||||
pub pulseaudio_version: Option<String>,
|
||||
}
|
||||
|
||||
/// Parse /etc/os-release (or the Flatpak host equivalent) into key/value pairs.
|
||||
fn read_os_release() -> std::collections::HashMap<String, String> {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
// Try Flatpak-exposed host file first, then the normal path.
|
||||
let candidates = ["/run/host/os-release", "/etc/os-release"];
|
||||
for path in candidates {
|
||||
if let Ok(text) = std::fs::read_to_string(path) {
|
||||
for line in text.lines() {
|
||||
if let Some(idx) = line.find('=') {
|
||||
let key = line[..idx].trim().to_string();
|
||||
let mut value = line[idx + 1..].trim().to_string();
|
||||
if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
|
||||
value = value[1..value.len() - 1].to_string();
|
||||
}
|
||||
map.insert(key, value);
|
||||
}
|
||||
}
|
||||
if !map.is_empty() {
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
fn detect_kernel_version() -> Option<String> {
|
||||
std::fs::read_to_string("/proc/sys/kernel/osrelease")
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
fn detect_install_method() -> (String, Option<String>, Option<String>) {
|
||||
// (method, flatpak_runtime, flatpak_runtime_version)
|
||||
if std::env::var("FLATPAK_ID").is_ok() || std::path::Path::new("/.flatpak-info").exists() {
|
||||
let mut runtime = None;
|
||||
let mut runtime_version = None;
|
||||
if let Ok(text) = std::fs::read_to_string("/.flatpak-info") {
|
||||
for line in text.lines() {
|
||||
if let Some(value) = line.strip_prefix("runtime=") {
|
||||
let v = value.trim();
|
||||
if let Some(slash) = v.rfind('/') {
|
||||
runtime = Some(v[..slash].to_string());
|
||||
runtime_version = Some(v[slash + 1..].to_string());
|
||||
} else {
|
||||
runtime = Some(v.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ("flatpak".to_string(), runtime, runtime_version);
|
||||
}
|
||||
if std::env::var("SNAP").is_ok() {
|
||||
return ("snap".to_string(), None, None);
|
||||
}
|
||||
if std::env::var("APPIMAGE").is_ok() {
|
||||
return ("appimage".to_string(), None, None);
|
||||
}
|
||||
if cfg!(debug_assertions) {
|
||||
return ("dev".to_string(), None, None);
|
||||
}
|
||||
("native".to_string(), None, None)
|
||||
}
|
||||
|
||||
/// Extract the best-available version string from the filename of a shared
|
||||
/// library loaded by the current process. Looks for patterns like
|
||||
/// `libfoo.so.0.15.7` → `0.15.7`, or `libfoo.so.2` → `2`.
|
||||
/// Returns `None` if the library isn't mapped.
|
||||
fn detect_loaded_lib_version(lib_name_stem: &str) -> Option<String> {
|
||||
let maps = std::fs::read_to_string("/proc/self/maps").ok()?;
|
||||
let mut best: Option<String> = None;
|
||||
for line in maps.lines() {
|
||||
// Last column is the path (may contain spaces, very rare).
|
||||
let path = line.splitn(6, ' ').nth(5).unwrap_or("").trim();
|
||||
if path.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let filename = std::path::Path::new(path)
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("");
|
||||
if !filename.starts_with(lib_name_stem) {
|
||||
continue;
|
||||
}
|
||||
// filename example: "libwebkit2gtk-4.1.so.0.15.7"
|
||||
// Strip the "lib_name_stem.so" prefix and leading dot.
|
||||
let tail = match filename.split_once(".so") {
|
||||
Some((_, rest)) => rest.trim_start_matches('.'),
|
||||
None => continue,
|
||||
};
|
||||
if tail.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Resolve symlink target if possible — often the real file carries
|
||||
// a fuller version than the SONAME alias.
|
||||
if let Ok(real) = std::fs::canonicalize(path) {
|
||||
if let Some(real_name) = real.file_name().and_then(|s| s.to_str()) {
|
||||
if let Some((_, rest)) = real_name.split_once(".so") {
|
||||
let real_tail = rest.trim_start_matches('.');
|
||||
if !real_tail.is_empty() {
|
||||
best = Some(real_tail.to_string());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
best.get_or_insert_with(|| tail.to_string());
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn v2_get_system_info() -> Result<SystemInfo, RuntimeError> {
|
||||
let os = std::env::consts::OS.to_string();
|
||||
let arch = std::env::consts::ARCH.to_string();
|
||||
let (install_method, flatpak_runtime, flatpak_runtime_version) = detect_install_method();
|
||||
let osr = read_os_release();
|
||||
|
||||
Ok(SystemInfo {
|
||||
os,
|
||||
arch,
|
||||
kernel_version: detect_kernel_version(),
|
||||
distro_id: osr.get("ID").cloned(),
|
||||
distro_version_id: osr.get("VERSION_ID").cloned(),
|
||||
distro_pretty_name: osr
|
||||
.get("PRETTY_NAME")
|
||||
.cloned()
|
||||
.or_else(|| osr.get("NAME").cloned()),
|
||||
install_method,
|
||||
flatpak_runtime,
|
||||
flatpak_runtime_version,
|
||||
// Runtime shared-library versions, parsed from /proc/self/maps.
|
||||
webkit2gtk_version: detect_loaded_lib_version("libwebkit2gtk-4.1"),
|
||||
gtk_version: detect_loaded_lib_version("libgtk-3")
|
||||
.or_else(|| detect_loaded_lib_version("libgtk-4")),
|
||||
glibc_version: detect_loaded_lib_version("libc"),
|
||||
alsa_version: detect_loaded_lib_version("libasound"),
|
||||
pipewire_version: detect_loaded_lib_version("libpipewire-0.3"),
|
||||
pulseaudio_version: detect_loaded_lib_version("libpulse"),
|
||||
})
|
||||
}
|
||||
@@ -1361,6 +1361,7 @@ pub fn run() {
|
||||
commands_v2::v2_set_visualizer_enabled,
|
||||
commands_v2::v2_get_developer_settings,
|
||||
commands_v2::v2_get_runtime_diagnostics,
|
||||
commands_v2::v2_get_system_info,
|
||||
commands_v2::v2_set_developer_force_dmabuf,
|
||||
commands_v2::v2_get_graphics_settings,
|
||||
commands_v2::v2_get_graphics_startup_status,
|
||||
|
||||
@@ -58,6 +58,24 @@
|
||||
status: 'match' | 'mismatch' | 'info';
|
||||
}
|
||||
|
||||
interface SystemInfo {
|
||||
os: string;
|
||||
arch: string;
|
||||
kernelVersion: string | null;
|
||||
distroId: string | null;
|
||||
distroVersionId: string | null;
|
||||
distroPrettyName: string | null;
|
||||
installMethod: string;
|
||||
flatpakRuntime: string | null;
|
||||
flatpakRuntimeVersion: string | null;
|
||||
webkit2gtkVersion: string | null;
|
||||
gtkVersion: string | null;
|
||||
glibcVersion: string | null;
|
||||
alsaVersion: string | null;
|
||||
pipewireVersion: string | null;
|
||||
pulseaudioVersion: string | null;
|
||||
}
|
||||
|
||||
interface PlaybackSnapshot {
|
||||
isPlaying: boolean;
|
||||
volumePercent: number;
|
||||
@@ -101,6 +119,7 @@
|
||||
}
|
||||
|
||||
let diagnostics = $state<RuntimeDiagnostics | null>(null);
|
||||
let systemInfo = $state<SystemInfo | null>(null);
|
||||
let playback = $state<PlaybackSnapshot | null>(null);
|
||||
let qconnect = $state<QconnectDiag | null>(null);
|
||||
let castScan = $state<CastScanResult | null>(null);
|
||||
@@ -116,6 +135,7 @@
|
||||
let playbackOpen = $state(true);
|
||||
let qconnectOpen = $state(true);
|
||||
let castOpen = $state(true);
|
||||
let systemOpen = $state(true);
|
||||
|
||||
const CAST_SCAN_DURATION_MS = 10000;
|
||||
|
||||
@@ -311,8 +331,9 @@
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const [diagRes, qcRes] = await Promise.allSettled([
|
||||
const [diagRes, sysRes, qcRes] = await Promise.allSettled([
|
||||
invoke<RuntimeDiagnostics>('v2_get_runtime_diagnostics'),
|
||||
invoke<SystemInfo>('v2_get_system_info'),
|
||||
snapshotQconnect(),
|
||||
]);
|
||||
if (diagRes.status === 'fulfilled') {
|
||||
@@ -320,6 +341,9 @@
|
||||
} else {
|
||||
error = String(diagRes.reason);
|
||||
}
|
||||
if (sysRes.status === 'fulfilled') {
|
||||
systemInfo = sysRes.value;
|
||||
}
|
||||
if (qcRes.status === 'fulfilled') {
|
||||
qconnect = qcRes.value;
|
||||
}
|
||||
@@ -334,6 +358,7 @@
|
||||
try {
|
||||
const exportData = {
|
||||
...diagnostics,
|
||||
systemInfo,
|
||||
playback,
|
||||
qconnect,
|
||||
castScan,
|
||||
@@ -345,7 +370,7 @@
|
||||
} catch {
|
||||
try {
|
||||
await navigator.clipboard.writeText(
|
||||
JSON.stringify({ ...diagnostics, playback, qconnect, castScan }, null, 2)
|
||||
JSON.stringify({ ...diagnostics, systemInfo, playback, qconnect, castScan }, null, 2)
|
||||
);
|
||||
copied = true;
|
||||
setTimeout(() => { copied = false; }, 1500);
|
||||
@@ -370,6 +395,30 @@
|
||||
// (intentional) don't leave any subscriptions behind
|
||||
});
|
||||
|
||||
function systemRows(s: SystemInfo): DiagRow[] {
|
||||
const rows: DiagRow[] = [
|
||||
{ label: 'OS', saved: '—', runtime: s.os, status: 'info' },
|
||||
{ label: 'Arch', saved: '—', runtime: s.arch, status: 'info' },
|
||||
{ label: 'Kernel', saved: '—', runtime: str(s.kernelVersion), status: 'info' },
|
||||
{ label: 'Distro', saved: '—', runtime: str(s.distroPrettyName), status: 'info' },
|
||||
{ label: 'Distro ID', saved: '—', runtime: str(s.distroId), status: 'info' },
|
||||
{ label: 'Distro Version', saved: '—', runtime: str(s.distroVersionId), status: 'info' },
|
||||
{ label: 'Install Method', saved: '—', runtime: s.installMethod, status: 'info' },
|
||||
];
|
||||
if (s.flatpakRuntime) {
|
||||
rows.push({ label: 'Flatpak Runtime', saved: '—', runtime: `${s.flatpakRuntime} ${str(s.flatpakRuntimeVersion)}`, status: 'info' });
|
||||
}
|
||||
rows.push(
|
||||
{ label: 'WebKit2GTK', saved: '—', runtime: str(s.webkit2gtkVersion), status: 'info' },
|
||||
{ label: 'GTK', saved: '—', runtime: str(s.gtkVersion), status: 'info' },
|
||||
{ label: 'glibc', saved: '—', runtime: str(s.glibcVersion), status: 'info' },
|
||||
{ label: 'ALSA', saved: '—', runtime: str(s.alsaVersion), status: 'info' },
|
||||
{ label: 'PipeWire', saved: '—', runtime: str(s.pipewireVersion), status: 'info' },
|
||||
{ label: 'PulseAudio', saved: '—', runtime: str(s.pulseaudioVersion), status: 'info' },
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
function playbackRows(p: PlaybackSnapshot): DiagRow[] {
|
||||
return [
|
||||
{ label: 'Playing', saved: '—', runtime: bool(p.isPlaying), status: 'info' },
|
||||
@@ -450,6 +499,34 @@
|
||||
{#if diagnostics}
|
||||
<div class="diag-version">QBZ v{diagnostics.appVersion}</div>
|
||||
|
||||
<!-- System Info Section -->
|
||||
{#if systemInfo}
|
||||
<button class="section-toggle" onclick={() => systemOpen = !systemOpen}>
|
||||
{#if systemOpen}<ChevronDown size={14} />{:else}<ChevronRight size={14} />{/if}
|
||||
{$t('settings.developer.diagnostics.sectionSystem')}
|
||||
</button>
|
||||
{#if systemOpen}
|
||||
<table class="diag-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{$t('settings.developer.diagnostics.colSetting')}</th>
|
||||
<th colspan="2">{$t('settings.developer.diagnostics.colRuntime')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each systemRows(systemInfo) as row (row.label)}
|
||||
<tr>
|
||||
<td class="label-cell">{row.label}</td>
|
||||
<td class="value-cell" colspan="2">{row.runtime}</td>
|
||||
<td class="status-cell info">·</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Playback Section -->
|
||||
{#if playback}
|
||||
<button class="section-toggle" onclick={() => playbackOpen = !playbackOpen}>
|
||||
|
||||
@@ -1373,6 +1373,7 @@
|
||||
"sectionGraphics": "Grafik & Rendering",
|
||||
"sectionEnv": "Umgebungsvariablen",
|
||||
"sectionPlayback": "Wiedergabe",
|
||||
"sectionSystem": "System",
|
||||
"sectionQconnect": "Qobuz Connect",
|
||||
"sectionCast": "Cast-Erkennung",
|
||||
"castScan": "10s scannen",
|
||||
|
||||
@@ -1379,6 +1379,7 @@
|
||||
"sectionGraphics": "Graphics & Rendering",
|
||||
"sectionEnv": "Environment Variables",
|
||||
"sectionPlayback": "Playback",
|
||||
"sectionSystem": "System",
|
||||
"sectionQconnect": "Qobuz Connect",
|
||||
"sectionCast": "Cast Discovery",
|
||||
"castScan": "Run 10s scan",
|
||||
|
||||
@@ -1379,6 +1379,7 @@
|
||||
"sectionGraphics": "Gráficos y renderizado",
|
||||
"sectionEnv": "Variables de entorno",
|
||||
"sectionPlayback": "Reproducción",
|
||||
"sectionSystem": "Sistema",
|
||||
"sectionQconnect": "Qobuz Connect",
|
||||
"sectionCast": "Descubrimiento Cast",
|
||||
"castScan": "Escanear 10s",
|
||||
|
||||
@@ -1373,6 +1373,7 @@
|
||||
"sectionGraphics": "Graphiques et rendu",
|
||||
"sectionEnv": "Variables d'environnement",
|
||||
"sectionPlayback": "Lecture",
|
||||
"sectionSystem": "Système",
|
||||
"sectionQconnect": "Qobuz Connect",
|
||||
"sectionCast": "Découverte Cast",
|
||||
"castScan": "Scan 10s",
|
||||
|
||||
@@ -1373,6 +1373,7 @@
|
||||
"sectionGraphics": "Gráficos e renderização",
|
||||
"sectionEnv": "Variáveis de ambiente",
|
||||
"sectionPlayback": "Reprodução",
|
||||
"sectionSystem": "Sistema",
|
||||
"sectionQconnect": "Qobuz Connect",
|
||||
"sectionCast": "Descoberta Cast",
|
||||
"castScan": "Escanear 10s",
|
||||
|
||||
Reference in new issue
Block a user