irelia_cli/utils/
process_info.rs

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