this is the worst shit ever but IT WORKS

This commit is contained in:
nora 2023-03-05 00:13:11 +01:00
parent 3e308c9d27
commit 18edf80888
5 changed files with 204 additions and 41 deletions

16
Cargo.lock generated
View file

@ -783,6 +783,20 @@ name = "serde"
version = "1.0.152" version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
@ -1213,6 +1227,8 @@ dependencies = [
"color-eyre", "color-eyre",
"futures", "futures",
"headers", "headers",
"serde",
"serde_json",
"tokio", "tokio",
"tower", "tower",
"tower-http", "tower-http",

View file

@ -10,6 +10,8 @@ axum = { version = "0.6.10", features = ["ws", "headers"] }
color-eyre = "0.6.2" color-eyre = "0.6.2"
futures = "0.3.26" futures = "0.3.26"
headers = "0.3.8" headers = "0.3.8"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
tokio = { version = "1.26.0", features = ["full"] } tokio = { version = "1.26.0", features = ["full"] }
tower = "0.4.13" tower = "0.4.13"
tower-http = { version = "0.4.0", features = ["trace", "fs"] } tower-http = { version = "0.4.0", features = ["trace", "fs"] }

View file

@ -9,14 +9,37 @@
<body> <body>
<button onclick="startWs()">build me a compiler</button> <button onclick="startWs()">build me a compiler</button>
<br />
<br />
<textarea id="stdout" rows="30" cols="80"></textarea>
<textarea id="stderr" rows="30" cols="80"></textarea>
<script> <script>
function startWs() { function startWs() {
console.log("starting ws"); console.log("starting ws");
const stdout = document.getElementById("stdout");
const stderr = document.getElementById("stderr");
const ws = new WebSocket("ws://localhost:3000/ws"); const ws = new WebSocket("ws://localhost:3000/ws");
ws.addEventListener("open", () => { ws.addEventListener("open", () => {
ws.send("uwu!"); ws.send("bootstrap me");
});
ws.addEventListener("message", (event) => {
console.log(event.data);
const msg = JSON.parse(event.data);
console.log(msg);
if ("Stdout" in msg) {
stdout.value += msg.Stdout;
}
if ("Stderr" in msg) {
stdout.value += msg.Stderr;
}
}); });
} }
</script> </script>

128
src/bootstrap.rs Normal file
View file

@ -0,0 +1,128 @@
use std::{path::Path, pin::Pin, process::Stdio};
use axum::extract::ws::{self, WebSocket};
use color_eyre::{eyre::Context, Result};
use futures::{stream::SplitSink, SinkExt};
use tokio::{
io::{AsyncRead, AsyncReadExt},
process::{Child, Command},
};
use crate::ServerMessage;
pub async fn build_a_compiler(
output: &mut SplitSink<WebSocket, ws::Message>,
entrypoint: &Path,
) -> Result<()> {
let cwd = entrypoint.parent().unwrap();
let mut cmd = Command::new(entrypoint);
cmd.current_dir(cwd);
cmd.args(["build", "--stage", "1", "library"]);
// cmd.arg("--help");
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
debug!(?cmd, "about to run command");
let mut cmd = cmd.spawn().context("failed to spawn entrypoint")?;
let stdout = cmd.stdout.take().unwrap();
let stderr = cmd.stderr.take().unwrap();
handle_stdouts(Box::pin(stdout), Box::pin(stderr), cmd, output).await?;
Ok(())
}
async fn handle_stdouts(
mut stdout: Pin<Box<impl AsyncRead>>,
mut stderr: Pin<Box<impl AsyncRead>>,
mut child: Child,
output: &mut SplitSink<WebSocket, ws::Message>,
) -> Result<()> {
// this is a horrible 0 AM hack
let mut stdout_buf = [0; 1024];
let mut stderr_buf = [0; 1024];
loop {
tokio::select! {
stdout_read = stdout.read(&mut stdout_buf) => {
match stdout_read {
Ok(0) => {}
Ok(len) => {
let read = std::str::from_utf8(&stdout_buf[0..len])?;
debug!("Read {len} bytes from stdout");
output.send(ServerMessage::Stdout(read).into()).await?;
}
Err(err) => {
return Err(err.into());
}
}
}
stderr_read = stderr.read(&mut stderr_buf) => {
match stderr_read {
Ok(0) => {}
Ok(len) => {
let read = std::str::from_utf8(&stderr_buf[0..len])?;
debug!("Read {len} bytes from stderr");
output.send(ServerMessage::Stderr(read).into()).await?;
}
Err(err) => {
return Err(err.into());
}
}
}
_ = child.wait() => {
info!("Child process finished");
break;
}
}
}
let mut stdout_done = false;
let mut stderr_done = true;
loop {
tokio::select! {
stdout_read = stdout.read(&mut stdout_buf) => {
match stdout_read {
Ok(0) => {
stdout_done = true;
if stderr_done {
break;
}
}
Ok(len) => {
let read = std::str::from_utf8(&stdout_buf[0..len])?;
output.send(ServerMessage::Stdout(read).into()).await?;
}
Err(err) => {
return Err(err.into());
}
}
}
stderr_read = stderr.read(&mut stderr_buf) => {
match stderr_read {
Ok(0) => {
stderr_done = true;
if stdout_done {
break;
}
}
Ok(len) => {
let read = std::str::from_utf8(&stderr_buf[0..len])?;
output.send(ServerMessage::Stderr(read).into()).await?;
}
Err(err) => {
return Err(err.into());
}
}
}
}
}
Ok(())
}

View file

@ -1,23 +1,26 @@
#[macro_use] #[macro_use]
extern crate tracing; extern crate tracing;
use std::{borrow::Cow, net::SocketAddr, path::PathBuf}; use std::{net::SocketAddr, path::PathBuf};
use axum::{ use axum::{
extract::{ extract::{
ws::{CloseFrame, Message, WebSocket}, ws::{Message, WebSocket},
ConnectInfo, TypedHeader, WebSocketUpgrade, ConnectInfo, TypedHeader, WebSocketUpgrade,
}, },
response::IntoResponse, response::IntoResponse,
routing::get, routing::get,
Router, Router,
}; };
use futures::{sink::SinkExt, stream::StreamExt}; use futures::stream::StreamExt;
use serde::Serialize;
use tower_http::{ use tower_http::{
services::ServeDir, services::ServeDir,
trace::{DefaultMakeSpan, TraceLayer}, trace::{DefaultMakeSpan, TraceLayer},
}; };
use tracing_subscriber::{prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod bootstrap;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@ -47,7 +50,10 @@ async fn main() {
// build our application with some routes // build our application with some routes
let app = Router::new() let app = Router::new()
.fallback_service(ServeDir::new(assets_dir).append_index_html_on_directories(true)) .fallback_service(ServeDir::new(assets_dir).append_index_html_on_directories(true))
.route("/ws", get(ws_handler)) .route(
"/ws",
get(move |ws, user_agent, addr| ws_handler(ws, user_agent, addr, entrypoint.clone())),
)
// logging so we can see whats going on // logging so we can see whats going on
.layer( .layer(
TraceLayer::new_for_http() TraceLayer::new_for_http()
@ -71,6 +77,7 @@ async fn ws_handler(
ws: WebSocketUpgrade, ws: WebSocketUpgrade,
user_agent: Option<TypedHeader<headers::UserAgent>>, user_agent: Option<TypedHeader<headers::UserAgent>>,
ConnectInfo(addr): ConnectInfo<SocketAddr>, ConnectInfo(addr): ConnectInfo<SocketAddr>,
entrypoint: PathBuf,
) -> impl IntoResponse { ) -> impl IntoResponse {
let user_agent = if let Some(TypedHeader(user_agent)) = user_agent { let user_agent = if let Some(TypedHeader(user_agent)) = user_agent {
user_agent.to_string() user_agent.to_string()
@ -80,11 +87,24 @@ async fn ws_handler(
println!("`{user_agent}` at {addr} connected."); println!("`{user_agent}` at {addr} connected.");
// finalize the upgrade process by returning upgrade callback. // finalize the upgrade process by returning upgrade callback.
// we can customize the callback by sending additional info such as address. // we can customize the callback by sending additional info such as address.
ws.on_upgrade(move |socket| handle_socket(socket, addr)) ws.on_upgrade(move |socket| handle_socket(socket, addr, entrypoint))
}
#[derive(Debug, Clone, Serialize)]
enum ServerMessage<'a> {
Stdout(&'a str),
Stderr(&'a str),
}
impl<'a> From<ServerMessage<'a>> for Message {
fn from(value: ServerMessage<'a>) -> Self {
let text = serde_json::to_string(&value).unwrap();
Message::Text(text)
}
} }
/// Actual websocket statemachine (one will be spawned per connection) /// Actual websocket statemachine (one will be spawned per connection)
async fn handle_socket(mut socket: WebSocket, who: SocketAddr) { async fn handle_socket(mut socket: WebSocket, who: SocketAddr, entrypoint: PathBuf) {
//send a ping (unsupported by some browsers) just to kick things off and get a response //send a ping (unsupported by some browsers) just to kick things off and get a response
if socket.send(Message::Ping(vec![1, 2, 3])).await.is_ok() { if socket.send(Message::Ping(vec![1, 2, 3])).await.is_ok() {
info!("Pinged {}...", who); info!("Pinged {}...", who);
@ -112,42 +132,16 @@ async fn handle_socket(mut socket: WebSocket, who: SocketAddr) {
// unsolicited messages to client based on some sort of server's internal event (i.e .timer). // unsolicited messages to client based on some sort of server's internal event (i.e .timer).
let (mut sender, mut receiver) = socket.split(); let (mut sender, mut receiver) = socket.split();
// Spawn a task that will push several messages to the client (does not matter what client does)
let mut send_task = tokio::spawn(async move {
println!("Sending close to {who}...");
if let Err(e) = sender
.send(Message::Close(Some(CloseFrame {
code: axum::extract::ws::close_code::NORMAL,
reason: Cow::from("Goodbye"),
})))
.await
{
println!("Could not send Close due to {}, probably it is ok?", e);
}
});
// This second task will receive messages from client and print them on server console // This second task will receive messages from client and print them on server console
let mut recv_task = tokio::spawn(async move { while let Some(Ok(msg)) = receiver.next().await {
while let Some(Ok(msg)) = receiver.next().await { info!(?msg);
info!(?msg);
}
});
// If any one of the tasks exit, abort the other. if let Message::Text(msg) = msg {
tokio::select! { if msg == "bootstrap me" {
rv_a = (&mut send_task) => { if let Err(err) = bootstrap::build_a_compiler(&mut sender, &entrypoint).await {
match rv_a { error!(%err);
Ok(_) => {}, }
Err(a) => error!("Error sending messages {:?}", a)
} }
recv_task.abort();
},
rv_b = (&mut recv_task) => {
match rv_b {
Ok(_) => {},
Err(b) => error!("Error receiving messages {:?}", b)
}
send_task.abort();
} }
} }