-
Notifications
You must be signed in to change notification settings - Fork 0
Feedback #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feedback
Are you sure you want to change the base?
Feedback #1
Changes from all commits
1714b4f
9968084
7b12e47
314c451
ed85820
938d99c
16f1bc9
fa9dbbe
1340529
2cc4f8a
52a6a88
4f240e5
dbdd7ee
300fae1
b6c1e4a
a50dedf
c69ff8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,129 @@ | ||
# Frobnicator (this is a template) | ||
# CollabRustAtor | ||
|
||
## Authors | ||
- Andrzej Głuszak (@agluszak on GitHub) | ||
- Linus Torvalds (@torvalds on GitHub) | ||
|
||
- Jan Wojtach (@YannVoytaa on GitHub) | ||
|
||
## Description | ||
Frobnicator is going to be a platformer game similar to Super Mario Bros made using Bevy game engine. | ||
|
||
CollabRustAtor is going to be a live collaborative rust code editor and compiler (web app, client-server architecture) | ||
|
||
## Features | ||
- map generator | ||
- shooting | ||
- enemy AI | ||
- game state saving and loading | ||
- scores | ||
|
||
- user can create a 'room' available for others, | ||
- single room contains one text document editable by all room members simultaneously (potential feature- more advanced file structure: more files/directories) | ||
- apart from editing the document, users can compile/run it as a rust file (potential feature- allowing for choosing programming languages other than rust) (for security reasons (infinite loops/...), there would be a time limit for a program to run) | ||
|
||
## Plan | ||
In the first part we're going to implement the basics: movement, physics and shooting. The enemies will simply bounce from one edge of the platform to the other. There will be only a single map. | ||
|
||
In the second part we're going to add random map generator, saving/loading, scores and a more sophisticated AI. | ||
In the first part I'm going to implement basic web-based rust code editor: no rooms, no concurrent editing, only editing and compiling/running would be available. | ||
|
||
In the second part I'm going to add: | ||
|
||
1. rooms- files can be saved to database (one file per one room), other users can see the changes after refreshing the page (or by clicking the button to reload the file content only- for better user experience); | ||
2. concurrent editing- every client will be connected to WebSocket server and send/receive pieces of information about new file changes in certain room. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a jak ma działać to uruchamianie? Jak ma działać kompilowanie? Edytowanie kodu z podświetlaniem składni a uruchamianie go to kompletnie różne rzeczy There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Z tego co widziałem Rust standardowo potrafi wywoływać shellowe komendy (std::process::Command), więc widziałbym to tak:
(W celach bezpieczeństwa (użytkownik może chcieć uruchomić kod z nieskończoną pętlą/ też uruchamiać shellowe komendy/robić inne niebezpieczne rzeczy), komendy miałyby timeout (np. serwer wywołuje "timeout 5 cargo ..." zamiast "cargo ...") i byłyby uruchamiane np. na maszynie wirtualnej) |
||
|
||
## Libraries | ||
- Bevy | ||
- Serde (for serialization) | ||
|
||
- Yew (client-side/frontend) | ||
- Warp (server-side/backend) | ||
- Serde (serialization) | ||
- Syntect (syntax highlighting) | ||
- Reqwest (communication between frontend and REST Api) | ||
- Uuid (generating unique ids (for room ids)) | ||
- log + env_logger (logging) | ||
- redis (Redis client library) | ||
|
||
## Additional tech stack | ||
|
||
- Database (for now- Redis) | ||
|
||
## Running the application | ||
|
||
To run the application locally, you need Redis server up and running (the app stores each room's code piece inside a redis database). To install redis, you can check [this](https://redis.io/docs/getting-started/installation/) link. | ||
|
||
The code is divided into two parts- server and client part. | ||
|
||
To run the server side, go to the 'backend' directory and run | ||
|
||
``` | ||
cargo run | ||
``` | ||
|
||
To check if everyting is working, go to [localhost:8000/health/check](localhost:8000/health/check) (or any url localhost:8000/health/{String}) and check whether it returns 'check' (or the String value provided in the url other than 'check'). | ||
|
||
To run the client side, go to the 'frontend' directory and run | ||
|
||
``` | ||
trunk serve | ||
``` | ||
|
||
Then the website should be available under localhost:8080 (or any other free port; the information should be provided in the terminal after executing the 'serve' command). | ||
|
||
If the backend service is exposed under the port 8000, the project parts should communicate properly (that is, the code is saved for each room, it gets automatically updated for all room participants via Websockets + compiling/running it should produce a result). | ||
|
||
In other case (the address/port of the backend server is different than expected localhost:8000) the Trunk.toml file (located in the frontend/ directory) should be updated in order to establish communication between the two parts (the first proxy record with address "http://127.0.0.1:8000/" refers to REST Api and the second one- to the Websocket). | ||
|
||
It is also possible to host the project and make it public (to use it on multiple devices): | ||
|
||
- Thanks to the Trunk.toml file (which creates a Proxy for all the services with urls in the file) we just have to expose the port with the frontend part | ||
|
||
- One of the easiest solutions (and one which was used during testing/development) is to use ngrok | ||
|
||
- To do so, you can create a free-tier account [here](https://ngrok.com/) and then follow [this](https://ngrok.com/download) 3 step tutorial (install ngrok, add token, expose port (in our case port 8080, unless the frontend part started on another one))) | ||
|
||
- After exposing the port from your main device, you should see the url from which you can access the page from all devices (example url: https://8c5e-178-73-34-162.eu.ngrok.io) (it changes every time you expose a port via ngrok) | ||
|
||
## Example code snippets: | ||
|
||
``` | ||
fn main() { | ||
println!("abc"); | ||
} | ||
// shows 'abc' in the terminal as a result; | ||
``` | ||
|
||
``` | ||
fn main() { | ||
println("abc"); | ||
} | ||
// gives an error with a standard explanation of a rust compiler; | ||
``` | ||
|
||
``` | ||
fn main() { | ||
let a = 1; | ||
println!("abc"); | ||
} | ||
// gives a warning, but shows 'abc' as a result as well; | ||
``` | ||
|
||
``` | ||
fn main() { | ||
loop { | ||
} | ||
} | ||
// results in timeout after 10 seconds. | ||
``` | ||
|
||
``` | ||
fn main() { | ||
let mut x = 1; | ||
loop { | ||
x -= 1; | ||
x /= x; | ||
} | ||
} | ||
// results in a runtime error and 'panic' message. | ||
``` | ||
|
||
## Useful resources | ||
|
||
These are the links to the websites that helped me build the current project and learn about the technologies used: | ||
|
||
- [Yew tutorial](https://yew.rs/docs/tutorial) | ||
- [Building an API with Rust using Tokio and Warp](https://levelup.gitconnected.com/building-an-api-using-warp-and-tokio-26a52173860a) | ||
- [Syntect docs and examples](https://github.com/trishume/syntect) | ||
- [Redis library documentation](https://docs.rs/redis/latest/redis/) | ||
- [Websocket server in rust using Warp](https://blog.logrocket.com/how-to-build-a-websocket-server-with-rust/) | ||
- [Adding Websocket connection to yew frontend app](https://blog.devgenius.io/lets-build-a-websockets-project-with-rust-and-yew-0-19-60720367399f) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Generated by Cargo | ||
# will have compiled files and executables | ||
debug/ | ||
target/ | ||
|
||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries | ||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html | ||
Cargo.lock | ||
|
||
# These are backup files generated by rustfmt | ||
**/*.rs.bk | ||
|
||
# MSVC Windows builds of rustc generate these, which store debugging information | ||
*.pdb | ||
*.in | ||
*.txt |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
[package] | ||
name = "backend" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
tokio = { version = "1.23.0", features = ["macros", "sync", "rt-multi-thread"] } | ||
warp = "0.3" | ||
serde = {version = "1.0", features = ["derive"] } | ||
serde_json = "1.0" | ||
futures = { version = "0.3" } | ||
log = "0.4.17" | ||
env_logger = "0.10.0" | ||
redis = "0.21.5" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
use std::{collections::HashMap, sync::Arc}; | ||
|
||
use crate::run_code::code_handler; | ||
use futures::{lock::Mutex, stream::SplitSink}; | ||
use redis::Client; | ||
use room::{current_room_code_handler, health_handler, room_handler}; | ||
use serde::{Deserialize, Serialize}; | ||
use warp::{ | ||
self, | ||
hyper::Method, | ||
ws::{Message, WebSocket}, | ||
Filter, | ||
}; | ||
pub mod room; | ||
pub mod run_code; | ||
#[derive(Clone, Serialize, Deserialize)] | ||
pub struct Code { | ||
code: String, | ||
} | ||
|
||
async fn handle_rejection( | ||
err: warp::Rejection, | ||
) -> Result<impl warp::Reply, std::convert::Infallible> { | ||
Ok(warp::reply::json(&format!("{:?}", err))) | ||
} | ||
pub type UsersInRoom = Vec<SplitSink<WebSocket, Message>>; | ||
|
||
#[tokio::main] | ||
async fn main() { | ||
env_logger::init(); | ||
let cors = warp::cors() | ||
.allow_any_origin() | ||
.allow_headers(vec![ | ||
"Access-Control-Allow-Headers", | ||
"Access-Control-Request-Method", | ||
"Access-Control-Request-Headers", | ||
"Origin", | ||
"Accept", | ||
"X-Requested-With", | ||
"content-type", | ||
"Host", | ||
"Referer", | ||
"Accept", | ||
"Content-Length", | ||
]) | ||
.allow_methods(&[ | ||
Method::GET, | ||
Method::POST, | ||
Method::PUT, | ||
Method::PATCH, | ||
Method::DELETE, | ||
Method::OPTIONS, | ||
Method::HEAD, | ||
]); | ||
|
||
let active_users: Arc<Mutex<HashMap<String, Arc<Mutex<UsersInRoom>>>>> = | ||
Arc::new(Mutex::new(HashMap::new())); | ||
|
||
let room_route = warp::path!("ws" / "room") | ||
.and(warp::ws()) | ||
.and(warp::any().map(|| redis::Client::open("redis://127.0.0.1").unwrap())) | ||
.and(warp::any().map(move || active_users.clone())) | ||
.and_then( | ||
|ws: warp::ws::Ws, client: Client, active_users| async move { | ||
room_handler(ws, &client, active_users).await | ||
}, | ||
); | ||
|
||
let room_code_route = warp::path!("room" / String / "code") | ||
.and(warp::any().map(|| redis::Client::open("redis://127.0.0.1").unwrap())) | ||
.and_then(|key: String, client: Client| async move { | ||
current_room_code_handler(key, &client).await | ||
}); | ||
|
||
let health_route = warp::path!("health" / String).and_then(health_handler); | ||
|
||
let code_route = warp::path!("code" / String) | ||
.and(warp::post()) | ||
.and(warp::body::json()) | ||
.and_then(code_handler); | ||
|
||
let routes = room_route | ||
.or(room_code_route) | ||
.or(code_route) | ||
.or(health_route) | ||
.recover(handle_rejection) | ||
.with(&cors); | ||
|
||
warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
use futures::lock::Mutex; | ||
use log::info; | ||
use std::collections::HashMap; | ||
use std::sync::Arc; | ||
|
||
use futures::{SinkExt, StreamExt, TryStreamExt}; | ||
use redis::Client; | ||
use warp::{ws::Message, Rejection}; | ||
|
||
use crate::UsersInRoom; | ||
pub async fn current_room_code_handler(key: String, client: &Client) -> Result<String, Rejection> { | ||
let mut conn = client.get_connection().unwrap(); | ||
let code = redis::cmd("GET") | ||
.arg(key) | ||
.query(&mut conn) | ||
.unwrap_or_else(|_| String::from("")); | ||
Ok(code) | ||
} | ||
pub async fn room_handler( | ||
ws: warp::ws::Ws, | ||
client: &Client, | ||
active_users: Arc<Mutex<HashMap<String, Arc<Mutex<UsersInRoom>>>>>, | ||
) -> Result<impl warp::Reply, Rejection> { | ||
let conn_mutex = Mutex::new(client.get_connection().unwrap()); | ||
|
||
Ok(ws.on_upgrade(|mut socket| async move { | ||
let first_response = socket.next().await.unwrap().unwrap(); | ||
let connect_id = first_response.to_str().unwrap_or(""); | ||
let key = connect_id.to_string(); | ||
let (tx, rx) = socket.split(); | ||
info!("new user connected to room {key}"); | ||
{ | ||
let active_users = active_users.clone(); | ||
//let _ = tx.send(Message::text(code)).await; | ||
let mut users_locked = active_users.lock().await; | ||
let room_users = users_locked | ||
.entry(key.clone()) | ||
.or_insert_with(|| Arc::new(Mutex::new(Vec::new()))); | ||
let mut room_users_lock = room_users.lock().await; | ||
room_users_lock.push(tx); | ||
} | ||
info!("added new user connected to room {key}"); | ||
let _ = rx | ||
.try_for_each(|message| { | ||
info!("received message {:?} in room {key}", message); | ||
let active_users = active_users.clone(); | ||
let conn_mutex = &conn_mutex; | ||
let key = &key; | ||
async move { | ||
let mut conn = conn_mutex.lock().await; | ||
if !message.is_text() { | ||
info!("Message is not text"); | ||
return Ok(()); | ||
} | ||
let key = &key; | ||
let active_users = &active_users; | ||
let res = String::from_utf8(message.as_bytes().to_vec()).unwrap(); | ||
let json_res: serde_json::Value = serde_json::from_str(&res).unwrap(); | ||
if let Some(code) = json_res.get("code") { | ||
redis::cmd("SET") | ||
.arg(&(*key).clone()) | ||
.arg(code.as_str()) | ||
.query(&mut *conn) | ||
.unwrap_or_else(|_| String::from("")); | ||
drop(conn); | ||
info!("Received code update"); | ||
} else if let Some(_starts_running) = json_res.get("start_running") { | ||
info!("Room {key} started executing their code"); | ||
} else if let Some(_starts_running) = json_res.get("execution_response") { | ||
info!("Room {key} ended executing their code"); | ||
} | ||
let message = serde_json::to_string(&json_res).unwrap(); | ||
info!("Preparing to send message"); | ||
let mut users_locked = active_users.lock().await; | ||
let user_sockets = users_locked.get_mut(&(*key).clone()).unwrap(); | ||
let mut user_sockets_locked = user_sockets.lock().await; | ||
let sockets_iter = user_sockets_locked.iter_mut(); | ||
info!("Sending message {:?} to all users in room {key}", message); | ||
for socket in sockets_iter { | ||
let _ = socket | ||
.send(Message::text(serde_json::to_string(&message).unwrap())) | ||
.await; | ||
} | ||
info!("Message {:?} sent to all users in room {key}", message); | ||
Ok(()) | ||
} | ||
}) | ||
.await; | ||
})) | ||
} | ||
|
||
pub async fn health_handler(key: String) -> Result<impl warp::Reply, Rejection> { | ||
Ok(key) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to brzmi jak bardzo dużo