Skip to content

Commit

Permalink
feat: Javascript code autoreload
Browse files Browse the repository at this point in the history
Adds a new watch mode that will auto-reload the Javascript app code and
re-initialize the runner on changes.

Activated with the --watch flag.

Also renames --interface argument to --ip
  • Loading branch information
theduke committed Nov 13, 2023
1 parent ba125d8 commit 2a2c678
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 6 deletions.
5 changes: 5 additions & 0 deletions src/ion_runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ mod fetch_event;
mod performance;
mod request;

mod watch;
pub use watch::WatchRunner;

use std::{
path::Path,
str::FromStr,
Expand Down Expand Up @@ -313,6 +316,8 @@ pub struct IonRunner {
user_code: String,
}

pub type SharedIonRunner = Arc<Mutex<IonRunner>>;

impl IonRunner {
pub fn new(max_threads: usize, user_code: String) -> Self {
if max_threads == 0 {
Expand Down
87 changes: 87 additions & 0 deletions src/ion_runner/watch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::{path::PathBuf, sync::Arc};

use anyhow::Context as _;
use tokio::sync::Mutex;

use super::{IonRunner, SharedIonRunner};

/// Wraps an [`IonRunner`] with auto-reload capabilities.
///
/// Will auto-reload the JS code, and re-initialize the runner on changes.
#[derive(Clone)]
pub struct WatchRunner {
state: Arc<State>,
}

struct State {
js_path: PathBuf,
max_js_threads: usize,
mutable: Mutex<Option<MutableState>>,
}

struct MutableState {
contents: String,
runner: SharedIonRunner,
}

impl WatchRunner {
pub fn new(js_path: PathBuf, max_js_threads: usize) -> Self {
Self {
state: Arc::new(State {
js_path,
max_js_threads,
mutable: Mutex::new(None),
}),
}
}

async fn acquire_runner(&self) -> Result<SharedIonRunner, anyhow::Error> {
// WASIX does not support inotify file watching APIs, so we naively
// load the full file contents on every request.
//
// This is slow, but this should be sufficient for debugging.
//
// We should investigate just going by the file modification time.
// but loading the full contents is more reliable.

let mut mutable = self.state.mutable.lock().await;

let contents = tokio::fs::read_to_string(&self.state.js_path)
.await
.with_context(|| {
format!(
"Could not read Javascript file at '{}'",
self.state.js_path.display()
)
})?;

if let Some(mutable) = mutable.as_mut() {
if mutable.contents == contents {
return Ok(mutable.runner.clone());
}
}

tracing::info!(path=%self.state.js_path.display(), "reloaded application code");

let runner = IonRunner::new_request_handler(self.state.max_js_threads, contents.clone());
*mutable = Some(MutableState {
contents,
runner: runner.clone(),
});

Ok(runner)
}
}

#[async_trait::async_trait]
impl crate::server::RequestHandler for WatchRunner {
async fn handle(
&self,
addr: std::net::SocketAddr,
req: http::request::Parts,
body: Option<bytes::Bytes>,
) -> Result<hyper::Response<hyper::Body>, anyhow::Error> {
let runner = self.acquire_runner().await?;
runner.handle(addr, req, body).await
}
}
23 changes: 17 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,7 @@ async fn run() -> Result<(), anyhow::Error> {
)
})?;

// let listen = cmd.liste


let interface = if let Some(iface) = cmd.interface {
let interface = if let Some(iface) = cmd.ip {
iface
} else if let Ok(value) = std::env::var("LISTEN_IP") {
value
Expand All @@ -105,8 +102,18 @@ async fn run() -> Result<(), anyhow::Error> {
runtime::config::CONFIG
.set(runtime::config::Config::default().log_level(runtime::config::LogLevel::Error))
.unwrap();

if cmd.watch {
let runner = ion_runner::WatchRunner::new(cmd.js_path.clone(), cmd.max_js_threads);
crate::server::run_server(config, runner).await
} else {
let runner = ion_runner::IonRunner::new_request_handler(cmd.max_js_threads, code);
crate::server::run_server(config, runner).await
}
}
}
}

/// winterjs CLI
#[derive(clap::Parser, Debug)]
#[clap(version)]
Expand All @@ -130,13 +137,17 @@ struct CmdServe {

/// The interface to listen on.
/// Defaults to 127.0.0.1
#[clap(long, default_value = "127.0.0.1", env = "WINTERJS_INTERFACE")]
interface: Option<IpAddr>,
#[clap(long, default_value = "127.0.0.1", env = "WINTERJS_IP")]
ip: Option<IpAddr>,

/// Maximum amount of Javascript worker threads to spawn.
#[clap(long, default_value = "16", env = "WINTERJS_MAX_JS_THREADS")]
max_js_threads: usize,

/// Watch the Javascript file for changes and automatically reload.
#[clap(short, long, env = "WINTERJS_WATCH")]
watch: bool,

/// Path to a Javascript file to serve.
#[clap(env = "JS_PATH")]
js_path: PathBuf,
Expand Down

0 comments on commit 2a2c678

Please sign in to comment.