diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..226dec9 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[target.wasm32-unknown-unknown] +runner = "wasm-bindgen-test-runner" +rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cbbd2e1..d157b38 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -147,6 +147,40 @@ jobs: env: RUST_LOG: ${{ runner.debug && 'TRACE' || 'DEBUG' }} + wasm_test: + name: Build & test wasm32 for browsers + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Add wasm target + run: rustup target add wasm32-unknown-unknown + + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@main + + - name: Install wasm-bindgen-test-runner + run: cargo binstall wasm-bindgen-cli --locked --no-confirm + + - name: Install wasm-tools + uses: bytecodealliance/actions/wasm-tools/setup@v1 + + - name: wasm32 build (netwatch) + run: cargo build --target wasm32-unknown-unknown -p netwatch + + # If the Wasm file contains any 'import "env"' declarations, then + # some non-Wasm-compatible code made it into the final code. + - name: Ensure no 'import "env"' in netwatch Wasm + run: | + ! wasm-tools print --skeleton target/wasm32-unknown-unknown/debug/netwatch.wasm | grep 'import "env"' + + - name: Run smoke test in wasm + run: cargo test -p netwatch --test smoke --target=wasm32-unknown-unknown + check_semver: runs-on: ubuntu-latest env: diff --git a/Cargo.lock b/Cargo.lock index f1381b9..96c7f07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -38,6 +47,12 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + [[package]] name = "async-trait" version = "0.1.85" @@ -93,6 +108,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -157,6 +181,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "cordyceps" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec10f0a762d93c4498d2e97a333805cb6250d60bead623f71d8034f9a4152ba3" +dependencies = [ + "loom", + "tracing", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -203,6 +237,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + [[package]] name = "displaydoc" version = "0.2.5" @@ -288,6 +328,19 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-buffered" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe940397c8b744b9c2c974791c2c08bca2c3242ce0290393249e98f215a00472" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -375,6 +428,19 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -392,6 +458,172 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "gloo" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" +dependencies = [ + "gloo-console", + "gloo-dialogs", + "gloo-events", + "gloo-file", + "gloo-history", + "gloo-net", + "gloo-render", + "gloo-storage", + "gloo-timers", + "gloo-utils", + "gloo-worker", +] + +[[package]] +name = "gloo-console" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7" +dependencies = [ + "gloo-events", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f" +dependencies = [ + "gloo-events", + "gloo-utils", + "serde", + "serde-wasm-bindgen", + "serde_urlencoded", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a" +dependencies = [ + "anymap2", + "bincode", + "gloo-console", + "gloo-utils", + "js-sys", + "serde", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "h2" version = "0.4.7" @@ -756,6 +988,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.169" @@ -784,12 +1022,44 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "minicov" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "miniz_oxide" version = "0.8.3" @@ -810,6 +1080,27 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "n0-future" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399e11dc3b0e8d9d65b27170d22f5d779d52d9bed888db70d7e0c2c7ce3dfc52" +dependencies = [ + "cfg_aliases", + "derive_more", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + [[package]] name = "netdev" version = "0.31.0" @@ -911,17 +1202,16 @@ version = "0.3.0" dependencies = [ "atomic-waker", "bytes", + "cfg_aliases", "derive_more", - "futures-lite", - "futures-sink", - "futures-util", "iroh-quinn-udp", + "js-sys", "libc", + "n0-future", "netdev", "netlink-packet-core", "netlink-packet-route 0.19.0", "netlink-sys", - "once_cell", "rtnetlink 0.13.1", "rtnetlink 0.14.1", "serde", @@ -932,7 +1222,11 @@ dependencies = [ "tokio", "tokio-util", "tracing", - "windows", + "tracing-subscriber", + "tracing-subscriber-wasm", + "wasm-bindgen-test", + "web-sys", + "windows 0.59.0", "windows-result", "wmi", ] @@ -992,6 +1286,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1043,6 +1347,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking" version = "2.2.1" @@ -1084,6 +1394,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1228,6 +1558,50 @@ dependencies = [ "bitflags 2.8.0", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rtnetlink" version = "0.13.1" @@ -1276,12 +1650,39 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.217" @@ -1291,6 +1692,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.217" @@ -1302,6 +1714,39 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "serde_json" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1342,6 +1787,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1477,6 +1928,16 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.37" @@ -1602,6 +2063,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-subscriber-wasm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79804e80980173c6c8e53d98508eb24a2dbc4ee17a3e8d2ca8e5bad6bf13a898" +dependencies = [ + "gloo", + "tracing", + "tracing-subscriber", ] [[package]] @@ -1646,6 +2148,22 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -1687,6 +2205,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -1719,6 +2250,50 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-bindgen-test" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3" +dependencies = [ + "js-sys", + "minicov", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1735,12 +2310,30 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows" version = "0.59.0" @@ -1831,6 +2424,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -1863,6 +2471,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1875,6 +2489,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -1887,6 +2507,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1911,6 +2537,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -1923,6 +2555,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -1935,6 +2573,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -1947,6 +2591,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -1979,7 +2629,7 @@ dependencies = [ "log", "serde", "thiserror 2.0.11", - "windows", + "windows 0.59.0", "windows-core 0.59.0", ] diff --git a/netwatch/Cargo.toml b/netwatch/Cargo.toml index f71208a..e651745 100644 --- a/netwatch/Cargo.toml +++ b/netwatch/Cargo.toml @@ -18,16 +18,24 @@ workspace = true [dependencies] atomic-waker = "1.1.2" bytes = "1.7" -futures-lite = "2.5" -futures-sink = "0.3" -futures-util = "0.3" +n0-future = "0.1.1" +thiserror = "2" +time = "0.3.20" +tokio = { version = "1", features = [ + "io-util", + "macros", + "sync", + "time", +] } +tokio-util = { version = "0.7", features = ["rt"] } +tracing = "0.1" + +# non-browser dependencies +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] +quinn-udp = { package = "iroh-quinn-udp", version = "0.5.5" } libc = "0.2.139" netdev = "0.31.0" -once_cell = "1.18.0" -quinn-udp = { package = "iroh-quinn-udp", version = "0.5.5" } socket2 = "0.5.3" -thiserror = "2" -time = "0.3.20" tokio = { version = "1", features = [ "io-util", "macros", @@ -40,8 +48,6 @@ tokio = { version = "1", features = [ "process", "time", ] } -tokio-util = { version = "0.7", features = ["rt"] } -tracing = "0.1" [target.'cfg(all(target_os = "linux", not(target_os = "android")))'.dependencies] netlink-packet-core = "0.7.0" @@ -62,8 +68,16 @@ windows-result = "0.3" serde = { version = "1", features = ["derive"] } derive_more = { version = "1.0.0", features = ["debug"] } +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] +js-sys = "0.3" +web-sys = { version = "0.3", features = ["EventListener", "EventTarget"] } + [dev-dependencies] testresult = "0.4.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# *non*-wasm-in-browser test/dev dependencies +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies] tokio = { version = "1", features = [ "io-util", "sync", @@ -75,6 +89,14 @@ tokio = { version = "1", features = [ "test-util", ] } +# wasm-in-browser test/dev dependencies +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dev-dependencies] +tracing-subscriber-wasm = "0.1.0" +wasm-bindgen-test = "0.3" + +[build-dependencies] +cfg_aliases = { version = "0.2" } + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "iroh_docsrs"] diff --git a/netwatch/build.rs b/netwatch/build.rs new file mode 100644 index 0000000..7aae568 --- /dev/null +++ b/netwatch/build.rs @@ -0,0 +1,9 @@ +use cfg_aliases::cfg_aliases; + +fn main() { + // Setup cfg aliases + cfg_aliases! { + // Convenience aliases + wasm_browser: { all(target_family = "wasm", target_os = "unknown") }, + } +} diff --git a/netwatch/src/interfaces/bsd.rs b/netwatch/src/interfaces/bsd.rs index 183fc07..5826f9a 100644 --- a/netwatch/src/interfaces/bsd.rs +++ b/netwatch/src/interfaces/bsd.rs @@ -5,6 +5,7 @@ use std::{ collections::HashMap, net::{IpAddr, Ipv4Addr, Ipv6Addr}, + sync::LazyLock, }; use libc::{c_int, uintptr_t, AF_INET, AF_INET6, AF_LINK, AF_ROUTE, AF_UNSPEC, CTL_NET}; @@ -12,7 +13,6 @@ use libc::{c_int, uintptr_t, AF_INET, AF_INET6, AF_LINK, AF_ROUTE, AF_UNSPEC, CT use libc::{ NET_RT_DUMP, RTAX_BRD, RTAX_DST, RTAX_GATEWAY, RTAX_MAX, RTAX_NETMASK, RTA_IFP, RTF_GATEWAY, }; -use once_cell::sync::Lazy; use tracing::warn; use super::DefaultRouteDetails; @@ -459,7 +459,7 @@ enum MessageType { InterfaceAnnounce, } -static ROUTING_STACK: Lazy = Lazy::new(probe_routing_stack); +static ROUTING_STACK: LazyLock = LazyLock::new(probe_routing_stack); struct RoutingStack { rtm_version: i32, diff --git a/netwatch/src/interfaces/linux.rs b/netwatch/src/interfaces/linux.rs index 23296f7..4d5c117 100644 --- a/netwatch/src/interfaces/linux.rs +++ b/netwatch/src/interfaces/linux.rs @@ -1,7 +1,7 @@ //! Linux-specific network interfaces implementations. #[cfg(not(target_os = "android"))] -use futures_util::TryStreamExt; +use n0_future::TryStreamExt; use tokio::{ fs::File, io::{AsyncBufReadExt, BufReader}, diff --git a/netwatch/src/interfaces/wasm_browser.rs b/netwatch/src/interfaces/wasm_browser.rs new file mode 100644 index 0000000..190431b --- /dev/null +++ b/netwatch/src/interfaces/wasm_browser.rs @@ -0,0 +1,118 @@ +use std::{collections::HashMap, fmt}; + +use js_sys::{JsString, Reflect}; + +pub const BROWSER_INTERFACE: &str = "browserif"; + +/// Represents a network interface. +#[derive(Debug, PartialEq, Eq)] +pub struct Interface { + is_up: bool, +} + +impl fmt::Display for Interface { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "navigator.onLine={}", self.is_up) + } +} + +impl Interface { + async fn new() -> Self { + let is_up = Self::is_up(); + tracing::debug!(onLine = is_up, "Fetched globalThis.navigator.onLine"); + Self { + is_up: is_up.unwrap_or(true), + } + } + + fn is_up() -> Option { + let navigator = Reflect::get( + js_sys::global().as_ref(), + JsString::from("navigator").as_ref(), + ) + .ok()?; + + let is_up = Reflect::get(&navigator, JsString::from("onLine").as_ref()).ok()?; + + is_up.as_bool() + } + + /// The name of the interface. + pub(crate) fn name(&self) -> &str { + BROWSER_INTERFACE + } +} + +/// Intended to store the state of the machine's network interfaces, routing table, and +/// other network configuration. For now it's pretty basic. +#[derive(Debug, PartialEq, Eq)] +pub struct State { + /// Maps from an interface name interface. + pub interfaces: HashMap, + + /// Whether this machine has an IPv6 Global or Unique Local Address + /// which might provide connectivity. + pub have_v6: bool, + + /// Whether the machine has some non-localhost, non-link-local IPv4 address. + pub have_v4: bool, + + //// Whether the current network interface is considered "expensive", which currently means LTE/etc + /// instead of Wifi. This field is not populated by `get_state`. + pub(crate) is_expensive: bool, + + /// The interface name for the machine's default route. + /// + /// It is not yet populated on all OSes. + /// + /// When set, its value is the map key into `interface` and `interface_ips`. + pub(crate) default_route_interface: Option, + + /// The HTTP proxy to use, if any. + pub(crate) http_proxy: Option, + + /// The URL to the Proxy Autoconfig URL, if applicable. + pub(crate) pac: Option, +} + +impl fmt::Display for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for iface in self.interfaces.values() { + write!(f, "{iface}")?; + if let Some(ref default_if) = self.default_route_interface { + if iface.name() == default_if { + write!(f, " (default)")?; + } + } + if f.alternate() { + writeln!(f)?; + } else { + write!(f, "; ")?; + } + } + Ok(()) + } +} + +impl State { + /// Returns the state of all the current machine's network interfaces. + /// + /// It does not set the returned `State.is_expensive`. The caller can populate that. + pub async fn new() -> Self { + let mut interfaces = HashMap::new(); + let have_v6 = false; + let have_v4 = false; + + interfaces.insert(BROWSER_INTERFACE.to_string(), Interface::new().await); + + State { + interfaces, + have_v4, + have_v6, + is_expensive: false, + default_route_interface: Some(BROWSER_INTERFACE.to_string()), + http_proxy: None, + pac: None, + } + } +} diff --git a/netwatch/src/ip.rs b/netwatch/src/ip.rs index 45799ac..8aafeb3 100644 --- a/netwatch/src/ip.rs +++ b/netwatch/src/ip.rs @@ -1,11 +1,16 @@ //! IP address related utilities. -use std::net::{IpAddr, Ipv6Addr}; +#[cfg(not(wasm_browser))] +use std::net::IpAddr; +use std::net::Ipv6Addr; +#[cfg(not(wasm_browser))] const IFF_UP: u32 = 0x1; +#[cfg(not(wasm_browser))] const IFF_LOOPBACK: u32 = 0x8; /// List of machine's IP addresses. +#[cfg(not(wasm_browser))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct LocalAddresses { /// Loopback addresses. @@ -14,12 +19,14 @@ pub struct LocalAddresses { pub regular: Vec, } +#[cfg(not(wasm_browser))] impl Default for LocalAddresses { fn default() -> Self { Self::new() } } +#[cfg(not(wasm_browser))] impl LocalAddresses { /// Returns the machine's IP addresses. /// If there are no regular addresses it will return any IPv4 linklocal or IPv6 unique local @@ -91,10 +98,12 @@ impl LocalAddresses { } } +#[cfg(not(wasm_browser))] pub(crate) const fn is_up(interface: &netdev::Interface) -> bool { interface.flags & IFF_UP != 0 } +#[cfg(not(wasm_browser))] pub(crate) const fn is_loopback(interface: &netdev::Interface) -> bool { interface.flags & IFF_LOOPBACK != 0 } @@ -102,6 +111,7 @@ pub(crate) const fn is_loopback(interface: &netdev::Interface) -> bool { /// Reports whether ip is a private address, according to RFC 1918 /// (IPv4 addresses) and RFC 4193 (IPv6 addresses). That is, it reports whether /// ip is in 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, or fc00::/7. +#[cfg(not(wasm_browser))] pub(crate) fn is_private(ip: &IpAddr) -> bool { match ip { IpAddr::V4(ip) => { @@ -116,11 +126,13 @@ pub(crate) fn is_private(ip: &IpAddr) -> bool { } } +#[cfg(not(wasm_browser))] pub(crate) fn is_private_v6(ip: &Ipv6Addr) -> bool { // RFC 4193 allocates fc00::/7 as the unique local unicast IPv6 address subnet. ip.octets()[0] & 0xfe == 0xfc } +#[cfg(not(wasm_browser))] pub(super) fn is_link_local(ip: IpAddr) -> bool { match ip { IpAddr::V4(ip) => ip.is_link_local(), @@ -136,11 +148,10 @@ pub const fn is_unicast_link_local(addr: Ipv6Addr) -> bool { #[cfg(test)] mod tests { - use super::*; - + #[cfg(not(wasm_browser))] #[test] fn test_local_addresses() { - let addrs = LocalAddresses::new(); + let addrs = super::LocalAddresses::new(); dbg!(&addrs); assert!(!addrs.loopback.is_empty()); assert!(!addrs.regular.is_empty()); diff --git a/netwatch/src/ip_family.rs b/netwatch/src/ip_family.rs index 1033a60..882890b 100644 --- a/netwatch/src/ip_family.rs +++ b/netwatch/src/ip_family.rs @@ -36,6 +36,7 @@ impl IpFamily { } } +#[cfg(not(wasm_browser))] impl From for socket2::Domain { fn from(value: IpFamily) -> Self { match value { diff --git a/netwatch/src/lib.rs b/netwatch/src/lib.rs index 213fe78..d26af9e 100644 --- a/netwatch/src/lib.rs +++ b/netwatch/src/lib.rs @@ -1,9 +1,13 @@ //! Networking related utilities +#[cfg_attr(wasm_browser, path = "interfaces/wasm_browser.rs")] pub mod interfaces; pub mod ip; mod ip_family; pub mod netmon; +#[cfg(not(wasm_browser))] mod udp; -pub use self::{ip_family::IpFamily, udp::UdpSocket}; +pub use self::ip_family::IpFamily; +#[cfg(not(wasm_browser))] +pub use self::udp::UdpSocket; diff --git a/netwatch/src/netmon.rs b/netwatch/src/netmon.rs index 12edeb1..ab7031b 100644 --- a/netwatch/src/netmon.rs +++ b/netwatch/src/netmon.rs @@ -1,8 +1,10 @@ //! Monitoring of networking interfaces and route changes. -use futures_lite::future::Boxed as BoxFuture; +use n0_future::{ + boxed::BoxFuture, + task::{self, AbortOnDropHandle}, +}; use tokio::sync::{mpsc, oneshot}; -use tokio_util::task::AbortOnDropHandle; mod actor; #[cfg(target_os = "android")] @@ -17,6 +19,8 @@ mod android; mod bsd; #[cfg(target_os = "linux")] mod linux; +#[cfg(wasm_browser)] +mod wasm_browser; #[cfg(target_os = "windows")] mod windows; @@ -57,7 +61,7 @@ impl Monitor { let actor = Actor::new().await?; let actor_tx = actor.subscribe(); - let handle = tokio::task::spawn(async move { + let handle = task::spawn(async move { actor.run().await; }); @@ -99,7 +103,7 @@ impl Monitor { #[cfg(test)] mod tests { - use futures_util::FutureExt; + use n0_future::future::FutureExt; use super::*; diff --git a/netwatch/src/netmon/actor.rs b/netwatch/src/netmon/actor.rs index 58b1ed0..bd5743c 100644 --- a/netwatch/src/netmon/actor.rs +++ b/netwatch/src/netmon/actor.rs @@ -1,12 +1,14 @@ -use std::{ - collections::HashMap, - sync::Arc, - time::{Duration, Instant}, -}; +use std::{collections::HashMap, sync::Arc}; -use futures_lite::future::Boxed as BoxFuture; +use n0_future::{ + boxed::BoxFuture, + task, + time::{self, Duration, Instant}, +}; +#[cfg(not(wasm_browser))] +use os::is_interesting_interface; pub(super) use os::Error; -use os::{is_interesting_interface, RouteMonitor}; +use os::RouteMonitor; use tokio::sync::{mpsc, oneshot}; use tracing::{debug, trace}; @@ -22,12 +24,13 @@ use super::android as os; use super::bsd as os; #[cfg(target_os = "linux")] use super::linux as os; +#[cfg(wasm_browser)] +use super::wasm_browser as os; #[cfg(target_os = "windows")] use super::windows as os; -use crate::{ - interfaces::{IpNet, State}, - ip::is_link_local, -}; +use crate::interfaces::State; +#[cfg(not(wasm_browser))] +use crate::{interfaces::IpNet, ip::is_link_local}; /// The message sent by the OS specific monitors. #[derive(Debug, Copy, Clone)] @@ -105,8 +108,8 @@ impl Actor { const DEBOUNCE: Duration = Duration::from_millis(250); let mut last_event = None; - let mut debounce_interval = tokio::time::interval(DEBOUNCE); - let mut wall_time_interval = tokio::time::interval(POLL_WALL_TIME_INTERVAL); + let mut debounce_interval = time::interval(DEBOUNCE); + let mut wall_time_interval = time::interval(POLL_WALL_TIME_INTERVAL); loop { tokio::select! { @@ -191,7 +194,7 @@ impl Actor { debug!("triggering {} callbacks", self.callbacks.len()); for cb in self.callbacks.values() { let cb = cb.clone(); - tokio::task::spawn(async move { + task::spawn(async move { cb(is_major).await; }); } @@ -212,6 +215,14 @@ impl Actor { } } +#[cfg(wasm_browser)] +fn is_major_change(s1: &State, s2: &State) -> bool { + // All changes are major. + // In the browser, there only are changes from online to offline + s1 != s2 +} + +#[cfg(not(wasm_browser))] fn is_major_change(s1: &State, s2: &State) -> bool { if s1.have_v6 != s2.have_v6 || s1.have_v4 != s2.have_v4 @@ -240,6 +251,7 @@ fn is_major_change(s1: &State, s2: &State) -> bool { /// Checks whether `a` and `b` are equal after ignoring uninteresting /// things, like link-local, loopback and multicast addresses. +#[cfg(not(wasm_browser))] fn prefixes_major_equal(a: impl Iterator, b: impl Iterator) -> bool { fn is_interesting(p: &IpNet) -> bool { let a = p.addr(); diff --git a/netwatch/src/netmon/linux.rs b/netwatch/src/netmon/linux.rs index 2330249..d460cec 100644 --- a/netwatch/src/netmon/linux.rs +++ b/netwatch/src/netmon/linux.rs @@ -3,11 +3,11 @@ use std::{ net::IpAddr, }; -use futures_lite::StreamExt; use libc::{ RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV4_ROUTE, RTNLGRP_IPV4_RULE, RTNLGRP_IPV6_IFADDR, RTNLGRP_IPV6_ROUTE, RTNLGRP_IPV6_RULE, }; +use n0_future::StreamExt; use netlink_packet_core::NetlinkPayload; use netlink_packet_route::{address, route, RouteNetlinkMessage}; use netlink_sys::{AsyncSocket, SocketAddr}; diff --git a/netwatch/src/netmon/wasm_browser.rs b/netwatch/src/netmon/wasm_browser.rs new file mode 100644 index 0000000..dc2edfe --- /dev/null +++ b/netwatch/src/netmon/wasm_browser.rs @@ -0,0 +1,84 @@ +use js_sys::{ + wasm_bindgen::{prelude::Closure, JsCast}, + Function, +}; +use n0_future::task; +use tokio::sync::mpsc; +use web_sys::{EventListener, EventTarget}; + +use super::actor::NetworkMessage; + +#[derive(Debug, thiserror::Error)] +#[error("error")] +pub struct Error; + +#[derive(Debug)] +pub(super) struct RouteMonitor { + _listeners: Option, +} + +impl RouteMonitor { + pub(super) fn new(sender: mpsc::Sender) -> Result { + let closure: Function = Closure::::new(move || { + tracing::trace!("browser RouteMonitor event triggered"); + // task::spawn is effectively translated into a queueMicrotask in JS + let sender = sender.clone(); + task::spawn(async move { + sender + .send(NetworkMessage::Change) + .await + .inspect_err(|err| { + tracing::debug!(?err, "failed sending NetworkMessage::Change") + }) + }); + }) + .into_js_value() + .unchecked_into(); + // The closure keeps itself alive via reference counting internally + let _listeners = add_event_listeners(&closure); + Ok(RouteMonitor { _listeners }) + } +} + +fn add_event_listeners(f: &Function) -> Option { + let online_listener = EventListener::new(); + online_listener.set_handle_event(f); + let offline_listener = EventListener::new(); + offline_listener.set_handle_event(f); + + // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine#listening_for_changes_in_network_status + let window: EventTarget = js_sys::global().unchecked_into(); + window + .add_event_listener_with_event_listener("online", &online_listener) + .inspect_err(|err| tracing::debug!(?err, "failed adding event listener")) + .ok()?; + + window + .add_event_listener_with_event_listener("offline", &offline_listener) + .inspect_err(|err| tracing::debug!(?err, "failed adding event listener")) + .ok()?; + + Some(Listeners { + online_listener, + offline_listener, + }) +} + +#[derive(Debug)] +struct Listeners { + online_listener: EventListener, + offline_listener: EventListener, +} + +impl Drop for Listeners { + fn drop(&mut self) { + tracing::trace!("Removing online/offline event listeners"); + let window: EventTarget = js_sys::global().unchecked_into(); + window + .remove_event_listener_with_event_listener("online", &self.online_listener) + .ok(); + window + .remove_event_listener_with_event_listener("offline", &self.offline_listener) + .ok(); + } +} diff --git a/netwatch/src/udp.rs b/netwatch/src/udp.rs index b667389..1e0a6d2 100644 --- a/netwatch/src/udp.rs +++ b/netwatch/src/udp.rs @@ -291,7 +291,7 @@ impl UdpSocket { return Poll::Ready(Err(err)); } - let guard = futures_lite::ready!(self.poll_read_socket(&self.send_waker, cx)); + let guard = std::task::ready!(self.poll_read_socket(&self.send_waker, cx)); let (socket, _state) = guard.try_get_connected()?; match socket.poll_send_ready(cx) { @@ -352,7 +352,7 @@ impl UdpSocket { return Poll::Ready(Err(err)); } - let guard = futures_lite::ready!(self.poll_read_socket(&self.recv_waker, cx)); + let guard = n0_future::ready!(self.poll_read_socket(&self.recv_waker, cx)); let (socket, state) = guard.try_get_connected()?; match socket.poll_recv_ready(cx) { @@ -447,7 +447,7 @@ impl Future for RecvFut<'_, '_> { return Poll::Ready(Err(err)); } - let guard = futures_lite::ready!(socket.poll_read_socket(&socket.recv_waker, cx)); + let guard = n0_future::ready!(socket.poll_read_socket(&socket.recv_waker, cx)); let (inner_socket, _state) = guard.try_get_connected()?; match inner_socket.poll_recv_ready(cx) { @@ -497,7 +497,7 @@ impl Future for RecvFromFut<'_, '_> { return Poll::Ready(Err(err)); } - let guard = futures_lite::ready!(socket.poll_read_socket(&socket.recv_waker, cx)); + let guard = n0_future::ready!(socket.poll_read_socket(&socket.recv_waker, cx)); let (inner_socket, _state) = guard.try_get_connected()?; match inner_socket.poll_recv_ready(cx) { @@ -546,7 +546,7 @@ impl Future for SendFut<'_, '_> { } let guard = - futures_lite::ready!(self.socket.poll_read_socket(&self.socket.send_waker, cx)); + n0_future::ready!(self.socket.poll_read_socket(&self.socket.send_waker, cx)); let (socket, _state) = guard.try_get_connected()?; match socket.poll_send_ready(cx) { @@ -596,7 +596,7 @@ impl Future for SendToFut<'_, '_> { } let guard = - futures_lite::ready!(self.socket.poll_read_socket(&self.socket.send_waker, cx)); + n0_future::ready!(self.socket.poll_read_socket(&self.socket.send_waker, cx)); let (socket, _state) = guard.try_get_connected()?; match socket.poll_send_ready(cx) { diff --git a/netwatch/tests/smoke.rs b/netwatch/tests/smoke.rs new file mode 100644 index 0000000..04da94e --- /dev/null +++ b/netwatch/tests/smoke.rs @@ -0,0 +1,73 @@ +//! A very basic smoke test for netwatch, to make sure it doesn't error out immediately +//! in Wasm at all. +//! +//! We can't test browsers easily, because that would mean we need control over turning +//! the browser online/offline. +//! +//! However, this gives us a minimum guarantee that the Wasm build doesn't break fully. +use n0_future::FutureExt; +use netwatch::netmon; +use testresult::TestResult; +#[cfg(not(wasm_browser))] +use tokio::test; +#[cfg(wasm_browser)] +use wasm_bindgen_test::wasm_bindgen_test as test; + +// Enable this if you want to run these tests in the browser. +// Unfortunately it's either-or: Enable this and you can run in the browser, disable to run in nodejs. +// #[cfg(wasm_browser)] +// wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +#[test] +async fn smoke_test() -> TestResult { + setup_logging(); + + tracing::info!("Creating netmon::Monitor"); + let monitor = netmon::Monitor::new().await?; + tracing::info!("netmon::Monitor created."); + + // Unfortunately this doesn't do anything in node.js, because it doesn't have + // globalThis.navigator.onLine or globalThis.addEventListener("online"/"offline", ...) APIs, + // so this is more of a test to see if we gracefully handle these situations & if our + // .wasm files are without "env" imports. + tracing::info!("subscribing to netmon callback"); + let token = monitor + .subscribe(|is_major| { + async move { + tracing::info!(is_major, "network change"); + } + .boxed() + }) + .await?; + tracing::info!("successfully subscribed to netmon callback"); + + tracing::info!("unsubscribing"); + monitor.unsubscribe(token).await?; + tracing::info!("unsubscribed"); + + tracing::info!("dropping netmon::Monitor"); + drop(monitor); + tracing::info!("dropped."); + + Ok(()) +} + +#[cfg(wasm_browser)] +fn setup_logging() { + tracing_subscriber::fmt() + .with_max_level(tracing::level_filters::LevelFilter::DEBUG) + .with_writer( + // To avoide trace events in the browser from showing their JS backtrace + tracing_subscriber_wasm::MakeConsoleWriter::default() + .map_trace_level_to(tracing::Level::DEBUG), + ) + // If we don't do this in the browser, we get a runtime error. + .without_time() + .with_ansi(false) + .init(); +} + +#[cfg(not(wasm_browser))] +fn setup_logging() { + tracing_subscriber::fmt().init(); +}