Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bigame-engine/bigame-core/src/booster/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ impl BoosterEngine {
// nothing to put back keeps a Turbo switched on with nothing to
// change on, rather than reading off at once while its preset
// stays in force.
if !self.capabilities.falcond_installed {
if !self.capabilities.falcond_usable {
Journal::new(snapshot, plan).save()?;
}
progress(Progress::Finished);
Expand Down
21 changes: 19 additions & 2 deletions bigame-engine/bigame-core/src/booster/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ impl Plan {
// dispatches the bus's change signals, so a profile chosen later is
// still not seen. The report says so, because "restores it" reads as
// "the one before the game".
if caps.falcond_installed {
if caps.falcond_usable {
let detail = match owner {
PowerProfileOwner::Falcond { profile } => falcond_is_managing(profile),
PowerProfileOwner::Booster => Text::plain(N_(
Expand Down Expand Up @@ -483,7 +483,7 @@ impl Plan {
});
return;
};
if caps.falcond_installed {
if caps.falcond_usable {
self.skipped.push(Skipped::OwnedBy {
knob: knob.title_text(),
owner: "falcond".into(),
Expand Down Expand Up @@ -579,6 +579,7 @@ mod tests {
fn caps(performance: bool, ppd: bool) -> Capabilities {
Capabilities {
falcond_installed: false,
falcond_usable: false,
falcond_running: false,
..caps_with_falcond(performance, ppd)
}
Expand All @@ -591,6 +592,7 @@ mod tests {
mangohud: false,
mangoapp: false,
falcond_installed: true,
falcond_usable: true,
falcond_running: true,
gamemode: false,
power_profiles: ppd,
Expand Down Expand Up @@ -1008,6 +1010,21 @@ mod tests {
)));
}

#[test]
fn a_falcond_that_cannot_run_here_leaves_the_power_profile_to_booster() {
// Ivy Bridge with falcond 2.0.14 built for x86-64-v3 (a user's
// report): installed, crashed, recorded, never started again.
let h = hw(PowerSource::Ac, vec![dgpu("card1", true)]);
let s = snap(&[(Knob::PowerProfile, Some("balanced"))]);
let crashes = Capabilities {
falcond_usable: false,
falcond_running: false,
..caps_with_falcond(true, true)
};
let plan = Plan::build_with_owner(&h, &crashes, &s, &PowerProfileOwner::Booster);
assert!(plan.changes.iter().any(|c| c.knob == Knob::PowerProfile));
}

#[test]
fn on_amd_pstate_the_governor_belongs_to_the_power_profile() {
let h = amd_pstate(hw(PowerSource::Ac, vec![dgpu("card1", true)]));
Expand Down
5 changes: 5 additions & 0 deletions bigame-engine/bigame-core/src/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ pub struct Capabilities {
pub mangoapp: bool,
/// falcond binary present.
pub falcond_installed: bool,
/// falcond installed and not a build recorded as crashing on this
/// processor ([`crate::falcond_compat`]): the game backend Turbo uses.
/// Otherwise the Booster owns what falcond would.
pub falcond_usable: bool,
/// falcond currently running.
pub falcond_running: bool,
/// Feral `GameMode` present. Relevant because it and falcond contend for the
Expand Down Expand Up @@ -273,6 +277,7 @@ impl Capabilities {
mangohud: which("mangohud").is_some(),
mangoapp: which("mangoapp").is_some(),
falcond_installed: which("falcond").is_some(),
falcond_usable: which("falcond").is_some() && !crate::falcond_compat::crashes_here(),
falcond_running: falcond_running(),
gamemode: which("gamemoderun").is_some() || which("gamemoded").is_some(),
power_profiles: crate::dbus::power_profile_get().is_some(),
Expand Down
182 changes: 182 additions & 0 deletions bigame-engine/bigame-core/src/falcond_compat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//! A falcond build that crashed on this processor, remembered until it changes.
//!
//! falcond 2.0.14 built for x86-64-v3 dies with `SIGILL` on an x86-64-v2
//! processor (Sandy Bridge, Ivy Bridge: AVX, no AVX2 or BMI2), the same way
//! at every start. Started again it crash-loops to systemd's start limit, and
//! while Turbo's state is falcond's, Turbo never comes on.
//!
//! When it dies that way the helper writes [`RECORD`]: the binary's size and
//! modification time, and the processor's x86-64 level. While both still
//! match, that falcond is not started again, and Turbo works without it as it
//! does when falcond is not installed: the Booster applies the general
//! settings, the power profile included. A package update or a rebuild
//! changes the binary, the record no longer matches, and falcond is tried
//! again; reinstalling the same build does not, and it would crash the same.

use std::path::Path;

use serde::{Deserialize, Serialize};

/// Where the helper keeps the record. In its systemd `StateDirectory`,
/// world-readable so the UI reads it too.
pub const RECORD: &str = "/var/lib/bigame-mode/game-backend.incompatible.json";

/// The binary falcond's service runs (`ExecStart`).
pub const BINARY: &str = "/usr/bin/falcond";

/// Which build of a binary: what a package update changes.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Build {
/// Size in bytes.
pub size: u64,
/// Modification time, Unix seconds. pacman sets it from the package, so
/// it is the build's, not the installation's.
pub modified: u64,
}

impl Build {
/// The build at `path`, if it can be read.
#[must_use]
pub fn of(path: &Path) -> Option<Self> {
let meta = std::fs::metadata(path).ok()?;
let modified = meta
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs();
Some(Self {
size: meta.len(),
modified,
})
}
}

/// A falcond build that died on an illegal instruction on this processor.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Incompatible {
/// The build that crashed.
pub binary: Build,
/// The processor's x86-64 level then. The same disk in a newer machine
/// is not held to an older machine's crash.
pub cpu_level: Option<u8>,
/// Unix time it was recorded.
pub recorded_at: u64,
}

/// Remember that the build at `binary` crashed on this processor.
///
/// # Errors
/// Returns an error if the binary cannot be read or the record cannot be
/// written.
pub fn record_at(record: &Path, binary: &Path, cpu_level: Option<u8>) -> anyhow::Result<()> {
let build =
Build::of(binary).ok_or_else(|| anyhow::anyhow!("{} cannot be read", binary.display()))?;
let entry = Incompatible {
binary: build,
cpu_level,
recorded_at: crate::unix_now(),
};
std::fs::write(record, serde_json::to_string_pretty(&entry)?)?;
Ok(())
}

/// [`record_at`] for falcond on this machine.
///
/// # Errors
/// As [`record_at`].
pub fn record() -> anyhow::Result<()> {
record_at(
Path::new(RECORD),
Path::new(BINARY),
crate::isa::CpuIsa::detect().level(),
)
}

/// The record, when it still describes the build at `binary` on a processor
/// of `cpu_level`.
#[must_use]
pub fn matching_at(record: &Path, binary: &Path, cpu_level: Option<u8>) -> Option<Incompatible> {
let text = std::fs::read_to_string(record).ok()?;
let entry: Incompatible = serde_json::from_str(&text).ok()?;
(Build::of(binary).as_ref() == Some(&entry.binary) && entry.cpu_level == cpu_level)
.then_some(entry)
}

/// Whether the installed falcond is one that crashed on this processor.
#[must_use]
pub fn crashes_here() -> bool {
matching_at(
Path::new(RECORD),
Path::new(BINARY),
crate::isa::CpuIsa::detect().level(),
)
.is_some()
}

/// Forget the record: falcond ran.
pub fn forget() {
match std::fs::remove_file(RECORD) {
Ok(()) => tracing::info!("falcond runs on this processor; its crash record is gone"),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => tracing::warn!(error = %e, "falcond's crash record could not be removed"),
}
}

#[cfg(test)]
mod tests {
use super::*;

fn set_modified(path: &Path, secs: u64) {
let f = std::fs::File::options().write(true).open(path).unwrap();
f.set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs))
.unwrap();
}

#[test]
fn the_build_that_crashed_is_recognised_until_it_changes() {
let dir = tempfile::tempdir().unwrap();
let record = dir.path().join("incompatible.json");
let binary = dir.path().join("falcond");
std::fs::write(&binary, b"x86-64-v3 build").unwrap();
set_modified(&binary, 1_759_000_000);

assert!(
matching_at(&record, &binary, Some(2)).is_none(),
"no record yet"
);
record_at(&record, &binary, Some(2)).unwrap();
assert!(matching_at(&record, &binary, Some(2)).is_some());

// Reinstalling the same package puts back the same build.
std::fs::write(&binary, b"x86-64-v3 build").unwrap();
set_modified(&binary, 1_759_000_000);
assert!(matching_at(&record, &binary, Some(2)).is_some());

// An update: another build, tried again.
std::fs::write(&binary, b"baseline build!").unwrap();
set_modified(&binary, 1_759_900_000);
assert!(matching_at(&record, &binary, Some(2)).is_none());
}

#[test]
fn another_processor_is_not_held_to_the_crash() {
let dir = tempfile::tempdir().unwrap();
let record = dir.path().join("incompatible.json");
let binary = dir.path().join("falcond");
std::fs::write(&binary, b"x86-64-v3 build").unwrap();
record_at(&record, &binary, Some(2)).unwrap();
assert!(matching_at(&record, &binary, Some(3)).is_none());
}

#[test]
fn a_missing_binary_or_a_damaged_record_matches_nothing() {
let dir = tempfile::tempdir().unwrap();
let record = dir.path().join("incompatible.json");
let binary = dir.path().join("falcond");
assert!(record_at(&record, &binary, Some(2)).is_err());
std::fs::write(&binary, b"build").unwrap();
std::fs::write(&record, b"{ not json").unwrap();
assert!(matching_at(&record, &binary, Some(2)).is_none());
}
}
14 changes: 14 additions & 0 deletions bigame-engine/bigame-core/src/health.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,20 @@ pub fn collect() -> Vec<Check> {
let version = package_version(db, "falcond");
let v = version.as_deref().unwrap_or("?");
out.push(match (&backend, caps.falcond_installed) {
// Recorded as crashing here: not started again, so systemd no longer
// says why after a reboot; the record does.
(_, true) if !caps.falcond_usable => {
let why = crate::turbo::explain_backend_failure(
&crate::systemd::Failure::IllegalInstruction,
&crate::isa::CpuIsa::detect(),
);
Check {
title: Text::plain(N_("falcond")),
status: Status::Warning,
detail: Text::with(N_("%s · %s"), [Arg::Raw(v.to_owned()), Arg::Text(why.summary)]),
fix: Some(Fix::Advice(why.advice)),
}
}
(_, false) => check(
N_("falcond"),
Status::Warning,
Expand Down
1 change: 1 addition & 0 deletions bigame-engine/bigame-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod dbus_client;
pub mod diagnostics;
pub mod dns_config;
pub mod error;
pub mod falcond_compat;
pub mod fg;
pub mod game_launch;
pub mod game_settings;
Expand Down
12 changes: 12 additions & 0 deletions bigame-engine/bigame-core/src/overview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,18 @@ struct TurboReading {
/// (and why), and whether systemd could not be asked at all. Without
/// falcond, Turbo's own state (Booster only), as Home shows it.
fn read_turbo() -> TurboReading {
// A build that crashed on this processor is not started again: Turbo is
// the Booster's, and falcond stays a failure to explain, after a reboot
// too, when systemd no longer says why.
if crate::falcond_compat::crashes_here() {
let state = crate::turbo::state_blocking();
return TurboReading {
on: state.as_ref().is_ok_and(|s| *s == crate::turbo::State::On),
failed: true,
failure: Some(crate::systemd::Failure::IllegalInstruction),
unreadable: state.is_err(),
};
}
let unit = crate::systemd::Reader::shared()
.and_then(|r| r.unit_state(crate::turbo::BACKEND_UNIT))
.filter(crate::systemd::UnitState::is_installed);
Expand Down
33 changes: 33 additions & 0 deletions bigame-engine/bigame-core/src/running.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ const INFRASTRUCTURE: &[&str] = &[
"python",
"sh",
"bash",
// pressure-vessel rebuilds the container's library cache with it before
// the game starts; Debian-based runtimes name the binary ldconfig.real.
"ldconfig",
"ldconfig.real",
// Wine infrastructure
"wineserver",
"wine",
Expand Down Expand Up @@ -2013,6 +2017,35 @@ mod tests {
assert!(!is_infrastructure("supertuxkart"));
}

#[test]
fn the_containers_ldconfig_is_not_the_game() {
// BLOODSTRIKE on SteamLinuxRuntime_4 (a user's log): pressure-vessel
// rebuilds the container's library cache before Proton starts, and
// its ldconfig, waiting on a busy lock, was announced as the game.
let tree = vec![
p(
1,
0,
"/h/.local/share/Steam/ubuntu12_32/reaper|SteamLaunch AppId=3199170 --",
1,
),
p(
2,
1,
"/s/steamapps/common/SteamLinuxRuntime_4/pressure-vessel/bin/pressure-vessel-wrap|--",
5,
),
p(
3,
2,
"/sbin/ldconfig|-X -C /run/pressure-vessel/ldso/ld.so.cache",
80,
),
p(4, 2, "/usr/sbin/ldconfig.real|-p", 20),
];
assert!(identify(&tree).is_empty(), "{:?}", identify(&tree));
}

#[test]
fn a_process_without_a_command_line_is_not_the_game() {
// A process on its way out has an empty cmdline. Left in the pool it
Expand Down
Loading