Skip to content

Commit 2721536

Browse files
committed
feat: capture AI stderr and return it to the client
1 parent 9b03363 commit 2721536

File tree

4 files changed

+56
-28
lines changed

4 files changed

+56
-28
lines changed

backend/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Errors are returned as status code. Most notable ones are:
9595
interface GameState {
9696
board: Board;
9797
current_player: Player;
98+
ai_output: string; // Everything printed by the AI on stderr since the start/last move.
9899
}
99100

100101
type Board = (Piece | null)[][];

backend/src/api/play.rs

+27-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::{AppState, Error, User};
22
use crate::game::{GameState, Move, Player};
3-
use async_process::{Child, ChildStdin, ChildStdout};
3+
use async_process::{Child, ChildStderr, ChildStdin, ChildStdout};
44
use rocket::{
55
futures::{io::BufReader, AsyncBufReadExt, AsyncWriteExt},
66
get, post,
@@ -14,6 +14,7 @@ pub struct Game {
1414
handle: Child,
1515
stdin: ChildStdin,
1616
stdout: BufReader<ChildStdout>,
17+
stderr: BufReader<ChildStderr>,
1718
checkers: GameState,
1819
human_player: Player,
1920
}
@@ -72,24 +73,40 @@ pub async fn start(
7273
user: User,
7374
is_first_player: bool,
7475
) -> Result<Json<GameState>, Error> {
75-
let mut lock = state.lock().unwrap();
76+
let game = {
77+
let lock = state.lock().unwrap();
78+
lock.games.get(&user.name).cloned()
79+
};
7680

77-
if lock.games.contains_key(&user.name) {
78-
return Err(Error::GameAlreadyInProgress);
81+
if let Some(game) = game {
82+
if game.lock().await.handle.try_status()?.is_none() {
83+
return Err(Error::GameAlreadyInProgress);
84+
}
7985
}
8086

81-
let mut child = lock
82-
.submissions
83-
.get(&user.name)
84-
.ok_or(Error::NotFound)?
85-
.start()?;
86-
let checkers: GameState = Default::default();
87+
let mut child = {
88+
let lock = state.lock().unwrap();
89+
lock.submissions
90+
.get(&user.name)
91+
.ok_or(Error::NotFound)?
92+
.start()?
93+
};
94+
let mut checkers: GameState = Default::default();
8795

96+
let mut stderr = BufReader::new(child.stderr.take().unwrap());
97+
98+
let mut output = vec![];
99+
stderr.read_until(1, &mut output).await?;
100+
101+
checkers.ai_output = String::from_utf8_lossy(&output).to_string();
102+
103+
let mut lock = state.lock().unwrap();
88104
lock.games.insert(
89105
user.name,
90106
Arc::new(Mutex::new(Game {
91107
stdin: child.stdin.take().unwrap(),
92108
stdout: BufReader::new(child.stdout.take().unwrap()),
109+
stderr,
93110
handle: child,
94111
human_player: if is_first_player {
95112
Player::White

backend/src/api/submissions.rs

+22-17
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,28 @@ impl Submission {
5151
}
5252

5353
pub fn start(&self) -> Result<Child, Error> {
54-
match self.lang {
55-
Language::Python => Command::new("docker")
56-
.args([
57-
"run",
58-
"-v",
59-
format!("{}:/script.py", self.code.to_string_lossy()).as_str(),
60-
"-i",
61-
"python:3-bullseye",
62-
"python",
63-
"/script.py",
64-
])
65-
.stdin(Stdio::piped())
66-
.stdout(Stdio::piped())
67-
.spawn()
68-
.map_err(Error::from),
69-
_ => todo!(),
70-
}
54+
let (image, command) = match self.lang {
55+
Language::Cpp => ("ghcr.io/clicepfl/s4s-2024-cpp:main", "cp /script /script.cpp && g++ /script.cpp -o /exe && /exe"),
56+
Language::Java => ("cimg/openjdk:17.0", "java /script"),
57+
Language::Python => ("python:3-bullseye", "python /script"),
58+
};
59+
60+
Command::new("docker")
61+
.args([
62+
"run",
63+
"-v",
64+
format!("{}:/script", self.code.to_string_lossy()).as_str(),
65+
"-i",
66+
image,
67+
"sh",
68+
"-c",
69+
command,
70+
])
71+
.stdin(Stdio::piped())
72+
.stdout(Stdio::piped())
73+
.stderr(Stdio::piped())
74+
.spawn()
75+
.map_err(Error::from)
7176
}
7277
}
7378

backend/src/game.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,15 @@ fn default_board() -> Board {
8383
pub struct GameState {
8484
pub board: Board,
8585
pub current_player: Player,
86+
pub ai_output: String,
8687
}
8788

8889
impl Default for GameState {
8990
fn default() -> Self {
9091
Self {
9192
board: default_board(),
9293
current_player: Player::White,
94+
ai_output: Default::default(),
9395
}
9496
}
9597
}
@@ -383,7 +385,7 @@ impl GameState {
383385

384386
#[cfg(test)]
385387
mod test {
386-
use super::{Board, GameState, Move, MoveSequence, Piece, Position};
388+
use super::{Board, GameState, Move, MoveSequence, Piece};
387389
use core::hash::Hash;
388390
use std::collections::HashSet;
389391

@@ -414,11 +416,13 @@ mod test {
414416
GameState {
415417
board: board.clone(),
416418
current_player: crate::game::Player::White,
419+
..Default::default()
417420
}
418421
.list_valid_moves(),
419422
GameState {
420423
board,
421424
current_player: crate::game::Player::Black,
425+
..Default::default()
422426
}
423427
.list_valid_moves(),
424428
)
@@ -448,6 +452,7 @@ mod test {
448452
[None, None, None, None, None, None, None, None, None, None],
449453
],
450454
current_player: super::Player::White,
455+
..Default::default()
451456
};
452457
assert!(state.list_valid_moves().is_empty());
453458
}

0 commit comments

Comments
 (0)