irelia_cli/utils/process_info.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
//! Constants, as well as the schema for the lock file can be found here
//! <https://hextechdocs.dev/getting-started-with-the-lcu-api/>
//! This module also contains a list of constants for the different names
//! of the processes for `macOS`, and `Windows`
use irelia_encoder::Encoder;
use std::path::Path;
use sysinfo::{ProcessRefreshKind, RefreshKind, System};
use crate::Error;
// Linux will be unplayable soon, so support has been removed
const CLIENT_PROCESS_NAME: &str = "CrBrowserMain";
const GAME_PROCESS_NAME: &str = "CrBrowserMain";
/// const copy of the encoder
pub(crate) const ENCODER: Encoder = Encoder::new();
/// Gets the port and auth for the client via the process id
/// This is done to avoid needing to find the lock file, but
/// a fallback could be implemented in theory using the fact
/// that you can get the exe location, and go backwards.
///
/// # Errors
/// This will return an error if the LCU is truly not running,
/// or the lock file is inaccessibly for some reason.
/// If it returns an error for any other reason, this code
/// likely needs the client and game process names updated.
///
pub fn get_running_client(force_lock_file: bool) -> Result<(String, String), Error> {
// If we always read the lock file, we never need to get the command line of the process
let cmd = if force_lock_file {
sysinfo::UpdateKind::Never
} else {
sysinfo::UpdateKind::OnlyIfNotSet
};
// No matter what, the path to the process is required
let refresh_kind = ProcessRefreshKind::new()
.with_exe(sysinfo::UpdateKind::OnlyIfNotSet)
.with_cmd(cmd);
// Get the current list of processes
let system = System::new_with_specifics(
// This creates a new instance of `system` every time, so this only
// needs to be updated if it's not set
RefreshKind::new().with_processes(refresh_kind),
);
// Is the client running, or is it the game?
let mut client = false;
// Iterate through all the processes, using .values() because
// We don't need the PID. Look for a process with the same name
// as the constant for that platform, otherwise return an error.
let process = system
.processes()
.values()
.find(|process| {
// Get the name of the process
let name = process.name();
// If it matches the name of the client,
// set the flag, and return it
if name == CLIENT_PROCESS_NAME {
client = true;
client
// Otherwise return if it matches the game name process
} else {
name == GAME_PROCESS_NAME
}
})
.ok_or_else(|| Error::LCUProcessNotRunning)?;
// Move these to an earlier scope to avoid an allocation
// And deduplicate some code later on
let mut lock_file: String = String::new();
let port: &str;
let auth: &str;
if client {
if force_lock_file {
// Get the client location
let path = process.exe().ok_or_else(|| Error::LockFileNotFound)?;
// Walk back once, being in the folder
let path = path.parent().ok_or_else(|| Error::LockFileNotFound)?;
// Read and init the path and auth
(port, auth) = read_and_parse_lock(path, &mut lock_file)?;
} else {
let cmd = process.cmd();
// Assuming the order doesn't change (which I haven't seen it do)
// we can avoid a second iteration over the cmd args
let mut iter = cmd.iter();
// Look for an auth key to put inside the command line, otherwise return an error.
auth = iter
.find_map(|s| s.strip_prefix("--remoting-auth-token="))
.ok_or(Error::AuthTokenNotFound)?;
// Look for a port to connect to the LCU with, otherwise return an error.
port = iter
.find_map(|s| s.strip_prefix("--app-port="))
.ok_or(Error::PortNotFound)?;
}
} else {
// We have to walk back twice to get the path of the lock file relative to the path of the game
// This can only be None on Linux according to the docs, so we should be fine everywhere else
let path = process.exe().ok_or_else(|| Error::LockFileNotFound)?;
// Sadly, we're relying on how the client structures things here
// Walking back a whole folder in order to get the lock file
let path = path
.parent()
.ok_or_else(|| Error::LockFileNotFound)?
.parent()
.ok_or_else(|| Error::LockFileNotFound)?;
(port, auth) = read_and_parse_lock(path, &mut lock_file)?;
}
// The auth header has to be base64 encoded, so that's happens here
let auth_header = ENCODER.encode(format!("riot:{auth}"));
// Format the port and header so that they can be used as headers
// For the LCU API
Ok((
format!("127.0.0.1:{port}"),
format!("Basic {auth_header}", ),
))
}
fn read_and_parse_lock<'a>(
path: &Path,
lock_file: &'a mut String,
) -> Result<(&'a str, &'a str), Error> {
// Read the lock file, putting the value in a higher scope
*lock_file = std::fs::read_to_string(path.join("lockfile")).map_err(Error::StdIo)?;
// Split the lock file on `:` which separates the different fields
// Because lock_file is from a higher scope, we can split the string here
// and return two string references later on
let mut split = lock_file.split(':');
// Get the 3rd field, which should be the port
let port = split.nth(2).ok_or(Error::PortNotFound)?;
// We moved the cursor, so the fourth element is the very next one
// Which should be the auth string
let auth = split.next().ok_or(Error::AuthTokenNotFound)?;
Ok((port, auth))
}
#[cfg(test)]
mod tests {
use super::get_running_client;
#[ignore = "This is only needed for testing, and doesn't need to be run all the time"]
#[test]
fn test_process_info() {
let (port, pass) = get_running_client(false).unwrap();
println!("{port} {pass}");
}
}