Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
285 changes: 262 additions & 23 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ time = { version = "0.3.44", default-features = false }
tokio = { version = "1.48", default-features = false }
toml = { version = "0.9", default-features = false }
tower = "0.5.2"
tower-livereload = "0.9.6"
#tower-livereload = { git = "https://github.com/leotaku/tower-livereload.git", rev = "05d1d9acf7a265b91e800a6dd3599dd6f0359c8e" }
tower-livereload = "0.9"
tower-sessions = { version = "0.14", default-features = false }
tracing = { version = "0.1", default-features = false }
tracing-subscriber = "0.3"
Expand Down
3 changes: 1 addition & 2 deletions cot-cli/src/new_project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ macro_rules! project_file {
};
}

const PROJECT_FILES: [(&str, &str); 10] = [
const PROJECT_FILES: [(&str, &str); 9] = [
project_file!("Cargo.toml.template"),
project_file!("Cargo.lock.template"),
project_file!("bacon.toml"),
project_file!(".gitignore"),
project_file!("src/main.rs"),
project_file!("src/migrations.rs"),
Expand Down
5 changes: 0 additions & 5 deletions cot-cli/src/project_template/bacon.toml

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
---
source: cot-cli/tests/snapshot_testing/new/mod.rs
assertion_line: 19
description: "Verbosity level: debug"
info:
program: cot
args:
- new
- "-vvvv"
- /tmp/cot-test-o4uWVf/project
- /tmp/cot-test-cGGhse/project
---
success: true
exit_code: 0
----- stdout -----
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.toml"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.lock"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/bacon.toml"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/.gitignore"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/main.rs"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/migrations.rs"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
---
source: cot-cli/tests/snapshot_testing/new/mod.rs
assertion_line: 19
description: "Verbosity level: trace"
info:
program: cot
args:
- new
- "-vvvvv"
- /tmp/cot-test-QUOaBC/project
- /tmp/cot-test-npgw5S/project
---
success: true
exit_code: 0
----- stdout -----
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.toml"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.lock"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/bacon.toml"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/.gitignore"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/main.rs"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/migrations.rs"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: cot-cli/tests/snapshot_testing/new/mod.rs
assertion_line: 39
description: "Verbosity level: debug"
info:
program: cot
Expand All @@ -8,14 +9,13 @@ info:
- "--name"
- my_project
- "-vvvv"
- /tmp/cot-test-BEJYfS/project
- /tmp/cot-test-3W8pnv/project
---
success: true
exit_code: 0
----- stdout -----
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.toml"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.lock"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/bacon.toml"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/.gitignore"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/main.rs"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/migrations.rs"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: cot-cli/tests/snapshot_testing/new/mod.rs
assertion_line: 39
description: "Verbosity level: trace"
info:
program: cot
Expand All @@ -8,14 +9,13 @@ info:
- "--name"
- my_project
- "-vvvvv"
- /tmp/cot-test-IWoQbg/project
- /tmp/cot-test-fHh4vf/project
---
success: true
exit_code: 0
----- stdout -----
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.toml"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.lock"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/bacon.toml"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/.gitignore"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/main.rs"
TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/migrations.rs"
Expand Down
6 changes: 3 additions & 3 deletions cot-macros/src/main_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,21 @@ pub(super) fn fn_to_cot_main(main_function_decl: ItemFn) -> syn::Result<TokenStr
let crate_name = cot_ident();
let result = quote! {
fn main() {
let body = async {
async fn body() {
let project = __cot_main();
#crate_name::run_cli(project).await.expect(
"failed to run the Cot project"
);

#new_main_decl
};
}
#[expect(clippy::expect_used)]
{
return #crate_name::__private::tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(body);
.block_on(::cot::__private::hot_patching::serve(body));
}
}
};
Expand Down
7 changes: 6 additions & 1 deletion cot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ deadpool-redis = { workspace = true, features = ["tokio-comp", "rt_tokio_1"], op
derive_builder.workspace = true
derive_more = { workspace = true, features = ["debug", "deref", "display", "from"] }
digest.workspace = true
#dioxus-devtools = { version = "0.7.2", features = ["serve"], optional = true }
dioxus-devtools = { path = "/home/m4tx/projects/dioxus/packages/devtools/", features = ["serve"], optional = true }
email_address.workspace = true
fake = { workspace = true, optional = true, features = ["derive", "chrono"] }
form_urlencoded.workspace = true
Expand Down Expand Up @@ -57,6 +59,8 @@ serde_json = { workspace = true, optional = true }
serde_path_to_error = { workspace = true }
sha2.workspace = true
sqlx = { workspace = true, features = ["runtime-tokio", "chrono"], optional = true }
#subsecond = { version = "0.7.2", optional = true }
subsecond = { path = "/home/m4tx/projects/dioxus/packages/subsecond/subsecond/", optional = true }
subtle = { workspace = true, features = ["std"] }
swagger-ui-redist = { workspace = true, optional = true }
sync_wrapper.workspace = true
Expand Down Expand Up @@ -103,7 +107,7 @@ ignored = [

[features]
default = ["sqlite", "postgres", "mysql", "json"]
full = ["default", "fake", "live-reload", "test", "cache", "redis"]
full = ["default", "fake", "live-reload", "hot-patching", "test", "cache", "redis"]
fake = ["dep:fake"]
db = ["dep:sea-query", "dep:sea-query-binder", "dep:sqlx"]
sqlite = ["db", "sea-query/backend-sqlite", "sea-query-binder/sqlx-sqlite", "sqlx/sqlite"]
Expand All @@ -114,6 +118,7 @@ json = ["dep:serde_json"]
openapi = ["json", "dep:aide", "dep:schemars"]
swagger-ui = ["openapi", "dep:swagger-ui-redist"]
live-reload = ["dep:tower-livereload"]
hot-patching = ["dep:dioxus-devtools", "dep:subsecond"]
cache = ["json"]
test = []

Expand Down
30 changes: 24 additions & 6 deletions cot/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ pub(crate) fn into_box_request_handler<T, H: RequestHandler<T> + Send + Sync>(
&self,
request: Request,
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + '_>> {
Box::pin(self.0.handle(request))
Box::pin(crate::hot_patching::call_hot(
|req| {
Box::pin(self.0.handle(req))
as Pin<Box<dyn Future<Output = Result<Response>> + Send>>
},
request,
))
}
}

Expand All @@ -78,8 +84,8 @@ macro_rules! impl_request_handler {
where
Func: FnOnce($($ty,)*) -> Fut + Clone + Send + Sync + 'static,
$($ty: FromRequestHead + Send,)*
Fut: Future<Output = R> + Send,
R: IntoResponse,
Fut: Future<Output = R> + Send + 'static,
R: IntoResponse + 'static,
{
#[allow(
clippy::allow_attributes,
Expand All @@ -98,7 +104,12 @@ macro_rules! impl_request_handler {
let $ty = <$ty as FromRequestHead>::from_request_head(&head).await?;
)*

self.clone()($($ty,)*).await.into_response()
$crate::__private::hot_patching::call_async(
move |($($ty,)*)| Box::pin(self.clone()($($ty,)*)) as Pin<Box<dyn Future<Output = R> + Send>>,
($($ty,)*),
)
.await
.into_response()
}
}
};
Expand All @@ -112,7 +123,7 @@ macro_rules! impl_request_handler_from_request {
$($ty_lhs: FromRequestHead + Send,)*
$ty_from_request: FromRequest + Send,
$($ty_rhs: FromRequestHead + Send,)*
Fut: Future<Output = R> + Send,
Fut: Future<Output = R> + Send + 'static,
R: IntoResponse,
{
#[allow(
Expand All @@ -136,7 +147,14 @@ macro_rules! impl_request_handler_from_request {

let $ty_from_request = $ty_from_request::from_request(&head, body).await?;

self.clone()($($ty_lhs,)* $ty_from_request, $($ty_rhs),*).await.into_response()
$crate::__private::hot_patching::call_async(
move |($($ty_lhs,)* $ty_from_request, $($ty_rhs),*)| {
self.clone()($($ty_lhs,)* $ty_from_request, $($ty_rhs),*)
},
($($ty_lhs,)* $ty_from_request, $($ty_rhs),*),
)
.await
.into_response()
}
}
};
Expand Down
108 changes: 108 additions & 0 deletions cot/src/hot_patching.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use std::panic::AssertUnwindSafe;
use std::pin::Pin;

use subsecond::{HotFn, HotFnPanic};

/// Runs given future with [`subsecond`], dropping the future and re-running it
/// when the code changes.
///
/// When the hot-patching feature is not enabled, the function just runs the
/// future once.
#[allow(
clippy::allow_attributes,
reason = "Only happens when hot-patching is enabled"
)]
#[allow(
clippy::future_not_send,
reason = "Send not needed; serve/Bootstrapper is run async in a single thread"
)]
pub async fn serve<O, F>(callback: impl FnMut() -> F)
where
F: Future<Output = O> + 'static,
{
println!("1");

#[cfg(feature = "hot-patching")]
{
println!("2");
// dioxus_devtools::serve_subsecond(callback).await;
dioxus_devtools::connect_subsecond();
let mut callback = callback;
callback().await;
}

#[cfg(not(feature = "hot-patching"))]
{
let mut callback = callback; // avoid "variable does not need to be mutable" warnings
callback().await;
}
}

