chess work

This commit is contained in:
dr-frmr 2023-11-21 17:49:18 -05:00
parent 5a4813b8c4
commit 84a5bbbe6c
No known key found for this signature in database
10 changed files with 687 additions and 959 deletions

View File

@ -62,7 +62,7 @@ snow = { version = "0.9.3", features = ["ring-resolver"] }
thiserror = "1.0"
tokio = { version = "1.28", features = ["fs", "macros", "rt-multi-thread", "sync"] }
tokio-tungstenite = "*"
url = "*"
url = "2.4.1"
uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "e53c124" }
uuid = { version = "1.1.2", features = ["serde", "v4"] }
warp = "0.3.5"

View File

@ -50,6 +50,12 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "bytes"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -124,6 +130,21 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
dependencies = [
"percent-encoding",
]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
@ -162,12 +183,33 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]]
name = "http"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "id-arena"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
[[package]]
name = "idna"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "2.1.0"
@ -234,6 +276,12 @@ dependencies = [
"libc",
]
[[package]]
name = "percent-encoding"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "pleco"
version = "0.5.0"
@ -532,12 +580,42 @@ dependencies = [
"syn",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "unicode-bidi"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.10.1"
@ -553,16 +631,30 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "uqbar_process_lib"
version = "0.3.0"
source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=db26c5b#db26c5b1607ba6532bcc7687bf8902a21ebd3393"
dependencies = [
"anyhow",
"bincode",
"http",
"rand 0.8.5",
"serde",
"serde_json",
"thiserror",
"url",
"wit-bindgen",
]
[[package]]
name = "url"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"

View File

@ -17,7 +17,7 @@ bincode = "1.3.3"
pleco = "0.5"
serde = {version = "1.0", features = ["derive"] }
serde_json = "1.0"
uqbar_process_lib = { path = "../../../process_lib" }
uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "db26c5b" }
wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "5390bab780733f1660d14c254ec985df2816bf1d" }
[lib]

View File

@ -1,79 +0,0 @@
import os
import subprocess
import sys
import json
import glob
import shutil
def compile_process(process_dir, pkg_dir, root_dir):
# Get the path to the source code and the compiled WASM file
src_path = os.path.join(process_dir, "src")
wasm_path = os.path.join(pkg_dir, os.path.basename(process_dir) + ".wasm")
# Check if the source code or the Cargo.toml file has been modified since the last compile
src_mtime = max(os.path.getmtime(f) for f in glob.glob(os.path.join(src_path, '**'), recursive=True))
wasm_mtime = os.path.getmtime(wasm_path) if os.path.exists(wasm_path) else 0
# Change to the process directory
os.chdir(process_dir)
# Create the target/bindings/$name/ directory
bindings_dir = os.path.join(process_dir, "target", "bindings", os.path.basename(process_dir))
os.makedirs(bindings_dir, exist_ok=True)
# Create target.wasm (compiled .wit) & world
subprocess.check_call([
"wasm-tools", "component", "wit",
os.path.join(root_dir, "wit"),
"-o", os.path.join(bindings_dir, "target.wasm"),
"--wasm"
])
# Copy /wit (world is empty file currently)
shutil.copytree(os.path.join(root_dir, "wit"), os.path.join(bindings_dir, "wit"), dirs_exist_ok=True)
# shutil.copy(os.path.join(root_dir, "world"), os.path.join(bindings_dir, "world"))
# Create an empty world file
with open(os.path.join(bindings_dir, "world"), 'w') as f:
pass
# Build the module using Cargo
subprocess.check_call([
"cargo", "+nightly", "build",
"--release",
"--no-default-features",
"--target", "wasm32-wasi"
])
# Adapt the module using wasm-tools
wasm_file = os.path.join(process_dir, "target", "wasm32-wasi", "release", os.path.basename(process_dir) + ".wasm")
adapted_wasm_file = wasm_file.replace(".wasm", "_adapted.wasm")
subprocess.check_call([
"wasm-tools", "component", "new",
wasm_file,
"-o", adapted_wasm_file,
"--adapt", os.path.join(root_dir, "wasi_snapshot_preview1.wasm")
])
# Embed "wit" into the component and place it in the expected location
subprocess.check_call([
"wasm-tools", "component", "embed", os.path.join(root_dir, "wit"),
"--world", "process",
adapted_wasm_file,
"-o", wasm_path
])
if __name__ == "__main__":
root_dir = os.getcwd()
pkg_dir = os.path.join(root_dir, "pkg")
# If a specific process is provided, compile it
if len(sys.argv) > 1:
process_dir = os.path.abspath(os.path.join(root_dir, sys.argv[1]))
compile_process(process_dir, pkg_dir, root_dir)
else:
# Compile each base dir folder that has a Cargo.toml
for root, dirs, files in os.walk(root_dir):
if 'Cargo.toml' in files and "process_lib" not in root:
process_dir = os.path.abspath(root)
compile_process(process_dir, pkg_dir, root_dir)

