diff --git a/Cargo.lock b/Cargo.lock index f8e2d2d..c4be851 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,6 +199,19 @@ dependencies = [ "toml", ] +[[package]] +name = "amdportsd" +version = "0.1.0" +dependencies = [ + "amdgpu", + "futures", + "log", + "pretty_env_logger", + "ron 0.7.1", + "serde", + "tokio", +] + [[package]] name = "amdvold" version = "1.0.10" @@ -903,6 +916,95 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "getrandom" version = "0.2.7" @@ -1650,6 +1752,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.25" @@ -1923,6 +2031,12 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + [[package]] name = "slotmap" version = "1.0.6" diff --git a/Cargo.toml b/Cargo.toml index e9eed73..8529e71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,5 @@ members = [ "amdmond-lib", "amdguid", "amdgui-helper", + "amdportsd", ] diff --git a/amdfan/src/main.rs b/amdfan/src/main.rs index efabe27..534558f 100644 --- a/amdfan/src/main.rs +++ b/amdfan/src/main.rs @@ -140,7 +140,7 @@ fn handle_event(app: &mut App) -> io::Result { } }, }, - KeyCode::Up => match app.selected_point.clone() { + KeyCode::Up => match app.selected_point { Some(index) => { change_value(app, index, 1.0, y, y_mut); } @@ -155,12 +155,12 @@ fn handle_event(app: &mut App) -> io::Result { } }, KeyCode::Left => { - if let Some(index) = app.selected_point.clone() { + if let Some(index) = app.selected_point { change_value(app, index, -1.0, x, x_mut); } } KeyCode::Right => { - if let Some(index) = app.selected_point.clone() { + if let Some(index) = app.selected_point { change_value(app, index, 1.0, x, x_mut); } } @@ -271,13 +271,13 @@ where let prev = index .checked_sub(1) .and_then(|i| app.config.speed_matrix().get(i)) - .map(|v| read(v)) + .map(read) .unwrap_or(0.0); let next = app .config .speed_matrix() .get(index + 1) - .map(|v| read(v)) + .map(read) .unwrap_or(100.0); let current = app .config diff --git a/amdgpu/src/error.rs b/amdgpu/src/error.rs index a31dff9..6e559a0 100644 --- a/amdgpu/src/error.rs +++ b/amdgpu/src/error.rs @@ -1,6 +1,7 @@ #[cfg(feature = "gui-helper")] use crate::helper_cmd::GuiHelperError; use crate::lock_file::LockFileError; +use crate::ports::PortsError; use pidlock::PidlockError; use std::fmt::{Debug, Display, Formatter}; @@ -48,6 +49,8 @@ pub enum AmdGpuError { #[error("{0:?}")] GuiHelper(#[from] GuiHelperError), #[error("{0:?}")] + Ports(#[from] PortsError), + #[error("{0:?}")] LockFile(#[from] LockFileError), } diff --git a/amdgpu/src/lib.rs b/amdgpu/src/lib.rs index decdb3f..9cc0917 100644 --- a/amdgpu/src/lib.rs +++ b/amdgpu/src/lib.rs @@ -10,6 +10,7 @@ mod error; pub mod helper_cmd; pub mod hw_mon; pub mod lock_file; +pub mod ports; mod temp_input; pub mod utils; diff --git a/amdgpu/src/ports.rs b/amdgpu/src/ports.rs new file mode 100644 index 0000000..3d0b17e --- /dev/null +++ b/amdgpu/src/ports.rs @@ -0,0 +1,124 @@ +//! AMD GUI helper communication toolkit + +use std::io::{Read, Write}; +use std::ops::Deref; +use std::os::unix::net::UnixStream; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, thiserror::Error)] +pub enum PortsError { + #[error("AMD GPU ports socket file not found. Is service running?")] + NoSockFile, + #[error("Failed to connect to /tmp/amdgpu-ports.sock. {0}")] + UnableToConnect(#[from] std::io::Error), + #[error("Failed to ports command. {0}")] + Serialize(#[from] ron::Error), +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Output { + #[serde(rename = "c")] + pub card: String, + #[serde(rename = "t")] + pub port_type: String, + #[serde(rename = "m")] + pub port_name: Option, + #[serde(rename = "n")] + pub port_number: u8, + #[serde(rename = "s")] + pub status: Status, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Status { + #[serde(rename = "c")] + Connected, + #[serde(rename = "d")] + Disconnected, +} + +impl Default for Status { + fn default() -> Self { + Self::Disconnected + } +} + +impl Output { + fn to_path(&self) -> PathBuf { + PathBuf::new().join("/sys/class/drm").join(format!( + "card{}-{}{}-{}", + self.card, + self.port_type, + self.port_name + .as_deref() + .map(|s| format!("-{s}")) + .unwrap_or_default(), + self.port_number + )) + } + + fn status_path(&self) -> PathBuf { + self.to_path().join("status") + } + + pub fn read_status(&self) -> Option { + Some( + match std::fs::read_to_string(self.status_path()).ok()?.trim() { + "connected" => Status::Connected, + "disconnected" => Status::Disconnected, + _ => return None, + }, + ) + } +} + +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] +#[serde(transparent)] +pub struct Pid(pub i32); + +impl Deref for Pid { + type Target = i32; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub enum Command { + Ports, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub enum Response { + Ports(Vec), + NoOp, +} + +pub fn sock_file() -> PathBuf { + std::path::Path::new("/tmp").join("amdgpu-ports.sock") +} + +pub fn send_command(cmd: Command) -> crate::Result { + let sock_path = sock_file(); + + if !sock_path.exists() { + return Err(PortsError::NoSockFile.into()); + } + + let mut stream = UnixStream::connect(&sock_path).map_err(PortsError::UnableToConnect)?; + let s = ron::to_string(&cmd).map_err(PortsError::Serialize)?; + if stream.write_all(format!("{}\n", s).as_bytes()).is_ok() { + log::info!("Command send"); + } + + let res: Response = { + let mut s = String::with_capacity(100); + let _ = stream.read_to_string(&mut s); + ron::from_str(&s).map_err(PortsError::Serialize)? + }; + + Ok(res) +} diff --git a/amdgui-helper/src/main.rs b/amdgui-helper/src/main.rs index f96f6ec..8753520 100644 --- a/amdgui-helper/src/main.rs +++ b/amdgui-helper/src/main.rs @@ -47,7 +47,7 @@ fn main() -> Result<()> { path: sock_path.clone(), })? }; - if let Err(e) = std::fs::set_permissions(&sock_path, Permissions::from_mode(0x777)) { + if let Err(e) = std::fs::set_permissions(&sock_path, Permissions::from_mode(0o777)) { log::error!("Failed to change gui helper socket file mode. {:?}", e); } diff --git a/amdportsd/Cargo.toml b/amdportsd/Cargo.toml new file mode 100644 index 0000000..ead1d53 --- /dev/null +++ b/amdportsd/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "amdportsd" +version = "0.1.0" +edition = "2021" + +[dependencies] +amdgpu = { path = "../amdgpu", features = ["gui-helper"] } + +tokio = { version = "1.19.2", features = ["full"] } +futures = { version = "0.3", features = [] } +ron = { version = "0.7.1" } +serde = { version = "1.0.137", features = ["derive"] } + +log = { version = "0.4" } +pretty_env_logger = { version = "0.4" } diff --git a/amdportsd/src/main.rs b/amdportsd/src/main.rs new file mode 100644 index 0000000..b8b0a09 --- /dev/null +++ b/amdportsd/src/main.rs @@ -0,0 +1,186 @@ +use amdgpu::ports::{sock_file, Command, Response}; +use amdgpu::{ports::*, IoFailure}; +use std::fs::{DirEntry, Permissions}; +use std::io::{Read, Write}; +use std::net::Shutdown; +use std::os::unix::fs::PermissionsExt; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +fn parse_output(entry: DirEntry) -> Option { + let ty = entry.file_type().ok()?; + if ty.is_dir() { + return None; + } + let file_name = entry.file_name(); + let path = file_name.to_str()?; + let mut it = path + .split('-') + .map(String::from) + .collect::>() + .into_iter(); + let mut output = Output { + card: it.next()?.strip_prefix("card")?.to_string(), + port_type: it.next()?, + ..Default::default() + }; + let mut it = it.rev(); + output.port_number = it.next()?.parse().ok()?; + + let mut it = it.rev().peekable(); + + if it.peek().is_some() { + output.port_name = Some(it.collect::>().join("-")); + } + + output.status = output.read_status()?; + + Some(output) +} + +async fn read_outputs(state: Arc>>) { + loop { + let outputs = std::fs::read_dir("/sys/class/drm") + .unwrap() + .filter_map(|r| r.ok()) + .filter(|e| { + e.path() + .to_str() + .map(|s| s.contains("card")) + .unwrap_or_default() + }) + .filter_map(parse_output) + .collect::>(); + if let Ok(mut lock) = state.lock() { + *lock = outputs; + } + + tokio::time::sleep(Duration::from_millis(1_000 / 3)).await; + } +} + +pub struct Service(UnixStream); + +impl Service { + /// Serialize and send command + pub fn write_response(&mut self, res: Response) { + match ron::to_string(&res) { + Ok(buffer) => match self.0.write_all(buffer.as_bytes()) { + Ok(_) => { + log::info!("Response successfully written") + } + Err(e) => log::warn!("Failed to write response. {:?}", e), + }, + Err(e) => { + log::warn!("Failed to serialize response {:?}. {:?}", res, e) + } + } + } + + /// Read from `.sock` file new line separated commands + pub fn read_command(&mut self) -> Option { + let mut command = String::with_capacity(100); + log::info!("Reading stream..."); + read_line(&mut self.0, &mut command); + if command.is_empty() { + return None; + } + Some(command) + } + + /// Close connection with no operation response + pub fn kill(mut self) { + self.write_response(Response::NoOp); + self.close(); + } + + pub fn close(self) { + let _ = self.0.shutdown(Shutdown::Both); + } +} + +async fn service(state: Arc>>) { + let sock_path = sock_file(); + let listener = { + let _ = std::fs::remove_file(&sock_path); + + UnixListener::bind(&sock_path) + .map_err(|io| IoFailure { + io, + path: sock_path.clone(), + }) + .expect("Creating pid file for ports failed") + }; + if let Err(e) = std::fs::set_permissions(&sock_path, Permissions::from_mode(0o777)) { + log::error!("Failed to change gui helper socket file mode. {:?}", e); + } + + while let Ok((stream, _addr)) = listener.accept() { + handle_connection(stream, state.clone()); + } +} + +fn read_line(stream: &mut UnixStream, command: &mut String) { + let mut buffer = [0]; + while stream.read_exact(&mut buffer).is_ok() { + if buffer[0] == b'\n' { + break; + } + match std::str::from_utf8(&buffer) { + Ok(s) => { + command.push_str(s); + } + Err(e) => { + log::error!("Failed to read from client. {:?}", e); + let _ = stream.shutdown(Shutdown::Both); + continue; + } + } + } +} + +fn handle_connection(stream: UnixStream, state: Arc>>) { + let mut service = Service(stream); + + let command = match service.read_command() { + Some(s) => s, + _ => return service.kill(), + }; + + log::info!("Incoming {:?}", command); + let cmd = match ron::from_str::(command.trim()) { + Ok(cmd) => cmd, + Err(e) => { + log::warn!("Invalid message {:?}. {:?}", command, e); + return service.kill(); + } + }; + handle_command(service, cmd, state); +} + +fn handle_command(mut service: Service, cmd: Command, state: Arc>>) { + match cmd { + Command::Ports => { + if let Ok(outputs) = state.lock() { + service.write_response(Response::Ports(outputs.iter().map(Clone::clone).collect())); + } + } + } +} + +fn main() { + let executor = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + let state = Arc::new(Mutex::new(Vec::new())); + + executor.block_on(async { + let sync = read_outputs(state.clone()); + let handle = service(state); + + tokio::join!(sync, handle); + }); +}