/// Calls the function using [`subsecond::HotFn`].
///
/// This causes the function passed to be hot-reloadable. If the hot-reloading
/// feature is not enabled, the function is called directly.
pub fn call_hot<F, A, R>(func: F, args: A) -> R
where
F: FnMut(A) -> R,
{
#[cfg(feature = "hot-patching")]
{
let mut hot_fn = subsecond::HotFn::current(func);
hot_fn.call((args,))
}

#[cfg(not(feature = "hot-patching"))]
{
let mut func = func; // avoid "variable does not need to be mutable" warnings
func(args)
}
}

pub fn call_async<F, Fut, A, O>(f: F, args: A) -> Pin<Box<dyn Future<Output = O> + Send>>
where
F: FnOnce(A) -> Fut,
Fut: Future<Output = O> + Send + 'static,
{
// return Box::pin(f(args));

// For FnOnce, we need to handle this differently since we can only call it once
// We'll store the closure in an Option and take it when needed
let mut f_option = Some(f);

// Create a wrapper function that boxes the future
let wrapper = move |args| -> Pin<Box<dyn Future<Output = O> + Send>> {
if let Some(closure) = f_option.take() {
Box::pin(closure(args))
} else {
// This shouldn't happen in normal hot reload scenarios since each
// hot reload creates a new call_async invocation
panic!(
"Hot reload closure already consumed - this indicates a problem with the hot reload system"
)
}
};

let mut hotfn = HotFn::current(wrapper);
loop {
let res = std::panic::catch_unwind(AssertUnwindSafe(|| hotfn.call((args,))));

// If the call succeeds just return the result, otherwise we try to handle the
// panic if its our own.
let err = match res {
Ok(res) => return res,
Err(err) => err,
};

// If this is our panic then let's handle it, otherwise we just resume unwinding
let Some(_hot_payload) = err.downcast_ref::<HotFnPanic>() else {
std::panic::resume_unwind(err);
};

// For hot reload with FnOnce, we can't retry with the same closure
// The hot reload system should create a new function call entirely
panic!(
"Hot reload detected but cannot retry with FnOnce closure - hot reload should create new function instance"
);
}
}
1 change: 1 addition & 0 deletions cot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub mod config;
mod error_page;
#[macro_use]
pub(crate) mod handler;
pub(crate) mod hot_patching;
pub mod html;
#[cfg(feature = "json")]
pub mod json;
Expand Down
2 changes: 1 addition & 1 deletion cot/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use crate::session::store::redis::RedisStore;
use crate::{Body, Error};

#[cfg(feature = "live-reload")]
mod live_reload;
pub(crate) mod live_reload;

#[cfg(feature = "live-reload")]
pub use live_reload::LiveReloadMiddleware;
Expand Down
Loading
Loading