use irelia_encoder::Encoder;
use std::fmt::{Display, Formatter};
use std::io::Read;
use std::net::{Ipv4Addr, SocketAddrV4};
use std::num::ParseIntError;
use std::path::Path;
use std::str::FromStr;
use sysinfo::{ProcessRefreshKind, RefreshKind, System};
#[cfg(target_os = "windows")]
pub const CLIENT_PROCESS_NAME: &str = "LeagueClientUx.exe";
#[cfg(target_os = "macos")]
pub const CLIENT_PROCESS_NAME: &str = "LeagueClientUx";
#[cfg(target_os = "windows")]
pub const GAME_PROCESS_NAME: &str = "League of Legends.exe";
#[cfg(target_os = "macos")]
pub const GAME_PROCESS_NAME: &str = "League of Legends";
pub(crate) const ENCODER: Encoder = Encoder::new();
#[cfg(all(docsrs, target_os = "linux"))]
pub const GAME_PROCESS_NAME: &str = "";
#[cfg(all(docsrs, target_os = "linux"))]
pub const CLIENT_PROCESS_NAME: &str = "";
const NOT_RUNNING: Error = Error::new(
ErrorKind::NotRunning,
"neither the game or client process were running",
);
const PORT_NOT_FOUND: Error = Error::new(ErrorKind::PortNotFound, "port was not found");
const AUTH_NOT_FOUND: Error = Error::new(ErrorKind::AuthTokenNotFound, "auth token was not found");
const LOCK_FILE_NOT_FOUND: Error = Error::new(
ErrorKind::LockFileNotFound,
"Did not follow the typical install structure",
)
.set_lockfile_error(true);
pub fn get_running_client<T>(
client_process_name: &str,
game_process_name: &str,
force_lock_file: bool,
force_directory: Option<&Path>,
) -> Result<(SocketAddrV4, Result<T, T::Err>), Error>
where
T: FromStr,
{
const RIOT_PREFIX: &[u8] = b"riot:";
const BASIC_PREFIX: &[u8] = b"Basic ";
let cmd = if force_lock_file {
sysinfo::UpdateKind::Never
} else {
sysinfo::UpdateKind::OnlyIfNotSet
};
let exe = if force_directory.is_some() {
sysinfo::UpdateKind::Never
} else {
sysinfo::UpdateKind::OnlyIfNotSet
};
let refresh_kind = ProcessRefreshKind::nothing().with_exe(exe).with_cmd(cmd);
let system = System::new_with_specifics(
RefreshKind::nothing().with_processes(refresh_kind),
);
let mut client = false;
let process = if let Some(force_directory) = force_directory {
Err(force_directory)
} else {
Ok(system
.processes()
.values()
.find(|process| {
client = process.name() == client_process_name;
client || (process.name() == game_process_name)
})
.ok_or(NOT_RUNNING)?)
};
let mut lock_file = [0; 60];
let [port, auth] = match (process, client, force_lock_file) {
(Ok(process), true, false) => pull_client_info(process)?,
_ => read_lock_file(&mut lock_file, client, &process)?,
};
let pre_encoded_buffer_len = auth.len() + RIOT_PREFIX.len();
let buffer: &mut [u8] = if pre_encoded_buffer_len > 22 + RIOT_PREFIX.len() {
&mut vec![0; pre_encoded_buffer_len].into_boxed_slice()
} else {
&mut [0; 22 + RIOT_PREFIX.len()]
};
buffer[..RIOT_PREFIX.len()].copy_from_slice(RIOT_PREFIX);
buffer[RIOT_PREFIX.len()..auth.len() + RIOT_PREFIX.len()].copy_from_slice(auth.as_bytes());
let auth_header_len = pre_encoded_buffer_len.div_ceil(3) * 4;
let auth_header_buffer: &mut [u8] = if auth_header_len > 36 {
&mut vec![b'='; auth_header_len + BASIC_PREFIX.len()].into_boxed_slice()
} else {
&mut [b'='; 36 + BASIC_PREFIX.len()]
};
auth_header_buffer[..BASIC_PREFIX.len()].copy_from_slice(BASIC_PREFIX);
ENCODER.internal_encode(buffer, &mut auth_header_buffer[BASIC_PREFIX.len()..]);
let port: u16 = port.parse().map_err(|err: ParseIntError| {
Error::new_string(ErrorKind::PortNotFound, err.to_string())
})?;
let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, port);
let auth_header_buffer = std::str::from_utf8(auth_header_buffer)?;
let res = T::from_str(auth_header_buffer);
Ok((addr, res))
}
fn pull_client_info(process: &sysinfo::Process) -> Result<[&str; 2], Error> {
let cmd = process.cmd().iter().filter_map(|os_str| os_str.to_str());
let mut scoped_auth = None;
let mut scoped_port = None;
for s in cmd {
if scoped_auth.is_some() && scoped_port.is_some() {
break;
}
if scoped_auth.is_none() {
scoped_auth = s.strip_prefix("--remoting-auth-token=");
}
if scoped_port.is_none() {
scoped_port = s.strip_prefix("--app-port=");
}
}
Ok([
scoped_port.ok_or(PORT_NOT_FOUND)?,
scoped_auth.ok_or(AUTH_NOT_FOUND)?,
])
}
fn read_lock_file<'a>(
lock_file: &'a mut [u8; 60],
client: bool,
process: &Result<&sysinfo::Process, &Path>,
) -> Result<[&'a str; 2], Error> {
let dir = match process {
Err(path) => *path,
Ok(process) => {
let path = process.exe().ok_or(LOCK_FILE_NOT_FOUND)?;
let mut dir = path.parent().ok_or(LOCK_FILE_NOT_FOUND)?;
if !client {
dir = dir.parent().ok_or(LOCK_FILE_NOT_FOUND)?;
}
dir
}
};
let mut file = std::fs::File::open(dir.join("lockfile"))?;
let len = file
.metadata()?
.len()
.try_into()
.expect("This file is always ~60 bytes");
let mut read = file.read(lock_file)?;
while read != len {
read += file.read(&mut lock_file[read..])?;
}
let lock_file = std::str::from_utf8(&lock_file[..len])?;
let mut split = lock_file.split(':');
Ok([
split
.nth(2)
.ok_or(PORT_NOT_FOUND.set_lockfile_error(true))?,
split
.next()
.ok_or(AUTH_NOT_FOUND.set_lockfile_error(true))?,
])
}
#[derive(Debug, Clone)]
pub struct Error {
kind: ErrorKind,
message: std::borrow::Cow<'static, str>,
lock_file: bool,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for Error {}
impl Error {
const fn new(kind: ErrorKind, message: &'static str) -> Self {
Self {
kind,
message: std::borrow::Cow::Borrowed(message),
lock_file: false,
}
}
const fn new_string(kind: ErrorKind, message: String) -> Self {
Self {
kind,
message: std::borrow::Cow::Owned(message),
lock_file: false,
}
}
const fn set_lockfile_error(mut self, lock_fie_error: bool) -> Self {
self.lock_file = lock_fie_error;
self
}
#[must_use]
pub const fn is_lockfile_error(&self) -> bool {
self.lock_file
}
#[must_use]
pub const fn is_io_error(&self) -> bool {
matches!(self.kind, ErrorKind::Io(_))
}
#[must_use]
pub fn kind(&self) -> ErrorKind {
self.kind.clone()
}
#[must_use]
pub fn reason(&self) -> &str {
&self.message
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum ErrorKind {
Io(std::io::ErrorKind),
LockFileNotFound,
AuthTokenNotFound,
PortNotFound,
NotRunning,
}
impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Self {
kind: ErrorKind::Io(value.kind()),
message: value.to_string().into(),
lock_file: true,
}
}
}
impl From<std::str::Utf8Error> for Error {
fn from(_: std::str::Utf8Error) -> Self {
const {
Self::new(
ErrorKind::Io(std::io::ErrorKind::InvalidData),
"stream did not contain valid UTF-8",
)
.set_lockfile_error(true)
}
}
}
#[cfg(test)]
mod tests {
use super::{CLIENT_PROCESS_NAME, GAME_PROCESS_NAME, get_running_client};
use http::HeaderValue;
use sysinfo::{ProcessRefreshKind, RefreshKind, System};
#[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): (_, Result<HeaderValue, _>) =
get_running_client(CLIENT_PROCESS_NAME, GAME_PROCESS_NAME, true, None).unwrap();
println!("{port} {pass:?}");
}
#[ignore = "This is only needed for testing, and doesn't need to be run all the time"]
#[test]
fn test_process_args() {
let refresh_kind = ProcessRefreshKind::nothing()
.with_cwd(sysinfo::UpdateKind::OnlyIfNotSet)
.with_root(sysinfo::UpdateKind::OnlyIfNotSet)
.with_exe(sysinfo::UpdateKind::OnlyIfNotSet)
.with_cmd(sysinfo::UpdateKind::OnlyIfNotSet);
let system = System::new_with_specifics(
RefreshKind::nothing().with_processes(refresh_kind),
);
let process = system
.processes()
.values()
.find(|process| process.name() == GAME_PROCESS_NAME)
.unwrap();
println!("{:?}", process.exe());
println!("{:?}", process.root());
println!("{:?}", process.cmd());
println!("{:?}", process.cwd());
println!("{:?}", process.environ());
let parent = process.parent().unwrap();
let process = system.process(parent).unwrap();
println!("{process:?}");
}
}