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" thiserror = "1.0"
tokio = { version = "1.28", features = ["fs", "macros", "rt-multi-thread", "sync"] } tokio = { version = "1.28", features = ["fs", "macros", "rt-multi-thread", "sync"] }
tokio-tungstenite = "*" tokio-tungstenite = "*"
url = "*" url = "2.4.1"
uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "e53c124" } uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "e53c124" }
uuid = { version = "1.1.2", features = ["serde", "v4"] } uuid = { version = "1.1.2", features = ["serde", "v4"] }
warp = "0.3.5" warp = "0.3.5"

View File

@ -50,6 +50,12 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "bytes"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -124,6 +130,21 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 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]] [[package]]
name = "fuchsia-cprng" name = "fuchsia-cprng"
version = "0.1.1" version = "0.1.1"
@ -162,12 +183,33 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 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]] [[package]]
name = "id-arena" name = "id-arena"
version = "2.2.1" version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" 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]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.1.0" version = "2.1.0"
@ -234,6 +276,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "percent-encoding"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]] [[package]]
name = "pleco" name = "pleco"
version = "0.5.0" version = "0.5.0"
@ -532,12 +580,42 @@ dependencies = [
"syn", "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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.12" version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 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]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.10.1" version = "1.10.1"
@ -553,16 +631,30 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]] [[package]]
name = "uqbar_process_lib" name = "uqbar_process_lib"
version = "0.3.0" version = "0.3.0"
source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=db26c5b#db26c5b1607ba6532bcc7687bf8902a21ebd3393"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
"http",
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
"url",
"wit-bindgen", "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]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View File

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

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> { ) -> Result<impl warp::Reply, warp::Rejection> {
// TODO this is all so dirty. Figure out what actually matters. // 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 "/" // trim trailing "/"
let original_path = normalize_path(path.as_str()); let original_path = normalize_path(path.as_str());
let id: u64 = rand::random(); let id: u64 = rand::random();
@ -188,6 +194,8 @@ async fn http_handler(
}; };
let bound_path = route.handler(); let bound_path = route.handler();
println!("here1\r");
if bound_path.authenticated { if bound_path.authenticated {
let auth_token = serialized_headers let auth_token = serialized_headers
.get("cookie") .get("cookie")
@ -198,6 +206,8 @@ async fn http_handler(
} }
} }
println!("here2\r");
let is_local = socket_addr let is_local = socket_addr
.map(|addr| addr.ip().is_loopback()) .map(|addr| addr.ip().is_loopback())
.unwrap_or(false); .unwrap_or(false);
@ -206,6 +216,8 @@ async fn http_handler(
return Ok(warp::reply::with_status(vec![], StatusCode::FORBIDDEN).into_response()); return Ok(warp::reply::with_status(vec![], StatusCode::FORBIDDEN).into_response());
} }
println!("here3\r");
// if path has static content, serve it // if path has static content, serve it
if let Some(static_content) = &bound_path.static_content { if let Some(static_content) = &bound_path.static_content {
return Ok(warp::http::Response::builder() return Ok(warp::http::Response::builder()
@ -221,6 +233,8 @@ async fn http_handler(
.into_response()); .into_response());
} }
println!("here4\r");
// RPC functionality: if path is /rpc:sys:uqbar/message, // RPC functionality: if path is /rpc:sys:uqbar/message,
// we extract message from base64 encoded bytes in data // we extract message from base64 encoded bytes in data
// and send it to the correct app. // and send it to the correct app.
@ -376,7 +390,7 @@ async fn handle_rpc_message(
async fn maintain_websocket( async fn maintain_websocket(
ws: WebSocket, ws: WebSocket,
our: Arc<String>, our: Arc<String>,
jwt_secret_bytes: Arc<Vec<u8>>, _jwt_secret_bytes: Arc<Vec<u8>>,
ws_senders: WebSocketSenders, ws_senders: WebSocketSenders,
send_to_loop: MessageSender, send_to_loop: MessageSender,
) { ) {
@ -386,7 +400,7 @@ async fn maintain_websocket(
// channel and verify their identity using JWT. Then we can forward their // channel and verify their identity using JWT. Then we can forward their
// messages to a specific process. // 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_channel_id: u64 = rand::random();
let (ws_sender, mut ws_receiver) = tokio::sync::mpsc::channel(100); let (ws_sender, mut ws_receiver) = tokio::sync::mpsc::channel(100);
@ -399,17 +413,44 @@ async fn maintain_websocket(
None => { None => {
// stream closed, remove and exit // stream closed, remove and exit
websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await; websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await;
return; break;
} }
Some(Err(e)) => { Some(Err(e)) => {
// stream error, remove and exit // stream error, remove and exit
println!("http_server websocket channel error: {e}"); println!("http_server websocket channel error: {e}");
websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await; websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await;
return; break;
} }
Some(Ok(msg)) => { Some(Ok(msg)) => {
// forward message to process associated with this channel // 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 // stream error, remove and exit
println!("http_server websocket channel error: {e}"); println!("http_server websocket channel error: {e}");
websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await; 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( async fn websocket_close(
@ -601,7 +644,6 @@ async fn handle_app_message(
} => { } => {
let mut path_bindings = path_bindings.write().await; let mut path_bindings = path_bindings.write().await;
if km.source.process != "homepage:homepage:uqbar" { if km.source.process != "homepage:homepage:uqbar" {
// TODO ???
path = if path.starts_with('/') { path = if path.starts_with('/') {
format!("/{}{}", km.source.process, path) format!("/{}{}", km.source.process, path)
} else { } else {