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}