View File

@ -1,5 +1,5 @@
{
"package": "chess2",
"package": "chess",
"publisher": "uqbar",
"version": [0, 1, 0]
"version": [0, 2, 0]
}

View File

@ -1,15 +1,17 @@
#![feature(let_chains)]
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
extern crate base64;
extern crate pleco;
use pleco::Board;
use uqbar_process_lib::uqbar::process::standard as wit;
use uqbar_process_lib::{
get_payload, get_typed_state, grant_messaging, println, receive, set_state, Address, Message,
Payload, ProcessId, Request, Response,
get_payload, get_typed_state, grant_messaging, http, println, receive, set_state, Address,
Message, Payload, ProcessId, Request, Response,
};
mod utils;
wit_bindgen::generate!({
path: "../../wit",
world: "process",
@ -21,7 +23,7 @@ wit_bindgen::generate!({
struct Component;
#[derive(Clone, Debug)]
struct Game {
pub struct Game {
pub id: String, // the node with whom we are playing
pub turns: u64,
pub board: Board,
@ -31,7 +33,7 @@ struct Game {
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct StoredGame {
pub struct StoredGame {
pub id: String, // the node with whom we are playing
pub turns: u64,
pub board: String,
@ -41,132 +43,17 @@ struct StoredGame {
}
#[derive(Clone, Debug)]
struct ChessState {
pub struct ChessState {
pub games: HashMap<String, Game>, // game is by opposing player id
pub records: HashMap<String, (u64, u64, u64)>, // wins, losses, draws
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct StoredChessState {
pub struct StoredChessState {
pub games: HashMap<String, StoredGame>, // game is by opposing player id
pub records: HashMap<String, (u64, u64, u64)>, // wins, losses, draws
}
fn convert_game(game: Game) -> StoredGame {
StoredGame {
id: game.id,
turns: game.turns,
board: game.board.fen(),
white: game.white,
black: game.black,
ended: game.ended,
}
}
fn convert_state(state: ChessState) -> StoredChessState {
StoredChessState {
games: state
.games
.iter()
.map(|(id, game)| (id.to_string(), convert_game(game.clone())))
.collect(),
records: state.records.clone(),
}
}
fn json_game(game: &Game) -> serde_json::Value {
serde_json::json!({
"id": game.id,
"turns": game.turns,
"board": game.board.fen(),
"white": game.white,
"black": game.black,
"ended": game.ended,
})
}
fn send_http_response(
status: u16,
headers: HashMap<String, String>,
payload_bytes: Vec<u8>,
) -> anyhow::Result<()> {
Response::new()
.ipc(
serde_json::json!({
"status": status,
"headers": headers,
})
.to_string()
.as_bytes()
.to_vec(),
)
.payload(Payload {
mime: Some("application/octet-stream".to_string()),
bytes: payload_bytes,
})
.send()
}
fn send_ws_update(our: Address, game: Game) -> anyhow::Result<()> {
Request::new()
.target((&our.node, "encryptor", "sys", "uqbar"))
.ipc(
serde_json::json!({
"EncryptAndForward": {
"channel_id": our.process.to_string(),
"forward_to": {
"node": our.node.clone(),
"process": {
"process_name": "http_server",
"package_name": "sys",
"publisher_node": "uqbar"
}
}, // node, process
"json": Some(serde_json::json!({ // this is the JSON to forward
"WebSocketPush": {
"target": {
"node": our.node.clone(),
"id": "chess", // If the message passed in an ID then we could send to just that ID
}
}
})),
}
})
.to_string()
.as_bytes()
.to_vec(),
)
.payload(Payload {
mime: Some("application/json".to_string()),
bytes: serde_json::json!({
"kind": "game_update",
"data": json_game(&game),
})
.to_string()
.as_bytes()
.to_vec(),
})
.send()
}
fn response_success() -> bool {
let Some(payload) = get_payload() else {
return false;
};
let Ok(status) = String::from_utf8(payload.bytes) else {
return false;
};
status == "success"
}
fn save_chess_state(state: ChessState) {
let stored_state = convert_state(state);
set_state(&bincode::serialize(&stored_state).unwrap());
}
const CHESS_PAGE: &str = include_str!("../pkg/chess.html");
const CHESS_JS: &str = include_str!("../pkg/index.js");
const CHESS_CSS: &str = include_str!("../pkg/index.css");
@ -174,67 +61,56 @@ const CHESS_CSS: &str = include_str!("../pkg/index.css");
impl Guest for Component {
fn init(our: String) {
let our = Address::from_str(&our).unwrap();
println!("chess: start");
println!("{our}: start");
grant_messaging(
&our,
vec![ProcessId::new(Some("http_server"), "sys", "uqbar")],
);
for path in ["/", "/games"] {
let _ = Request::new()
.target((our.node.as_str(), "http_server", "sys", "uqbar"))
.ipc(
serde_json::json!({
"BindPath": {
"path": path,
"authenticated": true,
"local_only": false
}
})
.to_string()
// serve static page at /
// dynamically handle requests to /games
http::bind_http_static_path(
"/",
true,
false,
Some("text/html".to_string()),
CHESS_PAGE
.replace("${node}", &our.node)
.replace("${process}", &our.process.to_string())
// TODO serve these independently on paths..
// also build utils for just serving a vfs dir
.replace("${js}", CHESS_JS)
.replace("${css}", CHESS_CSS)
.as_bytes()
.to_vec(),
)
.send();
}
.unwrap();
http::bind_http_path("/games", true, false).unwrap();
let mut state: ChessState =
match get_typed_state(|bytes| Ok(bincode::deserialize::<StoredChessState>(bytes)?)) {
Some(state) => {
let mut games = HashMap::new();
for (id, game) in state.games {
if let Ok(board) = Board::from_fen(&game.board) {
games.insert(
id,
let mut state: ChessState = match get_typed_state(|bytes| {
Ok(bincode::deserialize::<StoredChessState>(bytes)?)
}) {
Some(mut state) => ChessState {
games: state
.games
.iter_mut()
.map(|(id, game)| {
(
id.clone(),
Game {
id: game.id.clone(),
id: id.to_owned(),
turns: game.turns,
board,
white: game.white.clone(),
black: game.black.clone(),
board: Board::from_fen(&game.board).unwrap_or(Board::start_pos()),
white: game.white.to_owned(),
black: game.black.to_owned(),
ended: game.ended,
},
);
} else {
games.insert(
id,
Game {
id: game.id.clone(),
turns: 0,
board: Board::start_pos(),
white: game.white.clone(),
black: game.black.clone(),
ended: game.ended,
},
);
}
}
ChessState {
games,
)
})
.collect(),
records: state.records,
}
}
},
None => ChessState {
games: HashMap::new(),
records: HashMap::new(),
@ -250,12 +126,9 @@ impl Guest for Component {
println!("chess: got unexpected Response");
continue;
};
match handle_request(&our, &source, &request, &mut state) {
Ok(_) => {}
Err(e) => {
println!("chess: error handling request: {:?}", e);
}
Ok(()) => continue,
Err(e) => println!("chess: error handling request: {:?}", e),
}
}
}
@ -267,21 +140,29 @@ fn handle_request(
request: &wit::Request,
state: &mut ChessState,
) -> anyhow::Result<()> {
let message_json: serde_json::Value = match serde_json::from_slice(&request.ipc) {
Ok(v) => v,
Err(_) => return Err(anyhow::anyhow!("chess: failed to parse ipc JSON, skipping")),
};
// print_to_terminal(1, &format!("chess: parsed ipc JSON: {:?}", message_json));
if source.process == "chess:chess:uqbar" {
let action = message_json["action"].as_str().unwrap_or("");
let game_id = source.node.clone();
let message_json = serde_json::from_slice::<serde_json::Value>(&request.ipc)?;
handle_chess_request(our, source, message_json, state)
} else if source.process.to_string() == "http_server:sys:uqbar" {
let http_request = serde_json::from_slice::<http::IncomingHttpRequest>(&request.ipc)?;
handle_http_request(our, http_request, state)
} else {
return Err(anyhow::anyhow!("chess: got request from unexpected source"));
}
}
fn handle_chess_request(
our: &Address,
source: &Address,
message_json: serde_json::Value,
state: &mut ChessState,
) -> anyhow::Result<()> {
let action = message_json["action"].as_str().unwrap_or("");
let game_id = &source.node;
match action {
"new_game" => {
// make a new game with source.node if the current game has ended
if let Some(game) = state.games.get(&game_id) {
if let Some(game) = state.games.get(game_id) {
if !game.ended {
return Response::new()
.ipc(vec![])
@ -293,24 +174,23 @@ fn handle_request(
}
}
let game = Game {
id: game_id.clone(),
id: game_id.to_string(),
turns: 0,
board: Board::start_pos(),
white: message_json["white"]
.as_str()
.unwrap_or(game_id.as_str())
.unwrap_or(game_id)
.to_string(),
black: message_json["black"]
.as_str()
.unwrap_or(our.node.as_str())
.unwrap_or(&our.node)
.to_string(),
ended: false,
};
state.games.insert(game_id.clone(), game.clone());
state.games.insert(game_id.to_string(), game.clone());
let _ = send_ws_update(our.clone(), game.clone());
save_chess_state(state.clone());
utils::send_ws_update(&our, &game)?;
utils::save_chess_state(&state);
Response::new()
.ipc(vec![])
@ -322,7 +202,7 @@ fn handle_request(
}
"make_move" => {
// check the move and then update if correct and send WS update
let Some(game) = state.games.get_mut(&game_id) else {
let Some(game) = state.games.get_mut(game_id) else {
return Response::new()
.ipc(vec![])
.payload(Payload {
@ -375,8 +255,8 @@ fn handle_request(
}
}
let _ = send_ws_update(our.clone(), game.clone());
save_chess_state(state.clone());
utils::send_ws_update(&our, &game)?;
utils::save_chess_state(&state);
Response::new()
.ipc(vec![])
@ -397,7 +277,7 @@ fn handle_request(
}
"end_game" => {
// end the game and send WS update, update the standings
let Some(game) = state.games.get_mut(&game_id) else {
let Some(game) = state.games.get_mut(game_id) else {
return Response::new()
.ipc(vec![])
.payload(Payload {
@ -415,8 +295,8 @@ fn handle_request(
state.records.insert(game.id.clone(), (1, 0, 0));
}
let _ = send_ws_update(our.clone(), game.clone());
save_chess_state(state.clone());
utils::send_ws_update(&our, &game)?;
utils::save_chess_state(&state);
Response::new()
.ipc(vec![])
@ -428,218 +308,140 @@ fn handle_request(
}
_ => return Err(anyhow::anyhow!("chess: got unexpected action")),
}
} else if source.process.to_string() == "http_server:sys:uqbar" {
let path = message_json["path"].as_str().unwrap_or("");
let method = message_json["method"].as_str().unwrap_or("");
}
let mut default_headers = HashMap::new();
default_headers.insert("Content-Type".to_string(), "text/html".to_string());
// Handle incoming http
match path {
"/" => {
return send_http_response(
200,
default_headers.clone(),
CHESS_PAGE
.replace("${node}", &our.node)
.replace("${process}", &our.process.to_string())
.replace("${js}", CHESS_JS)
.replace("${css}", CHESS_CSS)
.to_string()
.as_bytes()
.to_vec(),
);
}
"/games" => {
match method {
"GET" => {
return send_http_response(
200,
{
let mut headers = default_headers.clone();
headers.insert(
"Content-Type".to_string(),
"application/json".to_string(),
);
headers
},
{
let mut json_games: HashMap<String, serde_json::Value> =
HashMap::new();
for (id, game) in &state.games {
json_games.insert(id.to_string(), json_game(&game));
}
json!(json_games).to_string().as_bytes().to_vec()
},
fn handle_http_request(
our: &Address,
http_request: http::IncomingHttpRequest,
state: &mut ChessState,
) -> anyhow::Result<()> {
if http_request.path()? != "/games" {
return http::send_response(
http::StatusCode::NOT_FOUND,
None,
"Not Found".to_string().as_bytes().to_vec(),
);
}
match http_request.method.as_str() {
"GET" => http::send_response(
http::StatusCode::OK,
Some(HashMap::from([(
String::from("Content-Type"),
String::from("application/json"),
)])),
serde_json::to_vec(&serde_json::json!(state
.games
.iter()
.map(|(id, game)| (id.to_string(), utils::json_game(game)))
.collect::<HashMap<String, serde_json::Value>>()))?,
),
"POST" => {
// create a new game
if let Some(payload) = get_payload() {
if let Ok(payload_json) =
serde_json::from_slice::<serde_json::Value>(&payload.bytes)
let Some(payload) = get_payload() else {
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
};
let payload_json = serde_json::from_slice::<serde_json::Value>(&payload.bytes)?;
let Some(game_id) = payload_json["id"].as_str() else {
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
};
if let Some(game) = state.games.get(game_id)
&& !game.ended
{
let game_id =
String::from(payload_json["id"].as_str().unwrap_or(""));
if game_id == "" {
return send_http_response(
400,
default_headers.clone(),
"Bad Request".to_string().as_bytes().to_vec(),
);
}
return http::send_response(http::StatusCode::CONFLICT, None, vec![]);
};
if let Some(game) = state.games.get(&game_id) {
if !game.ended {
return send_http_response(
409,
default_headers.clone(),
"Conflict".to_string().as_bytes().to_vec(),
);
}
}
let white = payload_json["white"]
let player_white = payload_json["white"]
.as_str()
.unwrap_or(our.node.as_str())
.to_string();
let black = payload_json["black"]
let player_black = payload_json["black"]
.as_str()
.unwrap_or(game_id.as_str())
.unwrap_or(game_id)
.to_string();
// send the other player a new game request
let response = Request::new()
.target((game_id.as_str(), "chess", "chess", "uqbar"))
.ipc(
serde_json::json!({
.target((game_id, "chess", "chess", "uqbar"))
.ipc(serde_json::to_vec(&serde_json::json!({
"action": "new_game",
"white": white.clone(),
"black": black.clone(),
})
.to_string()
.as_bytes()
.to_vec(),
)
"white": player_white.clone(),
"black": player_black.clone(),
}))?)
.send_and_await_response(30)?;
match response {
Ok(_response) => {
if !response_success() {
return send_http_response(
503,
default_headers.clone(),
"Service Unavailable"
.to_string()
.as_bytes()
.to_vec(),
// if they accept, create a new game
// otherwise, should surface error to FE...
let Ok((_source, Message::Response((resp, _context)))) = response else {
return http::send_response(
http::StatusCode::SERVICE_UNAVAILABLE,
None,
"Service Unavailable".to_string().as_bytes().to_vec(),
);
};
if resp.ipc != "success".as_bytes() {
return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]);
}
// create a new game
let game = Game {
id: game_id.clone(),
id: game_id.to_string(),
turns: 0,
board: Board::start_pos(),
white: white.clone(),
black: black.clone(),
white: player_white,
black: player_black,
ended: false,
};
state.games.insert(game_id.clone(), game.clone());
save_chess_state(state.clone());
return send_http_response(
200,
{
let mut headers = default_headers.clone();
headers.insert(
"Content-Type".to_string(),
"application/json".to_string(),
);
headers
},
json_game(&game).to_string().as_bytes().to_vec(),
);
}
Err(_) => {
return send_http_response(
503,
default_headers.clone(),
"Service Unavailable".to_string().as_bytes().to_vec(),
let body = serde_json::to_vec(&utils::json_game(&game))?;
state.games.insert(game_id.to_string(), game);
utils::save_chess_state(&state);
http::send_response(
http::StatusCode::OK,
Some(HashMap::from([(
String::from("Content-Type"),
String::from("application/json"),
)])),
body,
)
}
}
}
}
return send_http_response(
400,
default_headers.clone(),
"Bad Request".to_string().as_bytes().to_vec(),
);
}
"PUT" => {
// make a move
if let Some(payload) = get_payload() {
if let Ok(payload_json) =
serde_json::from_slice::<serde_json::Value>(&payload.bytes)
{
let game_id =
String::from(payload_json["id"].as_str().unwrap_or(""));
if game_id == "" {
return send_http_response(
400,
default_headers.clone(),
"No game ID".to_string().as_bytes().to_vec(),
);
}
if let Some(game) = state.games.get_mut(&game_id) {
let Some(payload) = get_payload() else {
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
};
let payload_json = serde_json::from_slice::<serde_json::Value>(&payload.bytes)?;
let Some(game_id) = payload_json["id"].as_str() else {
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
};
let Some(game) = state.games.get_mut(game_id) else {
return http::send_response(http::StatusCode::NOT_FOUND, None, vec![]);
};
if (game.turns % 2 == 0 && game.white != our.node)
|| (game.turns % 2 == 1 && game.black != our.node)
{
return send_http_response(
403,
default_headers.clone(),
"Forbidden".to_string().as_bytes().to_vec(),
);
return http::send_response(http::StatusCode::FORBIDDEN, None, vec![]);
} else if game.ended {
return send_http_response(
409,
default_headers.clone(),
"Conflict".to_string().as_bytes().to_vec(),
);
return http::send_response(http::StatusCode::CONFLICT, None, vec![]);
}
let move_str = payload_json["move"].as_str().unwrap_or("");
let valid_move = game.board.apply_uci_move(move_str);
if valid_move {
if !game.board.apply_uci_move(move_str) {
// TODO surface illegal move to player or something here
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
}
// send the move to the other player
// check if the game is over
// if so, update the records
let response = Request::new()
.target((game_id.as_str(), "chess", "chess", "uqbar"))
.ipc(
serde_json::json!({
.target((game_id, "chess", "chess", "uqbar"))
.ipc(serde_json::to_vec(&serde_json::json!({
"action": "make_move",
"move": move_str,
})
.to_string()
.as_bytes()
.to_vec(),
)
}))?)
.send_and_await_response(30)?;
match response {
Ok(_response) => {
if !response_success() {
return send_http_response(
503,
default_headers.clone(),
"Service Unavailable"
.to_string()
.as_bytes()
.to_vec(),
);
let Ok((_source, Message::Response((resp, _context)))) = response else {
// TODO surface error to player, let them know other player is
// offline or whatever they respond here was invalid
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
};
if resp.ipc != "success".as_bytes() {
return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]);
}
// update the game
game.turns += 1;
@ -650,29 +452,22 @@ fn handle_request(
game.ended = true;
let winner = if checkmate {
if game.turns % 2 == 1 {
game.white.clone()
&game.white
} else {
game.black.clone()
&game.black
}
} else {
"".to_string()
""
};
// update the records
if draw {
if let Some(record) =
state.records.get_mut(&game.id)
{
if let Some(record) = state.records.get_mut(&game.id) {
record.2 += 1;
} else {
state
.records
.insert(game.id.clone(), (0, 0, 1));
state.records.insert(game.id.clone(), (0, 0, 1));
}
} else {
if let Some(record) =
state.records.get_mut(&game.id)
{
if let Some(record) = state.records.get_mut(&game.id) {
if winner == our.node {
record.0 += 1;
} else {
@ -680,155 +475,75 @@ fn handle_request(
}
} else {
if winner == our.node {
state.records.insert(
game.id.clone(),
(1, 0, 0),
);
state.records.insert(game.id.clone(), (1, 0, 0));
} else {
state.records.insert(
game.id.clone(),
(0, 1, 0),
);
state.records.insert(game.id.clone(), (0, 1, 0));
}
}
}
}
let game = game.clone();
save_chess_state(state.clone());
// game is not over, update state and return to FE
let body = serde_json::to_vec(&utils::json_game(&game))?;
utils::save_chess_state(&state);
// return the game
return send_http_response(
200,
{
let mut headers = default_headers.clone();
headers.insert(
"Content-Type".to_string(),
"application/json".to_string(),
);
headers
},
json_game(&game)
.to_string()
.as_bytes()
.to_vec(),
);
}
Err(_) => {
return send_http_response(
503,
default_headers.clone(),
"Service Unavailable"
.to_string()
.as_bytes()
.to_vec(),
http::send_response(
http::StatusCode::OK,
Some(HashMap::from([(
String::from("Content-Type"),
String::from("application/json"),
)])),
body,
)
}
}
}
}
}
}
println!("chess: never got a response");
return send_http_response(
400,
default_headers.clone(),
"Bad Request".to_string().as_bytes().to_vec(),
);
}
"DELETE" => {
let game_id = message_json["query_params"]["id"]
.as_str()
.unwrap_or("");
if game_id == "" {
return send_http_response(
400,
default_headers.clone(),
"Bad Request".to_string().as_bytes().to_vec(),
);
} else {
let Some(game) = state.games.get_mut(game_id) else {
return send_http_response(
400,
default_headers.clone(),
"Bad Request".to_string().as_bytes().to_vec(),
);
// "end the game"?
let query_params = http_request.query_params()?;
let Some(game_id) = query_params.get("id") else {
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
};
let Some(game) = state.games.get_mut(game_id) else {
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
};
// send the other player an end game request
let response = Request::new()
.target((game_id, "chess", "chess", "uqbar"))
.ipc(
serde_json::json!({
.ipc(serde_json::to_vec(&serde_json::json!({
"action": "end_game",
})
.to_string()
.as_bytes()
.to_vec(),
)
}))?)
.send_and_await_response(30)?;
match response {
Ok(_response) => {
if !response_success() {
return send_http_response(
503,
default_headers.clone(),
"Service Unavailable".to_string().as_bytes().to_vec(),
);
let Ok((_source, Message::Response((resp, _context)))) = response else {
// TODO surface error to player, let them know other player is
// offline or whatever they respond here was invalid
return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]);
};
if resp.ipc != "success".as_bytes() {
return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]);
}
game.ended = true;
if let Some(record) = state.records.get_mut(&game.id) {
record.1 += 1;
} else {
state.records.insert(game.id.clone(), (0, 1, 0));
}
let game = game.clone();
save_chess_state(state.clone());
// return the game
return send_http_response(
200,
{
let mut headers = default_headers.clone();
headers.insert(
"Content-Type".to_string(),
"application/json".to_string(),
);
headers
},
json_game(&game).to_string().as_bytes().to_vec(),
);
}
Err(_) => {
return send_http_response(
503,
default_headers.clone(),
"Service Unavailable".to_string().as_bytes().to_vec(),
);
}
}
}
}
_ => {
return send_http_response(
404,
default_headers.clone(),
"Not Found".to_string().as_bytes().to_vec(),
let body = serde_json::to_vec(&utils::json_game(&game))?;
utils::save_chess_state(&state);
http::send_response(
http::StatusCode::OK,
Some(HashMap::from([(
String::from("Content-Type"),
String::from("application/json"),
)])),
body,
)
}
}
}
_ => {
return send_http_response(
404,
default_headers.clone(),
"Not Found".to_string().as_bytes().to_vec(),
)
}
}
} else {
return Err(anyhow::anyhow!("chess: got request from unexpected source"));
_ => Response::new()
.ipc(serde_json::to_vec(&http::HttpResponse {
status: 405,
headers: HashMap::new(),
})?)
.send(),
}
}

View File

@ -0,0 +1,82 @@
use crate::*;
pub fn save_chess_state(state: &ChessState) {
let stored_state = convert_state(&state);
set_state(&bincode::serialize(&stored_state).unwrap());
}
fn convert_game(game: Game) -> StoredGame {
StoredGame {
id: game.id,
turns: game.turns,
board: game.board.fen(),
white: game.white,
black: game.black,
ended: game.ended,
}
}
fn convert_state(state: &ChessState) -> StoredChessState {
StoredChessState {
games: state
.games
.iter()
.map(|(id, game)| (id.to_string(), convert_game(game.clone())))
.collect(),
records: state.records.clone(),
}
}
pub fn json_game(game: &Game) -> serde_json::Value {
serde_json::json!({
"id": game.id,
"turns": game.turns,
"board": game.board.fen(),
"white": game.white,
"black": game.black,
"ended": game.ended,
})
}
pub fn send_ws_update(our: &Address, game: &Game) -> anyhow::Result<()> {
Request::new()
.target((&our.node, "http_server", "sys", "uqbar"))
.ipc(
serde_json::json!({
"EncryptAndForward": {
"channel_id": our.process.to_string(),
"forward_to": {
"node": our.node.clone(),
"process": {
"process_name": "http_server",
"package_name": "sys",
"publisher_node": "uqbar"
}
}, // node, process
"json": Some(serde_json::json!({ // this is the JSON to forward
"WebSocketPush": {
"target": {
"node": our.node.clone(),
"id": "chess", // If the message passed in an ID then we could send to just that ID
}
}
})),
}
})
.to_string()
.as_bytes()
.to_vec(),
)
.payload(Payload {
mime: Some("application/json".to_string()),
bytes: serde_json::json!({
"kind": "game_update",
"data": json_game(game),
})
.to_string()
.as_bytes()
.to_vec(),
})
.send()
}

View File

@ -1,124 +0,0 @@
#!/usr/bin/env python3
import sys
import json
import base64
import os
import shutil
import http.client
from urllib.parse import urlparse
### helpers
def send_request(path, json_data):
conn = http.client.HTTPConnection(HOST, PORT)
headers = {'Content-Type': 'application/json'}
conn.request("POST", path, json_data, headers)
response = conn.getresponse()
conn.close()
return response
def new_package(package_name, publisher_node, zip_file):
request = {
"node": NODE,
"process": "main:app_store:uqbar",
"inherit": False,
"expects_response": None,
"ipc": json.dumps({
"NewPackage": {
"package": {"package_name": package_name, "publisher_node": publisher_node },
"mirror": True
}
}),
"metadata": None,
"context": None,
"mime": "application/zip",
"data": zip_file
}
return json.dumps(request)
def install_package(package_name, publisher_node):
request = {
"node": NODE,
"process": "main:app_store:uqbar",
"inherit": False,
"expects_response": None,
"ipc": json.dumps({
"Install": {"package_name": package_name, "publisher_node": publisher_node },
}),
"metadata": None,
"context": None,
"mime": None,
"data": None,
}
return json.dumps(request)
# zip a directory
def zip_directory(directory, zip_filename):
shutil.make_archive(zip_filename, 'zip', directory)
# encode a file with base64
def encode_file_in_base64(file_path):
with open(file_path, 'rb') as file:
return base64.b64encode(file.read()).decode('utf-8')
# check if there are enough parameters provided.
if len(sys.argv) < 3 or len(sys.argv) > 4:
print("Usage: python3 start-package.py <url> <pkg-dir> [node-id]")
sys.exit(1)
URL = sys.argv[1]
PKG_DIR = os.path.abspath(sys.argv[2])
# If NODE is provided, use it. Otherwise, set it to None.
NODE = sys.argv[3] if len(sys.argv) == 4 else None
parsed_url = urlparse(URL)
HOST = parsed_url.hostname
PORT = parsed_url.port
# parse metadata.json to get the package and publisher
with open(f"{PKG_DIR}/metadata.json", 'r') as f:
metadata = json.load(f)
PACKAGE = metadata['package']
PUBLISHER = metadata['publisher']
PKG_PUBLISHER = f"{PACKAGE}:{PUBLISHER}"
print(PKG_PUBLISHER)
# create zip and put it in /target
parent_dir = os.path.dirname(PKG_DIR)
parent_dir = os.path.abspath(parent_dir)
os.makedirs(os.path.join(parent_dir, 'target'), exist_ok=True)
zip_filename = os.path.join(parent_dir, 'target', PKG_PUBLISHER)
zip_directory(PKG_DIR, zip_filename)
encoded_zip_file = encode_file_in_base64(zip_filename + '.zip')
# create a new package
new_pkg = new_package(PACKAGE, PUBLISHER, encoded_zip_file)
res = send_request("/rpc:sys:uqbar/message", new_pkg)
if not res.status == 200:
print("Failed to send new package request, status: ", res.status)
sys.exit(1)
# install/start/reboot the package
install_pkg = install_package(PACKAGE, PUBLISHER)
resp = send_request("/rpc:sys:uqbar/message", install_pkg)
if not resp.status == 200:
print("Failed to send install package request, status: ", resp.status)
sys.exit(1)
print("Successfully installed package: ", PKG_PUBLISHER)
sys.exit(0)

View File

@ -177,6 +177,12 @@ async fn http_handler(
) -> Result<impl warp::Reply, warp::Rejection> {
// TODO this is all so dirty. Figure out what actually matters.
println!(
"http_server: got request from {:?} for {}\r",
socket_addr,
path.as_str()
);
// trim trailing "/"
let original_path = normalize_path(path.as_str());
let id: u64 = rand::random();
@ -188,6 +194,8 @@ async fn http_handler(
};
let bound_path = route.handler();
println!("here1\r");
if bound_path.authenticated {
let auth_token = serialized_headers
.get("cookie")
@ -198,6 +206,8 @@ async fn http_handler(
}
}
println!("here2\r");
let is_local = socket_addr
.map(|addr| addr.ip().is_loopback())
.unwrap_or(false);
@ -206,6 +216,8 @@ async fn http_handler(
return Ok(warp::reply::with_status(vec![], StatusCode::FORBIDDEN).into_response());
}
println!("here3\r");
// if path has static content, serve it
if let Some(static_content) = &bound_path.static_content {
return Ok(warp::http::Response::builder()
@ -221,6 +233,8 @@ async fn http_handler(
.into_response());
}
println!("here4\r");
// RPC functionality: if path is /rpc:sys:uqbar/message,
// we extract message from base64 encoded bytes in data
// and send it to the correct app.
@ -376,7 +390,7 @@ async fn handle_rpc_message(
async fn maintain_websocket(
ws: WebSocket,
our: Arc<String>,
jwt_secret_bytes: Arc<Vec<u8>>,
_jwt_secret_bytes: Arc<Vec<u8>>,
ws_senders: WebSocketSenders,
send_to_loop: MessageSender,
) {
@ -386,7 +400,7 @@ async fn maintain_websocket(
// channel and verify their identity using JWT. Then we can forward their
// messages to a specific process.
let owner_process: ProcessId = todo!();
let owner_process: ProcessId = ProcessId::new(Some("chess"), "chess2", "uqbar");
let ws_channel_id: u64 = rand::random();
let (ws_sender, mut ws_receiver) = tokio::sync::mpsc::channel(100);
@ -399,17 +413,44 @@ async fn maintain_websocket(
None => {
// stream closed, remove and exit
websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await;
return;
break;
}
Some(Err(e)) => {
// stream error, remove and exit
println!("http_server websocket channel error: {e}");
websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await;
return;
break;
}
Some(Ok(msg)) => {
// forward message to process associated with this channel
todo!();
let _ = send_to_loop
.send(KernelMessage {
id: rand::random(),
source: Address {
node: our.to_string(),
process: HTTP_SERVER_PROCESS_ID.clone(),
},
target: Address {
node: our.to_string(),
process: owner_process.clone(),
},
rsvp: None,
message: Message::Request(Request {
inherit: false,
expects_response: None,
ipc: serde_json::to_vec(&HttpServerAction::WebSocketPush {
channel_id: ws_channel_id,
message_type: WsMessageType::Binary,
}).unwrap(),
metadata: None,
}),
payload: Some(Payload {
mime: None,
bytes: msg.into_bytes(),
}),
signed_capabilities: None,
})
.await;
}
}
}
@ -421,12 +462,14 @@ async fn maintain_websocket(
// stream error, remove and exit
println!("http_server websocket channel error: {e}");
websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await;
return;
break;
}
}
}
}
}
let stream = write_stream.reunite(read_stream).unwrap();
let _ = stream.close().await;
}
async fn websocket_close(
@ -601,7 +644,6 @@ async fn handle_app_message(
} => {
let mut path_bindings = path_bindings.write().await;
if km.source.process != "homepage:homepage:uqbar" {
// TODO ???
path = if path.starts_with('/') {
format!("/{}{}", km.source.process, path)
} else {