diff --git a/Cargo.lock b/Cargo.lock index 82fff1ba..2427a0dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,9 +302,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -428,9 +428,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.50" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "shlex", @@ -616,6 +616,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -692,6 +712,7 @@ dependencies = [ "derive_builder", "derive_more", "digest", + "dioxus-devtools", "email_address", "fake", "fantoccini", @@ -726,6 +747,7 @@ dependencies = [ "serde_urlencoded", "sha2", "sqlx", + "subsecond", "subtle", "swagger-ui-redist", "sync_wrapper", @@ -984,6 +1006,12 @@ dependencies = [ "syn", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + [[package]] name = "deadpool" version = "0.12.3" @@ -1114,6 +1142,75 @@ dependencies = [ "subtle", ] +[[package]] +name = "dioxus-cli-config" +version = "0.7.2" + +[[package]] +name = "dioxus-core" +version = "0.7.2" +dependencies = [ + "anyhow", + "const_format", + "dioxus-core-types", + "futures-channel", + "futures-util", + "generational-box", + "longest-increasing-subsequence", + "rustc-hash", + "rustversion", + "serde", + "slab", + "slotmap", + "subsecond", + "tracing", +] + +[[package]] +name = "dioxus-core-types" +version = "0.7.2" + +[[package]] +name = "dioxus-devtools" +version = "0.7.2" +dependencies = [ + "dioxus-cli-config", + "dioxus-core", + "dioxus-devtools-types", + "dioxus-signals", + "futures-channel", + "futures-util", + "serde", + "serde_json", + "subsecond", + "thiserror 2.0.17", + "tracing", + "tungstenite", +] + +[[package]] +name = "dioxus-devtools-types" +version = "0.7.2" +dependencies = [ + "dioxus-core", + "serde", + "subsecond-types", +] + +[[package]] +name = "dioxus-signals" +version = "0.7.2" +dependencies = [ + "dioxus-core", + "futures-channel", + "futures-util", + "generational-box", + "parking_lot", + "rustc-hash", + "tracing", + "warnings", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1344,9 +1441,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "fixedbitset" @@ -1515,6 +1612,14 @@ dependencies = [ "slab", ] +[[package]] +name = "generational-box" +version = "0.7.2" +dependencies = [ + "parking_lot", + "tracing", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -2088,9 +2193,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" @@ -2126,6 +2231,16 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.15" @@ -2134,13 +2249,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags", "libc", - "redox_syscall 0.6.0", + "redox_syscall 0.7.0", ] [[package]] @@ -2188,6 +2303,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "longest-increasing-subsequence" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" + [[package]] name = "matchers" version = "0.2.0" @@ -2219,6 +2340,24 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + [[package]] name = "mime" version = "0.3.17" @@ -2615,6 +2754,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2754,9 +2913,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] @@ -2887,9 +3046,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ "bitflags", ] @@ -3043,9 +3202,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "same-file" @@ -3203,9 +3362,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.147" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", @@ -3285,10 +3444,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -3320,6 +3480,16 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "serde", + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -3573,6 +3743,30 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subsecond" +version = "0.7.2" +dependencies = [ + "js-sys", + "libc", + "libloading", + "memfd", + "memmap2", + "serde", + "subsecond-types", + "thiserror 2.0.17", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "subsecond-types" +version = "0.7.2" +dependencies = [ + "serde", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4130,6 +4324,23 @@ dependencies = [ "toml", ] +[[package]] +name = "tungstenite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + [[package]] name = "typenum" version = "1.19.0" @@ -4193,6 +4404,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4251,6 +4468,28 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warnings" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f68998838dab65727c9b30465595c6f7c953313559371ca8bf31759b3680ad" +dependencies = [ + "pin-project", + "tracing", + "warnings-macro", +] + +[[package]] +name = "warnings-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59195a1db0e95b920366d949ba5e0d3fc0e70b67c09be15ce5abb790106b0571" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -4860,6 +5099,6 @@ dependencies = [ [[package]] name = "zmij" -version = "0.1.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e404bcd8afdaf006e529269d3e85a743f9480c3cef60034d77860d02964f3ba" +checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" diff --git a/Cargo.toml b/Cargo.toml index fa089aa4..b464fa9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/cot-cli/src/new_project.rs b/cot-cli/src/new_project.rs index 1241bc15..254f2090 100644 --- a/cot-cli/src/new_project.rs +++ b/cot-cli/src/new_project.rs @@ -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"), diff --git a/cot-cli/src/project_template/bacon.toml b/cot-cli/src/project_template/bacon.toml deleted file mode 100644 index fbbca7d5..00000000 --- a/cot-cli/src/project_template/bacon.toml +++ /dev/null @@ -1,5 +0,0 @@ -[jobs.serve] -command = ["cargo", "run"] -background = false -on_change_strategy = "kill_then_restart" -watch = ["templates", "static"] diff --git a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-5.snap b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-5.snap index 28005faf..d4b8e043 100644 --- a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-5.snap +++ b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-5.snap @@ -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" diff --git a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-6.snap b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-6.snap index f1a8e8da..c584c528 100644 --- a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-6.snap +++ b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-6.snap @@ -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" diff --git a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-5.snap b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-5.snap index e9fb34e5..df02b840 100644 --- a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-5.snap +++ b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-5.snap @@ -1,5 +1,6 @@ --- source: cot-cli/tests/snapshot_testing/new/mod.rs +assertion_line: 39 description: "Verbosity level: debug" info: program: cot @@ -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" diff --git a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-6.snap b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-6.snap index 2d703e8f..d0b9ad96 100644 --- a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-6.snap +++ b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-6.snap @@ -1,5 +1,6 @@ --- source: cot-cli/tests/snapshot_testing/new/mod.rs +assertion_line: 39 description: "Verbosity level: trace" info: program: cot @@ -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" diff --git a/cot-macros/src/main_fn.rs b/cot-macros/src/main_fn.rs index 804b3f3a..4d39a61f 100644 --- a/cot-macros/src/main_fn.rs +++ b/cot-macros/src/main_fn.rs @@ -18,21 +18,21 @@ pub(super) fn fn_to_cot_main(main_function_decl: ItemFn) -> syn::Result + Send + Sync>( &self, request: Request, ) -> Pin> + Send + '_>> { - Box::pin(self.0.handle(request)) + Box::pin(crate::hot_patching::call_hot( + |req| { + Box::pin(self.0.handle(req)) + as Pin> + Send>> + }, + request, + )) } } @@ -78,8 +84,8 @@ macro_rules! impl_request_handler { where Func: FnOnce($($ty,)*) -> Fut + Clone + Send + Sync + 'static, $($ty: FromRequestHead + Send,)* - Fut: Future + Send, - R: IntoResponse, + Fut: Future + Send + 'static, + R: IntoResponse + 'static, { #[allow( clippy::allow_attributes, @@ -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 + Send>>, + ($($ty,)*), + ) + .await + .into_response() } } }; @@ -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 + Send, + Fut: Future + Send + 'static, R: IntoResponse, { #[allow( @@ -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() } } }; diff --git a/cot/src/hot_patching.rs b/cot/src/hot_patching.rs new file mode 100644 index 00000000..2287241b --- /dev/null +++ b/cot/src/hot_patching.rs @@ -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(callback: impl FnMut() -> F) +where + F: Future + '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(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: F, args: A) -> Pin + Send>> +where + F: FnOnce(A) -> Fut, + Fut: Future + 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 + 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::() 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" + ); + } +} diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 3cfb5994..6d1e0e68 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -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; diff --git a/cot/src/middleware.rs b/cot/src/middleware.rs index b9e6e27c..e3b10ccd 100644 --- a/cot/src/middleware.rs +++ b/cot/src/middleware.rs @@ -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; diff --git a/cot/src/middleware/live_reload.rs b/cot/src/middleware/live_reload.rs index 9ea31521..bde3eaa1 100644 --- a/cot/src/middleware/live_reload.rs +++ b/cot/src/middleware/live_reload.rs @@ -1,5 +1,12 @@ +mod reset_listener; + +use std::sync::Mutex; + use cot::middleware::{IntoCotErrorLayer, IntoCotResponseLayer}; use cot::project::MiddlewareContext; +pub(crate) use reset_listener::ResetListener; +use tower_livereload::Reloader; +use tracing::trace; #[cfg(feature = "live-reload")] type LiveReloadLayerType = tower::util::Either< @@ -131,13 +138,41 @@ impl LiveReloadMiddleware { ( IntoCotErrorLayer::new(), IntoCotResponseLayer::new(), - tower_livereload::LiveReloadLayer::new(), + Self::create_live_reload_layer(), ) }); Self(tower::util::option_layer(option_layer)) } + + fn create_live_reload_layer() -> tower_livereload::LiveReloadLayer { + let layer = tower_livereload::LiveReloadLayer::new(); + + let mut reloaders = RELOADERS + .lock() + .expect("reloaders mutex was poisoned; please restart the server"); + if reloaders.is_empty() { + subsecond::register_handler(std::sync::Arc::new(reload_clients)); + } + reloaders.push(layer.reloader()); + layer + } } +fn reload_clients() { + let reloaders = RELOADERS + .lock() + .expect("reloaders mutex was poisoned; please restart the server"); + for reloader in &*reloaders { + reloader.reload(); + } + if let Some(notify) = reset_listener::RELOAD_NOTIFY.get() { + trace!("reloading connected clients"); + notify.notify_waiters(); + } +} + +static RELOADERS: Mutex> = Mutex::new(Vec::new()); + #[cfg(feature = "live-reload")] impl Default for LiveReloadMiddleware { fn default() -> Self { diff --git a/cot/src/middleware/live_reload/reset_listener.rs b/cot/src/middleware/live_reload/reset_listener.rs new file mode 100644 index 00000000..7b210c5a --- /dev/null +++ b/cot/src/middleware/live_reload/reset_listener.rs @@ -0,0 +1,111 @@ +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use derive_more::with_trait::Debug; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::{TcpListener, TcpStream}; + +pub(super) static RELOAD_NOTIFY: std::sync::OnceLock> = + std::sync::OnceLock::new(); + +/// A wrapper over [`TcpListener`] that can reset existing TCP connections when +/// a globally defined signal is set. +/// +/// This is useful for hot-patching, so that the clients that are already +/// connected won't use the connection to the old code and will use the +/// hotpatched versions of the handlers instead. +#[derive(Debug)] +pub(crate) struct ResetListener { + inner: TcpListener, +} + +impl ResetListener { + pub(crate) fn new(inner: TcpListener) -> Self { + Self { inner } + } +} + +impl axum::serve::Listener for ResetListener { + type Io = ResetStream; + type Addr = std::net::SocketAddr; + + async fn accept(&mut self) -> (Self::Io, Self::Addr) { + loop { + match self.inner.accept().await { + Ok((stream, addr)) => { + let notify = RELOAD_NOTIFY.get_or_init(|| Arc::new(tokio::sync::Notify::new())); + let notify = notify.clone(); + return ( + ResetStream { + inner: stream, + reset_fut: Box::pin(async move { notify.notified().await }), + }, + addr, + ); + } + Err(_err) => { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + } + } + } + + fn local_addr(&self) -> std::io::Result { + self.inner.local_addr() + } +} + +#[derive(Debug)] +pub(crate) struct ResetStream { + inner: TcpStream, + #[debug("..")] + reset_fut: Pin + Send>>, +} + +impl ResetStream { + fn forward_to_inner( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + f: F, + ) -> Poll> + where + F: FnOnce(Pin<&mut TcpStream>, &mut Context<'_>) -> Poll>, + { + if self.reset_fut.as_mut().poll(cx).is_ready() { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::ConnectionReset, + "connection reset by live reload", + ))); + } + f(Pin::new(&mut self.inner), cx) + } +} + +impl AsyncRead for ResetStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + self.forward_to_inner(cx, |inner, cx| inner.poll_read(cx, buf)) + } +} + +impl AsyncWrite for ResetStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + self.forward_to_inner(cx, |inner, cx| inner.poll_write(cx, buf)) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.forward_to_inner(cx, AsyncWrite::poll_flush) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.forward_to_inner(cx, AsyncWrite::poll_shutdown) + } +} diff --git a/cot/src/private.rs b/cot/src/private.rs index 8fe02bdd..fffc3dec 100644 --- a/cot/src/private.rs +++ b/cot/src/private.rs @@ -26,3 +26,7 @@ pub use crate::utils::graph::apply_permutation; /// This is used in the CLI to specify the version of the crate to use in the /// `Cargo.toml` file when creating a new Cot project. pub const COT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub mod hot_patching { + pub use crate::hot_patching::{call_async, call_hot, serve}; +} diff --git a/cot/src/project.rs b/cot/src/project.rs index c4014cb0..db123b6d 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -1386,6 +1386,7 @@ impl Bootstrapper { let auth_backend = self.project.auth_backend(&self.context); let context = self.context.with_auth(auth_backend); + // dioxus_devtools:: Ok(Bootstrapper { project: self.project, context, @@ -2140,7 +2141,7 @@ pub async fn run_at_with_shutdown( }; std::panic::set_hook(Box::new(new_hook)); } - axum::serve(listener, handler.into_make_service()) + axum::serve(make_reset_listener(listener), handler.into_make_service()) .with_graceful_shutdown(shutdown_signal) .await .map_err(StartServerError)?; @@ -2155,6 +2156,20 @@ pub async fn run_at_with_shutdown( Ok(()) } +fn make_reset_listener( + listener: tokio::net::TcpListener, +) -> impl axum::serve::Listener { + #[cfg(feature = "live-reload")] + { + crate::middleware::live_reload::ResetListener::new(listener) + } + + #[cfg(not(feature = "live-reload"))] + { + listener + } +} + #[derive(Debug, Error)] #[error("failed to start the server: {0}")] pub(crate) struct StartServerError(#[from] pub(crate) std::io::Error);