From 84a5bbbe6cb74389983fcaee07f170e0043bd820 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Tue, 21 Nov 2023 17:49:18 -0500 Subject: [PATCH] chess work --- Cargo.toml | 2 +- modules/chess/Cargo.lock | 92 ++ modules/chess/Cargo.toml | 2 +- modules/chess/build.py | 79 -- modules/chess/pkg/metadata.json | 4 +- modules/chess/src/lib.rs | 1205 ++++++++------------- modules/chess/src/utils.rs | 82 ++ modules/chess/start-package.py | 124 --- modules/chess/wasi_snapshot_preview1.wasm | Bin 109411 -> 0 bytes src/http/server.rs | 56 +- 10 files changed, 687 insertions(+), 959 deletions(-) delete mode 100644 modules/chess/build.py create mode 100644 modules/chess/src/utils.rs delete mode 100644 modules/chess/start-package.py delete mode 100644 modules/chess/wasi_snapshot_preview1.wasm diff --git a/Cargo.toml b/Cargo.toml index 7892db10..f8cc5868 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/modules/chess/Cargo.lock b/modules/chess/Cargo.lock index 013617c7..2c884776 100644 --- a/modules/chess/Cargo.lock +++ b/modules/chess/Cargo.lock @@ -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" diff --git a/modules/chess/Cargo.toml b/modules/chess/Cargo.toml index e145eaa1..75ce876d 100644 --- a/modules/chess/Cargo.toml +++ b/modules/chess/Cargo.toml @@ -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] diff --git a/modules/chess/build.py b/modules/chess/build.py deleted file mode 100644 index 76cac8b5..00000000 --- a/modules/chess/build.py +++ /dev/null @@ -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) diff --git a/modules/chess/pkg/metadata.json b/modules/chess/pkg/metadata.json index 699a9189..59360b8e 100644 --- a/modules/chess/pkg/metadata.json +++ b/modules/chess/pkg/metadata.json @@ -1,5 +1,5 @@ { - "package": "chess2", + "package": "chess", "publisher": "uqbar", - "version": [0, 1, 0] + "version": [0, 2, 0] } diff --git a/modules/chess/src/lib.rs b/modules/chess/src/lib.rs index 2f58c349..1456d0be 100644 --- a/modules/chess/src/lib.rs +++ b/modules/chess/src/lib.rs @@ -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, // game is by opposing player id pub records: HashMap, // wins, losses, draws } #[derive(Clone, Debug, Serialize, Deserialize)] -struct StoredChessState { +pub struct StoredChessState { pub games: HashMap, // game is by opposing player id pub records: HashMap, // 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, - payload_bytes: Vec, -) -> 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,72 +61,61 @@ 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() - .as_bytes() - .to_vec(), - ) - .send(); - } + // 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(), + ) + .unwrap(); + http::bind_http_path("/games", true, false).unwrap(); - let mut state: ChessState = - match get_typed_state(|bytes| Ok(bincode::deserialize::(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, - Game { - id: game.id.clone(), - turns: game.turns, - board, - white: game.white.clone(), - black: game.black.clone(), - 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, - records: state.records, - } - } - None => ChessState { - games: HashMap::new(), - records: HashMap::new(), - }, - }; + let mut state: ChessState = match get_typed_state(|bytes| { + Ok(bincode::deserialize::(bytes)?) + }) { + Some(mut state) => ChessState { + games: state + .games + .iter_mut() + .map(|(id, game)| { + ( + id.clone(), + Game { + id: id.to_owned(), + turns: game.turns, + board: Board::from_fen(&game.board).unwrap_or(Board::start_pos()), + white: game.white.to_owned(), + black: game.black.to_owned(), + ended: game.ended, + }, + ) + }) + .collect(), + records: state.records, + }, + None => ChessState { + games: HashMap::new(), + records: HashMap::new(), + }, + }; loop { let Ok((source, message)) = receive() else { @@ -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,568 +140,410 @@ 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(); - - 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 !game.ended { - return Response::new() - .ipc(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "conflict".as_bytes().to_vec(), - }) - .send(); - } - } - let game = Game { - id: game_id.clone(), - turns: 0, - board: Board::start_pos(), - white: message_json["white"] - .as_str() - .unwrap_or(game_id.as_str()) - .to_string(), - black: message_json["black"] - .as_str() - .unwrap_or(our.node.as_str()) - .to_string(), - ended: false, - }; - state.games.insert(game_id.clone(), game.clone()); - - let _ = send_ws_update(our.clone(), game.clone()); - - save_chess_state(state.clone()); - - Response::new() - .ipc(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "success".as_bytes().to_vec(), - }) - .send() - } - "make_move" => { - // check the move and then update if correct and send WS update - let Some(game) = state.games.get_mut(&game_id) else { - return Response::new() - .ipc(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "not found".as_bytes().to_vec(), - }) - .send(); - }; - let valid_move = game - .board - .apply_uci_move(message_json["move"].as_str().unwrap_or("")); - if valid_move { - game.turns += 1; - let checkmate = game.board.checkmate(); - let draw = game.board.stalemate(); - - if checkmate || draw { - game.ended = true; - let winner = if checkmate { - if game.turns % 2 == 1 { - game.white.clone() - } else { - game.black.clone() - } - } else { - "".to_string() - }; - - // update the records - if draw { - if let Some(record) = state.records.get_mut(&game.id) { - record.2 += 1; - } else { - state.records.insert(game.id.clone(), (0, 0, 1)); - } - } else { - if let Some(record) = state.records.get_mut(&game.id) { - if winner == our.node { - record.0 += 1; - } else { - record.1 += 1; - } - } else { - if winner == our.node { - state.records.insert(game.id.clone(), (1, 0, 0)); - } else { - state.records.insert(game.id.clone(), (0, 1, 0)); - } - } - } - } - - let _ = send_ws_update(our.clone(), game.clone()); - save_chess_state(state.clone()); - - Response::new() - .ipc(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "success".as_bytes().to_vec(), - }) - .send() - } else { - Response::new() - .ipc(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "invalid move".as_bytes().to_vec(), - }) - .send() - } - } - "end_game" => { - // end the game and send WS update, update the standings - let Some(game) = state.games.get_mut(&game_id) else { - return Response::new() - .ipc(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "not found".as_bytes().to_vec(), - }) - .send(); - }; - - game.ended = true; - - if let Some(record) = state.records.get_mut(&game.id) { - record.0 += 1; - } else { - state.records.insert(game.id.clone(), (1, 0, 0)); - } - - let _ = send_ws_update(our.clone(), game.clone()); - save_chess_state(state.clone()); - - Response::new() - .ipc(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "success".as_bytes().to_vec(), - }) - .send() - } - _ => return Err(anyhow::anyhow!("chess: got unexpected action")), - } + let message_json = serde_json::from_slice::(&request.ipc)?; + handle_chess_request(our, source, message_json, state) } 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 = - 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() - }, - ); - } - "POST" => { - // create a new game - if let Some(payload) = get_payload() { - if let Ok(payload_json) = - serde_json::from_slice::(&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(), - "Bad Request".to_string().as_bytes().to_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"] - .as_str() - .unwrap_or(our.node.as_str()) - .to_string(); - let black = payload_json["black"] - .as_str() - .unwrap_or(game_id.as_str()) - .to_string(); - - let response = Request::new() - .target((game_id.as_str(), "chess", "chess", "uqbar")) - .ipc( - serde_json::json!({ - "action": "new_game", - "white": white.clone(), - "black": black.clone(), - }) - .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(), - ); - } - // create a new game - let game = Game { - id: game_id.clone(), - turns: 0, - board: Board::start_pos(), - white: white.clone(), - black: black.clone(), - 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(), - ) - } - } - } - } - 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::(&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) { - 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(), - ); - } else if game.ended { - return send_http_response( - 409, - default_headers.clone(), - "Conflict".to_string().as_bytes().to_vec(), - ); - } - - let move_str = payload_json["move"].as_str().unwrap_or(""); - let valid_move = game.board.apply_uci_move(move_str); - if valid_move { - // 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!({ - "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(), - ); - } - // update the game - game.turns += 1; - let checkmate = game.board.checkmate(); - let draw = game.board.stalemate(); - - if checkmate || draw { - game.ended = true; - let winner = if checkmate { - if game.turns % 2 == 1 { - game.white.clone() - } else { - game.black.clone() - } - } else { - "".to_string() - }; - - // update the records - if draw { - if let Some(record) = - state.records.get_mut(&game.id) - { - record.2 += 1; - } else { - state - .records - .insert(game.id.clone(), (0, 0, 1)); - } - } else { - if let Some(record) = - state.records.get_mut(&game.id) - { - if winner == our.node { - record.0 += 1; - } else { - record.1 += 1; - } - } else { - if winner == our.node { - state.records.insert( - game.id.clone(), - (1, 0, 0), - ); - } 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(), - ) - } - } - } - } - } - } - - 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(), - ); - }; - let response = Request::new() - .target((game_id, "chess", "chess", "uqbar")) - .ipc( - 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(), - ); - } - - 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(), - ) - } - } - } - _ => { - return send_http_response( - 404, - default_headers.clone(), - "Not Found".to_string().as_bytes().to_vec(), - ) - } - } + let http_request = serde_json::from_slice::(&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 !game.ended { + return Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "conflict".as_bytes().to_vec(), + }) + .send(); + } + } + let game = Game { + id: game_id.to_string(), + turns: 0, + board: Board::start_pos(), + white: message_json["white"] + .as_str() + .unwrap_or(game_id) + .to_string(), + black: message_json["black"] + .as_str() + .unwrap_or(&our.node) + .to_string(), + ended: false, + }; + state.games.insert(game_id.to_string(), game.clone()); + + utils::send_ws_update(&our, &game)?; + utils::save_chess_state(&state); + + Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "success".as_bytes().to_vec(), + }) + .send() + } + "make_move" => { + // check the move and then update if correct and send WS update + let Some(game) = state.games.get_mut(game_id) else { + return Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "not found".as_bytes().to_vec(), + }) + .send(); + }; + let valid_move = game + .board + .apply_uci_move(message_json["move"].as_str().unwrap_or("")); + if valid_move { + game.turns += 1; + let checkmate = game.board.checkmate(); + let draw = game.board.stalemate(); + + if checkmate || draw { + game.ended = true; + let winner = if checkmate { + if game.turns % 2 == 1 { + game.white.clone() + } else { + game.black.clone() + } + } else { + "".to_string() + }; + + // update the records + if draw { + if let Some(record) = state.records.get_mut(&game.id) { + record.2 += 1; + } else { + state.records.insert(game.id.clone(), (0, 0, 1)); + } + } else { + if let Some(record) = state.records.get_mut(&game.id) { + if winner == our.node { + record.0 += 1; + } else { + record.1 += 1; + } + } else { + if winner == our.node { + state.records.insert(game.id.clone(), (1, 0, 0)); + } else { + state.records.insert(game.id.clone(), (0, 1, 0)); + } + } + } + } + + utils::send_ws_update(&our, &game)?; + utils::save_chess_state(&state); + + Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "success".as_bytes().to_vec(), + }) + .send() + } else { + Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "invalid move".as_bytes().to_vec(), + }) + .send() + } + } + "end_game" => { + // end the game and send WS update, update the standings + let Some(game) = state.games.get_mut(game_id) else { + return Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "not found".as_bytes().to_vec(), + }) + .send(); + }; + + game.ended = true; + + if let Some(record) = state.records.get_mut(&game.id) { + record.0 += 1; + } else { + state.records.insert(game.id.clone(), (1, 0, 0)); + } + + utils::send_ws_update(&our, &game)?; + utils::save_chess_state(&state); + + Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "success".as_bytes().to_vec(), + }) + .send() + } + _ => return Err(anyhow::anyhow!("chess: got unexpected action")), + } +} + +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::>()))?, + ), + "POST" => { + // create a new game + let Some(payload) = get_payload() else { + return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); + }; + let payload_json = serde_json::from_slice::(&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 + { + return http::send_response(http::StatusCode::CONFLICT, None, vec![]); + }; + + let player_white = payload_json["white"] + .as_str() + .unwrap_or(our.node.as_str()) + .to_string(); + let player_black = payload_json["black"] + .as_str() + .unwrap_or(game_id) + .to_string(); + + // send the other player a new game request + let response = Request::new() + .target((game_id, "chess", "chess", "uqbar")) + .ipc(serde_json::to_vec(&serde_json::json!({ + "action": "new_game", + "white": player_white.clone(), + "black": player_black.clone(), + }))?) + .send_and_await_response(30)?; + // 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.to_string(), + turns: 0, + board: Board::start_pos(), + white: player_white, + black: player_black, + ended: false, + }; + 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, + ) + } + "PUT" => { + // make a move + let Some(payload) = get_payload() else { + return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); + }; + let payload_json = serde_json::from_slice::(&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 http::send_response(http::StatusCode::FORBIDDEN, None, vec![]); + } else if game.ended { + return http::send_response(http::StatusCode::CONFLICT, None, vec![]); + } + let move_str = payload_json["move"].as_str().unwrap_or(""); + 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, "chess", "chess", "uqbar")) + .ipc(serde_json::to_vec(&serde_json::json!({ + "action": "make_move", + "move": move_str, + }))?) + .send_and_await_response(30)?; + 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; + let checkmate = game.board.checkmate(); + let draw = game.board.stalemate(); + + if checkmate || draw { + game.ended = true; + let winner = if checkmate { + if game.turns % 2 == 1 { + &game.white + } else { + &game.black + } + } else { + "" + }; + // update the records + if draw { + if let Some(record) = state.records.get_mut(&game.id) { + record.2 += 1; + } else { + state.records.insert(game.id.clone(), (0, 0, 1)); + } + } else { + if let Some(record) = state.records.get_mut(&game.id) { + if winner == our.node { + record.0 += 1; + } else { + record.1 += 1; + } + } else { + if winner == our.node { + state.records.insert(game.id.clone(), (1, 0, 0)); + } else { + state.records.insert(game.id.clone(), (0, 1, 0)); + } + } + } + } + // 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 + http::send_response( + http::StatusCode::OK, + Some(HashMap::from([( + String::from("Content-Type"), + String::from("application/json"), + )])), + body, + ) + } + "DELETE" => { + // "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::to_vec(&serde_json::json!({ + "action": "end_game", + }))?) + .send_and_await_response(30)?; + 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)); + } + // return the game + 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, + ) + } + _ => Response::new() + .ipc(serde_json::to_vec(&http::HttpResponse { + status: 405, + headers: HashMap::new(), + })?) + .send(), + } +} diff --git a/modules/chess/src/utils.rs b/modules/chess/src/utils.rs new file mode 100644 index 00000000..4416af91 --- /dev/null +++ b/modules/chess/src/utils.rs @@ -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() +} diff --git a/modules/chess/start-package.py b/modules/chess/start-package.py deleted file mode 100644 index 35b3b6b3..00000000 --- a/modules/chess/start-package.py +++ /dev/null @@ -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 [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) diff --git a/modules/chess/wasi_snapshot_preview1.wasm b/modules/chess/wasi_snapshot_preview1.wasm deleted file mode 100644 index 9b621b988c95abc793f1e0c49e2290495b04dcc4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109411 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�Ok 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 Result { // 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, - jwt_secret_bytes: Arc>, + _jwt_secret_bytes: Arc>, 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 {