Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Steam background #18 #21

Merged
merged 7 commits into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ tui = { version = "0.14", default-features = false, features = ['termion', 'serd
fuzzy-matcher = "0.3.7"
lazy_static = "1.4.0"

port_scanner = "0.1.5"

image = "0.19.0"
[dependencies.tui-image-rgba-updated]
version = "0.2.0"
119 changes: 110 additions & 9 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ use crate::interface::*;
use crate::util::{
error::STError,
parser::*,
paths::{cache_location, executable_exists, install_script_location, steam_run_wrapper},
paths::{
cache_location, executable_exists, install_script_location, launch_script_location,
steam_run_wrapper,
},
};

use port_scanner::scan_port;

use std::process;
use std::sync::Arc;

Expand All @@ -16,6 +21,8 @@ use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Mutex;
use std::thread;

const STEAM_PORT: u16 = 57343;

#[derive(PartialEq, Clone)]
pub enum State {
LoggedOut,
Expand All @@ -34,13 +41,48 @@ fn execute(
let mut queue = VecDeque::new();
let mut games = Vec::new();
let mut account: Option<Account> = None;

// TODO(#20) Pass in Arcs for download status into threads. If requested state is in
// downloading, show satus.
let mut downloading: HashSet<i32> = HashSet::new();

// Cleanup the steam process if steam-tui quits.
let mut cleanup: Option<Sender<bool>> = None;

loop {
queue.push_front(receiver.recv()?);
loop {
match queue.pop_back() {
None => break,
Some(Command::StartClient) => {
if !scan_port(STEAM_PORT) {
let (sender, termination) = channel();
cleanup = Some(sender);
thread::spawn(move || {
let mut child = process::Command::new("steam")
.args(vec![
"-console",
"-dev",
"-nofriendsui",
"-no-browser",
"+open",
"steam://",
])
.stdout(process::Stdio::null())
.stderr(process::Stdio::null())
.spawn()
.unwrap();

// TODO: Currently doesn't kill all grand-children processes.
while let Ok(terminate) = termination.recv() {
if terminate {
let _ = child.kill();
break;
}
}
});
}
}
Some(Command::Restart) => {
let mut state = state.lock()?;
*state = State::LoggedOut;
Expand Down Expand Up @@ -69,12 +111,26 @@ fn execute(
});
};
}
Some(Command::Run(launchables)) => {
Some(Command::Run(id, launchables)) => {
// IF steam is running (we can check for port tcp/57343), then
// SteamCmd::script("login, app_run <>, quit")
// otherwise attempt to launch normally.
if scan_port(STEAM_PORT) {
if let Some(ref acct) = account {
let name = acct.account.clone();
thread::spawn(move || {
SteamCmd::script(
launch_script_location(name, id)
.unwrap()
.to_str()
.expect("Launch thread failed."),
)
.unwrap();
});
break;
}
}
for launchable in launchables {
// TODO: Check if steam is currently running.
// IF steam is running (we can check for port tcp/57343), then
// SteamCmd::script("login, app_run <>, quit")
// otherwise proceed as follows.
if let Ok(path) = executable_exists(&launchable.executable) {
let mut command = match launchable.platform {
Platform::Windows => vec![
Expand All @@ -99,13 +155,15 @@ fn execute(
process::Command::new(entry)
.args(command)
.stdout(process::Stdio::null())
.stderr(process::Stdio::null())
.spawn()
.unwrap();
});
break;
}
}
}
// Execute and handles response to various SteamCmd Commands.
Some(Command::Cli(line)) => {
cmd.write(&line)?;
let mut updated = 0;
Expand Down Expand Up @@ -191,12 +249,25 @@ fn execute(
["app_status", _id] => {
sender.send(response.to_string())?;
}
["quit"] => return Ok(()),
["quit"] => {
if let Some(cleanup) = cleanup {
let _ = cleanup.send(true);
}
sender.send(response.to_string())?;
return Ok(());
}
_ => {
// Send back response for debugging reasons.
sender.send(response.to_string())?;
// Fail since unknown commands should never be executed.
return Err(STError::Problem(format!(
"Unknown command sent {}",
response
)));
}
}

// If in Loading state, update progress.
let mut state = state.lock()?;
if let State::Loaded(o, e) = *state {
updated += o;
Expand All @@ -218,13 +289,15 @@ fn execute(
}
}

/// Manages and interfaces with SteamCmd threads.
pub struct Client {
receiver: Mutex<Receiver<String>>,
sender: Mutex<Sender<Command>>,
state: Arc<Mutex<State>>,
}

impl Client {
/// Spawns a StemCmd process to interface with.
pub fn new() -> Client {
let (tx1, rx1) = channel();
let (tx2, rx2) = channel();
Expand All @@ -238,6 +311,7 @@ impl Client {
client
}

/// Ensures `State` is `State::LoggedIn`.
pub fn is_logged_in(&self) -> Result<bool, STError> {
Ok(self.get_state()? == State::LoggedIn)
}
Expand All @@ -246,24 +320,30 @@ impl Client {
Ok(self.state.lock()?.clone())
}

/// Runs installation script for the provided game id.
pub fn install(&self, id: i32) -> Result<(), STError> {
let sender = self.sender.lock()?;
sender.send(Command::Install(id))?;
Ok(())
}

/// Quits previous SteamCmd instance, and spawns a new one. This can be useful for getting more
/// state data. Old processes fail to update due to short comings in SteamCmd.
pub fn restart(&self) -> Result<(), STError> {
let sender = self.sender.lock()?;
sender.send(Command::Restart)?;
Ok(())
}

pub fn run(&self, launchables: &[Launch]) -> Result<(), STError> {
/// Launches the provided game id using 'app_run' in steemcmd, or the raw executable depending
/// on the Steam client state.
pub fn run(&self, id: i32, launchables: &[Launch]) -> Result<(), STError> {
let sender = self.sender.lock()?;
sender.send(Command::Run(launchables.to_owned().to_vec()))?;
sender.send(Command::Run(id, launchables.to_owned().to_vec()))?;
Ok(())
}

/// Attempts to login the provided user string.
pub fn login(&self, user: &str) -> Result<(), STError> {
let mut state = self.state.lock()?;
*state = State::LoggedOut;
Expand All @@ -272,6 +352,15 @@ impl Client {
Ok(())
}

/// Starts off the process of parsing all games from SteamCmd. First `State` is set to be in an
/// unloaded state for `State::Loaded`. The process start by calling 'licenses_print' which
/// then extracts packageIDs, and calls 'package_info_print' for each package. This in turn
/// extracts appIDs, and gets app particular data by calling 'app_info_print' and binds it to a
/// `Game` object. When all data is loaded, the games are dumped to a file and the state is
/// changed to `State::LoggedIn` indicating that all data has been extracted and can be
/// presented.
/// TODO(#8): Check for cached games prior to reloading everything, unless explicitly
/// restarted.
pub fn load_games(&self) -> Result<(), STError> {
let mut state = self.state.lock()?;
*state = State::Loaded(0, -1);
Expand All @@ -280,19 +369,29 @@ impl Client {
Ok(())
}

/// Extracts games from cached location.
pub fn games(&self) -> Result<Vec<Game>, STError> {
let db_content = fs::read_to_string(cache_location()?)?;
let parsed: Vec<Game> = serde_json::from_str(&db_content)?;
Ok(parsed)
}

/// Binds data from 'app_status' to a `GameStatus` object.
pub fn status(&self, id: i32) -> Result<GameStatus, STError> {
let sender = self.sender.lock()?;
sender.send(Command::Cli(format!("app_status {}\n", id)))?;
let receiver = self.receiver.lock()?;
GameStatus::new(&receiver.recv()?)
}

/// Started up a headless steam instance in the background so that games can be launched
/// through steamcmd.
pub fn start_client(&self) -> Result<(), STError> {
let sender = self.sender.lock()?;
sender.send(Command::StartClient)?;
Ok(())
}

fn start_process(
state: Arc<Mutex<State>>,
sender: Sender<String>,
Expand Down Expand Up @@ -326,6 +425,8 @@ impl Drop for Client {
.lock()
.expect("In destructor, error handling is meaningless");
let _ = sender.send(Command::Cli(String::from("quit\n")));
let receiver = self.receiver.lock().expect("In destructor");
let _ = receiver.recv();
}
}
#[cfg(test)]
Expand Down
4 changes: 2 additions & 2 deletions src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ impl Iterator for SteamCmd {

impl Drop for SteamCmd {
fn drop(&mut self) {
self.write(&String::from("quit\n"))
.expect("Stopping anyway.");
// Failure is fine, because stopping anyway.
let _ = self.write(&String::from("quit\n"));
}
}
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,12 @@ fn entry() -> Result<(), Box<dyn std::error::Error>> {
}
Key::Char('\n') => {
if let Some(game) = game_list.selected() {
client.run(&game.launch)?;
client.run(game.id, &game.launch)?;
}
}
Key::Char(' ') => {
client.start_client()?;
}
Key::Char('d') => {
if let Some(game) = game_list.selected() {
client.install(game.id as i32)?;
Expand All @@ -162,6 +165,8 @@ fn entry() -> Result<(), Box<dyn std::error::Error>> {
app.mode = Mode::Searched;
}
app.user = config.default_user.clone();
} else {
break;
}
}
Key::Char('\n') => {
Expand Down
4 changes: 2 additions & 2 deletions src/util/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ impl Datum {
pub enum Command {
Cli(String),
Install(i32),
Run(Vec<Launch>),
Run(i32, Vec<Launch>),
StartClient,
Restart,
}

Expand Down Expand Up @@ -157,7 +158,6 @@ mod tests {
.maybe_nest()
.expect("Failed to properly parse");
assert_eq!(inner.len(), 0);
eprintln!("{:?}", map);
let complex = map
.get(&"otherØ 天 🎉".to_string())
.unwrap()
Expand Down
31 changes: 25 additions & 6 deletions src/util/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,17 @@ pub fn executable_exists(executable: &str) -> Result<PathBuf, STError> {
}
}

pub fn install_script_location(login: String, id: i32) -> Result<PathBuf, STError> {
fn script_location(file: &Path, contents: &str) -> Result<PathBuf, STError> {
let dir = config_directory()?;
let script_path = &format!("{}.install", id);
let script_path = Path::new(script_path);
let script_path = dir.join(script_path);
let script_path = dir.join(file);
let mut f = fs::File::create(&script_path)?;
f.write_all(contents.as_bytes())?;
Ok(script_path)
}

pub fn install_script_location(login: String, id: i32) -> Result<PathBuf, STError> {
let file = &format!("{}.install", id);
let file = Path::new(file);
let contents = format!(
r#"
login {}
Expand All @@ -102,8 +107,22 @@ quit
"#,
login, id
);
f.write_all(contents.as_bytes())?;
Ok(script_path)
script_location(file, &contents)
}

pub fn launch_script_location(login: String, id: i32) -> Result<PathBuf, STError> {
let file = &format!("{}.launch", id);
let file = Path::new(file);
let contents = format!(
r#"
login {}
app_update "{}" -validate
app_run {}
quit
"#,
login, id, id
);
script_location(file, &contents)
}

pub fn config_location() -> Result<PathBuf, STError> {
Expand Down