initial commit

This commit is contained in:
Audrey 2025-08-28 18:43:17 -07:00
commit f022b6ac19
7 changed files with 1370 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/target
# Added by cargo
#
# already existing elements were commented out
#/target

1010
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "shelld"
version = "0.1.0"
edition = "2024"
[dependencies]
hyper = { version = "1", features = ["full"] }
tokio = { version = "1", features = ["full"] }
http-body-util = "0.1"
hyper-util = { version = "0.1", features = ["full"] }
futures-util = { version = "0.3", default-features = false }
hyper-tungstenite = "0.18.0"
futures = "0.3.31"
thiserror = "2.0.16"

92
flake.lock Normal file
View File

@ -0,0 +1,92 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 0,
"narHash": "sha256-AyhxamGqcbKhYjeHN/5R0X10i16vJWno2HqDSoKwa6g=",
"path": "/nix/store/mjia8vb4zrv7d00z45k9w1n4aq66xy7v-source",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay",
"utils": "utils"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1756175826,
"narHash": "sha256-cQNnntKWve+vnqo6pGGXl4NFT4dgnMKXl4+bpwLELvU=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "d137b47bde8a6783b961db81254013b454eab46a",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

27
flake.nix Normal file
View File

@ -0,0 +1,27 @@
{
inputs = {
nixpkgs.url = "nixpkgs";
utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = { self, nixpkgs, utils, rust-overlay }:
utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = (import nixpkgs) {
inherit system overlays;
};
in {
devShell = with pkgs; mkShell {
nativeBuildInputs = [
# This sets up the rust suite, automatically selecting the latest nightly version
(rust-bin.selectLatestNightlyWith
(toolchain: toolchain.default.override {
extensions = [ "rust-src" "clippy" "rust-analyzer" ];
}))
];
};
}
);
}

0
src/j Normal file
View File

218
src/main.rs Normal file
View File

