1use crate::interface::{
2 account::Account, executable::*, game::Game, game_status::*, steam_cmd::SteamCmd,
3};
4
5use crate::util::{
6 error::STError,
7 log::log,
8 parser::*,
9 paths::{
10 cache_location, executable_exists, install_script_location, launch_script_location,
11 steam_run_wrapper,
12 },
13};
14
15use port_scanner::scan_port;
16
17use std::process;
18use std::sync::Arc;
19
20use std::collections::HashSet;
21use std::collections::VecDeque;
22use std::fs;
23use std::sync::mpsc::{channel, Receiver, Sender};
24use std::sync::Mutex;
25use std::thread;
26
27const STEAM_PORT: u16 = 57343;
28
29#[derive(PartialEq, Clone)]
30pub enum State {
31 LoggedOut,
32 LoggedIn,
33 Failed,
34 Terminated(String),
35 Loaded(i32, i32),
36}
37
38pub enum Command {
39 Cli(String),
40 Install(i32, Arc<Mutex<Option<GameStatus>>>),
41 Run(i32, Vec<Executable>, Arc<Mutex<Option<GameStatus>>>),
42 StartClient,
43 Restart,
44}
45
46fn execute(
47 state: Arc<Mutex<State>>,
48 sender: Sender<String>,
49 receiver: Receiver<Command>,
50) -> Result<(), STError> {
51 let mut cmd = SteamCmd::new()?;
52 let mut queue = VecDeque::new();
53 let mut games = Vec::new();
54 let mut account: Option<Account> = None;
55
56 let mut downloading: HashSet<i32> = HashSet::new();
59
60 let mut cleanup: Option<Sender<bool>> = None;
62
63 loop {
64 queue.push_front(receiver.recv()?);
65 loop {
66 match queue.pop_back() {
67 None => break,
68 Some(Command::StartClient) => {
72 if !scan_port(STEAM_PORT) {
73 let (sender, termination) = channel();
74 cleanup = Some(sender);
75 thread::spawn(move || {
76 let mut child = process::Command::new("steam")
77 .args(vec![
78 "-console",
79 "-dev",
80 "-nofriendsui",
81 "-no-browser",
82 "+open",
83 "steam://",
84 ])
85 .stdout(process::Stdio::null())
86 .stderr(process::Stdio::null())
87 .spawn()
88 .unwrap();
89
90 while let Ok(terminate) = termination.recv() {
92 if terminate {
93 let _ = child.kill();
94 break;
95 }
96 }
97 });
98 }
99 }
100 Some(Command::Restart) => {
101 let mut state = state.lock()?;
102 *state = State::LoggedOut;
103 cmd = SteamCmd::new()?;
104 let user = match account {
105 Some(ref acct) => acct.account.clone(),
106 _ => "".to_string(),
107 };
108 queue.push_front(Command::Cli(format!("login {}", user)));
109 }
110 Some(Command::Install(id, status)) => {
111 if let Some(ref acct) = account {
112 if downloading.contains(&id) {
113 continue;
114 }
115 downloading.insert(id);
116 let name = acct.account.clone();
117 thread::spawn(move || {
118 {
119 let mut reference = status.lock().unwrap();
120 *reference = Some(GameStatus::msg(&*reference, "processing..."));
121 }
122 match SteamCmd::script(
123 install_script_location(name.clone(), id)
124 .unwrap()
125 .to_str()
126 .expect("Installation thread failed."),
127 ) {
128 Ok(mut cmd) => {
129 for _ in 1..15 {
131 cmd.next();
132 }
133 while let Ok(buf) = cmd.maybe_next() {
134 let response = String::from_utf8_lossy(&buf);
135 match *INSTALL_LEX.tokenize(&response).as_slice() {
138 ["Update", a, b] => {
139 let a = a.parse::<f64>().unwrap_or(0.);
140 let b = b.parse::<f64>().unwrap_or(1.);
141 let mut reference = status.lock().unwrap();
142 let update =
143 format!("downloading {}%", 100. * a / b);
144 *reference =
145 Some(GameStatus::msg(&*reference, &update));
146 }
147 ["ERROR", msg] => {
148 let mut reference = status.lock().unwrap();
149 let update = format!("Failed: {}", msg);
150 *reference =
151 Some(GameStatus::msg(&*reference, &update));
152 }
153 ["Success"] => {
154 let mut reference = status.lock().unwrap();
155 let size = match &*reference {
156 Some(gs) => gs.size,
157 _ => 0.,
158 };
159 *reference = Some(GameStatus {
160 state: "Success!".to_string(),
161 installdir: "".to_string(),
162 size,
163 });
164 }
166 _ => {
167 log!("unmatched", response);
168 }
169 }
170 }
171 }
172 Err(err) => {
173 let err = format!("{:?}", err);
174 let mut reference = status.lock().unwrap();
175 *reference = Some(GameStatus {
176 state: format!("Failed: {}", err),
177 installdir: "".to_string(),
178 size: 0.,
179 });
180 log!("Install script for:", name, "failed", err);
181 }
182 }
183 });
184 };
185 }
186 Some(Command::Run(id, executables, status)) => {
187 {
188 let mut reference = status.lock().unwrap();
189 *reference = Some(GameStatus::msg(&*reference, "launching..."));
190 }
191 if scan_port(STEAM_PORT) {
195 if let Some(ref acct) = account {
196 let name = acct.account.clone();
197 thread::spawn(move || {
198 if let Err(err) = SteamCmd::script(
199 launch_script_location(name.clone(), id)
200 .unwrap()
201 .to_str()
202 .expect("Launch thread failed."),
203 ) {
204 let err = format!("{:?}", err);
205 log!("Run script for:", name, "failed", err);
206 {
207 let mut reference = status.lock().unwrap();
208 *reference = Some(GameStatus::msg(
209 &*reference,
210 &format!("Error with script (trying direct): {}", err),
211 ));
212 }
213 run_process(
215 "steam".to_string(),
216 vec![
217 "-silent".to_string(),
218 "-applaunch".to_string(),
219 id.to_string(),
220 ],
221 status,
222 );
223 }
224 });
225 break;
226 }
227 }
228 let mut launched = false;
229 for launchable in executables {
230 if let Ok(path) = executable_exists(&launchable.executable) {
231 log!(path);
232 let mut command = match launchable.platform {
233 Platform::Windows => vec![
234 "wine".to_string(),
235 path.into_os_string().into_string().unwrap(),
236 ],
237 _ => vec![path.to_str().unwrap_or("").to_string()],
238 };
239 let mut args = launchable
240 .arguments
241 .clone()
242 .split(' ')
243 .map(|x| x.to_string())
244 .collect::<Vec<String>>();
245 command.append(&mut args);
246 log!("Finding entry");
247 let entry = match steam_run_wrapper(id) {
248 Ok(wrapper) => wrapper.into_os_string().into_string().unwrap(),
249 Err(STError::Problem(_)) => command.remove(0),
250 Err(err) => {
251 let mut reference = status.lock().unwrap();
252 *reference = Some(GameStatus::msg(
253 &*reference,
254 "Could not find entry program.",
255 ));
256 return Err(err);
257 } };
259 log!("Exits loop");
260 let status = status.clone();
261 thread::spawn(move || {
262 {
263 let mut reference = status.lock().unwrap();
264 *reference = Some(GameStatus::msg(&*reference, "running..."));
265 }
266 run_process(entry, command, status);
267 });
268 launched = true;
269 break;
270 } else {
271 log!("Tried", launchable.executable);
272 }
273 }
274 if !launched {
275 let mut reference = status.lock().unwrap();
276 *reference = Some(GameStatus::msg(
277 &*reference,
278 "Failed: Could not find executable to launch. Try setting $STEAM_APP_DIR",
279 ));
280 }
281 }
282 Some(Command::Cli(line)) => {
284 cmd.write(&line)?;
285 let mut updated = 0;
286 let waiting = queue.len();
287 let buf = cmd.maybe_next()?;
288 let response = String::from_utf8_lossy(&buf);
289 match *INPUT_LEX.tokenize(&line).as_slice() {
290 ["login", _] => {
291 let response = response.to_string();
292 if response.contains("Login Failure") || response.contains("FAILED") {
293 let mut state = state.lock()?;
294 *state = State::Failed;
295 } else {
296 queue.push_front(Command::Cli("info".to_string()));
297 }
298 log!("login");
299 }
300 ["info"] => {
301 account = match Account::new(&response.to_string()) {
302 Ok(acct) => Some(acct),
303 _ => None,
304 };
305 let mut state = state.lock()?;
306 *state = State::Loaded(0, -2);
307 log!("info");
308 }
309 ["licenses_print"] => {
310 if response == "[0m" {
312 continue;
313 }
314
315 games = Vec::new();
316 let licenses = response.to_string();
317 let keys = keys_from_licenses(licenses);
318 let total = keys.len();
319 updated += total as i32;
320 for key in keys {
321 queue.push_front(Command::Cli(format!(
322 "package_info_print {}",
323 key
324 )));
325 }
326 let mut state = state.lock()?;
327 *state = State::Loaded(0, total as i32);
328 log!("licenses_print");
329 }
330 ["package_info_print", key] => {
331 let mut lines = response.lines();
332 updated += 1;
333 if let Datum::Nest(map) = parse(&mut lines) {
334 if let Some(map) = map.get(key) {
335 if let Some(Datum::Nest(apps)) = map.maybe_nest()?.get("appids")
336 {
337 for wrapper in apps.values() {
338 if let Datum::Value(id) = wrapper {
339 let key = id.parse::<i32>().unwrap_or(-1);
340 if key >= 0 {
341 queue.push_front(Command::Cli(format!(
342 "app_info_print {}",
343 key
344 )));
345 }
346 }
347 }
348 }
349 }
350 };
351 let mut state = state.lock()?;
352 match *state {
353 State::Loaded(_, _) => {}
354 _ => *state = State::Loaded(updated, queue.len() as i32),
355 }
356 log!("package_info_print");
357 }
358 ["app_info_print", key] => {
359 updated += 1;
360 log!("Checking game");
361 let mut response = response;
365 log!(response);
366 if response == "[0m" {
367 cmd.write("")?;
368 cmd.write("")?;
369 while !response.starts_with("[0mAppID") {
370 if let Ok(buf) = cmd.maybe_next() {
371 response =
372 String::from_utf8_lossy(&buf).into_owned().into();
373 }
374 }
375 }
376 let mut lines = response.lines();
377
378 match Game::new(key, &mut lines) {
379 Ok(game) => {
380 log!("got game");
381 games.push(game);
382 log!(key);
383 }
384 Err(err) => {
385 log!(err)
386 }
387 };
388 }
389 ["app_status", _id] => {
390 sender.send(response.to_string())?;
391 }
392 ["quit"] => {
393 if let Some(cleanup) = cleanup {
394 let _ = cleanup.send(true);
395 }
396 sender.send(response.to_string())?;
397 return Ok(());
398 }
399 _ => {
400 sender.send(response.to_string())?;
402 return Err(STError::Problem(format!(
404 "Unknown command sent {}",
405 response
406 )));
407 }
408 }
409
410 let mut state = state.lock()?;
412 if let State::Loaded(o, e) = *state {
413 updated += o;
414 let total = e + (queue.len() - waiting) as i32;
415 *state = if updated == total {
416 games.sort_by(|a, b| a.name.cmp(&b.name));
417 fs::write(cache_location()?, serde_json::to_string(&games)?)?;
418 games = Vec::new();
419 State::LoggedIn
420 } else {
421 State::Loaded(updated, total)
422 }
423 }
424 let buf = cmd.maybe_next()?;
426 let mut prompt = String::from_utf8_lossy(&buf);
427 log!(prompt);
428 while prompt != "[1m\nSteam>" {
429 if let Ok(buf) = cmd.maybe_next() {
430 prompt = String::from_utf8_lossy(&buf).into_owned().into();
431 } else {
432 cmd.write("")?;
433 }
434 }
435 }
436 }
437 }
438 }
439}
440
441fn run_process(entry: String, command: Vec<String>, status: Arc<Mutex<Option<GameStatus>>>) {
442 match process::Command::new(entry).args(command).output() {
443 Ok(output) => {
444 let stderr = output.stderr.clone();
445 let stderr_snippet = &(String::from_utf8_lossy(&stderr)[..50]);
446 let mut reference = status.lock().unwrap();
447 *reference = Some(GameStatus::msg(
448 &*reference,
449 &(match output.status.code() {
450 Some(0) => format!("ran (success)"),
451 Some(n) => format!("failed with code {}: ({}...)", n, stderr_snippet),
452 None => format!("Process terminated."),
453 }),
454 ));
455
456 log!("Launching stdout:", &std::str::from_utf8(&output.stdout));
457 log!("Launching stderr:", &std::str::from_utf8(&output.stderr));
458 }
459 Err(err) => {
460 let mut reference = status.lock().unwrap();
461 *reference = Some(GameStatus::msg(
462 &*reference,
463 &format!("failed to launch: {}", err),
464 ));
465 }
466 }
467}
468
469pub struct Client {
471 receiver: Mutex<Receiver<String>>,
472 sender: Mutex<Sender<Command>>,
473 state: Arc<Mutex<State>>,
474}
475
476impl Client {
477 pub fn new() -> Client {
479 let (tx1, rx1) = channel();
480 let (tx2, rx2) = channel();
481
482 let client = Client {
483 receiver: Mutex::new(rx1),
484 sender: Mutex::new(tx2),
485 state: Arc::new(Mutex::new(State::LoggedOut)),
486 };
487 Client::start_process(client.state.clone(), tx1, rx2);
488 client
489 }
490
491 pub fn is_logged_in(&self) -> Result<bool, STError> {
493 Ok(self.get_state()? == State::LoggedIn)
494 }
495
496 pub fn get_state(&self) -> Result<State, STError> {
497 Ok(self.state.lock()?.clone())
498 }
499
500 pub fn install(&self, game: &Game) -> Result<(), STError> {
502 let sender = self.sender.lock()?;
503 sender.send(Command::Install(game.id as i32, game.status_counter()))?;
504 Ok(())
505 }
506
507 pub fn restart(&self) -> Result<(), STError> {
510 let sender = self.sender.lock()?;
511 sender.send(Command::Restart)?;
512 Ok(())
513 }
514
515 pub fn run(&self, game: &Game) -> Result<(), STError> {
518 let sender = self.sender.lock()?;
519 sender.send(Command::Run(
520 game.id,
521 game.executable.to_owned().to_vec(),
522 game.status_counter(),
523 ))?;
524 Ok(())
525 }
526
527 pub fn login(&self, user: &str) -> Result<(), STError> {
529 if user.is_empty() {
530 return Err(STError::Problem(
531 "Blank string. Requires user to log in.".to_string(),
532 ));
533 }
534 let mut state = self.state.lock()?;
535 *state = State::LoggedOut;
536 let sender = self.sender.lock()?;
537 sender.send(Command::Cli(format!("login {}", user)))?;
538 Ok(())
539 }
540
541 pub fn load_games(&self) -> Result<(), STError> {
551 let mut state = self.state.lock()?;
552 *state = State::Loaded(0, -1);
553 let sender = self.sender.lock()?;
554 sender.send(Command::Cli(String::from("licenses_print")))?;
555 Ok(())
556 }
557
558 pub fn games(&self) -> Result<Vec<Game>, STError> {
560 let db_content = fs::read_to_string(cache_location()?)?;
561 let parsed: Vec<Game> = serde_json::from_str(&db_content)?;
562 let mut processed: Vec<Game> = parsed
563 .iter()
564 .map(|game| Game::move_with_status((*game).clone(), self.status(game.id).ok()))
565 .collect();
566 processed.dedup_by(|a, b| a.id == b.id);
567 Ok(processed)
568 }
569
570 pub fn status(&self, id: i32) -> Result<GameStatus, STError> {
572 log!("Getting status for", id);
573 let sender = self.sender.lock()?;
574 sender.send(Command::Cli(format!("app_status {}", id)))?;
575 let receiver = self.receiver.lock()?;
576 GameStatus::new(&receiver.recv()?)
577 }
578
579 pub fn start_client(&self) -> Result<(), STError> {
582 let sender = self.sender.lock()?;
583 sender.send(Command::StartClient)?;
584 Ok(())
585 }
586
587 fn start_process(
588 state: Arc<Mutex<State>>,
589 sender: Sender<String>,
590 receiver: Receiver<Command>,
591 ) {
592 thread::spawn(move || {
593 let local = state.clone();
594 match execute(state, sender, receiver) {
595 Ok(_) => {}
596 Err(e) => {
597 let mut state = local
598 .lock()
599 .expect("We need to inform the other thread that this broke.");
600 *state = State::Terminated(format!("Fatal Error in client thread:\n{}", e));
601 }
602 };
603 });
604 }
605}
606
607impl Default for Client {
608 fn default() -> Self {
609 Self::new()
610 }
611}
612
613impl Drop for Client {
614 fn drop(&mut self) {
615 let sender = self
616 .sender
617 .lock()
618 .expect("In destructor, error handling is meaningless");
619 let _ = sender.send(Command::Cli(String::from("quit")));
620 let receiver = self.receiver.lock().expect("In destructor");
621 let _ = receiver.recv();
622 }
623}
624
625fn keys_from_licenses(licenses: String) -> Vec<i32> {
627 licenses
628 .lines()
629 .enumerate()
630 .filter(|(i, _)| i % 4 == 0)
631 .map(|(_, l)| match *LICENSE_LEX.tokenize(l).as_slice() {
632 ["packageID", id] => id.parse::<i32>().unwrap_or(-1),
633 _ => -1,
634 })
635 .filter(|x| x >= &0)
636 .collect::<Vec<i32>>()
637}
638
639#[cfg(test)]
640mod tests {
641 use crate::client::{Client, Command, State};
642 use crate::util::error::STError;
643 use std::sync::mpsc::channel;
644 use std::sync::Arc;
645 use std::sync::Mutex;
646
647 #[test]
649 fn test_polluted_data_impure() {
650 let (tx1, receiver) = channel();
651 let (sender, rx2) = channel();
652 Client::start_process(Arc::new(Mutex::new(State::LoggedOut)), tx1, rx2);
653 let pollution = String::from("pollution ™️ ö ®Ø 天 🎉 Maxisâ¢\n\n\n\nquit\nbash");
654 sender
655 .send(Command::Cli(pollution.clone()))
656 .expect("Fails to send message...");
657 assert!(&receiver
658 .recv()
659 .expect("Channel dies")
660 .contains(&"pollution".to_string()));
661 }
662
663 #[test]
664 fn test_implicit_line_ending_impure() {
665 let (tx1, receiver) = channel();
666 let (sender, rx2) = channel();
667 Client::start_process(Arc::new(Mutex::new(State::LoggedOut)), tx1, rx2);
668 let message = String::from("doesn't hang");
669 sender
670 .send(Command::Cli(message.clone()))
671 .expect("Fails to send message...");
672 assert!(&receiver
673 .recv()
674 .expect("Channel dies")
675 .contains(&"Command not found: doesn't".to_string()));
676 }
677
678 #[test]
679 fn test_blank_login() {
680 let client = Client::new();
681 let result = client.login("");
682 if let Err(STError::Problem(expected)) = result {
683 assert!(expected.contains(&"Blank".to_string()));
684 return;
685 }
686 panic!("Failed to unwrap")
687 }
688}