Merge steamos-workerd in

This commit is contained in:
Vicki Pfau 2024-03-22 19:21:03 -07:00
parent 6f1f1c032c
commit 83e9de9bcb
10 changed files with 1445 additions and 139 deletions

601
src/ds_inhibit.rs Normal file
View file

@ -0,0 +1,601 @@
/* SPDX-License-Identifier: BSD-2-Clause */
use anyhow::{Error, Result};
use inotify::{Event, EventMask, EventStream, Inotify, WatchDescriptor, WatchMask};
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::thread::sleep;
use std::time::Duration;
use tokio::fs;
use tokio_stream::StreamExt;
use tracing::{debug, error, info, warn};
use crate::{sysbase, Service};
struct HidNode {
id: u32,
}
pub struct Inhibitor {
inotify: EventStream<[u8; 512]>,
dev_watch: WatchDescriptor,
watches: HashMap<WatchDescriptor, HidNode>,
}
impl HidNode {
fn new(id: u32) -> HidNode {
HidNode { id }
}
fn sys_base(&self) -> PathBuf {
PathBuf::from(format!("{}/sys/class/hidraw/hidraw{}/device", sysbase(), self.id).as_str())
}
fn hidraw(&self) -> PathBuf {
PathBuf::from(format!("{}/dev/hidraw{}", sysbase(), self.id).as_str())
}
async fn get_nodes(&self) -> Result<Vec<PathBuf>> {
let mut entries = Vec::new();
let mut dir = fs::read_dir(self.sys_base().join("input")).await?;
while let Some(entry) = dir.next_entry().await? {
let path = entry.path();
let mut dir = fs::read_dir(&path).await?;
while let Some(entry) = dir.next_entry().await? {
if entry
.path()
.file_name()
.map(|e| e.to_string_lossy())
.is_some_and(|e| e.starts_with("mouse"))
{
debug!("Found {}", path.display());
entries.push(path.join("inhibited"));
}
}
}
Ok(entries)
}
async fn can_inhibit(&self) -> bool {
debug!("Checking if hidraw{} can be inhibited", self.id);
let driver = match fs::read_link(self.sys_base().join("driver")).await {
Ok(driver) => driver,
Err(e) => {
warn!(
"Failed to find associated driver for hidraw{}: {}",
self.id, e
);
return false;
}
};
if !matches!(
driver.file_name().and_then(|d| d.to_str()),
Some("sony") | Some("playstation")
) {
debug!("Not a PlayStation controller");
return false;
}
let nodes = match self.get_nodes().await {
Ok(nodes) => nodes,
Err(e) => {
warn!("Failed to list inputs for hidraw{}: {e}", self.id);
return false;
}
};
if nodes.is_empty() {
debug!("No nodes to inhibit");
return false;
}
true
}
async fn check(&self) -> Result<()> {
let hidraw = self.hidraw();
let mut dir = fs::read_dir(sysbase() + "/proc").await?;
while let Some(entry) = dir.next_entry().await? {
let path = entry.path();
let proc = match path.file_name().map(|p| p.to_str()) {
Some(Some(p)) => p,
_ => continue,
};
let _: u32 = match proc.parse() {
Ok(i) => i,
_ => continue,
};
let mut fds = match fs::read_dir(path.join("fd")).await {
Ok(fds) => fds,
Err(e) => {
debug!("Process {proc} disappeared while scanning: {e}");
continue;
}
};
while let Ok(Some(f)) = fds.next_entry().await {
let path = match fs::read_link(f.path()).await {
Ok(p) => p,
Err(e) => {
debug!("Process {proc} disappeared while scanning: {e}");
continue;
}
};
if path == hidraw {
let comm = match fs::read(format!("{}/proc/{proc}/comm", sysbase())).await {
Ok(c) => c,
Err(e) => {
debug!("Process {proc} disappeared while scanning: {e}");
continue;
}
};
if String::from_utf8_lossy(comm.as_ref()) == "steam\n" {
info!("Inhibiting hidraw{}", self.id);
self.inhibit().await?;
return Ok(());
}
}
}
}
info!("Uninhibiting hidraw{}", self.id);
self.uninhibit().await?;
Ok(())
}
async fn inhibit(&self) -> Result<()> {
let mut res = Ok(());
for node in self.get_nodes().await?.into_iter() {
if let Err(err) = fs::write(node, "1\n").await {
error!("Encountered error inhibiting: {err}");
res = Err(err.into());
}
}
res
}
async fn uninhibit(&self) -> Result<()> {
let mut res = Ok(());
for node in self.get_nodes().await?.into_iter() {
if let Err(err) = fs::write(node, "0\n").await {
error!("Encountered error inhibiting: {err}");
res = Err(err.into());
}
}
res
}
}
impl Inhibitor {
pub fn new() -> Result<Inhibitor> {
let inotify = Inotify::init()?.into_event_stream([0; 512])?;
let dev_watch = inotify
.watches()
.add(sysbase() + "/dev", WatchMask::CREATE)?;
Ok(Inhibitor {
inotify,
dev_watch,
watches: HashMap::new(),
})
}
pub async fn init() -> Result<Inhibitor> {
let mut inhibitor = match Inhibitor::new() {
Ok(i) => i,
Err(e) => {
error!("Could not create inotify watches: {e}");
return Err(e);
}
};
let mut dir = fs::read_dir(sysbase() + "/dev").await?;
while let Some(entry) = dir.next_entry().await? {
if let Err(e) = inhibitor.watch(entry.path().as_path()).await {
error!("Encountered error attempting to watch: {e}");
}
}
Ok(inhibitor)
}
async fn watch(&mut self, path: &Path) -> Result<bool> {
let metadata = path.metadata()?;
if metadata.is_dir() {
return Ok(false);
}
let id = match path
.file_name()
.and_then(|f| f.to_str())
.and_then(|s| s.strip_prefix("hidraw"))
.and_then(|s| s.parse().ok())
{
Some(id) => id,
None => return Ok(false),
};
let node = HidNode::new(id);
if !node.can_inhibit().await {
return Ok(false);
}
info!("Adding {} to watchlist", path.display());
let watch = self.inotify.watches().add(
&node.hidraw(),
WatchMask::DELETE_SELF
| WatchMask::OPEN
| WatchMask::CLOSE_NOWRITE
| WatchMask::CLOSE_WRITE,
)?;
if let Err(e) = node.check().await {
error!(
"Encountered error attempting to check if hidraw{} can be inhibited: {e}",
node.id
);
}
self.watches.insert(watch, node);
Ok(true)
}
async fn process_event(&mut self, event: Event<OsString>) -> Result<()> {
const QSEC: Duration = Duration::from_millis(250);
debug!("Got event: {:08x}", event.mask);
if event.wd == self.dev_watch {
let path = match event.name {
Some(fname) => PathBuf::from(fname),
None => {
error!("Got an event without an associated filename!");
return Err(Error::msg("Got an event without an associated filename"));
}
};
debug!("New device {} found", path.display());
let path = PathBuf::from(sysbase() + "/dev").join(path);
sleep(QSEC); // Wait a quarter second for nodes to enumerate
if let Err(e) = self.watch(path.as_path()).await {
error!("Encountered error attempting to watch: {e}");
return Err(e);
}
} else if event.mask == EventMask::DELETE_SELF {
debug!("Device removed");
self.watches.remove(&event.wd);
let _ = self.inotify.watches().remove(event.wd);
} else if let Some(node) = self.watches.get(&event.wd) {
node.check().await?;
} else if event.mask != EventMask::IGNORED {
error!("Unhandled event: {:08x}", event.mask);
}
Ok(())
}
}
impl Service for Inhibitor {
const NAME: &'static str = "ds-inhibitor";
async fn run(&mut self) -> Result<()> {
loop {
let res = match self.inotify.next().await {
Some(Ok(event)) => self.process_event(event).await,
Some(Err(e)) => return Err(e.into()),
None => return Ok(()),
};
if let Err(e) = res {
warn!("Got error processing event: {e}");
}
}
}
async fn shutdown(&mut self) -> Result<()> {
let mut res = Ok(());
for (wd, node) in self.watches.drain() {
if let Err(e) = self.inotify.watches().remove(wd) {
warn!("Error removing watch while shutting down: {e}");
res = Err(e.into());
}
if let Err(e) = node.uninhibit().await {
warn!("Error uninhibiting {} while shutting down: {e}", node.id);
res = Err(e);
}
}
res
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::testing;
use std::fs::{create_dir_all, read_to_string, remove_file, write, File};
use std::os::unix::fs::symlink;
use tokio::time::sleep;
async fn nyield(times: u32) {
for i in 0..times {
sleep(Duration::from_millis(1)).await;
}
}
#[tokio::test]
async fn hid_nodes() {
let h = testing::start();
let path = h.test.path();
let hid = HidNode::new(0);
let sys_base = hid.sys_base();
create_dir_all(sys_base.join("input/input0/foo0")).expect("foo0");
create_dir_all(sys_base.join("input/input1/bar0")).expect("bar0");
create_dir_all(sys_base.join("input/input2/mouse0")).expect("mouse0");
assert_eq!(
hid.get_nodes().await.expect("get_nodes"),
&[sys_base.join("input/input2/inhibited")]
);
}
#[tokio::test]
async fn hid_can_inhibit() {
let h = testing::start();
let path = h.test.path();
let hids = [
HidNode::new(0),
HidNode::new(1),
HidNode::new(2),
HidNode::new(3),
HidNode::new(4),
HidNode::new(5),
HidNode::new(6),
];
create_dir_all(hids[0].sys_base().join("input/input0/foo0")).expect("foo0");
symlink("foo", hids[0].sys_base().join("driver")).expect("hidraw0");
create_dir_all(hids[1].sys_base().join("input/input1/mouse0")).expect("mouse0");
symlink("foo", hids[1].sys_base().join("driver")).expect("hidraw1");
create_dir_all(hids[2].sys_base().join("input/input2/foo1")).expect("foo1");
symlink("sony", hids[2].sys_base().join("driver")).expect("hidraw2");
create_dir_all(hids[3].sys_base().join("input/input3/mouse1")).expect("mouse1");
symlink("sony", hids[3].sys_base().join("driver")).expect("hidraw3");
create_dir_all(hids[4].sys_base().join("input/input4/foo2")).expect("foo2");
symlink("playstation", hids[4].sys_base().join("driver")).expect("hidraw4");
create_dir_all(hids[5].sys_base().join("input/input5/mouse2")).expect("mouse2");
symlink("playstation", hids[5].sys_base().join("driver")).expect("hidraw5");
create_dir_all(hids[6].sys_base().join("input/input6/mouse3")).expect("mouse3");
assert!(!hids[0].can_inhibit().await);
assert!(!hids[1].can_inhibit().await);
assert!(!hids[2].can_inhibit().await);
assert!(hids[3].can_inhibit().await);
assert!(!hids[4].can_inhibit().await);
assert!(hids[5].can_inhibit().await);
assert!(!hids[6].can_inhibit().await);
}
#[tokio::test]
async fn hid_inhibit() {
let h = testing::start();
let path = h.test.path();
let hid = HidNode::new(0);
let sys_base = hid.sys_base();
create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0");
symlink("sony", sys_base.join("driver")).expect("hidraw0");
assert!(hid.can_inhibit().await);
hid.inhibit().await;
assert_eq!(
read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"),
"1\n"
);
hid.uninhibit().await;
assert_eq!(
read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"),
"0\n"
);
}
#[tokio::test]
async fn hid_inhibit_error_continue() {
let h = testing::start();
let path = h.test.path();
let hid = HidNode::new(0);
let sys_base = hid.sys_base();
create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0");
create_dir_all(sys_base.join("input/input0/inhibited")).expect("inhibited");
create_dir_all(sys_base.join("input/input1/mouse1")).expect("mouse0");
symlink("sony", sys_base.join("driver")).expect("hidraw0");
assert!(hid.can_inhibit().await);
hid.inhibit().await;
assert_eq!(
read_to_string(sys_base.join("input/input1/inhibited")).expect("inhibited"),
"1\n"
);
hid.uninhibit().await;
assert_eq!(
read_to_string(sys_base.join("input/input1/inhibited")).expect("inhibited"),
"0\n"
);
}
#[tokio::test]
async fn hid_check() {
let h = testing::start();
let path = h.test.path();
let hid = HidNode::new(0);
let sys_base = hid.sys_base();
create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0");
symlink("sony", sys_base.join("driver")).expect("hidraw0");
create_dir_all(path.join("proc/1/fd")).expect("fd");
symlink(hid.hidraw(), path.join("proc/1/fd/3")).expect("symlink");
write(path.join("proc/1/comm"), "steam\n").expect("comm");
hid.check().await.expect("check");
assert_eq!(
read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"),
"1\n"
);
write(path.join("proc/1/comm"), "epic\n").expect("comm");
hid.check().await.expect("check");
assert_eq!(
read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"),
"0\n"
);
remove_file(path.join("proc/1/fd/3")).expect("rm");
write(path.join("proc/1/comm"), "steam\n").expect("comm");
hid.check().await.expect("check");
assert_eq!(
read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"),
"0\n"
);
}
#[tokio::test]
async fn inhibitor_start() {
let h = testing::start();
let path = h.test.path();
let hid = HidNode::new(0);
let sys_base = hid.sys_base();
create_dir_all(path.join("dev")).expect("dev");
create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0");
write(hid.hidraw(), "").expect("hidraw");
symlink("sony", sys_base.join("driver")).expect("driver");
create_dir_all(path.join("proc/1/fd")).expect("fd");
symlink(hid.hidraw(), path.join("proc/1/fd/3")).expect("symlink");
write(path.join("proc/1/comm"), "steam\n").expect("comm");
let mut inhibitor = Inhibitor::init().await.expect("init");
assert_eq!(
read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"),
"1\n"
);
inhibitor.shutdown().await.expect("stop");
assert_eq!(
read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"),
"0\n"
);
}
#[tokio::test]
async fn inhibitor_open_close() {
let h = testing::start();
let path = h.test.path();
let hid = HidNode::new(0);
let sys_base = hid.sys_base();
create_dir_all(path.join("dev")).expect("dev");
create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0");
File::create(hid.hidraw()).expect("hidraw");
symlink("sony", sys_base.join("driver")).expect("driver");
create_dir_all(path.join("proc/1/fd")).expect("fd");
write(path.join("proc/1/comm"), "steam\n").expect("comm");
let mut inhibitor = Inhibitor::init().await.expect("init");
let task = tokio::spawn(async move {
inhibitor.run().await;
});
nyield(1).await;
assert_eq!(
read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"),
"0\n"
);
symlink(hid.hidraw(), path.join("proc/1/fd/3")).expect("symlink");
let f = File::open(hid.hidraw()).expect("hidraw");
nyield(2).await;
assert_eq!(
read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"),
"1\n"
);
drop(f);
remove_file(path.join("proc/1/fd/3")).expect("rm");
nyield(1).await;
assert_eq!(
read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"),
"0\n"
);
task.abort();
}
#[tokio::test]
async fn inhibitor_fast_create() {
let h = testing::start();
let path = h.test.path();
let hid = HidNode::new(0);
let sys_base = hid.sys_base();
create_dir_all(path.join("dev")).expect("dev");
create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0");
symlink("sony", sys_base.join("driver")).expect("driver");
create_dir_all(path.join("proc/1/fd")).expect("fd");
write(path.join("proc/1/comm"), "steam\n").expect("comm");
let mut inhibitor = Inhibitor::init().await.expect("init");
let task = tokio::spawn(async move {
inhibitor.run().await;
});
nyield(1).await;
assert!(read_to_string(sys_base.join("input/input0/inhibited")).is_err());
File::create(hid.hidraw()).expect("hidraw");
symlink(hid.hidraw(), path.join("proc/1/fd/3")).expect("symlink");
let f = File::open(hid.hidraw()).expect("hidraw");
nyield(4).await;
assert_eq!(
read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"),
"1\n"
);
task.abort();
}
#[tokio::test]
async fn inhibitor_create() {
let h = testing::start();
let path = h.test.path();
let hid = HidNode::new(0);
let sys_base = hid.sys_base();
create_dir_all(path.join("dev")).expect("dev");
create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0");
symlink("sony", sys_base.join("driver")).expect("driver");
create_dir_all(path.join("proc/1/fd")).expect("fd");
write(path.join("proc/1/comm"), "steam\n").expect("comm");
let mut inhibitor = Inhibitor::init().await.expect("init");
let task = tokio::spawn(async move {
inhibitor.run().await;
});
nyield(3).await;
assert!(read_to_string(sys_base.join("input/input0/inhibited")).is_err());
File::create(hid.hidraw()).expect("hidraw");
nyield(3).await;
symlink(hid.hidraw(), path.join("proc/1/fd/3")).expect("symlink");
let f = File::open(hid.hidraw()).expect("hidraw");
nyield(3).await;
assert_eq!(
read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"),
"1\n"
);
task.abort();
}
}

View file

@ -24,32 +24,244 @@
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
use anyhow::{Error, Result};
use anyhow::{bail, Error, Result};
use tokio::signal::unix::{signal, SignalKind};
use tracing_subscriber;
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use tracing::{error, info, warn};
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, Registry};
use zbus::connection::Connection;
use zbus::ConnectionBuilder;
use crate::ds_inhibit::Inhibitor;
use crate::sls::ftrace::Ftrace;
use crate::sls::{LogLayer, LogReceiver};
mod ds_inhibit;
mod manager;
mod sls;
#[cfg(test)]
mod testing;
trait Service
where
Self: Sized,
{
const NAME: &'static str;
async fn run(&mut self) -> Result<()>;
async fn shutdown(&mut self) -> Result<()> {
Ok(())
}
async fn start(mut self, token: CancellationToken) -> Result<()> {
info!("Starting {}", Self::NAME);
let res = tokio::select! {
r = self.run() => r,
_ = token.cancelled() => Ok(()),
};
if res.is_err() {
warn!(
"{} encountered an error: {}",
Self::NAME,
res.as_ref().unwrap_err()
);
token.cancel();
}
info!("Shutting down {}", Self::NAME);
self.shutdown().await.and(res)
}
}
#[cfg(not(test))]
pub fn sysbase() -> String {
String::new()
}
#[cfg(test)]
pub fn sysbase() -> String {
let current_test = crate::testing::current();
let path = current_test.path();
String::from(path.as_os_str().to_str().unwrap())
}
pub fn read_comm(pid: u32) -> Result<String> {
let comm = std::fs::read_to_string(format!("{}/proc/{}/comm", sysbase(), pid))?;
Ok(comm.trim_end().to_string())
}
pub fn get_appid(pid: u32) -> Result<Option<u64>> {
let environ = std::fs::read_to_string(format!("{}/proc/{}/environ", sysbase(), pid))?;
for env_var in environ.split('\0') {
let (key, value) = match env_var.split_once('=') {
Some((k, v)) => (k, v),
None => continue,
};
if key != "SteamGameId" {
continue;
}
match value.parse() {
Ok(appid) => return Ok(Some(appid)),
Err(_) => break,
};
}
let stat = std::fs::read_to_string(format!("{}/proc/{}/stat", sysbase(), pid))?;
let stat = match stat.rsplit_once(") ") {
Some((_, v)) => v,
None => return Ok(None),
};
let ppid = match stat.split(' ').nth(1) {
Some(ppid) => ppid,
None => return Err(anyhow::Error::msg("stat data invalid")),
};
let ppid: u32 = ppid.parse()?;
if ppid > 1 {
get_appid(ppid)
} else {
Ok(None)
}
}
async fn reload() -> Result<()> {
loop {
let mut sighup = signal(SignalKind::hangup())?;
sighup.recv().await.ok_or(Error::msg(""))?;
}
}
async fn create_connection() -> Result<Connection> {
let manager = manager::SMManager::new()?;
ConnectionBuilder::system()?
.name("com.steampowered.SteamOSManager1")?
.serve_at("/com/steampowered/SteamOSManager1", manager)?
.build()
.await
.map_err(|e| e.into())
}
#[tokio::main]
async fn main() -> Result<()> {
// This daemon is responsible for creating a dbus api that steam client can use to do various OS
// level things. It implements com.steampowered.SteamOSManager1 interface
tracing_subscriber::fmt::init();
let stdout_log = fmt::layer();
let subscriber = Registry::default().with(stdout_log);
let connection = match create_connection().await {
Ok(c) => c,
Err(e) => {
let _guard = tracing::subscriber::set_default(subscriber);
error!("Error connecting to DBus: {}", e);
bail!(e);
}
};
let mut services = JoinSet::new();
let token = CancellationToken::new();
let mut log_receiver = LogReceiver::new(connection.clone()).await?;
let remote_logger = LogLayer::new(&log_receiver).await;
let subscriber = subscriber.with(remote_logger);
let _guard = tracing::subscriber::set_global_default(subscriber)?;
let mut sigterm = signal(SignalKind::terminate())?;
let mut sigquit = signal(SignalKind::quit())?;
let manager = manager::SMManager::new()?;
let ftrace = Ftrace::init(connection.clone()).await?;
services.spawn(ftrace.start(token.clone()));
let _system_connection = ConnectionBuilder::system()?
.name("com.steampowered.SteamOSManager1")?
.serve_at("/com/steampowered/SteamOSManager1", manager)?
.build()
.await?;
let inhibitor = Inhibitor::init().await?;
services.spawn(inhibitor.start(token.clone()));
tokio::select! {
e = sigterm.recv() => e.ok_or(Error::msg("SIGTERM pipe broke")),
e = tokio::signal::ctrl_c() => Ok(e?),
let mut res = tokio::select! {
e = log_receiver.run() => e,
e = services.join_next() => match e.unwrap() {
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => Err(e),
Err(e) => Err(e.into())
},
_ = tokio::signal::ctrl_c() => Ok(()),
e = sigterm.recv() => e.ok_or(Error::msg("SIGTERM machine broke")),
_ = sigquit.recv() => Err(Error::msg("Got SIGQUIT")),
e = reload() => e,
}
.inspect_err(|e| error!("Encountered error running: {e}"));
token.cancel();
info!("Shutting down");
while let Some(service_res) = services.join_next().await {
res = match service_res {
Ok(Err(e)) => Err(e),
Err(e) => Err(e.into()),
_ => continue,
};
}
res.inspect_err(|e| error!("Encountered error: {e}"))
}
#[cfg(test)]
mod test {
use crate::testing;
use std::fs;
#[test]
fn read_comm() {
let h = testing::start();
let path = h.test.path();
fs::create_dir_all(path.join("proc/123456")).expect("create_dir_all");
fs::write(path.join("proc/123456/comm"), "test\n").expect("write comm");
assert_eq!(crate::read_comm(123456).expect("read_comm"), "test");
assert!(crate::read_comm(123457).is_err());
}
#[test]
fn appid_environ() {
let h = testing::start();
let path = h.test.path();
fs::create_dir_all(path.join("proc/123456")).expect("create_dir_all");
fs::write(
path.join("proc/123456/environ"),
"A=B\0SteamGameId=98765\0C=D",
)
.expect("write environ");
assert_eq!(crate::get_appid(123456).expect("get_appid"), Some(98765));
assert!(crate::get_appid(123457).is_err());
}
#[test]
fn appid_parent_environ() {
let h = testing::start();
let path = h.test.path();
fs::create_dir_all(path.join("proc/123456")).expect("create_dir_all");
fs::write(
path.join("proc/123456/environ"),
"A=B\0SteamGameId=98765\0C=D",
)
.expect("write environ");
fs::create_dir_all(path.join("proc/123457")).expect("create_dir_all");
fs::write(path.join("proc/123457/environ"), "A=B\0C=D").expect("write environ");
fs::write(path.join("proc/123457/stat"), "0 (comm) S 123456 ...").expect("write stat");
assert_eq!(crate::get_appid(123457).expect("get_appid"), Some(98765));
}
#[test]
fn appid_missing() {
let h = testing::start();
let path = h.test.path();
fs::create_dir_all(path.join("proc/123457")).expect("create_dir_all");
fs::write(path.join("proc/123457/environ"), "A=B\0C=D").expect("write environ");
fs::write(path.join("proc/123457/stat"), "0 (comm) S 1 ...").expect("write stat");
assert_eq!(crate::get_appid(123457).expect("get_appid"), None);
}
}

286
src/sls/ftrace.rs Normal file
View file

@ -0,0 +1,286 @@
/* SPDX-License-Identifier: BSD-2-Clause */
use anyhow::{Error, Result};
use std::collections::HashMap;
use std::fmt::Debug;
use std::path::Path;
use tokio::fs;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::net::unix::pipe;
use tracing::{error, info};
use zbus::connection::Connection;
use zbus::zvariant;
use crate::{get_appid, read_comm, sysbase, Service};
#[zbus::proxy(
interface = "com.steampowered.SteamOSLogSubmitter.Trace",
default_service = "com.steampowered.SteamOSLogSubmitter",
default_path = "/com/steampowered/SteamOSLogSubmitter/helpers/Trace"
)]
trait TraceHelper {
async fn log_event(
&self,
trace: &str,
data: HashMap<&str, zvariant::Value<'_>>,
) -> zbus::Result<()>;
}
pub struct Ftrace
where
Self: 'static,
{
pipe: Option<BufReader<pipe::Receiver>>,
proxy: TraceHelperProxy<'static>,
}
async fn setup_traces(path: &Path) -> Result<()> {
fs::write(path.join("events/oom/mark_victim/enable"), "1").await?;
fs::write(path.join("set_ftrace_filter"), "split_lock_warn").await?;
fs::write(path.join("current_tracer"), "function").await?;
Ok(())
}
impl Ftrace {
pub async fn init(connection: Connection) -> Result<Ftrace> {
let base = Self::base();
let path = Path::new(base.as_str());
fs::create_dir_all(path).await?;
setup_traces(path).await?;
let file = pipe::OpenOptions::new()
.unchecked(true) // Thanks tracefs for making trace_pipe a "regular" file
.open_receiver(path.join("trace_pipe"))?;
Ok(Ftrace {
pipe: Some(BufReader::new(file)),
proxy: TraceHelperProxy::new(&connection).await?,
})
}
fn base() -> String {
sysbase() + "/sys/kernel/tracing/instances/steamos-log-submitter"
}
async fn handle_pid(data: &mut HashMap<&str, zvariant::Value<'_>>, pid: u32) -> Result<()> {
if let Ok(comm) = read_comm(pid) {
info!("├─ comm: {}", comm);
data.insert("comm", zvariant::Value::new(comm));
} else {
info!("├─ comm not found");
}
if let Ok(Some(appid)) = get_appid(pid) {
info!("└─ appid: {}", appid);
data.insert("appid", zvariant::Value::new(appid));
} else {
info!("└─ appid not found");
}
Ok(())
}
async fn handle_event(&mut self, line: &str) -> Result<()> {
info!("Forwarding line {}", line);
let mut data = HashMap::new();
let mut split = line.rsplit(' ');
if let Some(("pid", pid)) = split.next().and_then(|arg| arg.split_once('=')) {
let pid = pid.parse()?;
Ftrace::handle_pid(&mut data, pid).await?;
}
self.proxy.log_event(line, data).await?;
Ok(())
}
}
impl Service for Ftrace {
const NAME: &'static str = "ftrace";
async fn run(&mut self) -> Result<()> {
loop {
let mut string = String::new();
self.pipe
.as_mut()
.ok_or(Error::msg("BUG: trace_pipe missing"))?
.read_line(&mut string)
.await?;
if let Err(e) = self.handle_event(string.trim_end()).await {
error!("Encountered an error handling event: {}", e);
}
}
}
async fn shutdown(&mut self) -> Result<()> {
self.pipe.take();
fs::remove_dir(Self::base()).await?;
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::testing;
use nix::sys::stat::Mode;
use nix::unistd;
use std::cell::Cell;
use std::fs;
use std::path::PathBuf;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
struct MockTrace {
traces: UnboundedSender<(String, HashMap<String, zvariant::OwnedValue>)>,
}
#[zbus::interface(name = "com.steampowered.SteamOSLogSubmitter.Trace")]
impl MockTrace {
fn log_event(
&mut self,
trace: &str,
data: HashMap<&str, zvariant::Value<'_>>,
) -> zbus::fdo::Result<()> {
self.traces.send((
String::from(trace),
HashMap::from_iter(
data.iter()
.map(|(k, v)| (String::from(*k), v.try_to_owned().unwrap())),
),
));
Ok(())
}
}
#[tokio::test]
async fn handle_pid() {
let h = testing::start();
let path = h.test.path();
fs::create_dir_all(path.join("proc/1234")).expect("create_dir_all");
fs::write(path.join("proc/1234/comm"), "ftrace\n").expect("write comm");
fs::write(path.join("proc/1234/environ"), "SteamGameId=5678").expect("write environ");
fs::create_dir_all(path.join("proc/1235")).expect("create_dir_all");
fs::write(path.join("proc/1235/comm"), "ftrace\n").expect("write comm");
fs::create_dir_all(path.join("proc/1236")).expect("create_dir_all");
fs::write(path.join("proc/1236/environ"), "SteamGameId=5678").expect("write environ");
let mut map = HashMap::new();
assert!(Ftrace::handle_pid(&mut map, 1234).await.is_ok());
assert_eq!(
*map.get("comm").expect("comm"),
zvariant::Value::new("ftrace")
);
assert_eq!(
*map.get("appid").expect("appid"),
zvariant::Value::new(5678 as u64)
);
let mut map = HashMap::new();
assert!(Ftrace::handle_pid(&mut map, 1235).await.is_ok());
assert_eq!(
*map.get("comm").expect("comm"),
zvariant::Value::new("ftrace")
);
assert!(map.get("appid").is_none());
let mut map = HashMap::new();
assert!(Ftrace::handle_pid(&mut map, 1236).await.is_ok());
assert!(map.get("comm").is_none());
assert_eq!(
*map.get("appid").expect("appid"),
zvariant::Value::new(5678 as u64)
);
}
#[tokio::test]
async fn ftrace_init() {
let h = testing::start();
let path = h.test.path();
let tracefs = PathBuf::from(Ftrace::base());
fs::create_dir_all(tracefs.join("events/oom/mark_victim")).expect("create_dir_all");
unistd::mkfifo(
tracefs.join("trace_pipe").as_path(),
Mode::S_IRUSR | Mode::S_IWUSR,
)
.expect("trace_pipe");
let dbus = Connection::session().await.expect("dbus");
let ftrace = Ftrace::init(dbus).await.expect("ftrace");
assert_eq!(
fs::read_to_string(tracefs.join("events/oom/mark_victim/enable")).unwrap(),
"1"
);
}
#[tokio::test]
async fn ftrace_relay() {
let h = testing::start();
let path = h.test.path();
let tracefs = PathBuf::from(Ftrace::base());
fs::create_dir_all(tracefs.join("events/oom/mark_victim")).expect("create_dir_all");
unistd::mkfifo(
tracefs.join("trace_pipe").as_path(),
Mode::S_IRUSR | Mode::S_IWUSR,
)
.expect("trace_pipe");
fs::create_dir_all(path.join("proc/14351")).expect("create_dir_all");
fs::write(path.join("proc/14351/comm"), "ftrace\n").expect("write comm");
fs::write(path.join("proc/14351/environ"), "SteamGameId=5678").expect("write environ");
let (sender, mut receiver) = unbounded_channel();
let trace = MockTrace { traces: sender };
let dbus = zbus::connection::Builder::session()
.unwrap()
.name("com.steampowered.SteamOSLogSubmitter")
.unwrap()
.serve_at("/com/steampowered/SteamOSLogSubmitter/helpers/Trace", trace)
.unwrap()
.build()
.await
.expect("dbus");
let mut ftrace = Ftrace::init(dbus).await.expect("ftrace");
assert!(match receiver.try_recv() {
Empty => true,
_ => false,
});
ftrace
.handle_event(
" GamepadUI Input-4886 [003] .N.1. 23828.572941: mark_victim: pid=14351",
)
.await
.expect("event");
let (line, data) = match receiver.try_recv() {
Ok((line, data)) => (line, data),
_ => panic!("Test failed"),
};
assert_eq!(
line,
" GamepadUI Input-4886 [003] .N.1. 23828.572941: mark_victim: pid=14351"
);
assert_eq!(data.len(), 2);
assert_eq!(
data.get("appid").map(|v| v.downcast_ref()),
Some(Ok(5678 as u64))
);
assert_eq!(
data.get("comm").map(|v| v.downcast_ref()),
Some(Ok("ftrace"))
);
ftrace
.handle_event(" GamepadUI Input-4886 [003] .N.1. 23828.572941: split_lock_warn <-")
.await
.expect("event");
let (line, data) = match receiver.try_recv() {
Ok((line, data)) => (line, data),
_ => panic!("Test failed"),
};
assert_eq!(
line,
" GamepadUI Input-4886 [003] .N.1. 23828.572941: split_lock_warn <-"
);
assert_eq!(data.len(), 0);
}
}

137
src/sls/mod.rs Normal file
View file

@ -0,0 +1,137 @@
/* SPDX-License-Identifier: BSD-2-Clause */
pub mod ftrace;
use anyhow::Result;
use std::fmt::Debug;
use std::time::SystemTime;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use tracing::field::{Field, Visit};
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::layer::Context;
use tracing_subscriber::Layer;
use zbus::connection::Connection;
use crate::Service;
#[zbus::proxy(
interface = "com.steampowered.SteamOSLogSubmitter.Manager",
default_service = "com.steampowered.SteamOSLogSubmitter",
default_path = "/com/steampowered/SteamOSLogSubmitter/Manager"
)]
trait Daemon {
async fn log(
&self,
timestamp: f64,
module: &str,
level: u32,
message: &str,
) -> zbus::Result<()>;
}
struct StringVisitor {
string: String,
}
struct LogLine {
timestamp: f64,
module: String,
level: u32,
message: String,
}
pub struct LogReceiver
where
Self: 'static,
{
receiver: UnboundedReceiver<LogLine>,
sender: UnboundedSender<LogLine>,
proxy: DaemonProxy<'static>,
}
pub struct LogLayer {
queue: UnboundedSender<LogLine>,
}
impl Visit for StringVisitor {
fn record_debug(&mut self, _: &Field, value: &dyn Debug) {
self.string.push_str(format!("{value:?}").as_str());
}
}
impl LogReceiver {
pub async fn new(connection: Connection) -> Result<LogReceiver> {
let proxy = DaemonProxy::new(&connection).await?;
let (sender, receiver) = unbounded_channel();
Ok(LogReceiver {
receiver,
sender,
proxy,
})
}
}
impl Service for LogReceiver {
const NAME: &'static str = "SLS log receiver";
async fn run(&mut self) -> Result<()> {
while let Some(message) = self.receiver.recv().await {
let _ = self
.proxy
.log(
message.timestamp,
message.module.as_ref(),
message.level,
message.message.as_ref(),
)
.await;
}
Ok(())
}
}
impl LogLayer {
pub async fn new(receiver: &LogReceiver) -> LogLayer {
LogLayer {
queue: receiver.sender.clone(),
}
}
}
impl<S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>> Layer<S> for LogLayer {
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let target = event.metadata().target();
if !target.starts_with("steamos_workerd::sls") {
// Don't forward non-SLS-related logs to SLS
return;
}
let target = target
.split("::")
.skip(2)
.fold(String::from("steamos_workerd"), |prefix, suffix| {
prefix + "." + suffix
});
let level = match *event.metadata().level() {
Level::TRACE => 10,
Level::DEBUG => 10,
Level::INFO => 20,
Level::WARN => 30,
Level::ERROR => 40,
};
let mut builder = StringVisitor {
string: String::new(),
};
event.record(&mut builder);
let text = builder.string;
let now = SystemTime::now();
let time = match now.duration_since(SystemTime::UNIX_EPOCH) {
Ok(duration) => duration.as_secs_f64(),
Err(_) => 0.0,
};
let _ = self.queue.send(LogLine {
timestamp: time,
module: target,
level,
message: text,
});
}
}

47
src/testing.rs Normal file
View file

@ -0,0 +1,47 @@
use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;
use tempfile::{tempdir, TempDir};
thread_local! {
static TEST: RefCell<Option<Rc<Test>>> = RefCell::new(None);
}
pub fn start() -> TestHandle {
TEST.with(|lock| {
assert!(lock.borrow().as_ref().is_none());
let test: Rc<Test> = Rc::new(Test {
base: tempdir().expect("Couldn't create test directory"),
});
*lock.borrow_mut() = Some(test.clone());
TestHandle { test }
})
}
pub fn stop() {
TEST.with(|lock| *lock.borrow_mut() = None);
}
pub fn current() -> Rc<Test> {
TEST.with(|lock| lock.borrow().as_ref().unwrap().clone())
}
pub struct Test {
base: TempDir,
}
pub struct TestHandle {
pub test: Rc<Test>,
}
impl Test {
pub fn path(&self) -> &Path {
self.base.path()
}
}
impl Drop for TestHandle {
fn drop(&mut self) {
stop();
}
}