@ -0,0 +1,218 @@
use std::collections::BTreeMap;
use std::ops::{Deref, DerefMut};
use std::process::Child;
use std::time::Duration;
use futures::sink::SinkExt;
use futures::stream::StreamExt;
use http_body_util::Full;
use hyper::body::{Bytes, Incoming};
use hyper::{Request, Response};
use hyper_tungstenite::{tungstenite, HyperWebsocket};
use hyper_util::rt::TokioIo;
use tokio::time::sleep;
use tokio::sync::mpsc;
use tungstenite::Message;
type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
use thiserror::Error;
static STATE: std::sync::OnceLock<std::sync::Mutex<BTreeMap<u64, Option<SessionState>>>> = std::sync::OnceLock::new();
pub enum SessionLifecycle {
Prompt,
Execute,
}
pub struct SessionState {
id: u64,
process: Child,
status: SessionLifecycle,
cmd_channel: mpsc::Sender<String>,
}
impl SessionState {
pub async fn submit(&self, command: String) -> Result<(), Error> {
self.cmd_channel.send(command).await?;
Ok(())
}
}
pub struct SessionStateGuard(Option<SessionState>);
impl Deref for SessionStateGuard {
type Target = SessionState;
fn deref(&self) -> &Self::Target {
return self.0.as_ref().unwrap()
}
}
impl DerefMut for SessionStateGuard {
fn deref_mut(&mut self) -> &mut Self::Target {
return self.0.as_mut().unwrap()
}
}
impl Drop for SessionStateGuard {
fn drop(&mut self) {
if let Some(state) = self.0.take() {
STATE.get_or_init(|| std::sync::Mutex::new(BTreeMap::new())).lock().unwrap().insert(state.id, Some(state));
}
}
}
//impl AsyncDrop for SessionStateGuard {
// async fn drop(mut self: std::pin::Pin<&mut Self>) {
// if let Some(state) = self.0.take() {
// let mut map_guard = STATE.get_or_init(async || Arc::new(Mutex::new(BTreeMap::new()))).await.lock().await;
// map_guard.insert(state.id, Some(state));
// }
// }
//}
#[derive(Error, Debug)]
pub enum SessionStateLookupError {
#[error("No such session")]
Missing,
#[error("Session is busy")]
Busy,
}
#[derive(Error, Debug)]
pub enum CommandSubmitError {
#[error("No such session")]
BadSession,
}
pub fn session_state_oneshot(session_id: u64) -> Result<SessionStateGuard, SessionStateLookupError> {
let mut locked = STATE.get_or_init(|| std::sync::Mutex::new(BTreeMap::new())).lock().unwrap();
let Some(state) = locked.get_mut(&session_id) else {
return Err(SessionStateLookupError::Missing);
};
let Some(result) = state.take() else {
return Err(SessionStateLookupError::Busy);
};
Ok(SessionStateGuard(Some(result)))
}
pub async fn session_state(session_id: u64) -> Option<SessionStateGuard> {
loop {
match session_state_oneshot(session_id) {
Ok(state) => break Some(state),
Err(SessionStateLookupError::Busy) => sleep(Duration::from_millis(5)).await,
Err(SessionStateLookupError::Missing) => break None,
}
}
}
async fn submit_command(session_id: u64, command: String) -> Result<(), Error> {
session_state(session_id).await
.ok_or(CommandSubmitError::BadSession)?
.submit(command).await?;
Ok(())
}
async fn handle_request(mut request: Request<Incoming>) -> Result<Response<Full<Bytes>>, Error> {
let headers = request.headers();
let session_id = headers.get("Session")
.and_then(|session| session.to_str().ok())
.and_then(|session| session.parse::<u64>().ok());
let command = headers.get("Command")
.map(|s| s.as_bytes())
.and_then(|s| String::from_utf8(s.to_vec()).ok());
match request.uri().path() {
"/list" => {
todo!()
},
"/new" => {
todo!()
},
"/prompt" => {
todo!()
},
"/complete" => {
todo!()
},
"/run" | "/attach" => {
let Some(session_id) = session_id else {
return Ok(Response::builder().status(404).body(Full::<Bytes>::from("No session"))?);
};
if request.uri().path() == "/run" {
let command = command.unwrap_or("".to_string());
submit_command(session_id, command).await?;
}
if hyper_tungstenite::is_upgrade_request(&request) {
let (response, websocket) = hyper_tungstenite::upgrade(&mut request, None)?;
// Spawn a task to handle the websocket connection.
tokio::spawn(async move {
if let Err(e) = serve_websocket(websocket).await {
eprintln!("Error in websocket connection: {e}");
}
});
Ok(response)
} else {
Ok(Response::new(Full::<Bytes>::from("Ok")))
}
},
"/close" => {
todo!()
},
_ => {
Ok(Response::builder().status(404).body(Full::<Bytes>::from("Not found"))?)
},
}
}
/// Handle a websocket connection.
async fn serve_websocket(websocket: HyperWebsocket) -> Result<(), Error> {
let mut websocket = websocket.await?;
while let Some(message) = websocket.next().await {
match message? {
Message::Text(msg) => {
println!("Received text message: {msg}");
websocket.send(Message::text("Thank you, come again.")).await?;
},
Message::Binary(msg) => {
println!("Received binary message: {msg:02X?}");
websocket.send(Message::binary(b"Thank you, come again.".to_vec())).await?;
},
Message::Ping(_msg) => {},
Message::Pong(_msg) => {}
Message::Close(_msg) => {},
Message::Frame(_msg) => {
unreachable!();
}
}
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Error> {
let addr: std::net::SocketAddr = "[::1]:3000".parse()?;
let listener = tokio::net::TcpListener::bind(&addr).await?;
println!("Listening on http://{addr}");
let mut http = hyper::server::conn::http1::Builder::new();
http.keep_alive(true);
loop {
let (stream, _) = listener.accept().await?;
let connection = http
.serve_connection(TokioIo::new(stream), hyper::service::service_fn(handle_request))
.with_upgrades();
tokio::spawn(async move {
if let Err(err) = connection.await {
println!("Error serving HTTP connection: {err:?}");
}
});
}
}