aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2025-10-21 14:36:30 +0200
committerGitHub <noreply@github.com>2025-10-21 14:36:30 +0200
commit889bd06f447f9d50ee46b8efc7d9970d3693cb96 (patch)
tree171fa657faccf4b4ce7124847977626826a2a750
parentGive plugins the ability to read pane contents (#4465) (diff)
downloadsl0ppij-889bd06f447f9d50ee46b8efc7d9970d3693cb96.tar.gz
sl0ppij-889bd06f447f9d50ee46b8efc7d9970d3693cb96.tar.xz
sl0ppij-889bd06f447f9d50ee46b8efc7d9970d3693cb96.zip
Migrate from wasmtime to wasmi (#4449)
* initial implementation * fix stdio and host folder mounting * some memory usage optimizations * fix plugin tests * remove unused plugin cache * fix docker-compose for e2e tests when running locally * rustfmt * initial pinned executor implementation * EXPERIMENT: add jemalloc * initial dynamic pinned threadpool * cleanups and experiments * keep state on execution threads * reintroduce memory cache * add wasmi engine to executor threads * load plugins for new clients on the relevant executor thread * only show loading indication of loading for more than 0.4 secs * fix anonymous plugin loading without client id * refactor lots of stuff * update snapshots * tests for pinned executor * remove jemalloc * fix stderr from plugins * cleanups * rustfmt * cleanups * fix endless loading for additinal attachments * more cleanups * remove singlepass feature * fix displaying plugin permission message * render optimizations * always render for tests * rustfmt * optimize release build for wasmi * fixes for multiple client plugin loading * fix frame rendering for multiple clients * rustfmt * fix loading background plugins and e2e event ordering change * rustfmt * account for races in the pin_floating_panes e2e test * docs(changelog): add PR
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock1053
-rw-r--r--Cargo.toml2
-rw-r--r--default-plugins/tab-bar/src/line.rs5
-rw-r--r--default-plugins/tab-bar/src/main.rs7
-rw-r--r--docker-compose.yml1
-rw-r--r--src/tests/e2e/cases.rs15
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap2
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__detach_and_attach_session.snap2
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap3
-rw-r--r--xtask/src/flags.rs3
-rw-r--r--xtask/src/pipelines.rs8
-rw-r--r--zellij-server/Cargo.toml22
-rw-r--r--zellij-server/src/lib.rs15
-rw-r--r--zellij-server/src/logging_pipe.rs1
-rw-r--r--zellij-server/src/panes/plugin_pane.rs107
-rw-r--r--zellij-server/src/plugins/mod.rs9
-rw-r--r--zellij-server/src/plugins/pinned_executor.rs1363
-rw-r--r--zellij-server/src/plugins/pipes.rs5
-rw-r--r--zellij-server/src/plugins/plugin_loader.rs989
-rw-r--r--zellij-server/src/plugins/plugin_map.rs88
-rw-r--r--zellij-server/src/plugins/plugin_worker.rs2
-rw-r--r--zellij-server/src/plugins/unit/plugin_tests.rs30
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs1110
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs10
-rw-r--r--zellij-server/src/screen.rs66
-rw-r--r--zellij-server/src/tab/layout_applier.rs10
-rw-r--r--zellij-server/src/tab/mod.rs36
-rw-r--r--zellij-server/src/ui/loading_indication.rs204
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-2.snap4
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-3.snap4
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-4.snap4
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-6.snap4
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-7.snap4
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab.snap4
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-3.snap4
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-4.snap4
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab.snap4
38 files changed, 2759 insertions, 2446 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 745ad0e9..96d8b0d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* feat: allow attaching to remote Zellij sessions over https (eg. `zellij attach https://example.com/my-cool-session`) (https://github.com/zellij-org/zellij/pull/4460)
* build: Update Rust toolchain to 1.90.0 (https://github.com/zellij-org/zellij/pull/4457)
* feat: allow plugins to read pane scrollback (https://github.com/zellij-org/zellij/pull/4465)
+* infra: migrate wasm runtime from wasmtime to wasmi (https://github.com/zellij-org/zellij/pull/4449)
## [0.43.1] - 2025-08-08
* fix: pane rename backspace regression (https://github.com/zellij-org/zellij/pull/4346)
diff --git a/Cargo.lock b/Cargo.lock
index 66619051..e7fc87d2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -16,16 +16,7 @@ version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
- "gimli 0.26.1",
-]
-
-[[package]]
-name = "addr2line"
-version = "0.24.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
-dependencies = [
- "gimli 0.31.1",
+ "gimli",
]
[[package]]
@@ -110,12 +101,6 @@ dependencies = [
]
[[package]]
-name = "arbitrary"
-version = "1.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
-
-[[package]]
name = "arc-swap"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -129,9 +114,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "arrayvec"
-version = "0.7.2"
+version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-attributes"
@@ -438,12 +423,12 @@ version = "0.3.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61"
dependencies = [
- "addr2line 0.17.0",
+ "addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
- "object 0.28.4",
+ "object",
"rustc-demangle",
]
@@ -464,12 +449,6 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "base64"
-version = "0.21.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
-
-[[package]]
-name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
@@ -491,7 +470,7 @@ dependencies = [
"proc-macro2",
"quote",
"regex",
- "rustc-hash 1.1.0",
+ "rustc-hash",
"shlex",
"syn 2.0.96",
"which",
@@ -562,9 +541,6 @@ name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
-dependencies = [
- "allocator-api2",
-]
[[package]]
name = "bytemuck"
@@ -592,9 +568,9 @@ checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
[[package]]
name = "cap-fs-ext"
-version = "3.4.2"
+version = "3.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f78efdd7378980d79c0f36b519e51191742d2c9f91ffa5e228fba9f3806d2e1"
+checksum = "e41cc18551193fe8fa6f15c1e3c799bc5ec9e2cfbfaa8ed46f37013e3e6c173c"
dependencies = [
"cap-primitives",
"cap-std",
@@ -603,22 +579,10 @@ dependencies = [
]
[[package]]
-name = "cap-net-ext"
-version = "3.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ac68674a6042af2bcee1adad9f6abd432642cf03444ce3a5b36c3f39f23baf8"
-dependencies = [
- "cap-primitives",
- "cap-std",
- "rustix 0.38.44",
- "smallvec",
-]
-
-[[package]]
name = "cap-primitives"
-version = "3.4.2"
+version = "3.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc15faeed2223d8b8e8cc1857f5861935a06d06713c4ac106b722ae9ce3c369"
+checksum = "0a1e394ed14f39f8bc26f59d4c0c010dbe7f0a1b9bafff451b1f98b67c8af62a"
dependencies = [
"ambient-authority",
"fs-set-times",
@@ -626,16 +590,17 @@ dependencies = [
"io-lifetimes 2.0.3",
"ipnet",
"maybe-owned",
- "rustix 0.38.44",
+ "rustix 1.1.2",
+ "rustix-linux-procfs",
"windows-sys 0.59.0",
"winx",
]
[[package]]
name = "cap-rand"
-version = "3.4.2"
+version = "3.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dea13372b49df066d1ae654e5c6e41799c1efd9f6b36794b921e877ea4037977"
+checksum = "0acb89ccf798a28683f00089d0630dfaceec087234eae0d308c05ddeaa941b40"
dependencies = [
"ambient-authority",
"rand 0.8.5",
@@ -643,27 +608,27 @@ dependencies = [
[[package]]
name = "cap-std"
-version = "3.4.2"
+version = "3.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3dbd3e8e8d093d6ccb4b512264869e1281cdb032f7940bd50b2894f96f25609"
+checksum = "07c0355ca583dd58f176c3c12489d684163861ede3c9efa6fd8bba314c984189"
dependencies = [
"cap-primitives",
"io-extras",
"io-lifetimes 2.0.3",
- "rustix 0.38.44",
+ "rustix 1.1.2",
]
[[package]]
name = "cap-time-ext"
-version = "3.4.2"
+version = "3.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd736b20fc033f564a1995fb82fc349146de43aabba19c7368b4cb17d8f9ea53"
+checksum = "491af520b8770085daa0466978c75db90368c71896523f2464214e38359b1a5b"
dependencies = [
"ambient-authority",
"cap-primitives",
"iana-time-zone",
"once_cell",
- "rustix 0.38.44",
+ "rustix 1.1.2",
"winx",
]
@@ -804,12 +769,6 @@ dependencies = [
]
[[package]]
-name = "cobs"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
-
-[[package]]
name = "colored"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -907,15 +866,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
-name = "cpp_demangle"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e8227005286ec39567949b33df9896bcadfa6051bccca2488129f108ca23119"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
name = "cpufeatures"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -925,122 +875,6 @@ dependencies = [
]
[[package]]
-name = "cranelift-bforest"
-version = "0.116.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e15d04a0ce86cb36ead88ad68cf693ffd6cda47052b9e0ac114bc47fd9cd23c4"
-dependencies = [
- "cranelift-entity",
-]
-
-[[package]]
-name = "cranelift-bitset"
-version = "0.116.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c6e3969a7ce267259ce244b7867c5d3bc9e65b0a87e81039588dfdeaede9f34"
-dependencies = [
- "serde",
- "serde_derive",
-]
-
-[[package]]
-name = "cranelift-codegen"
-version = "0.116.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c22032c4cb42558371cf516bb47f26cdad1819d3475c133e93c49f50ebf304e"
-dependencies = [
- "bumpalo",
- "cranelift-bforest",
- "cranelift-bitset",
- "cranelift-codegen-meta",
- "cranelift-codegen-shared",
- "cranelift-control",
- "cranelift-entity",
- "cranelift-isle",
- "gimli 0.31.1",
- "hashbrown 0.14.5",
- "log",
- "regalloc2",
- "rustc-hash 2.1.0",
- "serde",
- "smallvec",
- "target-lexicon",
-]
-
-[[package]]
-name = "cranelift-codegen-meta"
-version = "0.116.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c904bc71c61b27fc57827f4a1379f29de64fe95653b620a3db77d59655eee0b8"
-dependencies = [
- "cranelift-codegen-shared",
-]
-
-[[package]]
-name = "cranelift-codegen-shared"
-version = "0.116.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40180f5497572f644ce88c255480981ae2ec1d7bb4d8e0c0136a13b87a2f2ceb"
-
-[[package]]
-name = "cranelift-control"
-version = "0.116.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d132c6d0bd8a489563472afc171759da0707804a65ece7ceb15a8c6d7dd5ef"
-dependencies = [
- "arbitrary",
-]
-
-[[package]]
-name = "cranelift-entity"
-version = "0.116.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b2d0d9618275474fbf679dd018ac6e009acbd6ae6850f6a67be33fb3b00b323"
-dependencies = [
- "cranelift-bitset",
- "serde",
- "serde_derive",
-]
-
-[[package]]
-name = "cranelift-frontend"
-version = "0.116.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fac41e16729107393174b0c9e3730fb072866100e1e64e80a1a963b2e484d57"
-dependencies = [
- "cranelift-codegen",
- "log",
- "smallvec",
- "target-lexicon",
-]
-
-[[package]]
-name = "cranelift-isle"
-version = "0.116.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ca20d576e5070044d0a72a9effc2deacf4d6aa650403189d8ea50126483944d"
-
-[[package]]
-name = "cranelift-native"
-version = "0.116.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8dee82f3f1f2c4cba9177f1cc5e350fe98764379bcd29340caa7b01f85076c7"
-dependencies = [
- "cranelift-codegen",
- "libc",
- "target-lexicon",
-]
-
-[[package]]
-name = "crc32fast"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
name = "crossbeam"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1185,15 +1019,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
-name = "debugid"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
-dependencies = [
- "uuid",
-]
-
-[[package]]
name = "deltae"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1250,16 +1075,6 @@ dependencies = [
]
[[package]]
-name = "directories-next"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc"
-dependencies = [
- "cfg-if",
- "dirs-sys-next",
-]
-
-[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1292,17 +1107,6 @@ dependencies = [
]
[[package]]
-name = "dirs-sys-next"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
-dependencies = [
- "libc",
- "redox_users",
- "winapi",
-]
-
-[[package]]
name = "dissimilar"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1321,12 +1125,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
-name = "embedded-io"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
-
-[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1678,28 +1476,6 @@ dependencies = [
]
[[package]]
-name = "fxhash"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
-dependencies = [
- "byteorder",
-]
-
-[[package]]
-name = "fxprof-processed-profile"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd"
-dependencies = [
- "bitflags 2.5.0",
- "debugid",
- "fxhash",
- "serde",
- "serde_json",
-]
-
-[[package]]
name = "generic-array"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1759,17 +1535,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4"
[[package]]
-name = "gimli"
-version = "0.31.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
-dependencies = [
- "fallible-iterator",
- "indexmap 2.7.1",
- "stable_deref_trait",
-]
-
-[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1842,7 +1607,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
"foldhash",
- "serde",
]
[[package]]
@@ -1891,6 +1655,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2025,12 +1795,6 @@ dependencies = [
]
[[package]]
-name = "id-arena"
-version = "2.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
-
-[[package]]
name = "idna"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2094,7 +1858,6 @@ checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
- "serde",
]
[[package]]
@@ -2266,26 +2029,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
-name = "ittapi"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1"
-dependencies = [
- "anyhow",
- "ittapi-sys",
- "log",
-]
-
-[[package]]
-name = "ittapi-sys"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc"
-dependencies = [
- "cc",
-]
-
-[[package]]
name = "jobserver"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2368,6 +2111,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
name = "lev_distance"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2375,9 +2124,9 @@ checksum = "72d9d1bd215936bc8647ad92986bb56f3f216550b53c44ab785e3217ae33625e"
[[package]]
name = "libc"
-version = "0.2.169"
+version = "0.2.176"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
+checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
[[package]]
name = "libloading"
@@ -2391,9 +2140,9 @@ dependencies = [
[[package]]
name = "libm"
-version = "0.2.8"
+version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libnghttp2-sys"
@@ -2472,6 +2221,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
+name = "linux-raw-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+[[package]]
name = "lock_api"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2526,15 +2281,6 @@ dependencies = [
]
[[package]]
-name = "mach2"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
-dependencies = [
- "libc",
-]
-
-[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2553,15 +2299,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
-name = "memfd"
-version = "0.6.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64"
-dependencies = [
- "rustix 0.38.44",
-]
-
-[[package]]
name = "memmem"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2828,11 +2565,11 @@ dependencies = [
[[package]]
name = "num_cpus"
-version = "1.13.1"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
dependencies = [
- "hermit-abi 0.1.19",
+ "hermit-abi 0.5.2",
"libc",
]
@@ -2846,18 +2583,6 @@ dependencies = [
]
[[package]]
-name = "object"
-version = "0.36.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
-dependencies = [
- "crc32fast",
- "hashbrown 0.15.2",
- "indexmap 2.7.1",
- "memchr",
-]
-
-[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2999,12 +2724,6 @@ dependencies = [
]
[[package]]
-name = "paste"
-version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc"
-
-[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3178,17 +2897,6 @@ dependencies = [
]
[[package]]
-name = "postcard"
-version = "1.0.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8"
-dependencies = [
- "cobs",
- "embedded-io",
- "serde",
-]
-
-[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3306,27 +3014,6 @@ dependencies = [
]
[[package]]
-name = "psm"
-version = "0.1.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874"
-dependencies = [
- "cc",
-]
-
-[[package]]
-name = "pulley-interpreter"
-version = "29.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62d95f8575df49a2708398182f49a888cf9dc30210fb1fd2df87c889edcee75d"
-dependencies = [
- "cranelift-bitset",
- "log",
- "sptr",
- "wasmtime-math",
-]
-
-[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3397,30 +3084,6 @@ dependencies = [
]
[[package]]
-name = "rayon"
-version = "1.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d"
-dependencies = [
- "autocfg",
- "crossbeam-deque",
- "either",
- "rayon-core",
-]
-
-[[package]]
-name = "rayon-core"
-version = "1.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f"
-dependencies = [
- "crossbeam-channel",
- "crossbeam-deque",
- "crossbeam-utils",
- "num_cpus",
-]
-
-[[package]]
name = "redox_syscall"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3459,20 +3122,6 @@ dependencies = [
]
[[package]]
-name = "regalloc2"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "145c1c267e14f20fb0f88aa76a1c5ffec42d592c1d28b3cd9148ae35916158d3"
-dependencies = [
- "allocator-api2",
- "bumpalo",
- "hashbrown 0.15.2",
- "log",
- "rustc-hash 2.1.0",
- "smallvec",
-]
-
-[[package]]
name = "regex"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3530,12 +3179,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
-name = "rustc-hash"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
-
-[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3566,14 +3209,35 @@ checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags 2.5.0",
"errno",
- "itoa",
"libc",
"linux-raw-sys 0.4.15",
- "once_cell",
"windows-sys 0.59.0",
]
[[package]]
+name = "rustix"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
+dependencies = [
+ "bitflags 2.5.0",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.11.0",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix-linux-procfs"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056"
+dependencies = [
+ "once_cell",
+ "rustix 1.1.2",
+]
+
+[[package]]
name = "rustls"
version = "0.23.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3705,9 +3369,6 @@ name = "semver"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
-dependencies = [
- "serde",
-]
[[package]]
name = "semver-parser"
@@ -3761,15 +3422,6 @@ dependencies = [
]
[[package]]
-name = "serde_spanned"
-version = "0.6.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
-dependencies = [
- "serde",
-]
-
-[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3870,15 +3522,6 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "shellexpand"
-version = "2.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4"
-dependencies = [
- "dirs",
-]
-
-[[package]]
-name = "shellexpand"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd1c7ddea665294d484c39fd0c0d2b7e35bbfe10035c5fe1854741a57f6880e1"
@@ -3944,7 +3587,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71b7f6629ac964d60179c1fb00dfb80da265fc3465a17245e26c1eaf678d476"
dependencies = [
- "arrayvec 0.7.2",
+ "arrayvec 0.7.6",
"thiserror 1.0.61",
]
@@ -3970,9 +3613,6 @@ name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
-dependencies = [
- "serde",
-]
[[package]]
name = "smawk"
@@ -4001,10 +3641,10 @@ dependencies = [
]
[[package]]
-name = "sptr"
-version = "0.3.2"
+name = "spin"
+version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "ssh2"
@@ -4019,12 +3659,6 @@ dependencies = [
]
[[package]]
-name = "stable_deref_trait"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
-
-[[package]]
name = "status-bar"
version = "0.1.0"
dependencies = [
@@ -4058,6 +3692,16 @@ dependencies = [
]
[[package]]
+name = "string-interner"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0"
+dependencies = [
+ "hashbrown 0.15.2",
+ "serde",
+]
+
+[[package]]
name = "strip-ansi-escapes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4210,12 +3854,6 @@ dependencies = [
]
[[package]]
-name = "target-lexicon"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc12939a1c9b9d391e0b7135f72fd30508b73450753e28341fed159317582a77"
-
-[[package]]
name = "tempfile"
version = "3.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4531,40 +4169,6 @@ dependencies = [
]
[[package]]
-name = "toml"
-version = "0.8.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
-dependencies = [
- "serde",
- "serde_spanned",
- "toml_datetime",
- "toml_edit",
-]
-
-[[package]]
-name = "toml_datetime"
-version = "0.6.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "toml_edit"
-version = "0.22.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
-dependencies = [
- "indexmap 2.7.1",
- "serde",
- "serde_spanned",
- "toml_datetime",
- "winnow",
-]
-
-[[package]]
name = "tower"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4649,17 +4253,6 @@ dependencies = [
]
[[package]]
-name = "trait-variant"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.96",
-]
-
-[[package]]
name = "tungstenite"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4776,15 +4369,9 @@ checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unicode-width"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
-
-[[package]]
-name = "unicode-xid"
-version = "0.2.4"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
+checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
[[package]]
name = "untrusted"
@@ -4927,6 +4514,30 @@ dependencies = [
]
[[package]]
+name = "wasi-common"
+version = "34.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82735a180c58e86727965ca39c0390d21c86a354f03cf7a3fc73f1de4c01ae1c"
+dependencies = [
+ "anyhow",
+ "bitflags 2.5.0",
+ "cap-fs-ext",
+ "cap-rand",
+ "cap-std",
+ "cap-time-ext",
+ "fs-set-times",
+ "io-extras",
+ "io-lifetimes 2.0.3",
+ "log",
+ "rustix 1.1.2",
+ "system-interface",
+ "thiserror 2.0.12",
+ "tracing",
+ "wiggle",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
name = "wasm-bindgen"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4994,341 +4605,84 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]]
name = "wasm-encoder"
-version = "0.221.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c17a3bd88f2155da63a1f2fcb8a56377a24f0b6dfed12733bb5f544e86f690c5"
-dependencies = [
- "leb128",
- "wasmparser 0.221.3",
-]
-
-[[package]]
-name = "wasm-encoder"
-version = "0.224.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7249cf8cb0c6b9cb42bce90c0a5feb276fbf963fa385ff3d818ab3d90818ed6"
-dependencies = [
- "leb128",
- "wasmparser 0.224.0",
-]
-
-[[package]]
-name = "wasmparser"
-version = "0.221.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185"
-dependencies = [
- "bitflags 2.5.0",
- "hashbrown 0.15.2",
- "indexmap 2.7.1",
- "semver 1.0.17",
- "serde",
-]
-
-[[package]]
-name = "wasmparser"
-version = "0.224.0"
+version = "0.239.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65881a664fdd43646b647bb27bf186ab09c05bf56779d40aed4c6dce47d423f5"
+checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c"
dependencies = [
- "bitflags 2.5.0",
- "indexmap 2.7.1",
- "semver 1.0.17",
+ "leb128fmt",
+ "wasmparser 0.239.0",
]
[[package]]
-name = "wasmprinter"
-version = "0.221.2"
+name = "wasmi"
+version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a80742ff1b9e6d8c231ac7c7247782c6fc5bce503af760bca071811e5fc9ee56"
+checksum = "cb4f6b71d5cb04a4615b9a8a2e522ba284c491ad847afd9e905d89be15e3efc0"
dependencies = [
- "anyhow",
- "termcolor",
- "wasmparser 0.221.3",
-]
-
-[[package]]
-name = "wasmtime"
-version = "29.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11976a250672556d1c4c04c6d5d7656ac9192ac9edc42a4587d6c21460010e69"
-dependencies = [
- "addr2line 0.24.2",
- "anyhow",
- "async-trait",
- "bitflags 2.5.0",
- "bumpalo",
- "cc",
- "cfg-if",
- "encoding_rs",
- "fxprof-processed-profile",
- "gimli 0.31.1",
- "hashbrown 0.14.5",
- "indexmap 2.7.1",
- "ittapi",
- "libc",
- "log",
- "mach2",
- "memfd",
- "object 0.36.7",
- "once_cell",
- "paste",
- "postcard",
- "psm",
- "pulley-interpreter",
- "rayon",
- "rustix 0.38.44",
- "semver 1.0.17",
- "serde",
- "serde_derive",
- "serde_json",
- "smallvec",
- "sptr",
- "target-lexicon",
- "trait-variant",
- "wasm-encoder 0.221.2",
- "wasmparser 0.221.3",
- "wasmtime-asm-macros",
- "wasmtime-cache",
- "wasmtime-component-macro",
- "wasmtime-component-util",
- "wasmtime-cranelift",
- "wasmtime-environ",
- "wasmtime-fiber",
- "wasmtime-jit-debug",
- "wasmtime-jit-icache-coherence",
- "wasmtime-math",
- "wasmtime-slab",
- "wasmtime-versioned-export-macros",
- "wasmtime-winch",
+ "spin",
+ "wasmi_collections",
+ "wasmi_core",
+ "wasmi_ir",
+ "wasmparser 0.228.0",
"wat",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "wasmtime-asm-macros"
-version = "29.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f178b0d125201fbe9f75beaf849bd3e511891f9e45ba216a5b620802ccf64f2"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "wasmtime-cache"
-version = "29.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b1161c8f62880deea07358bc40cceddc019f1c81d46007bc390710b2fe24ffc"
-dependencies = [
- "anyhow",
- "base64 0.21.0",
- "directories-next",
- "log",
- "postcard",
- "rustix 0.38.44",
- "serde",
- "serde_derive",
- "sha2",
- "toml 0.8.13",
- "windows-sys 0.59.0",
- "zstd",
-]
-
-[[package]]
-name = "wasmtime-component-macro"
-version = "29.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d74de6592ed945d0a602f71243982a304d5d02f1e501b638addf57f42d57dfaf"
-dependencies = [
- "anyhow",
- "proc-macro2",
- "quote",
- "syn 2.0.96",
- "wasmtime-component-util",
- "wasmtime-wit-bindgen",
- "wit-parser",
-]
-
-[[package]]
-name = "wasmtime-component-util"
-version = "29.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "707dc7b3c112ab5a366b30cfe2fb5b2f8e6a0f682f16df96a5ec582bfe6f056e"
-
-[[package]]
-name = "wasmtime-cranelift"
-version = "29.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "366be722674d4bf153290fbcbc4d7d16895cc82fb3e869f8d550ff768f9e9e87"
-dependencies = [
- "anyhow",
- "cfg-if",
- "cranelift-codegen",
- "cranelift-control",
- "cranelift-entity",
- "cranelift-frontend",
- "cranelift-native",
- "gimli 0.31.1",
- "itertools 0.12.1",
- "log",
- "object 0.36.7",
- "smallvec",
- "target-lexicon",
- "thiserror 1.0.61",
- "wasmparser 0.221.3",
- "wasmtime-environ",
- "wasmtime-versioned-export-macros",
-]
-
-[[package]]
-name = "wasmtime-environ"
-version = "29.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdadc1af7097347aa276a4f008929810f726b5b46946971c660b6d421e9994ad"
-dependencies = [
- "anyhow",
- "cpp_demangle",
- "cranelift-bitset",
- "cranelift-entity",
- "gimli 0.31.1",
- "indexmap 2.7.1",
- "log",
- "object 0.36.7",
- "postcard",
- "rustc-demangle",
- "semver 1.0.17",
- "serde",
- "serde_derive",
- "smallvec",
- "target-lexicon",
- "wasm-encoder 0.221.2",
- "wasmparser 0.221.3",
- "wasmprinter",
- "wasmtime-component-util",
]
[[package]]
-name = "wasmtime-fiber"
-version = "29.0.1"
+name = "wasmi_collections"
+version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccba90d4119f081bca91190485650730a617be1fff5228f8c4757ce133d21117"
+checksum = "6a4a11fa090c4d742e5a77dbbc8efbbe1aa151db7335ca6850232e6cafbb1023"
dependencies = [
- "anyhow",
- "cc",
- "cfg-if",
- "rustix 0.38.44",
- "wasmtime-asm-macros",
- "wasmtime-versioned-export-macros",
- "windows-sys 0.59.0",
+ "string-interner",
]
[[package]]
-name = "wasmtime-jit-debug"
-version = "29.0.1"
+name = "wasmi_core"
+version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e7b61488a5ee00c35c8c22de707c36c0aecacf419a3be803a6a2ba5e860f56a"
-dependencies = [
- "object 0.36.7",
- "rustix 0.38.44",
- "wasmtime-versioned-export-macros",
-]
-
-[[package]]
-name = "wasmtime-jit-icache-coherence"
-version = "29.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec5e8552e01692e6c2e5293171704fed8abdec79d1a6995a0870ab190e5747d1"
-dependencies = [
- "anyhow",
- "cfg-if",
- "libc",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "wasmtime-math"
-version = "29.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29210ec2aa25e00f4d54605cedaf080f39ec01a872c5bd520ad04c67af1dde17"
+checksum = "ab3e422fc1f4df78c9ded6ed48c4ca6d1f55f4609f04c99962fc07532e4db61d"
dependencies = [
"libm",
]
[[package]]
-name = "wasmtime-slab"
-version = "29.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcb5821a96fa04ac14bc7b158bb3d5cd7729a053db5a74dad396cd513a5e5ccf"
-
-[[package]]
-name = "wasmtime-versioned-export-macros"
-version = "29.0.1"
+name = "wasmi_ir"
+version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86ff86db216dc0240462de40c8290887a613dddf9685508eb39479037ba97b5b"
+checksum = "13fe9f9f1747ec81644e764c4dc798f063f5d54a495f0a3b4a375bce9af65399"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.96",
+ "wasmi_core",
]
[[package]]
-name = "wasmtime-wasi"
-version = "29.0.1"
+name = "wasmi_wasi"
+version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d1be69bfcab1bdac74daa7a1f9695ab992b9c8e21b9b061e7d66434097e0ca4"
+checksum = "c947a4060d0f6b5c1c6e6fedf447d5f0f786c0b47e3b544a4acbc89b1bcf6c32"
dependencies = [
- "anyhow",
- "async-trait",
- "bitflags 2.5.0",
- "bytes",
- "cap-fs-ext",
- "cap-net-ext",
- "cap-rand",
- "cap-std",
- "cap-time-ext",
- "fs-set-times",
- "futures",
- "io-extras",
- "io-lifetimes 2.0.3",
- "rustix 0.38.44",
- "system-interface",
- "thiserror 1.0.61",
- "tokio",
- "tracing",
- "trait-variant",
- "url",
- "wasmtime",
+ "wasi-common",
+ "wasmi",
"wiggle",
- "windows-sys 0.59.0",
]
[[package]]
-name = "wasmtime-winch"
-version = "29.0.1"
+name = "wasmparser"
+version = "0.228.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdbabfb8f20502d5e1d81092b9ead3682ae59988487aafcd7567387b7a43cf8f"
+checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3"
dependencies = [
- "anyhow",
- "cranelift-codegen",
- "gimli 0.31.1",
- "object 0.36.7",
- "target-lexicon",
- "wasmparser 0.221.3",
- "wasmtime-cranelift",
- "wasmtime-environ",
- "winch-codegen",
+ "bitflags 2.5.0",
+ "indexmap 2.7.1",
]
[[package]]
-name = "wasmtime-wit-bindgen"
-version = "29.0.1"
+name = "wasmparser"
+version = "0.239.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8358319c2dd1e4db79e3c1c5d3a5af84956615343f9f89f4e4996a36816e06e6"
+checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0"
dependencies = [
- "anyhow",
- "heck 0.5.0",
+ "bitflags 2.5.0",
"indexmap 2.7.1",
- "wit-parser",
]
[[package]]
@@ -5342,24 +4696,24 @@ dependencies = [
[[package]]
name = "wast"
-version = "224.0.0"
+version = "239.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d722a51e62b669d17e5a9f6bc8ec210178b37d869114355aa46989686c5c6391"
+checksum = "9139176fe8a2590e0fb174cdcaf373b224cb93c3dde08e4297c1361d2ba1ea5d"
dependencies = [
"bumpalo",
- "leb128",
+ "leb128fmt",
"memchr",
- "unicode-width 0.2.0",
- "wasm-encoder 0.224.0",
+ "unicode-width 0.2.1",
+ "wasm-encoder",
]
[[package]]
name = "wat"
-version = "1.224.0"
+version = "1.239.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71dece6a7dd5bcbcf8d256606c7fb3faa36286d46bf3f98185407719a5ceede2"
+checksum = "3e1c941927d34709f255558166f8901a2005f8ab4a9650432e9281b7cc6f3b75"
dependencies = [
- "wast 224.0.0",
+ "wast 239.0.0",
]
[[package]]
@@ -5466,39 +4820,37 @@ dependencies = [
[[package]]
name = "wiggle"
-version = "29.0.1"
+version = "34.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b9af35bc9629c52c261465320a9a07959164928b4241980ba1cf923b9e6751d"
+checksum = "b61d3bcb6a981233e8b774738f4495ad2c724a0d73ac626f373944e9ba1e3101"
dependencies = [
"anyhow",
"async-trait",
"bitflags 2.5.0",
- "thiserror 1.0.61",
+ "thiserror 2.0.12",
"tracing",
- "wasmtime",
"wiggle-macro",
]
[[package]]
name = "wiggle-generate"
-version = "29.0.1"
+version = "34.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cf267dd05673912c8138f4b54acabe6bd53407d9d1536f0fadb6520dd16e101"
+checksum = "6e7f9816357d0d2bd1c0af68b6e302013152dfdeef4b6a488de87dc2026ed6fa"
dependencies = [
"anyhow",
"heck 0.5.0",
"proc-macro2",
"quote",
- "shellexpand 2.1.2",
"syn 2.0.96",
"witx",
]
[[package]]
name = "wiggle-macro"
-version = "29.0.1"
+version = "34.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08c5c473d4198e6c2d377f3809f713ff0c110cab88a0805ae099a82119ee250c"
+checksum = "1e215597862a78dc57fb6988bd09b76af8e632ca4cb74bc26fe3ab129b8cb3b9"
dependencies = [
"proc-macro2",
"quote",
@@ -5538,24 +4890,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
-name = "winch-codegen"
-version = "29.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f849ef2c5f46cb0a20af4b4487aaa239846e52e2c03f13fa3c784684552859c"
-dependencies = [
- "anyhow",
- "cranelift-codegen",
- "gimli 0.31.1",
- "regalloc2",
- "smallvec",
- "target-lexicon",
- "thiserror 1.0.61",
- "wasmparser 0.221.3",
- "wasmtime-cranelift",
- "wasmtime-environ",
-]
-
-[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5713,15 +5047,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
-name = "winnow"
-version = "0.6.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d"
-dependencies = [
- "memchr",
-]
-
-[[package]]
name = "winx"
version = "0.36.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5741,24 +5066,6 @@ dependencies = [
]
[[package]]
-name = "wit-parser"
-version = "0.221.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "896112579ed56b4a538b07a3d16e562d101ff6265c46b515ce0c701eef16b2ac"
-dependencies = [
- "anyhow",
- "id-arena",
- "indexmap 2.7.1",
- "log",
- "semver 1.0.17",
- "serde",
- "serde_derive",
- "serde_json",
- "unicode-xid",
- "wasmparser 0.221.3",
-]
-
-[[package]]
name = "witx"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5807,7 +5114,7 @@ dependencies = [
"anyhow",
"prost-build",
"serde_json",
- "toml 0.5.10",
+ "toml",
"which",
"xflags",
"xshell",
@@ -5895,7 +5202,7 @@ version = "0.44.0"
dependencies = [
"ansi_term",
"anyhow",
- "arrayvec 0.7.2",
+ "arrayvec 0.7.6",
"async-channel",
"async-std",
"async-trait",
@@ -5916,6 +5223,8 @@ dependencies = [
"log",
"nix 0.23.1",
"notify-debouncer-full",
+ "num_cpus",
+ "once_cell",
"prost",
"regex",
"semver 0.11.0",
@@ -5932,8 +5241,8 @@ dependencies = [
"url",
"uuid",
"vte 0.11.0",
- "wasmtime",
- "wasmtime-wasi",
+ "wasmi",
+ "wasmi_wasi",
"zellij-utils",
]
@@ -5993,7 +5302,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
- "shellexpand 3.0.0",
+ "shellexpand",
"strip-ansi-escapes",
"strum",
"strum_macros",
@@ -6052,31 +5361,3 @@ name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
-
-[[package]]
-name = "zstd"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
-dependencies = [
- "zstd-safe",
-]
-
-[[package]]
-name = "zstd-safe"
-version = "7.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a"
-dependencies = [
- "zstd-sys",
-]
-
-[[package]]
-name = "zstd-sys"
-version = "2.0.10+zstd.1.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa"
-dependencies = [
- "cc",
- "pkg-config",
-]
diff --git a/Cargo.toml b/Cargo.toml
index ce3e704f..41f22088 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -116,6 +116,7 @@ opt-level = 3
[profile.release]
lto = true
strip = true
+codegen-units = 1
[package.metadata.deb]
depends = "$auto"
@@ -144,7 +145,6 @@ plugins_from_target = ["zellij-utils/plugins_from_target"]
disable_automatic_asset_installation = ["zellij-utils/disable_automatic_asset_installation"]
vendored_curl = ["zellij-utils/vendored_curl"]
unstable = ["zellij-client/unstable", "zellij-utils/unstable"]
-singlepass = ["zellij-server/singlepass"]
web_server_capability = ["zellij-client/web_server_capability", "zellij-server/web_server_capability", "zellij-utils/web_server_capability"]
# uncomment this when developing plugins in the Zellij UI to make plugin compilation faster
diff --git a/default-plugins/tab-bar/src/line.rs b/default-plugins/tab-bar/src/line.rs
index 5dd4f183..e7a26b98 100644
--- a/default-plugins/tab-bar/src/line.rs
+++ b/default-plugins/tab-bar/src/line.rs
@@ -253,11 +253,6 @@ pub fn tab_line(
let non_tab_len =
get_current_title_len(&prefix) + swap_layout_indicator.as_ref().map(|s| s.len).unwrap_or(0);
- // if active tab alone won't fit in cols, don't draw any tabs
- if non_tab_len + active_tab.len > cols {
- return prefix;
- }
-
let mut tabs_to_render = vec![active_tab];
populate_tabs_in_tab_line(
diff --git a/default-plugins/tab-bar/src/main.rs b/default-plugins/tab-bar/src/main.rs
index c8419715..4e9fcbad 100644
--- a/default-plugins/tab-bar/src/main.rs
+++ b/default-plugins/tab-bar/src/main.rs
@@ -25,7 +25,7 @@ impl LinePart {
}
}
-#[derive(Default)]
+#[derive(Default, Debug)]
struct State {
tabs: Vec<TabInfo>,
active_tab_idx: usize,
@@ -94,6 +94,11 @@ impl ZellijPlugin for State {
eprintln!("Got unrecognized event: {:?}", event);
},
}
+ if self.tabs.is_empty() {
+ // no need to render if we have no tabs, this can sometimes happen on startup before we
+ // get the tab update and then we definitely don't want to render
+ should_render = false;
+ }
should_render
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 27792b3d..e10aff2e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,6 +5,7 @@ services:
image: ghcr.io/linuxserver/openssh-server
container_name: zellij-e2e
hostname: zellij-e2e
+ network_mode: host
environment:
PUID: 1000
PGID: 1000
diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs
index c0e50056..bf5b6564 100644
--- a/src/tests/e2e/cases.rs
+++ b/src/tests/e2e/cases.rs
@@ -376,7 +376,9 @@ pub fn toggle_pane_fullscreen() {
name: "Wait for pane to become fullscreen",
instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
- if remote_terminal.cursor_position_is(3, 2) {
+ if remote_terminal.cursor_position_is(3, 2)
+ && remote_terminal.snapshot_contains("LOCK")
+ {
// cursor is in full screen pane now
step_is_complete = true;
}
@@ -1445,6 +1447,7 @@ pub fn scrolling_inside_a_pane_with_mouse() {
if remote_terminal.cursor_position_is(63, 21)
&& remote_terminal.snapshot_contains("line1 ")
&& remote_terminal.snapshot_contains("SCROLL: 3/3")
+ && remote_terminal.snapshot_contains("LOCK")
{
step_is_complete = true;
}
@@ -2485,6 +2488,7 @@ pub fn pin_floating_panes() {
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&TOGGLE_FLOATING_PANES);
+ std::thread::sleep(std::time::Duration::from_millis(100));
step_is_complete = true;
}
step_is_complete
@@ -2517,9 +2521,14 @@ pub fn pin_floating_panes() {
runner.run_all_steps();
let last_snapshot = runner.take_snapshot_after(Step {
name: "Wait for cursor to be behind pinned pane",
- instruction: |remote_terminal: RemoteTerminal| -> bool {
+ instruction: |mut remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
- if remote_terminal.snapshot_contains("hide") {
+ if !remote_terminal.snapshot_contains("LOCK") {
+ remote_terminal.send_key(&PANE_MODE);
+ }
+ if remote_terminal.snapshot_contains("hide")
+ && remote_terminal.snapshot_contains("LOCK")
+ {
// terminal has been filled with fixture text
step_is_complete = true;
}
diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap
index 8ddc1bbf..d83c54cb 100644
--- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap
+++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap
@@ -2,7 +2,7 @@
source: src/tests/e2e/cases.rs
expression: last_snapshot
---
- Zellij
+ 
┌──────┐
│$ █ │
│ │
diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__detach_and_attach_session.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__detach_and_attach_session.snap
index a979e956..09a46724 100644
--- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__detach_and_attach_session.snap
+++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__detach_and_attach_session.snap
@@ -25,4 +25,4 @@ expression: last_snapshot
│ ││ │
│ ││ │
└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘
- Ctrl + <o> SESSION   d  Detach  w  Manager  s  Share  c  Config  p  Plugins  a  About  ...
+ Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap
index 33f36714..f0367405 100644
--- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap
+++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap
@@ -1,6 +1,5 @@
---
source: src/tests/e2e/cases.rs
-assertion_line: 323
expression: last_snapshot
---
Zellij (e2e-test)  Tab #1 
@@ -26,4 +25,4 @@ expression: last_snapshot
│ ││line20 │
│ ││li█e21 │
└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘
- Ctrl + <s> SEARCH   s  Search  ↓↑  Scroll  PgDn|PgUp  Scroll  d|u  Scroll  e  Edit  ENTER  Select 
+ Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
diff --git a/xtask/src/flags.rs b/xtask/src/flags.rs
index 43342615..080406bb 100644
--- a/xtask/src/flags.rs
+++ b/xtask/src/flags.rs
@@ -77,8 +77,6 @@ xflags::xflags! {
optional --quick-run
/// Take plugins from here, skip building plugins. Passed to zellij verbatim
optional --data-dir path: PathBuf
- /// Enable the singlepass compiler for WASM plugins
- optional --singlepass
/// Disable optimizing dependencies
optional --disable-deps-optimize
/// Compile without web server support
@@ -204,7 +202,6 @@ pub struct Run {
pub quick_run: bool,
pub data_dir: Option<PathBuf>,
- pub singlepass: bool,
pub disable_deps_optimize: bool,
pub no_web: bool,
}
diff --git a/xtask/src/pipelines.rs b/xtask/src/pipelines.rs
index fe49c4ea..16129d43 100644
--- a/xtask/src/pipelines.rs
+++ b/xtask/src/pipelines.rs
@@ -105,7 +105,6 @@ pub fn run(sh: &Shell, mut flags: flags::Run) -> anyhow::Result<()> {
let err_context =
|flags: &flags::Run| format!("failed to run pipeline 'run' with args {:?}", flags);
- let singlepass = flags.singlepass.then_some(["--features", "singlepass"]);
if flags.quick_run {
if flags.data_dir.is_some() {
eprintln!("cannot use '--data-dir' and '--quick-run' at the same time!");
@@ -134,7 +133,6 @@ pub fn run(sh: &Shell, mut flags: flags::Run) -> anyhow::Result<()> {
.args(["--package", "zellij"])
.arg("--no-default-features")
.args(["--features", features])
- .args(singlepass.iter().flatten())
.args(["--profile", profile])
.args(["--", "--data-dir", &format!("{}", data_dir.display())])
.args(&flags.args)
@@ -160,9 +158,7 @@ pub fn run(sh: &Shell, mut flags: flags::Run) -> anyhow::Result<()> {
.context("Failed to check web features for main crate")?
{
Some(features) => {
- let mut cmd = cmd!(sh, "{cargo} run")
- .args(singlepass.iter().flatten())
- .args(["--no-default-features"]);
+ let mut cmd = cmd!(sh, "{cargo} run").args(["--no-default-features"]);
if !features.is_empty() {
cmd = cmd.args(["--features", &features]);
@@ -177,7 +173,6 @@ pub fn run(sh: &Shell, mut flags: flags::Run) -> anyhow::Result<()> {
None => {
// Main crate doesn't have web_server_capability, run normally
cmd!(sh, "{cargo} run")
- .args(singlepass.iter().flatten())
.args(["--profile", profile])
.args(["--"])
.args(&flags.args)
@@ -187,7 +182,6 @@ pub fn run(sh: &Shell, mut flags: flags::Run) -> anyhow::Result<()> {
}
} else {
cmd!(sh, "{cargo} run")
- .args(singlepass.iter().flatten())
.args(["--profile", profile])
.args(["--"])
.args(&flags.args)
diff --git a/zellij-server/Cargo.toml b/zellij-server/Cargo.toml
index 9b70dce7..263e66b1 100644
--- a/zellij-server/Cargo.toml
+++ b/zellij-server/Cargo.toml
@@ -46,29 +46,21 @@ unicode-width = { workspace = true }
url = { workspace = true }
uuid = { workspace = true }
vte = { workspace = true }
-wasmtime-wasi = { version = "29.0.1", default-features = false, features = ["preview1"] } # Keep in sync with wasmtime
+wasmi_wasi = { version = "0.51.1" }
zellij-utils = { workspace = true }
tokio = { workspace = true }
+once_cell = "1.19"
+num_cpus = "1.16"
-[dependencies.wasmtime]
-version = "29.0.1" # Keep in sync with wasmtime-wasi
+[dependencies.wasmi]
+version = "0.51.1"
default-features = false
-features = [
- 'addr2line',
- 'cranelift',
- 'demangle',
- 'gc',
- 'gc-drc',
- 'parallel-compilation',
- 'runtime',
- 'std',
-]
+features = ["std"]
[dev-dependencies]
insta = "1.6.0"
tempfile = "3.2.0"
-wasmtime = { version = "29.0.1", features = ["winch"] } # Keep in sync with the other wasmtime dep
+wasmi = { version = "0.51.1", features = ["std"] }
[features]
-singlepass = ["wasmtime/winch"]
web_server_capability = ["zellij-utils/web_server_capability"]
diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs
index a20cafcd..3da7366c 100644
--- a/zellij-server/src/lib.rs
+++ b/zellij-server/src/lib.rs
@@ -34,7 +34,7 @@ use zellij_utils::pane_size::Size;
use zellij_utils::input::cli_assets::CliAssets;
-use wasmtime::{Config as WasmtimeConfig, Engine, Strategy};
+use wasmi::Engine;
use crate::{
os_input_output::ServerOsApi,
@@ -1892,14 +1892,7 @@ fn update_new_saved_config(
}
}
-#[cfg(not(feature = "singlepass"))]
-fn get_engine() -> Engine {
- log::info!("Compiling plugins using Cranelift");
- Engine::new(WasmtimeConfig::new().strategy(Strategy::Cranelift)).unwrap()
-}
-
-#[cfg(feature = "singlepass")]
-fn get_engine() -> Engine {
- log::info!("Compiling plugins using Singlepass");
- Engine::new(WasmtimeConfig::new().strategy(Strategy::Winch)).unwrap()
+pub fn get_engine() -> Engine {
+ log::info!("Loading plugins using Wasmi interpreter");
+ Engine::default()
}
diff --git a/zellij-server/src/logging_pipe.rs b/zellij-server/src/logging_pipe.rs
index b179694f..4db44acd 100644
--- a/zellij-server/src/logging_pipe.rs
+++ b/zellij-server/src/logging_pipe.rs
@@ -51,6 +51,7 @@ impl Write for LoggingPipe {
}
self.buffer.extend(buf);
+ self.flush()?;
Ok(buf.len())
}
diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs
index 65372ffd..0e74a9de 100644
--- a/zellij-server/src/panes/plugin_pane.rs
+++ b/zellij-server/src/panes/plugin_pane.rs
@@ -362,6 +362,8 @@ impl Pane for PluginPane {
}
fn request_permissions_from_user(&mut self, permissions: Option<PluginPermission>) {
self.requesting_permissions = permissions;
+ self.handle_plugin_bytes_for_all_clients(Default::default()); // to trigger the render of
+ // the permission message
}
fn render(
&mut self,
@@ -401,56 +403,44 @@ impl Pane for PluginPane {
if self.borderless {
return Ok(None);
}
- if let Some(grid) = self.grids.get(&client_id) {
- let err_context = || format!("failed to render frame for client {client_id}");
- let pane_title = if let Some(text_color_override) = self
- .pane_frame_color_override
- .as_ref()
- .and_then(|(_color, text)| text.as_ref())
- {
- text_color_override.into()
- } else if self.pane_name.is_empty()
- && input_mode == InputMode::RenamePane
- && frame_params.is_main_client
- {
- String::from("Enter name...")
- } else if self.pane_name.is_empty() {
- grid.title
- .clone()
- .unwrap_or_else(|| self.pane_title.clone())
- } else {
- self.pane_name.clone()
- };
+ let frame_geom = self.current_geom();
+ let grid = get_or_create_grid!(self, client_id);
+ let err_context = || format!("failed to render frame for client {client_id}");
+ let pane_title = if let Some(text_color_override) = self
+ .pane_frame_color_override
+ .as_ref()
+ .and_then(|(_color, text)| text.as_ref())
+ {
+ text_color_override.into()
+ } else if self.pane_name.is_empty()
+ && input_mode == InputMode::RenamePane
+ && frame_params.is_main_client
+ {
+ String::from("Enter name...")
+ } else if self.pane_name.is_empty() {
+ grid.title
+ .clone()
+ .unwrap_or_else(|| self.pane_title.clone())
+ } else {
+ self.pane_name.clone()
+ };
- let frame_geom = self.current_geom();
- let is_pinned = frame_geom.is_pinned;
- let mut frame = PaneFrame::new(
- frame_geom.into(),
- grid.scrollback_position_and_length(),
- pane_title,
- frame_params,
- )
- .is_pinned(is_pinned);
- if let Some((frame_color_override, _text)) = self.pane_frame_color_override.as_ref() {
- frame.override_color(*frame_color_override);
- }
+ let is_pinned = frame_geom.is_pinned;
+ let mut frame = PaneFrame::new(
+ frame_geom.into(),
+ grid.scrollback_position_and_length(),
+ pane_title,
+ frame_params,
+ )
+ .is_pinned(is_pinned);
+ if let Some((frame_color_override, _text)) = self.pane_frame_color_override.as_ref() {
+ frame.override_color(*frame_color_override);
+ }
- let res = match self.frame.get(&client_id) {
- // TODO: use and_then or something?
- Some(last_frame) => {
- if &frame != last_frame {
- if !self.borderless {
- let frame_output = frame.render().with_context(err_context)?;
- self.frame.insert(client_id, frame);
- Some(frame_output)
- } else {
- None
- }
- } else {
- None
- }
- },
- None => {
+ let res = match self.frame.get(&client_id) {
+ // TODO: use and_then or something?
+ Some(last_frame) => {
+ if &frame != last_frame {
if !self.borderless {
let frame_output = frame.render().with_context(err_context)?;
self.frame.insert(client_id, frame);
@@ -458,12 +448,21 @@ impl Pane for PluginPane {
} else {
None
}
- },
- };
- Ok(res)
- } else {
- Ok(None)
- }
+ } else {
+ None
+ }
+ },
+ None => {
+ if !self.borderless {
+ let frame_output = frame.render().with_context(err_context)?;
+ self.frame.insert(client_id, frame);
+ Some(frame_output)
+ } else {
+ None
+ }
+ },
+ };
+ Ok(res)
}
fn render_fake_cursor(
&mut self,
diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs
index 14fbf345..126ec356 100644
--- a/zellij-server/src/plugins/mod.rs
+++ b/zellij-server/src/plugins/mod.rs
@@ -1,3 +1,4 @@
+mod pinned_executor;
mod pipes;
mod plugin_loader;
mod plugin_map;
@@ -6,13 +7,15 @@ mod wasm_bridge;
mod watch_filesystem;
mod zellij_exports;
use log::info;
+
+pub use pinned_executor::PinnedExecutor;
use std::{
collections::{BTreeMap, HashMap, HashSet},
fs,
path::PathBuf,
time::Duration,
};
-use wasmtime::Engine;
+use wasmi::Engine;
use crate::panes::PaneId;
use crate::screen::ScreenInstruction;
@@ -313,7 +316,6 @@ pub(crate) fn plugin_thread_main(
cwd.clone(),
skip_cache,
Some(client_id),
- None,
) {
Ok((plugin_id, client_id)) => {
drop(bus.senders.send_to_screen(ScreenInstruction::AddPlugin(
@@ -389,7 +391,6 @@ pub(crate) fn plugin_thread_main(
None,
skip_cache,
None,
- None,
) {
Ok((plugin_id, _client_id)) => {
let should_be_open_in_place = false;
@@ -501,7 +502,6 @@ pub(crate) fn plugin_thread_main(
cwd,
skip_cache,
Some(client_id),
- None,
)?;
plugin_ids
.entry(run_plugin_or_alias.clone())
@@ -1124,7 +1124,6 @@ fn load_background_plugin(
cwd.clone(),
skip_cache,
Some(client_id),
- None,
) {
Ok((plugin_id, client_id)) => {
let should_float = None;
diff --git a/zellij-server/src/plugins/pinned_executor.rs b/zellij-server/src/plugins/pinned_executor.rs
new file mode 100644
index 00000000..e02df637
--- /dev/null
+++ b/zellij-server/src/plugins/pinned_executor.rs
@@ -0,0 +1,1363 @@
+use crate::plugins::plugin_map::PluginMap;
+use crate::plugins::wasm_bridge::PluginCache;
+use crate::ClientId;
+use crate::ThreadSenders;
+use std::collections::{HashMap, HashSet};
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::mpsc::{channel, Sender};
+use std::sync::{Arc, Mutex};
+use std::thread;
+use wasmi::Engine;
+use zellij_utils::input::layout::Layout;
+
+/// A dynamic thread pool that pins jobs to specific threads based on plugin_id
+/// Starts with 1 thread and expands when threads are busy, shrinks when plugins unload
+pub struct PinnedExecutor {
+ // Sparse vector - Some(thread) for active threads, None for removed threads
+ execution_threads: Arc<Mutex<Vec<Option<ExecutionThread>>>>,
+
+ // Maps plugin_id -> thread_index (permanent assignment)
+ plugin_assignments: Arc<Mutex<HashMap<u32, usize>>>,
+
+ // Maps thread_index -> set of plugin_ids assigned to it
+ thread_plugins: Arc<Mutex<HashMap<usize, HashSet<u32>>>>,
+
+ // Next thread index to use when spawning (monotonically increasing)
+ next_thread_idx: AtomicUsize,
+
+ // Maximum threads allowed
+ max_threads: usize,
+
+ // state to send to plugins (to be kept on execution threads)
+ senders: ThreadSenders,
+ plugin_map: Arc<Mutex<PluginMap>>,
+ connected_clients: Arc<Mutex<Vec<ClientId>>>,
+ default_layout: Box<Layout>,
+ plugin_cache: PluginCache,
+ engine: Engine,
+}
+
+struct ExecutionThread {
+ sender: Sender<Job>,
+ jobs_in_flight: Arc<AtomicUsize>, // Busy state tracking
+}
+
+enum Job {
+ Work(
+ Box<
+ dyn FnOnce(
+ ThreadSenders,
+ Arc<Mutex<PluginMap>>,
+ Arc<Mutex<Vec<ClientId>>>,
+ Box<Layout>,
+ PluginCache,
+ Engine,
+ ) + Send
+ + 'static,
+ >,
+ ),
+ Shutdown, // Signal to exit the worker loop
+}
+
+impl PinnedExecutor {
+ /// Creates a new pinned executor with the specified maximum number of threads
+ /// Starts with exactly 1 thread
+ pub fn new(
+ max_threads: usize,
+ senders: &ThreadSenders,
+ plugin_map: &Arc<Mutex<PluginMap>>,
+ connected_clients: &Arc<Mutex<Vec<ClientId>>>,
+ default_layout: &Box<Layout>,
+ plugin_cache: &PluginCache,
+ engine: &Engine,
+ ) -> Self {
+ let max_threads = max_threads.max(1); // At least 1
+
+ let thread_0 = Self::spawn_thread(
+ 0,
+ senders.clone(),
+ plugin_map.clone(),
+ connected_clients.clone(),
+ default_layout.clone(),
+ plugin_cache.clone(),
+ engine.clone(),
+ );
+
+ PinnedExecutor {
+ execution_threads: Arc::new(Mutex::new(vec![Some(thread_0)])),
+ plugin_assignments: Arc::new(Mutex::new(HashMap::new())),
+ thread_plugins: Arc::new(Mutex::new(HashMap::new())),
+ next_thread_idx: AtomicUsize::new(1), // Next will be index 1
+ max_threads,
+ senders: senders.clone(),
+ plugin_map: plugin_map.clone(),
+ connected_clients: connected_clients.clone(),
+ default_layout: default_layout.clone(),
+ plugin_cache: plugin_cache.clone(),
+ engine: engine.clone(),
+ }
+ }
+
+ fn spawn_thread(
+ thread_idx: usize,
+ senders: ThreadSenders,
+ plugin_map: Arc<Mutex<PluginMap>>,
+ connected_clients: Arc<Mutex<Vec<ClientId>>>,
+ default_layout: Box<Layout>,
+ plugin_cache: PluginCache,
+ engine: Engine,
+ ) -> ExecutionThread {
+ let (sender, receiver) = channel::<Job>();
+ let jobs_in_flight = Arc::new(AtomicUsize::new(0));
+ let jobs_in_flight_clone = jobs_in_flight.clone();
+
+ let thread_handle = thread::Builder::new()
+ .name(format!("plugin-exec-{}", thread_idx))
+ .spawn({
+ move || {
+ let senders = senders;
+ let plugin_map = plugin_map;
+ let connected_clients = connected_clients;
+ let default_layout = default_layout;
+ let plugin_cache = plugin_cache;
+ let engine = engine;
+ while let Ok(job) = receiver.recv() {
+ match job {
+ Job::Work(work) => {
+ work(
+ senders.clone(),
+ plugin_map.clone(),
+ connected_clients.clone(),
+ default_layout.clone(),
+ plugin_cache.clone(),
+ engine.clone(),
+ );
+ jobs_in_flight_clone.fetch_sub(1, Ordering::SeqCst);
+ },
+ Job::Shutdown => break,
+ }
+ }
+ }
+ });
+ if let Err(e) = thread_handle {
+ log::error!("Failed to spawn plugin execution thread: {}", e);
+ }
+
+ ExecutionThread {
+ sender,
+ jobs_in_flight,
+ }
+ }
+
+ /// Register a plugin and assign it to a thread
+ /// Called from wasm_bridge when loading a plugin
+ pub fn register_plugin(&self, plugin_id: u32) -> usize {
+ let mut assignments = self.plugin_assignments.lock().unwrap();
+
+ // If already assigned (shouldn't happen, but defensive)
+ if let Some(&thread_idx) = assignments.get(&plugin_id) {
+ return thread_idx;
+ }
+
+ let mut thread_plugins = self.thread_plugins.lock().unwrap();
+ let threads = self.execution_threads.lock().unwrap();
+
+ // Find a non-busy thread with assigned plugins (prefer reusing threads)
+ let mut best_thread: Option<(usize, usize)> = None; // (index, load)
+
+ for (idx, thread_opt) in threads.iter().enumerate() {
+ if let Some(thread) = thread_opt {
+ let is_busy = thread.jobs_in_flight.load(Ordering::SeqCst) > 0;
+ if !is_busy {
+ let load = thread_plugins.get(&idx).map(|s| s.len()).unwrap_or(0);
+ if best_thread.is_none() || best_thread.map(|b| load < b.1).unwrap_or(false) {
+ best_thread = Some((idx, load));
+ }
+ }
+ }
+ }
+
+ let thread_idx = if let Some((idx, _)) = best_thread {
+ // Found a non-busy thread
+ idx
+ } else {
+ // All threads are busy - need to expand
+ if threads.len() < self.max_threads {
+ // Spawn a new thread
+ let new_idx = self.next_thread_idx.fetch_add(1, Ordering::SeqCst);
+ drop(threads); // Release lock before spawning
+ self.add_thread(new_idx);
+ new_idx
+ } else {
+ // At max capacity, assign to least-loaded thread
+ threads
+ .iter()
+ .enumerate()
+ .filter_map(|(idx, t)| t.as_ref().map(|_| idx))
+ .min_by_key(|&idx| thread_plugins.get(&idx).map(|s| s.len()).unwrap_or(0))
+ .unwrap_or_else(|| {
+ log::error!("Failed to find free thread to run the plugin!");
+ 0 // this is a misconfiguration, but we don't want to crash the app
+ // if it happens
+ })
+ }
+ };
+
+ // Update mappings
+ assignments.insert(plugin_id, thread_idx);
+ thread_plugins
+ .entry(thread_idx)
+ .or_insert_with(HashSet::new)
+ .insert(plugin_id);
+
+ thread_idx
+ }
+
+ fn add_thread(&self, thread_idx: usize) {
+ let mut threads = self.execution_threads.lock().unwrap();
+ let new_thread = Self::spawn_thread(
+ thread_idx,
+ self.senders.clone(),
+ self.plugin_map.clone(),
+ self.connected_clients.clone(),
+ self.default_layout.clone(),
+ self.plugin_cache.clone(),
+ self.engine.clone(),
+ );
+
+ // Extend vector if needed
+ while threads.len() <= thread_idx {
+ threads.push(None);
+ }
+ threads[thread_idx] = Some(new_thread);
+ }
+
+ /// Execute job pinned to plugin's assigned thread
+ pub fn execute_for_plugin<F>(&self, plugin_id: u32, f: F)
+ where
+ F: FnOnce(
+ ThreadSenders,
+ Arc<Mutex<PluginMap>>,
+ Arc<Mutex<Vec<ClientId>>>,
+ Box<Layout>,
+ PluginCache,
+ Engine,
+ ) + Send
+ + 'static,
+ {
+ // Look up assigned thread
+ let thread_idx = {
+ let assignments = self.plugin_assignments.lock().unwrap();
+ assignments.get(&plugin_id).copied()
+ };
+ let Some(thread_idx) = thread_idx else {
+ log::error!("Failed to find thread for plugin with id: {}", plugin_id);
+ return;
+ };
+
+ // Get thread and mark as busy
+ let threads = self.execution_threads.lock().unwrap();
+ let thread = threads[thread_idx].as_ref();
+ let Some(thread) = thread else {
+ log::error!("Failed to find thread for plugin with id: {}", plugin_id);
+ return;
+ };
+
+ // Increment busy counter BEFORE sending work
+ thread.jobs_in_flight.fetch_add(1, Ordering::SeqCst);
+
+ // Send work
+ let job = Job::Work(Box::new(f));
+ if let Err(_) = thread.sender.send(job) {
+ // Thread died unexpectedly - this is a critical error
+ thread.jobs_in_flight.fetch_sub(1, Ordering::SeqCst);
+ log::error!("Plugin executor thread {} has died", thread_idx);
+ }
+ }
+
+ /// Load a plugin: register it and execute the load work on its assigned thread
+ /// This combines registration + execution for plugin loading
+ pub fn execute_plugin_load<F>(&self, plugin_id: u32, f: F)
+ where
+ F: FnOnce(
+ ThreadSenders,
+ Arc<Mutex<PluginMap>>,
+ Arc<Mutex<Vec<ClientId>>>,
+ Box<Layout>,
+ PluginCache,
+ Engine,
+ ) + Send
+ + 'static,
+ {
+ // Register plugin and assign to a thread
+ self.register_plugin(plugin_id);
+
+ // Execute the load work on the assigned thread
+ self.execute_for_plugin(plugin_id, f);
+ }
+
+ /// Unload a plugin: execute cleanup work, then unregister and potentially shrink pool
+ /// This combines cleanup execution + unregistration for plugin unloading
+ /// Requires Arc<Self> so we can clone it into the closure for unregistration
+ pub fn execute_plugin_unload(
+ self: &Arc<Self>,
+ plugin_id: u32,
+ f: impl FnOnce(
+ ThreadSenders,
+ Arc<Mutex<PluginMap>>,
+ Arc<Mutex<Vec<ClientId>>>,
+ Box<Layout>,
+ PluginCache,
+ Engine,
+ ) + Send
+ + 'static,
+ ) {
+ let executor = self.clone();
+ self.execute_for_plugin(
+ plugin_id,
+ move |senders, plugin_map, connected_clients, default_layout, plugin_cache, engine| {
+ // Execute the cleanup work
+ f(
+ senders,
+ plugin_map,
+ connected_clients,
+ default_layout,
+ plugin_cache,
+ engine,
+ );
+
+ // Unregister plugin and potentially shrink the pool
+ executor.unregister_plugin(plugin_id);
+ },
+ );
+ }
+
+ /// Unregister a plugin and potentially shrink the pool
+ /// Called from wasm_bridge after plugin cleanup is complete
+ pub fn unregister_plugin(&self, plugin_id: u32) {
+ let mut assignments = self.plugin_assignments.lock().unwrap();
+ let mut thread_plugins = self.thread_plugins.lock().unwrap();
+
+ if let Some(thread_idx) = assignments.remove(&plugin_id) {
+ if let Some(plugins) = thread_plugins.get_mut(&thread_idx) {
+ plugins.remove(&plugin_id);
+ }
+ }
+
+ drop(assignments);
+ drop(thread_plugins);
+
+ // Try to shrink the pool
+ self.try_shrink_pool();
+ }
+
+ fn try_shrink_pool(&self) {
+ let mut threads = self.execution_threads.lock().unwrap();
+ let thread_plugins = self.thread_plugins.lock().unwrap();
+
+ // Find threads with no assigned plugins (except thread 0, always keep it)
+ let threads_to_remove: Vec<usize> = threads
+ .iter()
+ .enumerate()
+ .skip(1) // Never remove thread 0
+ .filter_map(|(idx, thread_opt)| {
+ if thread_opt.is_some() {
+ let has_plugins = thread_plugins
+ .get(&idx)
+ .map(|s| !s.is_empty())
+ .unwrap_or(false);
+ if !has_plugins {
+ Some(idx)
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ // Shutdown and remove idle threads
+ for idx in threads_to_remove {
+ if let Some(thread) = threads[idx].take() {
+ let _ = thread.sender.send(Job::Shutdown);
+ }
+ }
+ }
+
+ #[cfg(test)]
+ pub fn thread_count(&self) -> usize {
+ self.execution_threads
+ .lock()
+ .unwrap()
+ .iter()
+ .filter(|t| t.is_some())
+ .count()
+ }
+}
+
+impl Drop for PinnedExecutor {
+ fn drop(&mut self) {
+ let mut threads = self.execution_threads.lock().unwrap();
+
+ // Send shutdown to all threads
+ for thread_opt in threads.iter_mut() {
+ if let Some(thread) = thread_opt {
+ let _ = thread.sender.send(Job::Shutdown);
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::sync::atomic::{AtomicUsize, Ordering};
+ use std::sync::mpsc::{channel, Sender};
+ use std::sync::{Arc, Barrier, Mutex};
+ use std::thread;
+ use std::time::Duration;
+
+ // Test fixtures
+ fn create_test_dependencies() -> (
+ ThreadSenders,
+ Arc<Mutex<PluginMap>>,
+ Arc<Mutex<Vec<ClientId>>>,
+ Box<Layout>,
+ PluginCache,
+ Engine,
+ ) {
+ use std::path::PathBuf;
+ use wasmi::Module;
+ use zellij_utils::channels::{self, SenderWithContext};
+
+ let (send_to_pty, _receive_pty) = channels::bounded(1);
+ let (send_to_screen, _receive_screen) = channels::bounded(1);
+ let (send_to_plugin, _receive_plugin) = channels::bounded(1);
+ let (send_to_server, _receive_server) = channels::bounded(1);
+ let (send_to_pty_writer, _receive_pty_writer) = channels::bounded(1);
+ let (send_to_background_jobs, _receive_background_jobs) = channels::bounded(1);
+
+ let to_pty = SenderWithContext::new(send_to_pty);
+ let to_screen = SenderWithContext::new(send_to_screen);
+ let to_plugin = SenderWithContext::new(send_to_plugin);
+ let to_server = SenderWithContext::new(send_to_server);
+ let to_pty_writer = SenderWithContext::new(send_to_pty_writer);
+ let to_background_jobs = SenderWithContext::new(send_to_background_jobs);
+
+ let senders = ThreadSenders {
+ to_pty: Some(to_pty),
+ to_screen: Some(to_screen),
+ to_plugin: Some(to_plugin),
+ to_server: Some(to_server),
+ to_pty_writer: Some(to_pty_writer),
+ to_background_jobs: Some(to_background_jobs),
+ should_silently_fail: false,
+ };
+
+ let plugin_map = Arc::new(Mutex::new(PluginMap::default()));
+ let connected_clients = Arc::new(Mutex::new(vec![]));
+
+ let layout = Box::new(Layout::default());
+
+ let plugin_cache = Arc::new(Mutex::new(
+ std::collections::HashMap::<PathBuf, Module>::new(),
+ ));
+
+ let engine = Engine::default();
+
+ (
+ senders,
+ plugin_map,
+ connected_clients,
+ layout,
+ plugin_cache,
+ engine,
+ )
+ }
+
+ fn create_test_executor(max_threads: usize) -> Arc<PinnedExecutor> {
+ let (senders, plugin_map, clients, layout, cache, engine) = create_test_dependencies();
+ Arc::new(PinnedExecutor::new(
+ max_threads,
+ &senders,
+ &plugin_map,
+ &clients,
+ &layout,
+ &cache,
+ &engine,
+ ))
+ }
+
+ // Helper to create a job that signals completion via channel
+ fn make_signaling_job(
+ tx: Sender<()>,
+ ) -> impl FnOnce(
+ ThreadSenders,
+ Arc<Mutex<PluginMap>>,
+ Arc<Mutex<Vec<ClientId>>>,
+ Box<Layout>,
+ PluginCache,
+ Engine,
+ ) + Send
+ + 'static {
+ move |_senders, _plugin_map, _clients, _layout, _cache, _engine| {
+ tx.send(()).unwrap();
+ }
+ }
+
+ // Helper to verify thread assignment by capturing thread name in job
+ fn get_thread_name_for_plugin(executor: &Arc<PinnedExecutor>, plugin_id: u32) -> String {
+ let (tx, rx) = channel();
+ executor.execute_for_plugin(plugin_id, move |_s, _p, _c, _l, _ca, _e| {
+ let name = thread::current().name().unwrap().to_string();
+ tx.send(name).unwrap();
+ });
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Thread name should be received")
+ }
+
+ #[test]
+ fn test_new_creates_one_thread() {
+ let executor = create_test_executor(4);
+ assert_eq!(executor.thread_count(), 1);
+ }
+
+ #[test]
+ fn test_new_respects_min_threads() {
+ let executor = create_test_executor(0);
+ assert_eq!(
+ executor.thread_count(),
+ 1,
+ "Executor should enforce minimum of 1 thread"
+ );
+ }
+
+ #[test]
+ fn test_first_plugin_assigned_to_thread_zero() {
+ let executor = create_test_executor(4);
+ let thread_idx = executor.register_plugin(1);
+ assert_eq!(thread_idx, 0);
+ }
+
+ #[test]
+ fn test_multiple_plugins_share_thread_when_idle() {
+ let executor = create_test_executor(4);
+ let thread_idx1 = executor.register_plugin(1);
+ let thread_idx2 = executor.register_plugin(2);
+ assert_eq!(thread_idx1, 0);
+ assert_eq!(
+ thread_idx2, 0,
+ "Second plugin should share thread 0 when idle"
+ );
+ }
+
+ #[test]
+ fn test_new_thread_spawns_when_all_busy() {
+ let executor = create_test_executor(3);
+
+ // Register plugin 1, gets thread 0
+ let thread_idx1 = executor.register_plugin(1);
+ assert_eq!(thread_idx1, 0);
+
+ // Make thread 0 busy with a barrier
+ let barrier = Arc::new(Barrier::new(2));
+ let barrier_clone = barrier.clone();
+ executor.execute_for_plugin(1, move |_s, _p, _c, _l, _ca, _e| {
+ barrier_clone.wait();
+ });
+
+ // Give the job a moment to start executing and block
+ thread::sleep(Duration::from_millis(50));
+
+ // Register plugin 2 while thread 0 is busy
+ let thread_idx2 = executor.register_plugin(2);
+ assert_eq!(
+ thread_idx2, 1,
+ "Plugin 2 should get new thread 1 when thread 0 is busy"
+ );
+
+ // Verify thread count
+ assert_eq!(executor.thread_count(), 2);
+
+ // Release barrier
+ barrier.wait();
+ }
+
+ #[test]
+ fn test_respects_max_threads_limit() {
+ let executor = create_test_executor(2);
+
+ // Register plugin 1, gets thread 0
+ executor.register_plugin(1);
+
+ // Make thread 0 busy
+ let barrier1 = Arc::new(Barrier::new(2));
+ let barrier1_clone = barrier1.clone();
+ executor.execute_for_plugin(1, move |_s, _p, _c, _l, _ca, _e| {
+ barrier1_clone.wait();
+ });
+ thread::sleep(Duration::from_millis(50));
+
+ // Register plugin 2, gets thread 1
+ executor.register_plugin(2);
+
+ // Make thread 1 busy
+ let barrier2 = Arc::new(Barrier::new(2));
+ let barrier2_clone = barrier2.clone();
+ executor.execute_for_plugin(2, move |_s, _p, _c, _l, _ca, _e| {
+ barrier2_clone.wait();
+ });
+ thread::sleep(Duration::from_millis(50));
+
+ // Register plugin 3 when all threads busy
+ let thread_idx3 = executor.register_plugin(3);
+ assert!(
+ thread_idx3 == 0 || thread_idx3 == 1,
+ "Plugin 3 should be assigned to existing thread"
+ );
+ assert_eq!(executor.thread_count(), 2, "Should not exceed max_threads");
+
+ // Release barriers
+ barrier1.wait();
+ barrier2.wait();
+ }
+
+ #[test]
+ fn test_duplicate_registration_returns_same_thread() {
+ let executor = create_test_executor(4);
+ let thread_idx1 = executor.register_plugin(1);
+ let thread_idx2 = executor.register_plugin(1);
+ assert_eq!(
+ thread_idx1, thread_idx2,
+ "Duplicate registration should return same thread"
+ );
+ }
+
+ #[test]
+ fn test_load_balancing_prefers_least_loaded() {
+ let executor = create_test_executor(3);
+
+ // Register plugins 1, 2, 3 to thread 0 (when idle)
+ executor.register_plugin(1);
+ executor.register_plugin(2);
+ executor.register_plugin(3);
+
+ // Make thread 0 busy
+ let barrier = Arc::new(Barrier::new(2));
+ let barrier_clone = barrier.clone();
+ executor.execute_for_plugin(1, move |_s, _p, _c, _l, _ca, _e| {
+ barrier_clone.wait();
+ });
+ thread::sleep(Duration::from_millis(50));
+
+ // Register plugin 4 while thread 0 is busy (spawns thread 1)
+ let thread_idx4 = executor.register_plugin(4);
+ assert_eq!(thread_idx4, 1);
+
+ // Release barrier
+ barrier.wait();
+ thread::sleep(Duration::from_millis(50));
+
+ // Register plugin 5 when both threads idle
+ // Thread 0 has 3 plugins, thread 1 has 1 plugin
+ let thread_idx5 = executor.register_plugin(5);
+ assert_eq!(
+ thread_idx5, 1,
+ "Plugin 5 should be assigned to less loaded thread 1"
+ );
+ }
+
+ #[test]
+ fn test_execute_for_plugin_runs_on_correct_thread() {
+ let executor = create_test_executor(3);
+
+ // Register plugin 1 to thread 0
+ executor.register_plugin(1);
+
+ // Make thread 0 busy to force plugin 2 to thread 1
+ let barrier = Arc::new(Barrier::new(2));
+ let barrier_clone = barrier.clone();
+ executor.execute_for_plugin(1, move |_s, _p, _c, _l, _ca, _e| {
+ barrier_clone.wait();
+ });
+ thread::sleep(Duration::from_millis(50));
+
+ // Register plugin 2 to thread 1
+ executor.register_plugin(2);
+
+ // Release barrier
+ barrier.wait();
+ thread::sleep(Duration::from_millis(50));
+
+ // Get thread names for both plugins
+ let thread_name1 = get_thread_name_for_plugin(&executor, 1);
+ let thread_name2 = get_thread_name_for_plugin(&executor, 2);
+
+ assert_eq!(thread_name1, "plugin-exec-0");
+ assert_eq!(thread_name2, "plugin-exec-1");
+ }
+
+ #[test]
+ fn test_execute_for_plugin_unregistered() {
+ let executor = create_test_executor(4);
+ let (tx, rx) = channel();
+
+ // Execute job for unregistered plugin
+ executor.execute_for_plugin(999, make_signaling_job(tx));
+
+ // Try to receive with timeout - should timeout
+ let result = rx.recv_timeout(Duration::from_millis(100));
+ assert!(
+ result.is_err(),
+ "Job for unregistered plugin should not execute"
+ );
+ }
+
+ #[test]
+ fn test_job_execution_order_per_thread() {
+ let executor = create_test_executor(4);
+ executor.register_plugin(1);
+
+ let order = Arc::new(Mutex::new(Vec::new()));
+ let (tx, rx) = channel();
+
+ // Execute 3 jobs for plugin 1
+ for i in 1..=3 {
+ let order_clone = order.clone();
+ let tx_clone = tx.clone();
+ executor.execute_for_plugin(1, move |_s, _p, _c, _l, _ca, _e| {
+ order_clone.lock().unwrap().push(i);
+ tx_clone.send(()).unwrap();
+ });
+ }
+
+ // Wait for all 3 jobs to complete
+ for _ in 0..3 {
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Job should complete");
+ }
+
+ assert_eq!(
+ *order.lock().unwrap(),
+ vec![1, 2, 3],
+ "Jobs should execute in order"
+ );
+ }
+
+ #[test]
+ fn test_jobs_complete_successfully() {
+ let executor = create_test_executor(4);
+ executor.register_plugin(1);
+
+ let (tx, rx) = channel();
+ executor.execute_for_plugin(1, make_signaling_job(tx));
+
+ let result = rx.recv_timeout(Duration::from_secs(5));
+ assert!(result.is_ok(), "Job should complete successfully");
+ }
+
+ #[test]
+ fn test_concurrent_jobs_on_different_threads() {
+ let executor = create_test_executor(3);
+
+ // Register plugin 1 to thread 0
+ executor.register_plugin(1);
+
+ // Make thread 0 busy to force plugin 2 to thread 1
+ let barrier = Arc::new(Barrier::new(2));
+ let barrier_clone = barrier.clone();
+ executor.execute_for_plugin(1, move |_s, _p, _c, _l, _ca, _e| {
+ barrier_clone.wait();
+ });
+ thread::sleep(Duration::from_millis(50));
+
+ // Register plugin 2 to thread 1
+ executor.register_plugin(2);
+
+ // Release barrier
+ barrier.wait();
+ thread::sleep(Duration::from_millis(50));
+
+ // Execute jobs on both threads concurrently
+ let sync_barrier = Arc::new(Barrier::new(3)); // 2 jobs + test thread
+ let (tx1, rx1) = channel();
+ let (tx2, rx2) = channel();
+
+ let barrier1 = sync_barrier.clone();
+ executor.execute_for_plugin(1, move |_s, _p, _c, _l, _ca, _e| {
+ barrier1.wait();
+ tx1.send(()).unwrap();
+ });
+
+ let barrier2 = sync_barrier.clone();
+ executor.execute_for_plugin(2, move |_s, _p, _c, _l, _ca, _e| {
+ barrier2.wait();
+ tx2.send(()).unwrap();
+ });
+
+ // Release both jobs simultaneously
+ sync_barrier.wait();
+
+ // Both should complete
+ assert!(rx1.recv_timeout(Duration::from_secs(5)).is_ok());
+ assert!(rx2.recv_timeout(Duration::from_secs(5)).is_ok());
+ }
+
+ #[test]
+ fn test_execute_plugin_load_registers_and_executes() {
+ let executor = create_test_executor(4);
+
+ let (tx, rx) = channel();
+ executor.execute_plugin_load(1, make_signaling_job(tx));
+
+ // Wait for load to complete
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+
+ // Verify plugin is registered
+ let thread_idx = executor.register_plugin(1);
+ assert_eq!(thread_idx, 0, "Plugin should already be registered");
+ }
+
+ #[test]
+ fn test_execute_plugin_unload_runs_cleanup_before_unregister() {
+ let executor = create_test_executor(4);
+
+ // Load plugin
+ let (tx_load, rx_load) = channel();
+ executor.execute_plugin_load(1, make_signaling_job(tx_load));
+ rx_load
+ .recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+
+ // Unload plugin with cleanup
+ let counter = Arc::new(AtomicUsize::new(0));
+ let counter_clone = counter.clone();
+ let (tx_unload, rx_unload) = channel();
+
+ executor.execute_plugin_unload(1, move |_s, _p, _c, _l, _ca, _e| {
+ counter_clone.fetch_add(1, Ordering::SeqCst);
+ tx_unload.send(()).unwrap();
+ });
+
+ // Wait for unload to complete
+ rx_unload
+ .recv_timeout(Duration::from_secs(5))
+ .expect("Unload should complete");
+
+ // Verify cleanup ran
+ assert_eq!(counter.load(Ordering::SeqCst), 1, "Cleanup should have run");
+
+ // Give unregister a moment to complete
+ thread::sleep(Duration::from_millis(100));
+
+ // Plugin should be unregistered - registering again should assign new thread
+ let thread_idx = executor.register_plugin(1);
+ assert_eq!(thread_idx, 0, "Plugin should be re-registered to thread 0");
+ }
+
+ #[test]
+ fn test_unload_sequence_is_correct() {
+ let executor = create_test_executor(4);
+
+ // Load plugin
+ let (tx_load, rx_load) = channel();
+ executor.execute_plugin_load(1, make_signaling_job(tx_load));
+ rx_load
+ .recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+
+ // Unload with sequence tracking
+ let sequence = Arc::new(Mutex::new(Vec::new()));
+ let sequence_clone = sequence.clone();
+ let (tx_unload, rx_unload) = channel();
+
+ executor.execute_plugin_unload(1, move |_s, _p, _c, _l, _ca, _e| {
+ sequence_clone.lock().unwrap().push("cleanup");
+ tx_unload.send(()).unwrap();
+ });
+
+ // Wait for unload to complete
+ rx_unload
+ .recv_timeout(Duration::from_secs(5))
+ .expect("Unload should complete");
+ sequence.lock().unwrap().push("after");
+
+ assert_eq!(*sequence.lock().unwrap(), vec!["cleanup", "after"]);
+ }
+
+ #[test]
+ fn test_shrink_removes_idle_threads() {
+ let executor = create_test_executor(4);
+
+ // Load plugin 1 to thread 0
+ let (tx1, rx1) = channel();
+ executor.execute_plugin_load(1, make_signaling_job(tx1));
+ rx1.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+
+ // Make thread 0 busy to force plugin 2 to thread 1
+ let barrier = Arc::new(Barrier::new(2));
+ let barrier_clone = barrier.clone();
+ executor.execute_for_plugin(1, move |_s, _p, _c, _l, _ca, _e| {
+ barrier_clone.wait();
+ });
+ thread::sleep(Duration::from_millis(50));
+
+ // Load plugin 2 to thread 1
+ let (tx2, rx2) = channel();
+ executor.execute_plugin_load(2, make_signaling_job(tx2));
+ rx2.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+
+ // Release barrier
+ barrier.wait();
+ thread::sleep(Duration::from_millis(50));
+
+ let thread_count_before = executor.thread_count();
+ assert!(thread_count_before >= 2, "Should have at least 2 threads");
+
+ // Unload plugin 2
+ let (tx_unload2, rx_unload2) = channel();
+ executor.execute_plugin_unload(2, make_signaling_job(tx_unload2));
+ rx_unload2
+ .recv_timeout(Duration::from_secs(5))
+ .expect("Unload should complete");
+
+ // Give shrinking a moment to complete
+ thread::sleep(Duration::from_millis(100));
+
+ // Thread count should decrease after unloading
+ let thread_count_after = executor.thread_count();
+ assert!(
+ thread_count_after < thread_count_before,
+ "Idle threads should be removed"
+ );
+ assert!(thread_count_after >= 1, "Thread 0 should remain");
+ }
+
+ #[test]
+ fn test_thread_zero_never_removed() {
+ let executor = create_test_executor(4);
+
+ // Load a plugin and then unload it
+ let (tx_load, rx_load) = channel();
+ executor.execute_plugin_load(1, make_signaling_job(tx_load));
+ rx_load
+ .recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+
+ let (tx_unload, rx_unload) = channel();
+ executor.execute_plugin_unload(1, make_signaling_job(tx_unload));
+ rx_unload
+ .recv_timeout(Duration::from_secs(5))
+ .expect("Unload should complete");
+
+ // Give shrinking a moment
+ thread::sleep(Duration::from_millis(100));
+
+ // Thread 0 should remain
+ assert!(
+ executor.thread_count() >= 1,
+ "Thread 0 should never be removed"
+ );
+ }
+
+ #[test]
+ fn test_active_threads_not_removed() {
+ let executor = create_test_executor(4);
+
+ // Load plugin 1 to thread 0
+ let (tx1, rx1) = channel();
+ executor.execute_plugin_load(1, make_signaling_job(tx1));
+ rx1.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+
+ // Force plugin 2 to thread 1 by making thread 0 busy
+ let barrier = Arc::new(Barrier::new(2));
+ let barrier_clone = barrier.clone();
+ executor.execute_for_plugin(1, move |_s, _p, _c, _l, _ca, _e| {
+ barrier_clone.wait();
+ });
+ thread::sleep(Duration::from_millis(50));
+
+ let (tx2, rx2) = channel();
+ executor.execute_plugin_load(2, make_signaling_job(tx2));
+ rx2.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+
+ barrier.wait();
+ thread::sleep(Duration::from_millis(100));
+
+ let thread_count_with_both = executor.thread_count();
+ assert!(
+ thread_count_with_both >= 2,
+ "Should have at least 2 threads with 2 plugins"
+ );
+
+ // Unload plugin 2
+ let (tx_unload, rx_unload) = channel();
+ executor.execute_plugin_unload(2, make_signaling_job(tx_unload));
+ rx_unload
+ .recv_timeout(Duration::from_secs(5))
+ .expect("Unload should complete");
+
+ thread::sleep(Duration::from_millis(100));
+
+ // Plugin 1's thread should still work (verify active threads not affected)
+ let (tx_test, rx_test) = channel();
+ executor.execute_for_plugin(1, make_signaling_job(tx_test));
+ assert!(
+ rx_test.recv_timeout(Duration::from_secs(5)).is_ok(),
+ "Plugin 1's thread should still work"
+ );
+
+ // Thread count should decrease after unloading
+ let thread_count_after = executor.thread_count();
+ assert!(
+ thread_count_after < thread_count_with_both,
+ "Idle thread should be removed"
+ );
+ assert!(thread_count_after >= 1, "Active threads should remain");
+ }
+
+ #[test]
+ fn test_shrink_does_not_affect_remaining_threads() {
+ let executor = create_test_executor(4);
+
+ // Load plugin 1 to thread 0
+ let (tx1, rx1) = channel();
+ executor.execute_plugin_load(1, make_signaling_job(tx1));
+ rx1.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+
+ // Force plugin 2 to thread 1
+ let barrier = Arc::new(Barrier::new(2));
+ let barrier_clone = barrier.clone();
+ executor.execute_for_plugin(1, move |_s, _p, _c, _l, _ca, _e| {
+ barrier_clone.wait();
+ });
+ thread::sleep(Duration::from_millis(50));
+
+ let (tx2, rx2) = channel();
+ executor.execute_plugin_load(2, make_signaling_job(tx2));
+ rx2.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+
+ barrier.wait();
+ thread::sleep(Duration::from_millis(50));
+
+ // Unload plugin 2 (shrinks pool)
+ let (tx_unload, rx_unload) = channel();
+ executor.execute_plugin_unload(2, make_signaling_job(tx_unload));
+ rx_unload
+ .recv_timeout(Duration::from_secs(5))
+ .expect("Unload should complete");
+
+ thread::sleep(Duration::from_millis(100));
+
+ // Execute job for plugin 1
+ let (tx_test, rx_test) = channel();
+ executor.execute_for_plugin(1, make_signaling_job(tx_test));
+
+ assert!(
+ rx_test.recv_timeout(Duration::from_secs(5)).is_ok(),
+ "Plugin 1's thread should still work"
+ );
+ }
+
+ #[test]
+ fn test_drop_cleans_up_gracefully() {
+ let executor = create_test_executor(4);
+
+ // Load multiple plugins on different threads
+ let (tx1, rx1) = channel();
+ executor.execute_plugin_load(1, make_signaling_job(tx1));
+ rx1.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+
+ let barrier = Arc::new(Barrier::new(2));
+ let barrier_clone = barrier.clone();
+ executor.execute_for_plugin(1, move |_s, _p, _c, _l, _ca, _e| {
+ barrier_clone.wait();
+ });
+ thread::sleep(Duration::from_millis(50));
+
+ let (tx2, rx2) = channel();
+ executor.execute_plugin_load(2, make_signaling_job(tx2));
+ rx2.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+
+ barrier.wait();
+
+ // Drop executor
+ drop(executor);
+
+ // Test completes without panic
+ }
+
+ #[test]
+ fn test_drop_with_jobs_in_flight() {
+ let executor = create_test_executor(4);
+ executor.register_plugin(1);
+
+ let barrier = Arc::new(Barrier::new(2));
+ let barrier_clone = barrier.clone();
+ executor.execute_for_plugin(1, move |_s, _p, _c, _l, _ca, _e| {
+ barrier_clone.wait();
+ });
+
+ // Drop executor while job is blocked
+ drop(executor);
+
+ // Release barrier (job may or may not complete, but shouldn't panic)
+ barrier.wait();
+
+ // Test completes without panic
+ }
+
+ #[test]
+ fn test_concurrent_plugin_registrations() {
+ let executor = create_test_executor(4);
+
+ let handles: Vec<_> = (1..=10)
+ .map(|i| {
+ let exec = executor.clone();
+ thread::spawn(move || exec.register_plugin(i))
+ })
+ .collect();
+
+ for handle in handles {
+ let thread_idx = handle.join().expect("Thread should not panic");
+ assert!(thread_idx < 4, "Thread index should be valid");
+ }
+ }
+
+ #[test]
+ fn test_unregister_nonexistent_plugin() {
+ let executor = create_test_executor(4);
+
+ // Unregister non-existent plugin
+ executor.unregister_plugin(999);
+
+ // Executor should still work
+ let (tx, rx) = channel();
+ executor.execute_plugin_load(1, make_signaling_job(tx));
+ assert!(rx.recv_timeout(Duration::from_secs(5)).is_ok());
+ }
+
+ #[test]
+ fn test_max_threads_one() {
+ let executor = create_test_executor(1);
+
+ // Register multiple plugins
+ let thread_idx1 = executor.register_plugin(1);
+ let thread_idx2 = executor.register_plugin(2);
+ let thread_idx3 = executor.register_plugin(3);
+
+ assert_eq!(thread_idx1, 0);
+ assert_eq!(thread_idx2, 0);
+ assert_eq!(thread_idx3, 0);
+ assert_eq!(executor.thread_count(), 1);
+ }
+
+ #[test]
+ fn test_rapid_load_unload_cycles() {
+ let executor = create_test_executor(4);
+
+ // Load plugin 1
+ let (tx1, rx1) = channel();
+ executor.execute_plugin_load(1, make_signaling_job(tx1));
+ rx1.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+
+ // Unload plugin 1
+ let (tx2, rx2) = channel();
+ executor.execute_plugin_unload(1, make_signaling_job(tx2));
+ rx2.recv_timeout(Duration::from_secs(5))
+ .expect("Unload should complete");
+
+ thread::sleep(Duration::from_millis(100));
+
+ // Load plugin 1 again
+ let (tx3, rx3) = channel();
+ executor.execute_plugin_load(1, make_signaling_job(tx3));
+ rx3.recv_timeout(Duration::from_secs(5))
+ .expect("Second load should complete");
+
+ // Execute job for plugin 1
+ let (tx4, rx4) = channel();
+ executor.execute_for_plugin(1, make_signaling_job(tx4));
+ assert!(
+ rx4.recv_timeout(Duration::from_secs(5)).is_ok(),
+ "Executor should handle cycles correctly"
+ );
+ }
+
+ #[test]
+ fn test_many_plugins_limited_threads() {
+ let executor = create_test_executor(4);
+ let (tx, rx) = channel();
+
+ // Load 20 plugins
+ for i in 1..=20 {
+ let tx_clone = tx.clone();
+ executor.execute_plugin_load(i, make_signaling_job(tx_clone));
+ }
+
+ // Collect 20 completion signals
+ for _ in 1..=20 {
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+ }
+
+ assert!(
+ executor.thread_count() <= 4,
+ "Should not exceed max_threads"
+ );
+ }
+
+ #[test]
+ fn test_full_lifecycle() {
+ let executor = create_test_executor(4);
+ let (tx, rx) = channel();
+
+ // Load 5 plugins
+ for i in 1..=5 {
+ let tx_clone = tx.clone();
+ executor.execute_plugin_load(i, make_signaling_job(tx_clone));
+ }
+
+ // Wait for all loads
+ for _ in 0..5 {
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+ }
+
+ // Execute 2 jobs per plugin (10 total)
+ for i in 1..=5 {
+ for _ in 0..2 {
+ let tx_clone = tx.clone();
+ executor.execute_for_plugin(i, make_signaling_job(tx_clone));
+ }
+ }
+
+ // Wait for all jobs
+ for _ in 0..10 {
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Job should complete");
+ }
+
+ let thread_count_before = executor.thread_count();
+
+ // Unload 3 plugins
+ for i in 1..=3 {
+ let tx_clone = tx.clone();
+ executor.execute_plugin_unload(i, make_signaling_job(tx_clone));
+ }
+
+ // Wait for unloads
+ for _ in 0..3 {
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Unload should complete");
+ }
+
+ thread::sleep(Duration::from_millis(100));
+
+ // Thread count should decrease or stay the same
+ let thread_count_after = executor.thread_count();
+ assert!(
+ thread_count_after <= thread_count_before,
+ "Thread count should decrease after unloads"
+ );
+
+ // Execute jobs for remaining plugins
+ for i in 4..=5 {
+ let tx_clone = tx.clone();
+ executor.execute_for_plugin(i, make_signaling_job(tx_clone));
+ }
+
+ for _ in 0..2 {
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Job should complete");
+ }
+
+ // Drop executor
+ drop(executor);
+ }
+
+ #[test]
+ fn test_realistic_plugin_churn() {
+ let executor = create_test_executor(4);
+ let (tx, rx) = channel();
+
+ // Load plugins 1, 2, 3
+ for i in 1..=3 {
+ let tx_clone = tx.clone();
+ executor.execute_plugin_load(i, make_signaling_job(tx_clone));
+ }
+ for _ in 0..3 {
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+ }
+
+ // Execute jobs for each
+ for i in 1..=3 {
+ let tx_clone = tx.clone();
+ executor.execute_for_plugin(i, make_signaling_job(tx_clone));
+ }
+ for _ in 0..3 {
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Job should complete");
+ }
+
+ // Unload plugin 2
+ let tx_clone = tx.clone();
+ executor.execute_plugin_unload(2, make_signaling_job(tx_clone));
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Unload should complete");
+
+ thread::sleep(Duration::from_millis(100));
+
+ // Load plugins 4, 5
+ for i in 4..=5 {
+ let tx_clone = tx.clone();
+ executor.execute_plugin_load(i, make_signaling_job(tx_clone));
+ }
+ for _ in 0..2 {
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Load should complete");
+ }
+
+ // Execute jobs for plugins 1, 3, 4, 5
+ for i in &[1, 3, 4, 5] {
+ let tx_clone = tx.clone();
+ executor.execute_for_plugin(*i, make_signaling_job(tx_clone));
+ }
+ for _ in 0..4 {
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Job should complete");
+ }
+
+ // Unload plugins 1, 3
+ for i in &[1, 3] {
+ let tx_clone = tx.clone();
+ executor.execute_plugin_unload(*i, make_signaling_job(tx_clone));
+ }
+ for _ in 0..2 {
+ rx.recv_timeout(Duration::from_secs(5))
+ .expect("Unload should complete");
+ }
+
+ thread::sleep(Duration::from_millis(100));
+
+ // Verify thread count reflects active plugins (4 and 5)
+ assert!(
+ executor.thread_count() >= 1,
+ "Should have at least thread 0"
+ );
+
+ drop(executor);
+ }
+}
diff --git a/zellij-server/src/plugins/pipes.rs b/zellij-server/src/plugins/pipes.rs
index 840af17b..4e516c44 100644
--- a/zellij-server/src/plugins/pipes.rs
+++ b/zellij-server/src/plugins/pipes.rs
@@ -171,7 +171,10 @@ pub fn apply_pipe_message_to_plugin(
.and_then(|render| {
render.call(&mut running_plugin.store, (rows as i32, columns as i32))
})
- .and_then(|_| wasi_read_string(running_plugin.store.data()))
+ .map_err(|e| anyhow!(e))
+ .and_then(|_| {
+ wasi_read_string(running_plugin.store.data()).map_err(|e| anyhow!(e))
+ })
.with_context(err_context)?;
let pipes_to_block_or_unblock =
pipes_to_block_or_unblock(running_plugin, Some(&pipe_message.source));
diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs
index 4d8a4542..08c85464 100644
--- a/zellij-server/src/plugins/plugin_loader.rs
+++ b/zellij-server/src/plugins/plugin_loader.rs
@@ -2,10 +2,9 @@ use crate::plugins::plugin_map::{
PluginEnv, PluginMap, RunningPlugin, VecDequeInputStream, WriteOutputStream,
};
use crate::plugins::plugin_worker::{plugin_worker, RunningWorker};
+use crate::plugins::wasm_bridge::{LoadingContext, PluginCache};
use crate::plugins::zellij_exports::{wasi_write_object, zellij_exports};
use crate::plugins::PluginId;
-use highway::{HighwayHash, PortableHash};
-use log::info;
use prost::Message;
use std::{
collections::{HashMap, HashSet, VecDeque},
@@ -13,19 +12,20 @@ use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
-use url::Url;
-use wasmtime::{Engine, Instance, Linker, Module, Store};
-use wasmtime_wasi::{preview1::WasiP1Ctx, DirPerms, FilePerms, WasiCtxBuilder};
-use zellij_utils::consts::ZELLIJ_PLUGIN_ARTIFACT_DIR;
+use wasmi::{Engine, Instance, Linker, Module, Store, StoreLimits};
+use wasmi_wasi::sync::WasiCtxBuilder;
+use wasmi_wasi::wasi_common::pipe::{ReadPipe, WritePipe};
+use wasmi_wasi::Dir;
+use wasmi_wasi::WasiCtx;
use crate::{
- logging_pipe::LoggingPipe, screen::ScreenInstruction, thread_bus::ThreadSenders,
+ logging_pipe::LoggingPipe, thread_bus::ThreadSenders,
ui::loading_indication::LoadingIndication, ClientId,
};
use zellij_utils::plugin_api::action::ProtobufPluginConfiguration;
use zellij_utils::{
- consts::{ZELLIJ_CACHE_DIR, ZELLIJ_SESSION_CACHE_DIR, ZELLIJ_TMP_DIR},
+ consts::ZELLIJ_TMP_DIR,
data::{InputMode, PluginCapabilities},
errors::prelude::*,
input::command::TerminalAction,
@@ -36,597 +36,138 @@ use zellij_utils::{
pane_size::Size,
};
-macro_rules! display_loading_stage {
- ($loading_stage:ident, $loading_indication:expr, $senders:expr, $plugin_id:expr) => {{
- $loading_indication.$loading_stage();
- drop(
- $senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- $plugin_id,
- $loading_indication.clone(),
- )),
- );
- }};
+fn create_plugin_fs_entries(plugin_own_data_dir: &PathBuf, plugin_own_cache_dir: &PathBuf) {
+ // Create filesystem entries mounted into WASM.
+ // We create them here to get expressive error messages in case they fail.
+ if let Err(e) = fs::create_dir_all(&plugin_own_data_dir) {
+ log::error!("Failed to create plugin data dir: {}", e);
+ };
+ if let Err(e) = fs::create_dir_all(&plugin_own_cache_dir) {
+ log::error!("Failed to create plugin cache dir: {}", e);
+ }
+ if let Err(e) = fs::create_dir_all(ZELLIJ_TMP_DIR.as_path()) {
+ log::error!("Failed to create plugin tmp dir: {}", e);
+ }
}
pub struct PluginLoader<'a> {
- plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
- plugin_path: PathBuf,
- loading_indication: &'a mut LoadingIndication,
- senders: ThreadSenders,
+ skip_cache: bool,
plugin_id: PluginId,
client_id: ClientId,
- engine: Engine,
- plugin: PluginConfig,
- plugin_dir: &'a PathBuf,
- tab_index: Option<usize>,
+ plugin_cwd: PathBuf,
plugin_own_data_dir: PathBuf,
plugin_own_cache_dir: PathBuf,
- size: Size,
- wasm_blob_on_hd: Option<(Vec<u8>, PathBuf)>,
+ plugin_config: PluginConfig,
+ tab_index: Option<usize>,
path_to_default_shell: PathBuf,
- plugin_cwd: PathBuf,
capabilities: PluginCapabilities,
client_attributes: ClientAttributes,
default_shell: Option<TerminalAction>,
- default_layout: Box<Layout>,
layout_dir: Option<PathBuf>,
default_mode: InputMode,
keybinds: Keybinds,
+ plugin_dir: PathBuf,
+ size: Size,
+ loading_indication: LoadingIndication,
+ senders: ThreadSenders,
+ engine: Engine,
+ default_layout: Box<Layout>,
+ plugin_cache: PluginCache,
+ plugin_map: &'a mut PluginMap, // we receive a mutable reference rather than the Arc so that it
+ // will be held for the lifetime of this struct and thus loading
+ // plugins for all connected clients will be one transaction
+ connected_clients: Option<Arc<Mutex<Vec<ClientId>>>>,
}
impl<'a> PluginLoader<'a> {
- pub fn reload_plugin_from_memory(
- plugin_id: PluginId,
- plugin_dir: PathBuf,
- plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
- senders: ThreadSenders,
- engine: Engine,
- plugin_map: Arc<Mutex<PluginMap>>,
- connected_clients: Arc<Mutex<Vec<ClientId>>>,
- loading_indication: &mut LoadingIndication,
- path_to_default_shell: PathBuf,
- plugin_cwd: PathBuf,
- capabilities: PluginCapabilities,
- client_attributes: ClientAttributes,
- default_shell: Option<TerminalAction>,
- default_layout: Box<Layout>,
- layout_dir: Option<PathBuf>,
- ) -> Result<()> {
- let err_context = || format!("failed to reload plugin {plugin_id} from memory");
- let mut connected_clients: Vec<ClientId> =
- connected_clients.lock().unwrap().iter().copied().collect();
- if connected_clients.is_empty() {
- return Err(anyhow!("No connected clients, cannot reload plugin"));
- }
- let first_client_id = connected_clients.remove(0);
-
- let mut plugin_loader = PluginLoader::new_from_existing_plugin_attributes(
- &plugin_cache,
- &plugin_map,
- loading_indication,
- &senders,
- plugin_id,
- first_client_id,
- engine,
- &plugin_dir,
- path_to_default_shell,
- Some(plugin_cwd),
- capabilities,
- client_attributes,
- default_shell,
- default_layout,
- layout_dir,
- )?;
- plugin_loader
- .load_module_from_memory()
- .and_then(|module| plugin_loader.create_plugin_environment(module))
- .and_then(|(store, instance)| {
- plugin_loader.load_plugin_instance(store, &instance, &plugin_map)
- })
- .and_then(|_| {
- plugin_loader.clone_instance_for_other_clients(&connected_clients, &plugin_map)
- })
- .with_context(err_context)?;
- display_loading_stage!(end, loading_indication, senders, plugin_id);
- Ok(())
- }
-
- pub fn start_plugin(
- plugin_id: PluginId,
- client_id: ClientId,
- plugin: &PluginConfig,
- tab_index: Option<usize>,
- plugin_dir: PathBuf,
- plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
- senders: ThreadSenders,
- engine: Engine,
- plugin_map: Arc<Mutex<PluginMap>>,
- size: Size,
- connected_clients: Arc<Mutex<Vec<ClientId>>>,
- loading_indication: &mut LoadingIndication,
- path_to_default_shell: PathBuf,
- plugin_cwd: PathBuf,
- capabilities: PluginCapabilities,
- client_attributes: ClientAttributes,
- default_shell: Option<TerminalAction>,
- default_layout: Box<Layout>,
+ pub fn new(
skip_cache: bool,
- layout_dir: Option<PathBuf>,
- default_mode: InputMode,
- keybinds: Keybinds,
- ) -> Result<()> {
- let err_context = || format!("failed to start plugin {plugin_id} for client {client_id}");
- let mut plugin_loader = PluginLoader::new(
- &plugin_cache,
- loading_indication,
- &senders,
- plugin_id,
- client_id,
- engine,
- plugin.clone(),
- &plugin_dir,
- tab_index,
- size,
- path_to_default_shell,
- plugin_cwd,
- capabilities,
- client_attributes,
- default_shell,
- default_layout,
- layout_dir,
- default_mode,
- keybinds,
- )?;
- if skip_cache {
- plugin_loader
- .compile_module()
- .and_then(|module| plugin_loader.create_plugin_environment(module))
- .and_then(|(store, instance)| {
- plugin_loader.load_plugin_instance(store, &instance, &plugin_map)
- })
- .and_then(|_| {
- plugin_loader.clone_instance_for_other_clients(
- &connected_clients.lock().unwrap(),
- &plugin_map,
- )
- })
- .with_context(err_context)?;
- } else {
- plugin_loader
- .load_module_from_memory()
- .or_else(|_e| plugin_loader.load_module_from_hd_cache())
- .or_else(|_e| plugin_loader.compile_module())
- .and_then(|module| plugin_loader.create_plugin_environment(module))
- .and_then(|(store, instance)| {
- plugin_loader.load_plugin_instance(store, &instance, &plugin_map)
- })
- .and_then(|_| {
- plugin_loader.clone_instance_for_other_clients(
- &connected_clients.lock().unwrap(),
- &plugin_map,
- )
- })
- .with_context(err_context)?;
- };
- display_loading_stage!(end, loading_indication, senders, plugin_id);
- Ok(())
- }
-
- pub fn add_client(
- client_id: ClientId,
- plugin_dir: PathBuf,
- plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
+ loading_context: LoadingContext,
senders: ThreadSenders,
engine: Engine,
- plugin_map: Arc<Mutex<PluginMap>>,
- connected_clients: Arc<Mutex<Vec<ClientId>>>,
- loading_indication: &mut LoadingIndication,
- path_to_default_shell: PathBuf,
- plugin_cwd: PathBuf,
- capabilities: PluginCapabilities,
- client_attributes: ClientAttributes,
- default_shell: Option<TerminalAction>,
default_layout: Box<Layout>,
- layout_dir: Option<PathBuf>,
- default_mode: InputMode,
- keybinds: Keybinds,
- ) -> Result<()> {
- let mut new_plugins = HashSet::new();
- for plugin_id in plugin_map.lock().unwrap().plugin_ids() {
- new_plugins.insert((plugin_id, client_id));
- }
- for (plugin_id, existing_client_id) in new_plugins {
- let mut plugin_loader = PluginLoader::new_from_different_client_id(
- &plugin_cache,
- &plugin_map,
- loading_indication,
- &senders,
- plugin_id,
- existing_client_id,
- engine.clone(),
- &plugin_dir,
- path_to_default_shell.clone(),
- plugin_cwd.clone(),
- capabilities.clone(),
- client_attributes.clone(),
- default_shell.clone(),
- default_layout.clone(),
- layout_dir.clone(),
- default_mode,
- keybinds.clone(),
- )?;
- plugin_loader
- .load_module_from_memory()
- .and_then(|module| plugin_loader.create_plugin_environment(module))
- .and_then(|(store, instance)| {
- plugin_loader.load_plugin_instance(store, &instance, &plugin_map)
- })?
- }
- connected_clients.lock().unwrap().push(client_id);
- Ok(())
- }
-
- pub fn reload_plugin(
- plugin_id: PluginId,
- plugin_dir: PathBuf,
- plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
- senders: ThreadSenders,
- engine: Engine,
- plugin_map: Arc<Mutex<PluginMap>>,
+ plugin_cache: PluginCache,
+ plugin_map: &'a mut PluginMap,
connected_clients: Arc<Mutex<Vec<ClientId>>>,
- loading_indication: &mut LoadingIndication,
- path_to_default_shell: PathBuf,
- capabilities: PluginCapabilities,
- client_attributes: ClientAttributes,
- default_shell: Option<TerminalAction>,
- default_layout: Box<Layout>,
- layout_dir: Option<PathBuf>,
- ) -> Result<()> {
- let err_context = || format!("failed to reload plugin id {plugin_id}");
-
- let mut connected_clients: Vec<ClientId> =
- connected_clients.lock().unwrap().iter().copied().collect();
- if connected_clients.is_empty() {
- return Err(anyhow!("No connected clients, cannot reload plugin"));
- }
- let first_client_id = connected_clients.remove(0);
+ ) -> Self {
+ let loading_indication = LoadingIndication::new("".into());
+ create_plugin_fs_entries(
+ &loading_context.plugin_own_data_dir,
+ &loading_context.plugin_own_cache_dir,
+ );
+ Self {
+ plugin_id: loading_context.plugin_id,
+ client_id: loading_context.client_id,
+ plugin_cwd: loading_context.plugin_cwd,
+ plugin_own_data_dir: loading_context.plugin_own_data_dir,
+ plugin_own_cache_dir: loading_context.plugin_own_cache_dir,
+ plugin_config: loading_context.plugin_config,
+ tab_index: loading_context.tab_index,
+ path_to_default_shell: loading_context.path_to_default_shell,
+ capabilities: loading_context.capabilities,
+ client_attributes: loading_context.client_attributes,
+ default_shell: loading_context.default_shell,
+ layout_dir: loading_context.layout_dir,
+ default_mode: loading_context.default_mode,
+ keybinds: loading_context.keybinds,
+ plugin_dir: loading_context.plugin_dir,
+ size: loading_context.size,
- let mut plugin_loader = PluginLoader::new_from_existing_plugin_attributes(
- &plugin_cache,
- &plugin_map,
- loading_indication,
- &senders,
- plugin_id,
- first_client_id,
- engine,
- &plugin_dir,
- path_to_default_shell,
- None,
- capabilities,
- client_attributes,
- default_shell,
- default_layout,
- layout_dir,
- )?;
- plugin_loader
- .compile_module()
- .and_then(|module| plugin_loader.create_plugin_environment(module))
- .and_then(|(store, instance)| {
- plugin_loader.load_plugin_instance(store, &instance, &plugin_map)
- })
- .and_then(|_| {
- plugin_loader.clone_instance_for_other_clients(&connected_clients, &plugin_map)
- })
- .with_context(err_context)?;
- display_loading_stage!(end, loading_indication, senders, plugin_id);
- Ok(())
- }
- pub fn new(
- plugin_cache: &Arc<Mutex<HashMap<PathBuf, Module>>>,
- loading_indication: &'a mut LoadingIndication,
- senders: &ThreadSenders,
- plugin_id: PluginId,
- client_id: ClientId,
- engine: Engine,
- plugin: PluginConfig,
- plugin_dir: &'a PathBuf,
- tab_index: Option<usize>,
- size: Size,
- path_to_default_shell: PathBuf,
- plugin_cwd: PathBuf,
- capabilities: PluginCapabilities,
- client_attributes: ClientAttributes,
- default_shell: Option<TerminalAction>,
- default_layout: Box<Layout>,
- layout_dir: Option<PathBuf>,
- default_mode: InputMode,
- keybinds: Keybinds,
- ) -> Result<Self> {
- let plugin_own_data_dir = ZELLIJ_SESSION_CACHE_DIR
- .join(Url::from(&plugin.location).to_string())
- .join(format!("{}-{}", plugin_id, client_id));
- let plugin_own_cache_dir = ZELLIJ_SESSION_CACHE_DIR
- .join(Url::from(&plugin.location).to_string())
- .join(format!("plugin_cache"));
- create_plugin_fs_entries(&plugin_own_data_dir, &plugin_own_cache_dir)?;
- let plugin_path = plugin.path.clone();
- Ok(PluginLoader {
- plugin_cache: plugin_cache.clone(),
- plugin_path,
- loading_indication,
- senders: senders.clone(),
- plugin_id,
- client_id,
- engine,
- plugin,
- plugin_dir,
- tab_index,
- plugin_own_data_dir,
- plugin_own_cache_dir,
- size,
- wasm_blob_on_hd: None,
- path_to_default_shell,
- plugin_cwd,
- capabilities,
- client_attributes,
- default_shell,
- default_layout,
- layout_dir,
- default_mode,
- keybinds,
- })
- }
- pub fn new_from_existing_plugin_attributes(
- plugin_cache: &Arc<Mutex<HashMap<PathBuf, Module>>>,
- plugin_map: &Arc<Mutex<PluginMap>>,
- loading_indication: &'a mut LoadingIndication,
- senders: &ThreadSenders,
- plugin_id: PluginId,
- client_id: ClientId,
- engine: Engine,
- plugin_dir: &'a PathBuf,
- path_to_default_shell: PathBuf,
- cwd: Option<PathBuf>,
- capabilities: PluginCapabilities,
- client_attributes: ClientAttributes,
- default_shell: Option<TerminalAction>,
- default_layout: Box<Layout>,
- layout_dir: Option<PathBuf>,
- ) -> Result<Self> {
- let err_context = || "Failed to find existing plugin";
- let (running_plugin, _subscriptions, _workers) = {
- let mut plugin_map = plugin_map.lock().unwrap();
- plugin_map
- .remove_single_plugin(plugin_id, client_id)
- .with_context(err_context)?
- };
- let running_plugin = running_plugin.lock().unwrap();
- let tab_index = running_plugin.store.data().tab_index;
- let size = Size {
- rows: running_plugin.rows,
- cols: running_plugin.columns,
- };
- let keybinds = running_plugin.store.data().keybinds.clone();
- let default_mode = running_plugin.store.data().default_mode;
- let plugin_config = running_plugin.store.data().plugin.clone();
- // prefer the explicitly given cwd, otherwise copy it from the running plugin
- // (when reloading a plugin, we want to copy it, when starting a new plugin instance from
- // meomory, we want to reset it)
- let plugin_cwd = cwd.unwrap_or_else(|| running_plugin.store.data().plugin_cwd.clone());
- loading_indication.set_name(running_plugin.store.data().name());
- PluginLoader::new(
- plugin_cache,
- loading_indication,
+ skip_cache,
senders,
- plugin_id,
- client_id,
engine,
- plugin_config,
- plugin_dir,
- tab_index,
- size,
- path_to_default_shell,
- plugin_cwd,
- capabilities,
- client_attributes,
- default_shell,
default_layout,
- layout_dir,
- default_mode,
- keybinds,
- )
- }
- pub fn new_from_different_client_id(
- plugin_cache: &Arc<Mutex<HashMap<PathBuf, Module>>>,
- plugin_map: &Arc<Mutex<PluginMap>>,
- loading_indication: &'a mut LoadingIndication,
- senders: &ThreadSenders,
- plugin_id: PluginId,
- client_id: ClientId,
- engine: Engine,
- plugin_dir: &'a PathBuf,
- path_to_default_shell: PathBuf,
- plugin_cwd: PathBuf,
- capabilities: PluginCapabilities,
- client_attributes: ClientAttributes,
- default_shell: Option<TerminalAction>,
- default_layout: Box<Layout>,
- layout_dir: Option<PathBuf>,
- default_mode: InputMode,
- keybinds: Keybinds,
- ) -> Result<Self> {
- let err_context = || "Failed to find existing plugin";
- let running_plugin = {
- let plugin_map = plugin_map.lock().unwrap();
- plugin_map
- .get_running_plugin(plugin_id, None)
- .with_context(err_context)?
- .clone()
- };
- let running_plugin = running_plugin.lock().unwrap();
- let tab_index = running_plugin.store.data().tab_index;
- let size = Size {
- rows: running_plugin.rows,
- cols: running_plugin.columns,
- };
- let plugin_config = running_plugin.store.data().plugin.clone();
- loading_indication.set_name(running_plugin.store.data().name());
- PluginLoader::new(
plugin_cache,
+ plugin_map,
+ connected_clients: Some(connected_clients),
loading_indication,
- senders,
- plugin_id,
- client_id,
- engine,
- plugin_config,
- plugin_dir,
- tab_index,
- size,
- path_to_default_shell,
- plugin_cwd,
- capabilities,
- client_attributes,
- default_shell,
- default_layout,
- layout_dir,
- default_mode,
- keybinds,
- )
+ }
}
- pub fn load_module_from_memory(&mut self) -> Result<Module> {
- display_loading_stage!(
- indicate_loading_plugin_from_memory,
- self.loading_indication,
- self.senders,
- self.plugin_id
- );
- let module = self
- .plugin_cache
- .lock()
- .unwrap()
- .remove(&self.plugin_path)
- .ok_or(anyhow!("Plugin is not stored in memory"))?;
- display_loading_stage!(
- indicate_loading_plugin_from_memory_success,
- self.loading_indication,
- self.senders,
- self.plugin_id
- );
- Ok(module)
+ pub fn without_connected_clients(mut self) -> Self {
+ self.connected_clients = None;
+ self
}
- pub fn load_module_from_hd_cache(&mut self) -> Result<Module> {
- display_loading_stage!(
- indicate_loading_plugin_from_memory_notfound,
- self.loading_indication,
- self.senders,
- self.plugin_id
- );
- display_loading_stage!(
- indicate_loading_plugin_from_hd_cache,
- self.loading_indication,
- self.senders,
- self.plugin_id
- );
- let (_wasm_bytes, cached_path) = self.plugin_bytes_and_cache_path()?;
- let timer = std::time::Instant::now();
- let file_in_cache = std::fs::read(&cached_path)?;
- let module = unsafe { Module::deserialize(&self.engine, file_in_cache)? };
- log::info!(
- "Loaded plugin '{}' from cache folder at '{}' in {:?}",
- self.plugin_path.display(),
- ZELLIJ_CACHE_DIR.display(),
- timer.elapsed(),
- );
- display_loading_stage!(
- indicate_loading_plugin_from_hd_cache_success,
- self.loading_indication,
- self.senders,
- self.plugin_id
- );
- Ok(module)
+ pub fn start_plugin(&mut self) -> Result<()> {
+ let module = if self.skip_cache {
+ self.interpret_module()?
+ } else {
+ self.load_module_from_memory()
+ .or_else(|_e| self.interpret_module())?
+ };
+ let (store, instance) = self.create_plugin_environment(module)?;
+ self.load_plugin_instance(store, &instance)?;
+ self.clone_instance_for_other_clients()?;
+ Ok(())
}
- pub fn compile_module(&mut self) -> Result<Module> {
+ fn interpret_module(&mut self) -> Result<Module> {
self.loading_indication.override_previous_error();
- display_loading_stage!(
- indicate_loading_plugin_from_hd_cache_notfound,
- self.loading_indication,
- self.senders,
- self.plugin_id
- );
- display_loading_stage!(
- indicate_compiling_plugin,
- self.loading_indication,
- self.senders,
- self.plugin_id
- );
- let (wasm_bytes, cached_path) = self.plugin_bytes_and_cache_path()?;
+ let wasm_bytes = self.plugin_config.resolve_wasm_bytes(&self.plugin_dir)?;
let timer = std::time::Instant::now();
- let err_context = || "failed to recover cache dir";
- let module = fs::create_dir_all(ZELLIJ_PLUGIN_ARTIFACT_DIR.as_path())
- .map_err(anyError::new)
- .and_then(|_| {
- // compile module
- Module::new(&self.engine, &wasm_bytes)
- })
- .and_then(|m| {
- // serialize module to HD cache for faster loading in the future
- fs::write(&cached_path, m.serialize()?).map_err(anyError::new)?;
- log::info!(
- "Compiled plugin '{}' in {:?}",
- self.plugin_path.display(),
- timer.elapsed()
- );
- Ok(m)
- })
- .with_context(err_context)?;
+ let module = Module::new(&self.engine, &wasm_bytes)?;
+ log::info!(
+ "Loaded plugin '{}' in {:?}",
+ self.plugin_config.path.display(),
+ timer.elapsed()
+ );
Ok(module)
}
- pub fn create_plugin_environment(
- &mut self,
- module: Module,
- ) -> Result<(Store<PluginEnv>, Instance)> {
- let (store, instance) = self.create_plugin_instance_env(&module)?;
- // Only do an insert when everything went well!
- let cloned_plugin = self.plugin.clone();
- self.plugin_cache
- .lock()
- .unwrap()
- .insert(cloned_plugin.path, module);
- Ok((store, instance))
- }
- pub fn create_plugin_instance_and_wasi_env_for_worker(
- &mut self,
- ) -> Result<(Store<PluginEnv>, Instance)> {
- let err_context = || {
- format!(
- "Failed to create instance and plugin env for worker {}",
- self.plugin_id
- )
- };
+ fn load_module_from_memory(&mut self) -> Result<Module> {
let module = self
.plugin_cache
.lock()
.unwrap()
- .get(&self.plugin.path)
- .with_context(err_context)?
- .clone();
- let (store, instance) = self.create_plugin_instance_env(&module)?;
- Ok((store, instance))
+ .remove(&self.plugin_config.path) // TODO: do we still bring it back later?
+ // maybe we can forgo this dance?
+ .ok_or(anyhow!("Plugin is not stored in memory"))?;
+ Ok(module)
}
- pub fn load_plugin_instance(
+ fn load_plugin_instance(
&mut self,
mut store: Store<PluginEnv>,
instance: &Instance,
- plugin_map: &Arc<Mutex<PluginMap>>,
) -> Result<()> {
let err_context = || format!("failed to load plugin from instance {instance:#?}");
let main_user_instance = instance.clone();
- display_loading_stage!(
- indicate_starting_plugin,
- self.loading_indication,
- self.senders,
- self.plugin_id
- );
let start_function = instance
.get_typed_func::<(), ()>(&mut store, "_start")
.with_context(err_context)?;
@@ -641,7 +182,6 @@ impl<'a> PluginLoader<'a> {
if function_name.ends_with("_worker") {
let (mut store, instance) =
self.create_plugin_instance_and_wasi_env_for_worker()?;
-
let start_function_for_worker = instance
.get_typed_func::<(), ()>(&mut store, "_start")
.with_context(err_context)?;
@@ -662,7 +202,7 @@ impl<'a> PluginLoader<'a> {
self.size.rows,
self.size.cols,
)));
- plugin_map.lock().unwrap().insert(
+ self.plugin_map.insert(
self.plugin_id,
self.client_id,
plugin.clone(),
@@ -675,7 +215,7 @@ impl<'a> PluginLoader<'a> {
.with_context(err_context)?;
let protobuf_plugin_configuration: ProtobufPluginConfiguration = self
- .plugin
+ .plugin_config
.userspace_configuration
.clone()
.try_into()
@@ -687,143 +227,124 @@ impl<'a> PluginLoader<'a> {
.call(&mut plugin.lock().unwrap().store, ())
.with_context(err_context)?;
- display_loading_stage!(
- indicate_starting_plugin_success,
- self.loading_indication,
- self.senders,
- self.plugin_id
- );
- display_loading_stage!(
- indicate_writing_plugin_to_cache,
- self.loading_indication,
- self.senders,
- self.plugin_id
- );
- display_loading_stage!(
- indicate_writing_plugin_to_cache_success,
- self.loading_indication,
- self.senders,
- self.plugin_id
- );
-
Ok(())
}
- pub fn clone_instance_for_other_clients(
- &mut self,
- connected_clients: &[ClientId],
- plugin_map: &Arc<Mutex<PluginMap>>,
- ) -> Result<()> {
- if !connected_clients.is_empty() {
- display_loading_stage!(
- indicate_cloning_plugin_for_other_clients,
- self.loading_indication,
- self.senders,
+ pub fn create_plugin_environment(
+ &self,
+ module: Module,
+ ) -> Result<(Store<PluginEnv>, Instance)> {
+ let err_context = || {
+ format!(
+ "Failed to create instance, plugin env and subscriptions for plugin {}",
self.plugin_id
- );
+ )
+ };
+ let stdin_pipe = Arc::new(Mutex::new(VecDeque::new()));
+ let stdout_pipe = Arc::new(Mutex::new(VecDeque::new()));
+
+ let wasi_ctx = PluginLoader::create_wasi_ctx(
+ &self.plugin_cwd,
+ &self.plugin_own_data_dir,
+ &self.plugin_own_cache_dir,
+ &ZELLIJ_TMP_DIR,
+ &self.plugin_config.location.to_string(),
+ self.plugin_id,
+ stdin_pipe.clone(),
+ stdout_pipe.clone(),
+ )?;
+ let plugin_path = self.plugin_config.path.clone();
+ let plugin_env = PluginEnv {
+ plugin_id: self.plugin_id,
+ client_id: self.client_id,
+ plugin: self.plugin_config.clone(), // TODO: change field name in PluginEnv to plugin_config
+ permissions: Arc::new(Mutex::new(None)),
+ senders: self.senders.clone(),
+ wasi_ctx,
+ plugin_own_data_dir: self.plugin_own_data_dir.clone(),
+ plugin_own_cache_dir: self.plugin_own_cache_dir.clone(),
+ tab_index: self.tab_index,
+ path_to_default_shell: self.path_to_default_shell.clone(),
+ capabilities: self.capabilities.clone(),
+ client_attributes: self.client_attributes.clone(),
+ default_shell: self.default_shell.clone(),
+ default_layout: self.default_layout.clone(),
+ plugin_cwd: self.plugin_cwd.clone(),
+ input_pipes_to_unblock: Arc::new(Mutex::new(HashSet::new())),
+ input_pipes_to_block: Arc::new(Mutex::new(HashSet::new())),
+ layout_dir: self.layout_dir.clone(),
+ default_mode: self.default_mode.clone(),
+ subscriptions: Arc::new(Mutex::new(HashSet::new())),
+ keybinds: self.keybinds.clone(),
+ intercepting_key_presses: false,
+ stdin_pipe,
+ stdout_pipe,
+ store_limits: create_optimized_store_limits(),
+ };
+ let mut store = Store::new(&self.engine, plugin_env);
+
+ // Apply optimized resource limits for memory efficiency
+ store.limiter(|plugin_env| &mut plugin_env.store_limits);
+
+ let mut linker = Linker::new(&self.engine);
+ wasmi_wasi::add_to_linker(&mut linker, |plugin_env: &mut PluginEnv| {
+ &mut plugin_env.wasi_ctx
+ })?;
+ zellij_exports(&mut linker);
+
+ let instance = linker
+ .instantiate_and_start(&mut store, &module)
+ .with_context(err_context)?;
+
+ if let Some(func) = instance.get_func(&mut store, "_initialize") {
+ if let Ok(typed_func) = func.typed::<(), ()>(&store) {
+ let _ = typed_func.call(&mut store, ());
+ }
+ }
+
+ self.plugin_cache
+ .lock()
+ .unwrap()
+ .insert(plugin_path.clone(), module);
+ Ok((store, instance))
+ }
+ pub fn clone_instance_for_other_clients(&mut self) -> Result<()> {
+ let Some(connected_clients) = self.connected_clients.as_ref() else {
+ return Ok(());
+ };
+ let connected_clients: Vec<ClientId> =
+ connected_clients.lock().unwrap().iter().copied().collect();
+ if !connected_clients.is_empty() {
+ self.connected_clients = None; // so we don't have infinite loops
for client_id in connected_clients {
- if client_id == &self.client_id {
+ if client_id == self.client_id {
// don't reload the plugin once more for ourselves
continue;
}
- let mut loading_indication = LoadingIndication::new("".into());
- let mut plugin_loader_for_client = PluginLoader::new_from_different_client_id(
- &self.plugin_cache.clone(),
- &plugin_map,
- &mut loading_indication,
- &self.senders.clone(),
- self.plugin_id,
- *client_id,
- self.engine.clone(),
- &self.plugin_dir,
- self.path_to_default_shell.clone(),
- self.plugin_cwd.clone(),
- self.capabilities.clone(),
- self.client_attributes.clone(),
- self.default_shell.clone(),
- self.default_layout.clone(),
- self.layout_dir.clone(),
- self.default_mode,
- self.keybinds.clone(),
- )?;
- plugin_loader_for_client
- .load_module_from_memory()
- .and_then(|module| plugin_loader_for_client.create_plugin_environment(module))
- .and_then(|(store, instance)| {
- plugin_loader_for_client.load_plugin_instance(store, &instance, plugin_map)
- })?
+ self.client_id = client_id;
+ self.start_plugin()?;
}
- display_loading_stage!(
- indicate_cloning_plugin_for_other_clients_success,
- self.loading_indication,
- self.senders,
- self.plugin_id
- );
}
Ok(())
}
- fn plugin_bytes_and_cache_path(&mut self) -> Result<(Vec<u8>, PathBuf)> {
- match self.wasm_blob_on_hd.as_ref() {
- Some((wasm_bytes, cached_path)) => Ok((wasm_bytes.clone(), cached_path.clone())),
- None => {
- if self.plugin._allow_exec_host_cmd {
- info!(
- "Plugin({:?}) is able to run any host command, this may lead to some security issues!",
- self.plugin.path
- );
- }
- // The plugins blob as stored on the filesystem
- let wasm_bytes = self.plugin.resolve_wasm_bytes(&self.plugin_dir)?;
- let hash: String = PortableHash::default()
- .hash256(&wasm_bytes)
- .iter()
- .map(ToString::to_string)
- .collect();
- let cached_path = ZELLIJ_PLUGIN_ARTIFACT_DIR.join(&hash);
- self.wasm_blob_on_hd = Some((wasm_bytes.clone(), cached_path.clone()));
- Ok((wasm_bytes, cached_path))
- },
- }
- }
- pub fn create_wasi_ctx(
- host_dir: &PathBuf,
- data_dir: &PathBuf,
- cache_dir: &PathBuf,
- tmp_dir: &PathBuf,
- plugin_url: &String,
- plugin_id: PluginId,
- stdin_pipe: Arc<Mutex<VecDeque<u8>>>,
- stdout_pipe: Arc<Mutex<VecDeque<u8>>>,
- ) -> Result<WasiP1Ctx> {
- let err_context = || format!("Failed to create wasi_ctx");
- let dirs = vec![
- ("/host".to_owned(), host_dir.clone()),
- ("/data".to_owned(), data_dir.clone()),
- ("/cache".to_owned(), cache_dir.clone()),
- ("/tmp".to_owned(), tmp_dir.clone()),
- ];
- let dirs = dirs.into_iter().filter(|(_dir_name, dir)| {
- // note that this does not protect against TOCTOU errors
- // eg. if one or more of these folders existed at the time of check but was deleted
- // before we mounted in in the wasi environment, we'll crash
- // when we move to a new wasi environment, we should address this with locking if
- // there's no built-in solution
- dir.try_exists().ok().unwrap_or(false)
- });
- let mut wasi_ctx_builder = WasiCtxBuilder::new();
- wasi_ctx_builder.env("CLICOLOR_FORCE", "1");
- for (guest_path, host_path) in dirs {
- wasi_ctx_builder
- .preopened_dir(host_path, guest_path, DirPerms::all(), FilePerms::all())
- .with_context(err_context)?;
- }
- wasi_ctx_builder
- .stdin(VecDequeInputStream(stdin_pipe.clone()))
- .stdout(WriteOutputStream(stdout_pipe.clone()))
- .stderr(WriteOutputStream(Arc::new(Mutex::new(LoggingPipe::new(
- plugin_url, plugin_id,
- )))));
- let wasi_ctx = wasi_ctx_builder.build_p1();
- Ok(wasi_ctx)
+ pub fn create_plugin_instance_and_wasi_env_for_worker(
+ &self,
+ ) -> Result<(Store<PluginEnv>, Instance)> {
+ let plugin_id = self.plugin_id;
+ let err_context = || {
+ format!(
+ "Failed to create instance and plugin env for worker {}",
+ plugin_id
+ )
+ };
+ let module = self
+ .plugin_cache
+ .lock()
+ .unwrap()
+ .get(&self.plugin_config.path)
+ .with_context(err_context)?
+ .clone();
+ let (store, instance) = self.create_plugin_instance_env(&module)?;
+ Ok((store, instance))
}
fn create_plugin_instance_env(&self, module: &Module) -> Result<(Store<PluginEnv>, Instance)> {
let err_context = || {
@@ -840,16 +361,16 @@ impl<'a> PluginLoader<'a> {
&self.plugin_own_data_dir,
&self.plugin_own_cache_dir,
&ZELLIJ_TMP_DIR,
- &self.plugin.location.to_string(),
+ &self.plugin_config.location.to_string(),
self.plugin_id,
stdin_pipe.clone(),
stdout_pipe.clone(),
)?;
- let plugin = self.plugin.clone();
+ let plugin_config = self.plugin_config.clone();
let plugin_env = PluginEnv {
plugin_id: self.plugin_id,
client_id: self.client_id,
- plugin,
+ plugin: plugin_config,
permissions: Arc::new(Mutex::new(None)),
senders: self.senders.clone(),
wasi_ctx,
@@ -871,43 +392,97 @@ impl<'a> PluginLoader<'a> {
intercepting_key_presses: false,
stdin_pipe,
stdout_pipe,
+ store_limits: create_optimized_store_limits(),
};
let mut store = Store::new(&self.engine, plugin_env);
+ // Apply optimized resource limits for memory efficiency
+ store.limiter(|plugin_env| &mut plugin_env.store_limits);
+
let mut linker = Linker::new(&self.engine);
- wasmtime_wasi::preview1::add_to_linker_sync(&mut linker, |plugin_env: &mut PluginEnv| {
+ wasmi_wasi::add_to_linker(&mut linker, |plugin_env: &mut PluginEnv| {
&mut plugin_env.wasi_ctx
- })
- .unwrap();
+ })?;
zellij_exports(&mut linker);
let instance = linker
- .instantiate(&mut store, module)
+ .instantiate_and_start(&mut store, module)
.with_context(err_context)?;
if let Some(func) = instance.get_func(&mut store, "_initialize") {
- func.typed::<(), ()>(&store)?.call(&mut store, ())?;
+ if let Ok(typed_func) = func.typed::<(), ()>(&store) {
+ let _ = typed_func.call(&mut store, ());
+ }
}
Ok((store, instance))
}
+ pub fn create_wasi_ctx(
+ host_dir: &PathBuf,
+ data_dir: &PathBuf,
+ cache_dir: &PathBuf,
+ tmp_dir: &PathBuf,
+ plugin_url: &String,
+ plugin_id: PluginId,
+ stdin_pipe: Arc<Mutex<VecDeque<u8>>>,
+ stdout_pipe: Arc<Mutex<VecDeque<u8>>>,
+ ) -> Result<WasiCtx> {
+ let _err_context = || format!("Failed to create wasi_ctx");
+ let dirs = vec![
+ ("/host".to_owned(), host_dir.clone()),
+ ("/data".to_owned(), data_dir.clone()),
+ ("/cache".to_owned(), cache_dir.clone()),
+ ("/tmp".to_owned(), tmp_dir.clone()),
+ ];
+ let dirs = dirs.into_iter().filter(|(_dir_name, dir)| {
+ // note that this does not protect against TOCTOU errors
+ // eg. if one or more of these folders existed at the time of check but was deleted
+ // before we mounted in in the wasi environment, we'll crash
+ // when we move to a new wasi environment, we should address this with locking if
+ // there's no built-in solution
+ dir.try_exists().ok().unwrap_or(false)
+ });
+
+ let mut builder = WasiCtxBuilder::new();
+ builder.inherit_env()?;
+
+ // Mount directories using the builder
+ for (guest_path, host_path) in dirs {
+ match std::fs::File::open(&host_path) {
+ Ok(dir_file) => {
+ let dir = Dir::from_std_file(dir_file);
+ builder.preopened_dir(dir, guest_path)?;
+ },
+ Err(e) => {
+ log::warn!("Failed to mount directory {:?}: {}", host_path, e);
+ },
+ }
+ }
+
+ let ctx = builder.build();
+
+ // Set up custom stdin/stdout/stderr
+ ctx.set_stdin(Box::new(ReadPipe::new(VecDequeInputStream(
+ stdin_pipe.clone(),
+ ))));
+ ctx.set_stdout(Box::new(WritePipe::new(WriteOutputStream(
+ stdout_pipe.clone(),
+ ))));
+ ctx.set_stderr(Box::new(WritePipe::new(WriteOutputStream(Arc::new(
+ Mutex::new(LoggingPipe::new(plugin_url, plugin_id)),
+ )))));
+
+ Ok(ctx)
+ }
}
-fn create_plugin_fs_entries(
- plugin_own_data_dir: &PathBuf,
- plugin_own_cache_dir: &PathBuf,
-) -> Result<()> {
- let err_context = || "failed to create plugin fs entries";
- // Create filesystem entries mounted into WASM.
- // We create them here to get expressive error messages in case they fail.
- fs::create_dir_all(&plugin_own_data_dir)
- .with_context(|| format!("failed to create datadir in {plugin_own_data_dir:?}"))
- .with_context(err_context)?;
- fs::create_dir_all(&plugin_own_cache_dir)
- .with_context(|| format!("failed to create cache dir in {plugin_own_cache_dir:?}"))
- .with_context(err_context)?;
- fs::create_dir_all(ZELLIJ_TMP_DIR.as_path())
- .with_context(|| format!("failed to create tmpdir at {:?}", &ZELLIJ_TMP_DIR.as_path()))
- .with_context(err_context)?;
- Ok(())
+fn create_optimized_store_limits() -> StoreLimits {
+ use wasmi::StoreLimitsBuilder;
+ StoreLimitsBuilder::new()
+ .instances(1) // One instance per plugin
+ .memories(4) // Max 4 linear memories per plugin
+ .memory_size(16 * 1024 * 1024) // 16MB per memory maximum
+ .tables(16) // Small table element limit
+ .trap_on_grow_failure(true) // Fail fast on resource exhaustion
+ .build()
}
diff --git a/zellij-server/src/plugins/plugin_map.rs b/zellij-server/src/plugins/plugin_map.rs
index a92c291a..7520114f 100644
--- a/zellij-server/src/plugins/plugin_map.rs
+++ b/zellij-server/src/plugins/plugin_map.rs
@@ -1,18 +1,13 @@
use crate::plugins::plugin_worker::MessageToWorker;
use crate::plugins::PluginId;
-use bytes::Bytes;
use std::io::Write;
use std::{
collections::{BTreeMap, HashMap, HashSet, VecDeque},
path::PathBuf,
sync::{Arc, Mutex},
};
-use wasmtime::{Instance, Store};
-use wasmtime_wasi::preview1::WasiP1Ctx;
-use wasmtime_wasi::{
- HostInputStream, HostOutputStream, StdinStream, StdoutStream, StreamError, StreamResult,
- Subscribe,
-};
+use wasmi::{Instance, Store, StoreLimits};
+use wasmi_wasi::WasiCtx;
use crate::{thread_bus::ThreadSenders, ClientId};
@@ -70,17 +65,6 @@ impl PluginMap {
}
removed
}
- pub fn remove_single_plugin(
- &mut self,
- plugin_id: PluginId,
- client_id: ClientId,
- ) -> Option<(
- Arc<Mutex<RunningPlugin>>,
- Arc<Mutex<Subscriptions>>,
- HashMap<String, Sender<MessageToWorker>>,
- )> {
- self.plugin_assets.remove(&(plugin_id, client_id))
- }
pub fn plugin_ids(&self) -> Vec<PluginId> {
let mut unique_plugins: HashSet<PluginId> = self
.plugin_assets
@@ -293,7 +277,7 @@ pub struct PluginEnv {
pub plugin: PluginConfig,
pub permissions: Arc<Mutex<Option<HashSet<PermissionType>>>>,
pub senders: ThreadSenders,
- pub wasi_ctx: WasiP1Ctx,
+ pub wasi_ctx: WasiCtx,
pub tab_index: Option<usize>,
pub client_id: ClientId,
#[allow(dead_code)]
@@ -314,34 +298,23 @@ pub struct PluginEnv {
pub stdout_pipe: Arc<Mutex<VecDeque<u8>>>,
pub keybinds: Keybinds,
pub intercepting_key_presses: bool,
+ pub store_limits: StoreLimits,
}
#[derive(Clone)]
pub struct VecDequeInputStream(pub Arc<Mutex<VecDeque<u8>>>);
-impl StdinStream for VecDequeInputStream {
- fn stream(&self) -> Box<dyn wasmtime_wasi::HostInputStream> {
- Box::new(self.clone())
- }
-
- fn isatty(&self) -> bool {
- false
- }
-}
-
-impl HostInputStream for VecDequeInputStream {
- fn read(&mut self, size: usize) -> StreamResult<Bytes> {
+impl std::io::Read for VecDequeInputStream {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let mut inner = self.0.lock().unwrap();
- let len = std::cmp::min(size, inner.len());
- Ok(Bytes::from_iter(inner.drain(0..len)))
+ let len = std::cmp::min(buf.len(), inner.len());
+ for (i, byte) in inner.drain(0..len).enumerate() {
+ buf[i] = byte;
+ }
+ Ok(len)
}
}
-#[async_trait::async_trait]
-impl Subscribe for VecDequeInputStream {
- async fn ready(&mut self) {}
-}
-
pub struct WriteOutputStream<T>(pub Arc<Mutex<T>>);
impl<T> Clone for WriteOutputStream<T> {
@@ -350,43 +323,18 @@ impl<T> Clone for WriteOutputStream<T> {
}
}
-impl<T: Write + Send + 'static> StdoutStream for WriteOutputStream<T> {
- fn stream(&self) -> Box<dyn HostOutputStream> {
- Box::new((*self).clone())
- }
-
- fn isatty(&self) -> bool {
- false
- }
-}
-
-impl<T: Write + Send + 'static> HostOutputStream for WriteOutputStream<T> {
- fn write(&mut self, bytes: Bytes) -> StreamResult<()> {
- self.0
- .lock()
- .unwrap()
- .write_all(&*bytes)
- .map_err(|e| StreamError::LastOperationFailed(e.into()))
- }
-
- fn flush(&mut self) -> StreamResult<()> {
- self.0
- .lock()
- .unwrap()
- .flush()
- .map_err(|e| StreamError::LastOperationFailed(e.into()))
+impl<T: Write + Send + 'static> std::io::Write for WriteOutputStream<T> {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+ let mut inner = self.0.lock().unwrap();
+ inner.write(buf)
}
- fn check_write(&mut self) -> StreamResult<usize> {
- Ok(usize::MAX)
+ fn flush(&mut self) -> std::io::Result<()> {
+ let mut inner = self.0.lock().unwrap();
+ inner.flush()
}
}
-#[async_trait::async_trait]
-impl<T: Send + 'static> Subscribe for WriteOutputStream<T> {
- async fn ready(&mut self) {}
-}
-
impl PluginEnv {
// Get the name (path) of the containing plugin
pub fn name(&self) -> String {
diff --git a/zellij-server/src/plugins/plugin_worker.rs b/zellij-server/src/plugins/plugin_worker.rs
index 30085a3a..6e20a499 100644
--- a/zellij-server/src/plugins/plugin_worker.rs
+++ b/zellij-server/src/plugins/plugin_worker.rs
@@ -1,6 +1,6 @@
use crate::plugins::plugin_map::PluginEnv;
use crate::plugins::zellij_exports::wasi_write_object;
-use wasmtime::{Instance, Store};
+use wasmi::{Instance, Store};
use async_channel::{unbounded, Receiver, Sender};
use async_std::task;
diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs
index da725443..9631da91 100644
--- a/zellij-server/src/plugins/unit/plugin_tests.rs
+++ b/zellij-server/src/plugins/unit/plugin_tests.rs
@@ -6,7 +6,7 @@ use lazy_static::lazy_static;
use std::collections::BTreeMap;
use std::path::PathBuf;
use tempfile::tempdir;
-use wasmtime::Engine;
+use wasmi::Engine;
use zellij_utils::data::{
BareKey, Event, InputMode, KeyWithModifier, PermissionStatus, PermissionType,
PluginCapabilities,
@@ -333,7 +333,10 @@ fn create_plugin_thread(
None,
)
.should_silently_fail();
- let engine = Engine::new(wasmtime::Config::new().strategy(wasmtime::Strategy::Winch)).unwrap();
+ let mut config = wasmi::Config::default();
+ config.set_max_stack_height(1024 * 1024);
+ config.set_max_recursion_depth(1000);
+ let engine = Engine::new(&config);
let data_dir = PathBuf::from(tempdir().unwrap().path());
let default_shell = PathBuf::from(".");
let plugin_capabilities = PluginCapabilities::default();
@@ -427,7 +430,10 @@ fn create_plugin_thread_with_server_receiver(
None,
)
.should_silently_fail();
- let engine = Engine::new(wasmtime::Config::new().strategy(wasmtime::Strategy::Winch)).unwrap();
+ let mut config = wasmi::Config::default();
+ config.set_max_stack_height(1024 * 1024);
+ config.set_max_recursion_depth(1000);
+ let engine = Engine::new(&config);
let data_dir = PathBuf::from(tempdir().unwrap().path());
let default_shell = PathBuf::from(".");
let plugin_capabilities = PluginCapabilities::default();
@@ -517,7 +523,10 @@ fn create_plugin_thread_with_pty_receiver(
None,
)
.should_silently_fail();
- let engine = Engine::new(wasmtime::Config::new().strategy(wasmtime::Strategy::Winch)).unwrap();
+ let mut config = wasmi::Config::default();
+ config.set_max_stack_height(1024 * 1024);
+ config.set_max_recursion_depth(1000);
+ let engine = Engine::new(&config);
let data_dir = PathBuf::from(tempdir().unwrap().path());
let default_shell = PathBuf::from(".");
let plugin_capabilities = PluginCapabilities::default();
@@ -602,7 +611,10 @@ fn create_plugin_thread_with_background_jobs_receiver(
None,
)
.should_silently_fail();
- let engine = Engine::new(wasmtime::Config::new().strategy(wasmtime::Strategy::Winch)).unwrap();
+ let mut config = wasmi::Config::default();
+ config.set_max_stack_height(1024 * 1024);
+ config.set_max_recursion_depth(1000);
+ let engine = Engine::new(&config);
let data_dir = PathBuf::from(tempdir().unwrap().path());
let default_shell = PathBuf::from(".");
let plugin_capabilities = PluginCapabilities::default();
@@ -8666,7 +8678,7 @@ pub fn reload_plugin_plugin_command() {
received_screen_instructions,
ScreenInstruction::RequestStateUpdateForPlugins, // happens on successful plugin (re)load
screen_receiver,
- 3,
+ 2,
&PermissionType::ChangeApplicationState,
cache_path,
plugin_thread_sender,
@@ -8709,7 +8721,7 @@ pub fn reload_plugin_plugin_command() {
}
})
.count();
- assert_eq!(request_state_update_requests, 3);
+ assert_eq!(request_state_update_requests, 2);
}
#[test]
@@ -8740,7 +8752,7 @@ pub fn load_new_plugin_plugin_command() {
received_screen_instructions,
ScreenInstruction::RequestStateUpdateForPlugins, // happens on successful plugin (re)load
screen_receiver,
- 3,
+ 2,
&PermissionType::ChangeApplicationState,
cache_path,
plugin_thread_sender,
@@ -8783,7 +8795,7 @@ pub fn load_new_plugin_plugin_command() {
}
})
.count();
- assert_eq!(request_state_update_requests, 3);
+ assert_eq!(request_state_update_requests, 2);
}
#[test]
diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs
index ca9837a7..647e72ec 100644
--- a/zellij-server/src/plugins/wasm_bridge.rs
+++ b/zellij-server/src/plugins/wasm_bridge.rs
@@ -1,4 +1,4 @@
-use super::{PluginId, PluginInstruction};
+use super::{PinnedExecutor, PluginId, PluginInstruction};
use crate::plugins::pipes::{
apply_pipe_message_to_plugin, pipes_to_block_or_unblock, PendingPipes, PipeStateChange,
};
@@ -9,18 +9,20 @@ use crate::plugins::plugin_worker::MessageToWorker;
use crate::plugins::watch_filesystem::watch_filesystem;
use crate::plugins::zellij_exports::{wasi_read_string, wasi_write_object};
use async_channel::Sender;
-use async_std::task::{self, JoinHandle};
use highway::{HighwayHash, PortableHash};
use log::info;
use notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap};
+use once_cell::sync::OnceCell;
use std::{
collections::{BTreeMap, HashMap, HashSet},
path::PathBuf,
str::FromStr,
sync::{Arc, Mutex},
};
-use wasmtime::{Engine, Module};
-use zellij_utils::consts::{ZELLIJ_CACHE_DIR, ZELLIJ_TMP_DIR};
+use tokio::runtime::Runtime;
+use url::Url;
+use wasmi::{Engine, Module};
+use zellij_utils::consts::{ZELLIJ_CACHE_DIR, ZELLIJ_SESSION_CACHE_DIR, ZELLIJ_TMP_DIR};
use zellij_utils::data::{
FloatingPaneCoordinates, InputMode, PaneContents, PaneRenderReport, PermissionStatus,
PermissionType, PipeMessage, PipeSource,
@@ -49,6 +51,20 @@ use zellij_utils::{
pane_size::Size,
};
+// Global tokio runtime for async I/O operations (plugin downloads, timers)
+static TOKIO_RUNTIME: OnceCell<Runtime> = OnceCell::new();
+
+pub fn get_tokio_runtime() -> &'static Runtime {
+ TOKIO_RUNTIME.get_or_init(|| {
+ tokio::runtime::Builder::new_multi_thread()
+ .worker_threads(2) // Small pool for I/O only
+ .thread_name("tokio-plugin-io")
+ .enable_all()
+ .build()
+ .expect("Failed to create tokio runtime")
+ })
+}
+
#[derive(Debug, Clone)]
pub enum EventOrPipeMessage {
Event(Event),
@@ -79,13 +95,85 @@ impl PluginRenderAsset {
}
}
+#[derive(Debug, Clone)]
+pub struct LoadingContext {
+ pub plugin_id: PluginId,
+ pub client_id: ClientId,
+ pub plugin_cwd: PathBuf,
+ pub plugin_own_data_dir: PathBuf,
+ pub plugin_own_cache_dir: PathBuf,
+ pub plugin_config: PluginConfig,
+ pub tab_index: Option<usize>,
+ pub path_to_default_shell: PathBuf,
+ pub capabilities: PluginCapabilities,
+ pub client_attributes: ClientAttributes,
+ pub default_shell: Option<TerminalAction>,
+ pub layout_dir: Option<PathBuf>,
+ pub default_mode: InputMode,
+ pub keybinds: Keybinds,
+ pub plugin_dir: PathBuf,
+ pub size: Size,
+}
+
+impl LoadingContext {
+ pub fn new(
+ wasm_bridge: &WasmBridge,
+ cwd: Option<PathBuf>,
+ plugin_config: PluginConfig,
+ plugin_id: PluginId,
+ client_id: ClientId,
+ tab_index: Option<usize>,
+ size: Size,
+ ) -> Self {
+ let plugin_own_data_dir = ZELLIJ_SESSION_CACHE_DIR
+ .join(Url::from(&plugin_config.location).to_string())
+ .join(format!("{}-{}", plugin_id, client_id));
+ let plugin_own_cache_dir = ZELLIJ_SESSION_CACHE_DIR
+ .join(Url::from(&plugin_config.location).to_string())
+ .join(format!("plugin_cache"));
+ let default_mode = wasm_bridge
+ .base_modes
+ .get(&client_id)
+ .copied()
+ .unwrap_or(wasm_bridge.default_mode);
+ let keybinds = wasm_bridge
+ .keybinds
+ .get(&client_id)
+ .cloned()
+ .unwrap_or_else(|| wasm_bridge.default_keybinds.clone());
+
+ LoadingContext {
+ client_id,
+ plugin_id,
+ path_to_default_shell: wasm_bridge.path_to_default_shell.clone(),
+ plugin_cwd: cwd.unwrap_or_else(|| wasm_bridge.zellij_cwd.clone()),
+ capabilities: wasm_bridge.capabilities.clone(),
+ client_attributes: wasm_bridge.client_attributes.clone(),
+ default_shell: wasm_bridge.default_shell.clone(),
+ layout_dir: wasm_bridge.layout_dir.clone(),
+ keybinds,
+ default_mode,
+ plugin_own_data_dir,
+ plugin_own_cache_dir,
+ plugin_config,
+ tab_index,
+ plugin_dir: wasm_bridge.plugin_dir.clone(),
+ size,
+ }
+ }
+ pub fn update_plugin_path(&mut self, new_path: PathBuf) {
+ self.plugin_config.path = new_path;
+ }
+}
+
+pub type PluginCache = Arc<Mutex<HashMap<PathBuf, Module>>>;
+
pub struct WasmBridge {
connected_clients: Arc<Mutex<Vec<ClientId>>>,
senders: ThreadSenders,
- engine: Engine,
plugin_dir: PathBuf,
- plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
plugin_map: Arc<Mutex<PluginMap>>,
+ plugin_executor: Arc<PinnedExecutor>,
next_plugin_id: PluginId,
plugin_ids_waiting_for_permission_request: HashSet<PluginId>,
cached_events_for_pending_plugins: HashMap<PluginId, Vec<EventOrPipeMessage>>,
@@ -94,7 +182,7 @@ pub struct WasmBridge {
// worker_name,
// message,
// payload>
- loading_plugins: HashMap<(PluginId, RunPlugin), JoinHandle<()>>, // plugin_id to join-handle
+ loading_plugins: HashSet<(PluginId, RunPlugin)>, // tracks loading plugins without handles
pending_plugin_reloads: HashSet<RunPlugin>,
path_to_default_shell: PathBuf,
watcher: Option<Debouncer<RecommendedWatcher, FileIdMap>>,
@@ -102,7 +190,6 @@ pub struct WasmBridge {
capabilities: PluginCapabilities,
client_attributes: ClientAttributes,
default_shell: Option<TerminalAction>,
- default_layout: Box<Layout>,
cached_plugin_map:
HashMap<RunPluginLocation, HashMap<PluginUserConfiguration, Vec<(PluginId, ClientId)>>>,
pending_pipes: PendingPipes,
@@ -136,13 +223,22 @@ impl WasmBridge {
Arc::new(Mutex::new(HashMap::new()));
let watcher = None;
let downloader = Downloader::new(ZELLIJ_CACHE_DIR.to_path_buf());
+ let max_threads = num_cpus::get().max(4).min(16);
+ let plugin_executor = Arc::new(PinnedExecutor::new(
+ max_threads,
+ &senders,
+ &plugin_map,
+ &connected_clients,
+ &default_layout,
+ &plugin_cache,
+ &engine,
+ ));
WasmBridge {
connected_clients,
senders,
- engine,
plugin_dir,
- plugin_cache,
plugin_map,
+ plugin_executor,
path_to_default_shell,
watcher,
next_plugin_id: 0,
@@ -150,13 +246,12 @@ impl WasmBridge {
plugin_ids_waiting_for_permission_request: HashSet::new(),
cached_resizes_for_pending_plugins: HashMap::new(),
cached_worker_messages: HashMap::new(),
- loading_plugins: HashMap::new(),
+ loading_plugins: HashSet::new(),
pending_plugin_reloads: HashSet::new(),
zellij_cwd,
capabilities,
client_attributes,
default_shell,
- default_layout,
cached_plugin_map: HashMap::new(),
pending_pipes: Default::default(),
layout_dir,
@@ -176,13 +271,23 @@ impl WasmBridge {
cwd: Option<PathBuf>,
skip_cache: bool,
client_id: Option<ClientId>,
- cli_client_id: Option<ClientId>,
) -> Result<(PluginId, ClientId)> {
- // returns the plugin id
let err_context = move || format!("failed to load plugin");
let client_id = client_id
+ .and_then(|client_id| {
+ // first attempt to use a connected client (because this might be a cli_client that
+ // should not get plugins) and only if none is connected, load a "dummy" plugin for
+ // the cli client
+ let connected_clients = self.connected_clients.lock().unwrap();
+ if connected_clients.contains(&client_id) {
+ Some(client_id)
+ } else {
+ None
+ }
+ })
.or_else(|| {
+ // if no client id was provided, try to use the first connected client
self.connected_clients
.lock()
.unwrap()
@@ -190,6 +295,9 @@ impl WasmBridge {
.next()
.copied()
})
+ .or(client_id) // if we got here, this is likely a cli client with no other clients
+ // connected, or loading a background plugin on app start, we use the provided client id as a dummy to load the
+ // plugin anyway
.with_context(|| {
"Plugins must have a client id, none was provided and none are connected"
})?;
@@ -198,7 +306,7 @@ impl WasmBridge {
match run {
Some(run) => {
- let mut plugin = PluginConfig::from_run_plugin(run)
+ let plugin = PluginConfig::from_run_plugin(run)
.with_context(|| format!("failed to resolve plugin {run:?}"))
.with_context(err_context)?;
let plugin_name = run.location.to_string();
@@ -207,33 +315,29 @@ impl WasmBridge {
.insert(plugin_id, vec![]);
self.cached_resizes_for_pending_plugins
.insert(plugin_id, (size.rows, size.cols));
+ self.loading_plugins.insert((plugin_id, run.clone()));
+
+ // Clone for threaded contexts
+ let plugin_executor = self.plugin_executor.clone();
+ let senders = self.senders.clone();
+ let zellij_cwd = cwd.unwrap_or_else(|| self.zellij_cwd.clone());
+
+ // Check if we need to download (async I/O required)
+ let needs_download = matches!(plugin.location, RunPluginLocation::Remote(_));
+
+ let mut loading_context = LoadingContext::new(
+ &self,
+ Some(zellij_cwd.clone()),
+ plugin.clone(), // TODO: rename to plugin_config
+ plugin_id,
+ client_id,
+ tab_index,
+ size,
+ );
- let load_plugin_task = task::spawn({
- let plugin_dir = self.plugin_dir.clone();
- let plugin_cache = self.plugin_cache.clone();
- let senders = self.senders.clone();
- let engine = self.engine.clone();
- let plugin_map = self.plugin_map.clone();
- let connected_clients = self.connected_clients.clone();
- let path_to_default_shell = self.path_to_default_shell.clone();
- let zellij_cwd = cwd.unwrap_or_else(|| self.zellij_cwd.clone());
- let capabilities = self.capabilities.clone();
- let client_attributes = self.client_attributes.clone();
- let default_shell = self.default_shell.clone();
- let default_layout = self.default_layout.clone();
- let layout_dir = self.layout_dir.clone();
+ if needs_download {
let downloader = self.downloader.clone();
- let default_mode = self
- .base_modes
- .get(&client_id)
- .copied()
- .unwrap_or(self.default_mode);
- let keybinds = self
- .keybinds
- .get(&client_id)
- .cloned()
- .unwrap_or_else(|| self.default_keybinds.clone());
- async move {
+ get_tokio_runtime().spawn(async move {
let _ = senders.send_to_background_jobs(
BackgroundJob::AnimatePluginLoading(plugin_id),
);
@@ -246,64 +350,119 @@ impl WasmBridge {
.map(ToString::to_string)
.collect();
- // if the url is already in cache, we'll use that version, otherwise
- // we'll download it, place it in cache and then use it
match downloader.download(url, Some(&file_name)).await {
- Ok(_) => plugin.path = ZELLIJ_CACHE_DIR.join(&file_name),
+ Ok(_) => loading_context
+ .update_plugin_path(ZELLIJ_CACHE_DIR.join(&file_name)),
+ Err(e) => {
+ handle_plugin_loading_failure(
+ &senders,
+ plugin_id,
+ &mut loading_indication,
+ e,
+ Some(client_id),
+ );
+ return;
+ },
+ }
+ }
+
+ plugin_executor.execute_plugin_load(
+ plugin_id,
+ move |senders: ThreadSenders,
+ plugin_map: Arc<Mutex<PluginMap>>,
+ connected_clients: Arc<Mutex<Vec<ClientId>>>,
+ default_layout: Box<Layout>,
+ plugin_cache: PluginCache,
+ engine| {
+ let mut plugin_map = plugin_map.lock().unwrap();
+ match PluginLoader::new(
+ skip_cache,
+ loading_context,
+ senders.clone(),
+ engine.clone(),
+ default_layout.clone(),
+ plugin_cache.clone(),
+ &mut plugin_map,
+ connected_clients.clone(),
+ )
+ .start_plugin()
+ {
+ Ok(_) => {
+ let plugin_list = plugin_map.list_plugins();
+ handle_plugin_successful_loading(
+ &senders,
+ plugin_id,
+ plugin_list,
+ );
+ },
+ Err(e) => handle_plugin_loading_failure(
+ &senders,
+ plugin_id,
+ &mut loading_indication,
+ e,
+ Some(client_id),
+ ),
+ }
+
+ let _ =
+ senders.send_to_plugin(PluginInstruction::ApplyCachedEvents {
+ plugin_ids: vec![plugin_id],
+ done_receiving_permissions: false,
+ });
+ },
+ );
+ });
+ } else {
+ let _ = senders
+ .send_to_background_jobs(BackgroundJob::AnimatePluginLoading(plugin_id));
+ let mut loading_indication = LoadingIndication::new(plugin_name.clone());
+
+ self.plugin_executor.execute_plugin_load(
+ plugin_id,
+ move |senders,
+ plugin_map,
+ connected_clients,
+ default_layout,
+ plugin_cache: PluginCache,
+ engine: Engine| {
+ let mut plugin_map = plugin_map.lock().unwrap();
+ match PluginLoader::new(
+ skip_cache,
+ loading_context,
+ senders.clone(),
+ engine.clone(),
+ default_layout.clone(),
+ plugin_cache.clone(),
+ &mut plugin_map,
+ connected_clients.clone(),
+ )
+ .start_plugin()
+ {
+ Ok(_) => {
+ let plugin_list = plugin_map.list_plugins();
+ handle_plugin_successful_loading(
+ &senders,
+ plugin_id,
+ plugin_list,
+ );
+ },
Err(e) => handle_plugin_loading_failure(
&senders,
plugin_id,
&mut loading_indication,
e,
- cli_client_id,
+ Some(client_id),
),
}
- }
- match PluginLoader::start_plugin(
- plugin_id,
- client_id,
- &plugin,
- tab_index,
- plugin_dir,
- plugin_cache,
- senders.clone(),
- engine,
- plugin_map.clone(),
- size,
- connected_clients.clone(),
- &mut loading_indication,
- path_to_default_shell,
- zellij_cwd.clone(),
- capabilities,
- client_attributes,
- default_shell,
- default_layout,
- skip_cache,
- layout_dir,
- default_mode,
- keybinds,
- ) {
- Ok(_) => {
- let plugin_list = plugin_map.lock().unwrap().list_plugins();
- handle_plugin_successful_loading(&senders, plugin_id, plugin_list);
- },
- Err(e) => handle_plugin_loading_failure(
- &senders,
- plugin_id,
- &mut loading_indication,
- e,
- cli_client_id,
- ),
- }
- let _ = senders.send_to_plugin(PluginInstruction::ApplyCachedEvents {
- plugin_ids: vec![plugin_id],
- done_receiving_permissions: false,
- });
- }
- });
- self.loading_plugins
- .insert((plugin_id, run.clone()), load_plugin_task);
+ let _ = senders.send_to_plugin(PluginInstruction::ApplyCachedEvents {
+ plugin_ids: vec![plugin_id],
+ done_receiving_permissions: false,
+ });
+ },
+ );
+ }
+
self.next_plugin_id += 1;
},
None => {
@@ -322,55 +481,80 @@ impl WasmBridge {
}
pub fn unload_plugin(&mut self, pid: PluginId) -> Result<()> {
info!("Bye from plugin {}", &pid);
- let mut plugin_map = self.plugin_map.lock().unwrap();
- for ((plugin_id, client_id), (running_plugin, subscriptions, workers)) in
- plugin_map.remove_plugins(pid)
+
+ // Remove from plugin_map on main thread
+ let plugins_to_cleanup: Vec<_> = {
+ let mut plugin_map = self.plugin_map.lock().unwrap();
+ plugin_map.remove_plugins(pid).into_iter().collect()
+ };
+
+ // Schedule cleanup on each plugin's pinned thread
+ for ((plugin_id, client_id), (running_plugin, subscriptions, workers)) in plugins_to_cleanup
{
+ // Clear key intercepts if needed (on main thread is OK)
+ if running_plugin.lock().unwrap().intercepting_key_presses() {
+ let _ = self
+ .senders
+ .send_to_screen(ScreenInstruction::ClearKeyPressesIntercepts(client_id));
+ }
+
+ // Send worker exit messages
for (_worker_name, worker_sender) in workers {
drop(worker_sender.send(MessageToWorker::Exit));
}
- {
- // if the plugin was intercepting key presses and for some reason did not clear
- // this state, we make sure to do it ourselves so that the user will not get stuck
- if running_plugin.lock().unwrap().intercepting_key_presses() {
- let _ = self
- .senders
- .send_to_screen(ScreenInstruction::ClearKeyPressesIntercepts(client_id));
- }
- }
- let subscriptions = subscriptions.lock().unwrap();
- if subscriptions.contains(&EventType::BeforeClose) {
- let mut running_plugin = running_plugin.lock().unwrap();
- match apply_before_close_event_to_plugin(
- pid,
- client_id,
- &mut running_plugin,
- self.senders.clone(),
- ) {
- Ok(()) => {},
- Err(e) => {
- log::error!("{:?}", e);
- // https://stackoverflow.com/questions/66450942/in-rust-is-there-a-way-to-make-literal-newlines-in-r-using-windows-c
- let stringified_error = format!("{:?}", e).replace("\n", "\n\r");
+ self.plugin_executor.execute_plugin_unload(
+ plugin_id,
+ move |senders,
+ _plugin_map,
+ _connected_clients,
+ _default_layout,
+ _plugin_cache,
+ _engine| {
+ let subscriptions_guard = subscriptions.lock().unwrap();
+ let needs_before_close = subscriptions_guard.contains(&EventType::BeforeClose);
+ drop(subscriptions_guard); // Release lock before calling plugin
- handle_plugin_crash(plugin_id, stringified_error, self.senders.clone());
- },
- }
- let cache_dir = running_plugin.store.data().plugin_own_data_dir.clone();
- if let Err(e) = std::fs::remove_dir_all(cache_dir) {
- log::error!("Failed to remove cache dir for plugin: {:?}", e);
- }
- } else {
- // this is duplicated because of locking/unlocking order between running_plugin and
- // subscriptions
- let running_plugin = running_plugin.lock().unwrap();
- let cache_dir = running_plugin.store.data().plugin_own_data_dir.clone();
- if let Err(e) = std::fs::remove_dir_all(cache_dir) {
- log::error!("Failed to remove cache dir for plugin: {:?}", e);
- }
- }
+ if needs_before_close {
+ let mut rp = running_plugin.lock().unwrap();
+ match apply_before_close_event_to_plugin(
+ plugin_id,
+ client_id,
+ &mut rp,
+ senders.clone(),
+ ) {
+ Ok(()) => {},
+ Err(e) => {
+ log::error!("{:?}", e);
+ let stringified_error = format!("{:?}", e).replace("\n", "\n\r");
+ handle_plugin_crash(plugin_id, stringified_error, senders.clone());
+ },
+ }
+ let cache_dir = rp.store.data().plugin_own_data_dir.clone();
+ drop(rp); // Release lock before filesystem operation
+ if let Err(e) = std::fs::remove_dir_all(&cache_dir) {
+ log::error!("Failed to remove cache dir for plugin: {:?}", e);
+ }
+ } else {
+ let cache_dir = running_plugin
+ .lock()
+ .unwrap()
+ .store
+ .data()
+ .plugin_own_data_dir
+ .clone();
+ if let Err(e) = std::fs::remove_dir_all(&cache_dir) {
+ log::error!("Failed to remove cache dir for plugin: {:?}", e);
+ }
+ }
+
+ drop(running_plugin);
+ drop(subscriptions);
+ },
+ );
}
+
+ // Main thread cleanup
self.cached_plugin_map.clear();
let mut pipes_to_unblock = self.pending_pipes.unload_plugin(&pid);
for pipe_name in pipes_to_unblock.drain(..) {
@@ -379,10 +563,11 @@ impl WasmBridge {
.send_to_server(ServerInstruction::UnblockCliPipeInput(pipe_name))
.context("failed to unblock input pipe");
}
- let plugin_list = plugin_map.list_plugins();
+ let plugin_list = self.plugin_map.lock().unwrap().list_plugins();
let _ = self
.senders
.send_to_background_jobs(BackgroundJob::ReportPluginList(plugin_list));
+
Ok(())
}
pub fn reload_plugin_with_id(&mut self, plugin_id: u32) -> Result<()> {
@@ -399,58 +584,78 @@ impl WasmBridge {
let mut loading_indication = LoadingIndication::new(run_plugin.location.to_string());
self.start_plugin_loading_indication(&[plugin_id], &loading_indication);
- let load_plugin_task = task::spawn({
- let plugin_dir = self.plugin_dir.clone();
- let plugin_cache = self.plugin_cache.clone();
- let senders = self.senders.clone();
- let engine = self.engine.clone();
- let plugin_map = self.plugin_map.clone();
- let connected_clients = self.connected_clients.clone();
- let path_to_default_shell = self.path_to_default_shell.clone();
- let capabilities = self.capabilities.clone();
- let client_attributes = self.client_attributes.clone();
- let default_shell = self.default_shell.clone();
- let default_layout = self.default_layout.clone();
- let layout_dir = self.layout_dir.clone();
- async move {
- match PluginLoader::reload_plugin(
- plugin_id,
- plugin_dir.clone(),
- plugin_cache.clone(),
+ self.loading_plugins.insert((plugin_id, run_plugin.clone()));
+
+ let plugin_executor = self.plugin_executor.clone();
+
+ let Some(first_client_id) = self.get_first_client_id() else {
+ log::error!("No connected clients, cannot reload plugin.");
+ return Ok(());
+ };
+ let Some(plugin_config) = self.plugin_config_of_plugin_id(plugin_id) else {
+ log::error!("Could not find running plugin with id: {}", plugin_id);
+ return Ok(());
+ };
+ let tab_index = self.tab_index_of_plugin_id(plugin_id);
+ let Some(size) = self.size_of_plugin_id(plugin_id) else {
+ log::error!(
+ "Could not find size of running plugin with id: {}",
+ plugin_id
+ );
+ return Ok(());
+ };
+ let size = Size {
+ rows: size.0,
+ cols: size.1,
+ };
+
+ let cwd = self.cwd_of_plugin_id(plugin_id);
+
+ let loading_context = LoadingContext::new(
+ &self,
+ cwd,
+ plugin_config,
+ plugin_id,
+ first_client_id,
+ tab_index,
+ size,
+ );
+
+ plugin_executor.execute_for_plugin(
+ plugin_id,
+ move |senders, plugin_map, connected_clients, default_layout, plugin_cache, engine| {
+ let skip_cache = true; // we want to explicitly reload the plugin
+ let mut plugin_map = plugin_map.lock().unwrap();
+ match PluginLoader::new(
+ skip_cache,
+ loading_context,
senders.clone(),
engine.clone(),
- plugin_map.clone(),
- connected_clients.clone(),
- &mut loading_indication,
- path_to_default_shell.clone(),
- capabilities.clone(),
- client_attributes.clone(),
- default_shell.clone(),
default_layout.clone(),
- layout_dir.clone(),
- ) {
+ plugin_cache.clone(),
+ &mut plugin_map,
+ connected_clients.clone(),
+ )
+ .start_plugin()
+ {
Ok(_) => {
- let plugin_list = plugin_map.lock().unwrap().list_plugins();
+ let plugin_list = plugin_map.list_plugins();
handle_plugin_successful_loading(&senders, plugin_id, plugin_list);
},
- Err(e) => {
- handle_plugin_loading_failure(
- &senders,
- plugin_id,
- &mut loading_indication,
- &e,
- None,
- );
- },
+ Err(e) => handle_plugin_loading_failure(
+ &senders,
+ plugin_id,
+ &mut loading_indication,
+ e,
+ Some(first_client_id),
+ ),
}
let _ = senders.send_to_plugin(PluginInstruction::ApplyCachedEvents {
plugin_ids: vec![plugin_id],
done_receiving_permissions: false,
});
- }
- });
- self.loading_plugins
- .insert((plugin_id, run_plugin.clone()), load_plugin_task);
+ },
+ );
Ok(())
}
pub fn reload_plugin(&mut self, run_plugin: &RunPlugin) -> Result<()> {
@@ -462,146 +667,110 @@ impl WasmBridge {
let plugin_ids = self
.all_plugin_ids_for_plugin_location(&run_plugin.location, &run_plugin.configuration)?;
for plugin_id in &plugin_ids {
- let (rows, columns) = self.size_of_plugin_id(*plugin_id).unwrap_or((0, 0));
- self.cached_events_for_pending_plugins
- .insert(*plugin_id, vec![]);
- self.cached_resizes_for_pending_plugins
- .insert(*plugin_id, (rows, columns));
+ self.reload_plugin_with_id(*plugin_id)?;
}
-
- let first_plugin_id = *plugin_ids.get(0).unwrap(); // this is safe becaise the above
- // methods always returns at least 1 id
- let mut loading_indication = LoadingIndication::new(run_plugin.location.to_string());
- self.start_plugin_loading_indication(&plugin_ids, &loading_indication);
- let load_plugin_task = task::spawn({
- let plugin_dir = self.plugin_dir.clone();
- let plugin_cache = self.plugin_cache.clone();
- let senders = self.senders.clone();
- let engine = self.engine.clone();
- let plugin_map = self.plugin_map.clone();
- let connected_clients = self.connected_clients.clone();
- let path_to_default_shell = self.path_to_default_shell.clone();
- let zellij_cwd = self.zellij_cwd.clone();
- let capabilities = self.capabilities.clone();
- let client_attributes = self.client_attributes.clone();
- let default_shell = self.default_shell.clone();
- let default_layout = self.default_layout.clone();
- let layout_dir = self.layout_dir.clone();
- async move {
- match PluginLoader::reload_plugin(
- first_plugin_id,
- plugin_dir.clone(),
- plugin_cache.clone(),
- senders.clone(),
- engine.clone(),
- plugin_map.clone(),
- connected_clients.clone(),
- &mut loading_indication,
- path_to_default_shell.clone(),
- capabilities.clone(),
- client_attributes.clone(),
- default_shell.clone(),
- default_layout.clone(),
- layout_dir.clone(),
- ) {
- Ok(_) => {
- let plugin_list = plugin_map.lock().unwrap().list_plugins();
- handle_plugin_successful_loading(&senders, first_plugin_id, plugin_list);
- for plugin_id in &plugin_ids {
- if plugin_id == &first_plugin_id {
- // no need to reload the plugin we just reloaded
- continue;
- }
- let mut loading_indication = LoadingIndication::new("".into());
- match PluginLoader::reload_plugin_from_memory(
- *plugin_id,
- plugin_dir.clone(),
- plugin_cache.clone(),
- senders.clone(),
- engine.clone(),
- plugin_map.clone(),
- connected_clients.clone(),
- &mut loading_indication,
- path_to_default_shell.clone(),
- zellij_cwd.clone(),
- capabilities.clone(),
- client_attributes.clone(),
- default_shell.clone(),
- default_layout.clone(),
- layout_dir.clone(),
- ) {
- Ok(_) => {
- let plugin_list = plugin_map.lock().unwrap().list_plugins();
- handle_plugin_successful_loading(
- &senders,
- *plugin_id,
- plugin_list,
- );
- },
- Err(e) => handle_plugin_loading_failure(
- &senders,
- *plugin_id,
- &mut loading_indication,
- e,
- None,
- ),
- }
- }
- },
- Err(e) => {
- for plugin_id in &plugin_ids {
- handle_plugin_loading_failure(
- &senders,
- *plugin_id,
- &mut loading_indication,
- &e,
- None,
- );
- }
- },
- }
- let _ = senders.send_to_plugin(PluginInstruction::ApplyCachedEvents {
- plugin_ids,
- done_receiving_permissions: false,
- });
- }
- });
- self.loading_plugins
- .insert((first_plugin_id, run_plugin.clone()), load_plugin_task);
Ok(())
}
pub fn add_client(&mut self, client_id: ClientId) -> Result<()> {
- let mut loading_indication = LoadingIndication::new("".into());
- match PluginLoader::add_client(
- client_id,
- self.plugin_dir.clone(),
- self.plugin_cache.clone(),
- self.senders.clone(),
- self.engine.clone(),
- self.plugin_map.clone(),
- self.connected_clients.clone(),
- &mut loading_indication,
- self.path_to_default_shell.clone(),
- self.zellij_cwd.clone(),
- self.capabilities.clone(),
- self.client_attributes.clone(),
- self.default_shell.clone(),
- self.default_layout.clone(),
- self.layout_dir.clone(),
- self.default_mode,
- self.keybinds
- .get(&client_id)
- .cloned()
- .unwrap_or_else(|| self.default_keybinds.clone()),
- ) {
- Ok(_) => {
- let _ = self
- .senders
- .send_to_screen(ScreenInstruction::RequestStateUpdateForPlugins);
- Ok(())
- },
- Err(e) => Err(e),
+ if self.client_is_connected(&client_id) {
+ return Ok(());
}
+
+ let mut new_plugins = HashSet::new();
+ for plugin_id in self.plugin_map.lock().unwrap().plugin_ids() {
+ new_plugins.insert(plugin_id);
+ }
+ for plugin_id in new_plugins {
+ let Some(run_plugin) = self.run_plugin_of_plugin_id(plugin_id).map(|r| r.clone())
+ else {
+ log::error!("Failed to find plugin with id: {}", plugin_id);
+ return Ok(());
+ };
+
+ let (rows, columns) = self.size_of_plugin_id(plugin_id).unwrap_or((0, 0));
+ self.cached_events_for_pending_plugins
+ .insert(plugin_id, vec![]);
+ self.cached_resizes_for_pending_plugins
+ .insert(plugin_id, (rows, columns));
+
+ let loading_indication = LoadingIndication::new(run_plugin.location.to_string());
+ self.start_plugin_loading_indication(&[plugin_id], &loading_indication);
+ self.loading_plugins.insert((plugin_id, run_plugin.clone()));
+
+ let plugin_executor = self.plugin_executor.clone();
+
+ let Some(plugin_config) = self.plugin_config_of_plugin_id(plugin_id) else {
+ log::error!("Could not find running plugin with id: {}", plugin_id);
+ return Ok(());
+ };
+ let tab_index = self.tab_index_of_plugin_id(plugin_id);
+ let Some(size) = self.size_of_plugin_id(plugin_id) else {
+ log::error!(
+ "Could not find size of running plugin with id: {}",
+ plugin_id
+ );
+ return Ok(());
+ };
+ let size = Size {
+ rows: size.0,
+ cols: size.1,
+ };
+
+ let cwd = self.cwd_of_plugin_id(plugin_id);
+
+ let loading_context = LoadingContext::new(
+ &self,
+ cwd,
+ plugin_config,
+ plugin_id,
+ client_id,
+ tab_index,
+ size,
+ );
+
+ plugin_executor.execute_for_plugin(
+ plugin_id,
+ move |senders,
+ plugin_map,
+ connected_clients,
+ default_layout,
+ plugin_cache,
+ engine| {
+ let skip_cache = false;
+ let mut plugin_map = plugin_map.lock().unwrap();
+ match PluginLoader::new(
+ skip_cache,
+ loading_context,
+ senders.clone(),
+ engine.clone(),
+ default_layout.clone(),
+ plugin_cache.clone(),
+ &mut plugin_map,
+ connected_clients.clone(),
+ )
+ .without_connected_clients()
+ .start_plugin()
+ {
+ Ok(_) => {
+ let _ = senders
+ .send_to_screen(ScreenInstruction::RequestStateUpdateForPlugins);
+ let _ = senders.send_to_background_jobs(
+ BackgroundJob::StopPluginLoadingAnimation(plugin_id),
+ );
+ let _ = senders.send_to_plugin(PluginInstruction::ApplyCachedEvents {
+ plugin_ids: vec![plugin_id],
+ done_receiving_permissions: false,
+ });
+ },
+ Err(e) => {
+ log::error!("Failed to load plugin for new client: {}", e);
+ },
+ }
+ },
+ )
+ }
+ self.connected_clients.lock().unwrap().push(client_id);
+ Ok(())
}
pub fn resize_plugin(
&mut self,
@@ -631,11 +800,17 @@ impl WasmBridge {
.lock()
.unwrap()
.next_event_id(AtomicEvent::Resize);
- task::spawn({
- let senders = self.senders.clone();
+ // Execute directly on pinned thread (no async I/O needed for resize/render)
+ self.plugin_executor.execute_for_plugin(plugin_id, {
+ // let senders = self.senders.clone();
let running_plugin = running_plugin.clone();
let _s = shutdown_sender.clone();
- async move {
+ move |senders,
+ _plugin_map,
+ _connected_clients,
+ _default_layout,
+ _plugin_cache,
+ _engine| {
let mut running_plugin = running_plugin.lock().unwrap();
let _s = _s; // guard to allow the task to complete before cleanup/shutdown
if running_plugin.apply_event_id(AtomicEvent::Resize, event_id) {
@@ -660,7 +835,11 @@ impl WasmBridge {
(new_rows as i32, new_columns as i32),
)
})
- .and_then(|_| wasi_read_string(running_plugin.store.data()))
+ .map_err(|e| anyhow!(e))
+ .and_then(|_| {
+ wasi_read_string(running_plugin.store.data())
+ .map_err(|e| anyhow!(e))
+ })
.with_context(err_context);
match rendered_bytes {
Ok(rendered_bytes) => {
@@ -714,29 +893,38 @@ impl WasmBridge {
.contains_key(&plugin_id)
})
.collect();
- task::spawn({
- let mut updates = updates.clone();
- let senders = self.senders.clone();
- let s = shutdown_sender.clone();
- async move {
- let _s = s;
- for (pid, cid, event) in updates.drain(..) {
- for (plugin_id, client_id, running_plugin, subscriptions) in &plugins_to_update
+
+ // Execute each plugin update on its respective pinned thread
+ let plugin_executor = self.plugin_executor.clone();
+ // let senders = self.senders.clone();
+ for (pid, cid, event) in updates.clone().into_iter() {
+ for (plugin_id, client_id, running_plugin, subscriptions) in &plugins_to_update {
+ let subs = subscriptions.lock().unwrap().clone();
+ // FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType?
+ if let Ok(event_type) = EventType::from_str(&event.to_string()) {
+ if (subs.contains(&event_type)
+ || event_type == EventType::PermissionRequestResult)
+ && Self::message_is_directed_at_plugin(pid, cid, plugin_id, client_id)
{
- let subs = subscriptions.lock().unwrap().clone();
- // FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType?
- if let Ok(event_type) = EventType::from_str(&event.to_string()) {
- if (subs.contains(&event_type)
- || event_type == EventType::PermissionRequestResult)
- && Self::message_is_directed_at_plugin(
- pid, cid, plugin_id, client_id,
- )
- {
+ // Execute directly on pinned thread (no async I/O needed for event processing)
+ plugin_executor.execute_for_plugin(*plugin_id, {
+ let plugin_id = *plugin_id;
+ let client_id = *client_id;
+ let running_plugin = running_plugin.clone();
+ let event = event.clone();
+ let _s = shutdown_sender.clone();
+ move |senders,
+ _plugin_map,
+ _connected_clients,
+ _default_layout,
+ _plugin_cache,
+ _engine| {
+ let _s = _s; // guard to allow the task to complete before cleanup/shutdown
let mut running_plugin = running_plugin.lock().unwrap();
let mut plugin_render_assets = vec![];
match apply_event_to_plugin(
- *plugin_id,
- *client_id,
+ plugin_id,
+ client_id,
&mut running_plugin,
&event,
&mut plugin_render_assets,
@@ -755,18 +943,19 @@ impl WasmBridge {
format!("{:?}", e).replace("\n", "\n\r");
handle_plugin_crash(
- *plugin_id,
+ plugin_id,
stringified_error,
senders.clone(),
);
},
}
}
- }
+ });
}
}
}
- });
+ }
+
// loop once more to update the cached events for the pending plugins (probably currently
// being loaded, we'll send them these events when they load)
for (pid, _cid, event) in updates.drain(..) {
@@ -818,85 +1007,95 @@ impl WasmBridge {
.iter()
.cloned()
.collect();
- task::spawn({
- let senders = self.senders.clone();
- async move {
- match new_host_dir.try_exists() {
- Ok(false) => {
- log::error!(
- "Failed to change folder to {},: folder does not exist",
- new_host_dir.display()
- );
- let _ = senders.send_to_plugin(PluginInstruction::Update(vec![(
- Some(plugin_id_to_update),
- Some(client_id_to_update),
- Event::FailedToChangeHostFolder(Some(format!(
- "Folder {} does not exist",
+
+ // Execute directly on pinned thread (no async I/O needed for directory check/change)
+ self.plugin_executor
+ .execute_for_plugin(plugin_id_to_update, {
+ move |senders,
+ _plugin_map,
+ _connected_clients,
+ _default_layout,
+ _plugin_cache,
+ _engine| {
+ match new_host_dir.try_exists() {
+ Ok(false) => {
+ log::error!(
+ "Failed to change folder to {},: folder does not exist",
new_host_dir.display()
- ))),
- )]));
- return;
- },
- Err(e) => {
- log::error!(
- "Failed to change folder to {},: {}",
- new_host_dir.display(),
- e
- );
- let _ = senders.send_to_plugin(PluginInstruction::Update(vec![(
- Some(plugin_id_to_update),
- Some(client_id_to_update),
- Event::FailedToChangeHostFolder(Some(e.to_string())),
- )]));
- return;
- },
- _ => {},
- }
- for (plugin_id, client_id, running_plugin, _subscriptions) in &plugins_to_change {
- if plugin_id == &plugin_id_to_update && client_id == &client_id_to_update {
- let mut running_plugin = running_plugin.lock().unwrap();
- let plugin_env = running_plugin.store.data_mut();
- let stdin_pipe = plugin_env.stdin_pipe.clone();
- let stdout_pipe = plugin_env.stdout_pipe.clone();
- let wasi_ctx = PluginLoader::create_wasi_ctx(
- &new_host_dir,
- &plugin_env.plugin_own_data_dir,
- &plugin_env.plugin_own_cache_dir,
- &ZELLIJ_TMP_DIR,
- &plugin_env.plugin.location.to_string(),
- plugin_env.plugin_id,
- stdin_pipe.clone(),
- stdout_pipe.clone(),
- );
- match wasi_ctx {
- Ok(wasi_ctx) => {
- drop(std::mem::replace(&mut plugin_env.wasi_ctx, wasi_ctx));
- plugin_env.plugin_cwd = new_host_dir.clone();
+ );
+ let _ = senders.send_to_plugin(PluginInstruction::Update(vec![(
+ Some(plugin_id_to_update),
+ Some(client_id_to_update),
+ Event::FailedToChangeHostFolder(Some(format!(
+ "Folder {} does not exist",
+ new_host_dir.display()
+ ))),
+ )]));
+ return;
+ },
+ Err(e) => {
+ log::error!(
+ "Failed to change folder to {},: {}",
+ new_host_dir.display(),
+ e
+ );
+ let _ = senders.send_to_plugin(PluginInstruction::Update(vec![(
+ Some(plugin_id_to_update),
+ Some(client_id_to_update),
+ Event::FailedToChangeHostFolder(Some(e.to_string())),
+ )]));
+ return;
+ },
+ _ => {},
+ }
+ for (plugin_id, client_id, running_plugin, _subscriptions) in &plugins_to_change
+ {
+ if plugin_id == &plugin_id_to_update && client_id == &client_id_to_update {
+ let mut running_plugin = running_plugin.lock().unwrap();
+ let plugin_env = running_plugin.store.data_mut();
+ let stdin_pipe = plugin_env.stdin_pipe.clone();
+ let stdout_pipe = plugin_env.stdout_pipe.clone();
+ let wasi_ctx = PluginLoader::create_wasi_ctx(
+ &new_host_dir,
+ &plugin_env.plugin_own_data_dir,
+ &plugin_env.plugin_own_cache_dir,
+ &ZELLIJ_TMP_DIR,
+ &plugin_env.plugin.location.to_string(),
+ plugin_env.plugin_id,
+ stdin_pipe.clone(),
+ stdout_pipe.clone(),
+ );
+ match wasi_ctx {
+ Ok(wasi_ctx) => {
+ drop(std::mem::replace(&mut plugin_env.wasi_ctx, wasi_ctx));
+ plugin_env.plugin_cwd = new_host_dir.clone();
- let _ = senders.send_to_plugin(PluginInstruction::Update(vec![(
- Some(*plugin_id),
- Some(*client_id),
- Event::HostFolderChanged(new_host_dir.clone()),
- )]));
- },
- Err(e) => {
- let _ = senders.send_to_plugin(PluginInstruction::Update(vec![(
- Some(*plugin_id),
- Some(*client_id),
- Event::FailedToChangeHostFolder(Some(e.to_string())),
- )]));
- log::error!("Failed to create wasi ctx: {}", e);
- },
+ let _ =
+ senders.send_to_plugin(PluginInstruction::Update(vec![(
+ Some(*plugin_id),
+ Some(*client_id),
+ Event::HostFolderChanged(new_host_dir.clone()),
+ )]));
+ },
+ Err(e) => {
+ let _ =
+ senders.send_to_plugin(PluginInstruction::Update(vec![(
+ Some(*plugin_id),
+ Some(*client_id),
+ Event::FailedToChangeHostFolder(Some(e.to_string())),
+ )]));
+ log::error!("Failed to create wasi ctx: {}", e);
+ },
+ }
}
}
}
- }
- });
+ });
Ok(())
}
pub fn pipe_messages(
&mut self,
- mut messages: Vec<(Option<PluginId>, Option<ClientId>, PipeMessage)>,
+ messages: Vec<(Option<PluginId>, Option<ClientId>, PipeMessage)>,
shutdown_sender: Sender<()>,
) -> Result<()> {
let plugins_to_update: Vec<(
@@ -917,7 +1116,10 @@ impl WasmBridge {
.contains_key(&plugin_id)
})
.collect();
- for (message_pid, message_cid, pipe_message) in messages.drain(..) {
+
+ // Execute each pipe message on its respective plugin's pinned thread
+ let plugin_executor = self.plugin_executor.clone();
+ for (message_pid, message_cid, pipe_message) in messages.clone().into_iter() {
for (plugin_id, client_id, running_plugin, _subscriptions) in &plugins_to_update {
if Self::message_is_directed_at_plugin(
message_pid,
@@ -929,14 +1131,20 @@ impl WasmBridge {
self.pending_pipes
.mark_being_processed(pipe_id, plugin_id, client_id);
}
- task::spawn({
- let senders = self.senders.clone();
+ // Execute directly on pinned thread (no async I/O needed for pipe message processing)
+ plugin_executor.execute_for_plugin(*plugin_id, {
+ // let senders = senders.clone();
let running_plugin = running_plugin.clone();
let pipe_message = pipe_message.clone();
let plugin_id = *plugin_id;
let client_id = *client_id;
let _s = shutdown_sender.clone();
- async move {
+ move |senders,
+ _plugin_map,
+ _connected_clients,
+ _default_layout,
+ _plugin_cache,
+ _engine| {
let mut running_plugin = running_plugin.lock().unwrap();
let mut plugin_render_assets = vec![];
let _s = _s; // guard to allow the task to complete before cleanup/shutdown
@@ -1021,7 +1229,7 @@ impl WasmBridge {
applied_plugin_paths.insert(run_plugin.clone());
}
self.loading_plugins
- .retain(|(p_id, _run_plugin), _| p_id != &plugin_id);
+ .retain(|(p_id, _run_plugin)| p_id != &plugin_id);
self.clear_plugin_map_cache();
}
for run_plugin in applied_plugin_paths.drain() {
@@ -1103,9 +1311,8 @@ impl WasmBridge {
}
pub fn cleanup(&mut self) {
- for (_plugin_id, loading_plugin_task) in self.loading_plugins.drain() {
- drop(loading_plugin_task.cancel());
- }
+ self.loading_plugins.clear();
+
let plugin_ids = self.plugin_map.lock().unwrap().plugin_ids();
for plugin_id in &plugin_ids {
drop(self.unload_plugin(*plugin_id));
@@ -1117,8 +1324,8 @@ impl WasmBridge {
pub fn run_plugin_of_loading_plugin_id(&self, plugin_id: PluginId) -> Option<&RunPlugin> {
self.loading_plugins
.iter()
- .find(|((p_id, _run_plugin), _)| p_id == &plugin_id)
- .map(|((_p_id, run_plugin), _)| run_plugin)
+ .find(|(p_id, _run_plugin)| p_id == &plugin_id)
+ .map(|(_p_id, run_plugin)| run_plugin)
}
pub fn run_plugin_of_plugin_id(&self, plugin_id: PluginId) -> Option<RunPlugin> {
self.plugin_map
@@ -1134,16 +1341,16 @@ impl WasmBridge {
default_mode: Option<InputMode>,
default_shell: Option<TerminalAction>,
) -> Result<()> {
- let plugins_to_reconfigure: Vec<Arc<Mutex<RunningPlugin>>> = self
+ let plugins_to_reconfigure: Vec<(PluginId, Arc<Mutex<RunningPlugin>>)> = self
.plugin_map
.lock()
.unwrap()
.running_plugins()
.iter()
.cloned()
- .filter_map(|(_plugin_id, c_id, running_plugin)| {
+ .filter_map(|(plugin_id, c_id, running_plugin)| {
if c_id == client_id {
- Some(running_plugin.clone())
+ Some((plugin_id, running_plugin.clone()))
} else {
None
}
@@ -1156,12 +1363,17 @@ impl WasmBridge {
self.keybinds.insert(client_id, keybinds.clone());
}
self.default_shell = default_shell.clone();
- for running_plugin in plugins_to_reconfigure {
- task::spawn({
+ for (plugin_id, running_plugin) in plugins_to_reconfigure {
+ self.plugin_executor.execute_for_plugin(plugin_id, {
let running_plugin = running_plugin.clone();
let keybinds = keybinds.clone();
let default_shell = default_shell.clone();
- async move {
+ move |_senders,
+ _plugin_map,
+ _connected_clients,
+ _default_layout,
+ _plugin_cache,
+ _engine| {
let mut running_plugin = running_plugin.lock().unwrap();
if let Some(keybinds) = keybinds {
running_plugin.update_keybinds(keybinds);
@@ -1198,14 +1410,18 @@ impl WasmBridge {
.unwrap()
.get_running_plugin_and_subscriptions(plugin_id, *client_id)
{
- task::spawn({
- let senders = self.senders.clone();
+ let subs = subscriptions.lock().unwrap().clone();
+ self.plugin_executor.execute_for_plugin(plugin_id, {
let running_plugin = running_plugin.clone();
let client_id = *client_id;
let _s = shutdown_sender.clone();
let events_or_pipe_messages = events_or_pipe_messages.clone();
- async move {
- let subs = subscriptions.lock().unwrap().clone();
+ move |senders,
+ _plugin_map,
+ _connected_clients,
+ _default_layout,
+ _plugin_cache,
+ _engine| {
let _s = _s; // guard to allow the task to complete before cleanup/shutdown
for event_or_pipe_message in events_or_pipe_messages {
match event_or_pipe_message {
@@ -1311,7 +1527,7 @@ impl WasmBridge {
fn plugin_is_currently_being_loaded(&self, plugin_location: &RunPluginLocation) -> bool {
self.loading_plugins
.iter()
- .find(|((_plugin_id, run_plugin), _)| &run_plugin.location == plugin_location)
+ .find(|(_plugin_id, run_plugin)| &run_plugin.location == plugin_location)
.is_some()
}
fn plugin_id_of_loading_plugin(
@@ -1321,7 +1537,7 @@ impl WasmBridge {
) -> Option<PluginId> {
self.loading_plugins
.iter()
- .find_map(|((plugin_id, run_plugin), _)| {
+ .find_map(|(plugin_id, run_plugin)| {
if &run_plugin.location == plugin_location
&& &run_plugin.configuration == plugin_configuration
{
@@ -1375,6 +1591,36 @@ impl WasmBridge {
(r.rows, r.columns)
})
}
+ fn cwd_of_plugin_id(&self, plugin_id: PluginId) -> Option<PathBuf> {
+ self.plugin_map
+ .lock()
+ .unwrap()
+ .get_running_plugin(plugin_id, None)
+ .map(|r| {
+ let r = r.lock().unwrap();
+ r.store.data().plugin_cwd.clone()
+ })
+ }
+ fn plugin_config_of_plugin_id(&self, plugin_id: PluginId) -> Option<PluginConfig> {
+ self.plugin_map
+ .lock()
+ .unwrap()
+ .get_running_plugin(plugin_id, None)
+ .map(|r| {
+ let r = r.lock().unwrap();
+ r.store.data().plugin.clone()
+ })
+ }
+ fn tab_index_of_plugin_id(&self, plugin_id: PluginId) -> Option<usize> {
+ self.plugin_map
+ .lock()
+ .unwrap()
+ .get_running_plugin(plugin_id, None)
+ .and_then(|r| {
+ let r = r.lock().unwrap();
+ r.store.data().tab_index
+ })
+ }
fn start_plugin_loading_indication(
&self,
plugin_ids: &[PluginId],
@@ -1517,7 +1763,6 @@ impl WasmBridge {
size,
cwd.clone(),
skip_cache,
- None,
cli_client_id,
) {
Ok((plugin_id, client_id)) => {
@@ -1628,7 +1873,7 @@ fn handle_plugin_loading_failure(
plugin_id: PluginId,
loading_indication: &mut LoadingIndication,
error: impl std::fmt::Debug,
- cli_client_id: Option<ClientId>,
+ client_id: Option<ClientId>,
) {
log::error!("{:?}", error);
let _ = senders.send_to_background_jobs(BackgroundJob::StopPluginLoadingAnimation(plugin_id));
@@ -1637,10 +1882,10 @@ fn handle_plugin_loading_failure(
plugin_id,
loading_indication.clone(),
));
- if let Some(cli_client_id) = cli_client_id {
+ if let Some(client_id) = client_id {
let _ = senders.send_to_server(ServerInstruction::LogError(
vec![format!("{:?}", error)],
- cli_client_id,
+ client_id,
));
}
}
@@ -1735,7 +1980,10 @@ pub fn apply_event_to_plugin(
.and_then(|render| {
render.call(&mut running_plugin.store, (rows as i32, columns as i32))
})
- .and_then(|_| wasi_read_string(running_plugin.store.data()))
+ .map_err(|e| anyhow!(e))
+ .and_then(|_| {
+ wasi_read_string(running_plugin.store.data()).map_err(|e| anyhow!(e))
+ })
.with_context(err_context)?;
let pipes_to_block_or_unblock = pipes_to_block_or_unblock(running_plugin, None);
let plugin_render_asset = PluginRenderAsset::new(
diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs
index 7c287b0d..2b4ac7e1 100644
--- a/zellij-server/src/plugins/zellij_exports.rs
+++ b/zellij-server/src/plugins/zellij_exports.rs
@@ -1,11 +1,10 @@
use super::PluginInstruction;
use crate::background_jobs::BackgroundJob;
use crate::plugins::plugin_map::PluginEnv;
-use crate::plugins::wasm_bridge::handle_plugin_crash;
+use crate::plugins::wasm_bridge::{get_tokio_runtime, handle_plugin_crash};
use crate::pty::{ClientTabIndexOrPaneId, NewPanePlacement, PtyInstruction};
use crate::route::route_action;
use crate::ServerInstruction;
-use async_std::task;
use interprocess::local_socket::LocalSocketStream;
use log::warn;
use serde::Serialize;
@@ -18,7 +17,7 @@ use std::{
thread,
time::{Duration, Instant},
};
-use wasmtime::{Caller, Linker};
+use wasmi::{Caller, Linker};
use zellij_utils::data::{
CommandType, ConnectToSession, FloatingPaneCoordinates, HttpVerb, KeyWithModifier, LayoutInfo,
MessageToPlugin, OriginatingPlugin, PaneScrollbackResponse, PermissionStatus, PermissionType,
@@ -1246,9 +1245,10 @@ fn set_timeout(env: &PluginEnv, secs: f64) {
let update_target = Some(env.plugin_id);
let client_id = env.client_id;
let plugin_name = env.name();
- task::spawn(async move {
+ // Use tokio runtime for async I/O (timer operation)
+ get_tokio_runtime().spawn(async move {
let start_time = Instant::now();
- task::sleep(Duration::from_secs_f64(secs)).await;
+ tokio::time::sleep(Duration::from_secs_f64(secs)).await;
// FIXME: The way that elapsed time is being calculated here is not exact; it doesn't take into account the
// time it takes an event to actually reach the plugin after it's sent to the `wasm` thread.
let elapsed_time = Instant::now().duration_since(start_time).as_secs_f64();
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index efc5f6bd..1fdf687f 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -6,7 +6,7 @@ use std::net::{IpAddr, Ipv4Addr};
use std::path::PathBuf;
use std::rc::Rc;
use std::str;
-use std::time::Duration;
+use std::time::{Duration, Instant};
use log::{debug, warn};
use zellij_utils::data::{
@@ -702,6 +702,57 @@ impl CopyOptions {
}
}
+// We use this to delay rendering when a new tab opens so that we make sure all plugins
+// (representing portions of the UI) have been fully loaded before the tab is first rendered (with
+// a sensible timeout of 100ms)
+#[derive(Debug, Clone)]
+pub struct RenderBlocker {
+ blocking_plugins: HashMap<u32, Instant>,
+ timeout_ms: u64,
+}
+
+impl RenderBlocker {
+ pub fn new(timeout_ms: u64) -> Self {
+ Self {
+ blocking_plugins: HashMap::new(),
+ timeout_ms,
+ }
+ }
+
+ pub fn register_blocking_plugin(&mut self, plugin_id: u32) {
+ self.blocking_plugins.insert(plugin_id, Instant::now());
+ }
+
+ pub fn remove_blocking_plugin(&mut self, plugin_id: u32) {
+ self.blocking_plugins.remove(&plugin_id);
+ }
+
+ #[cfg(test)]
+ pub fn can_render(&mut self) -> bool {
+ // we want the tests to be more deterministic and so we always render without any
+ // optimizations
+ true
+ }
+
+ #[cfg(not(test))]
+ pub fn can_render(&mut self) -> bool {
+ let ret = if self.blocking_plugins.is_empty() {
+ true
+ } else {
+ let timeout = Duration::from_millis(self.timeout_ms);
+ let now = Instant::now();
+
+ self.blocking_plugins
+ .values()
+ .all(|&registered_at| now.duration_since(registered_at) >= timeout)
+ };
+ if ret {
+ self.blocking_plugins.clear();
+ }
+ ret
+ }
+}
+
/// A [`Screen`] holds multiple [`Tab`]s, each one holding multiple [`panes`](crate::client::panes).
/// It only directly controls which tab is active, delegating the rest to the individual `Tab`.
pub(crate) struct Screen {
@@ -758,6 +809,7 @@ pub(crate) struct Screen {
// is brought online
web_server_ip: IpAddr,
web_server_port: u16,
+ render_blocker: RenderBlocker,
}
impl Screen {
@@ -840,6 +892,7 @@ impl Screen {
advanced_mouse_actions,
web_server_ip,
web_server_port,
+ render_blocker: RenderBlocker::new(100),
}
}
@@ -3369,6 +3422,7 @@ pub(crate) fn screen_thread_main(
break;
}
}
+ screen.render_blocker.remove_blocking_plugin(plugin_id);
}
screen.render(Some(plugin_render_assets))?;
},
@@ -3376,7 +3430,14 @@ pub(crate) fn screen_thread_main(
screen.render(None)?;
},
ScreenInstruction::RenderToClients => {
- screen.render_to_clients()?;
+ // render_blocker.can_render() returning true means that either all pending plugins
+ // (only those waiting for a new tab layout to be applied!) have been rendered or
+ // that a 100ms timeout has been reached (more info in the RenderBlocker comment)
+ if screen.render_blocker.can_render() {
+ screen.render_to_clients()?;
+ } else {
+ screen.render(None)?;
+ }
},
ScreenInstruction::NewPane(
pid,
@@ -4267,6 +4328,7 @@ pub(crate) fn screen_thread_main(
screen.update_plugin_loading_stage(*plugin_id, loading_indication);
screen.render(None)?;
}
+ screen.render_blocker.register_blocking_plugin(*plugin_id);
}
}
diff --git a/zellij-server/src/tab/layout_applier.rs b/zellij-server/src/tab/layout_applier.rs
index 70da914b..6c047504 100644
--- a/zellij-server/src/tab/layout_applier.rs
+++ b/zellij-server/src/tab/layout_applier.rs
@@ -14,7 +14,7 @@ use crate::{
ClientId,
};
use std::cell::RefCell;
-use std::collections::{BTreeMap, HashMap, HashSet};
+use std::collections::{BTreeMap, HashMap};
use std::rc::Rc;
use zellij_utils::{
data::{Palette, Style},
@@ -30,7 +30,7 @@ pub struct LayoutApplier<'a> {
terminal_emulator_colors: Rc<RefCell<Palette>>,
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
- connected_clients: Rc<RefCell<HashSet<ClientId>>>,
+ connected_clients: Rc<RefCell<HashMap<ClientId, bool>>>,
style: Style,
display_area: Rc<RefCell<Size>>, // includes all panes (including eg. the status bar and tab bar in the default layout)
tiled_panes: &'a mut TiledPanes,
@@ -53,7 +53,7 @@ impl<'a> LayoutApplier<'a> {
terminal_emulator_colors: &Rc<RefCell<Palette>>,
terminal_emulator_color_codes: &Rc<RefCell<HashMap<usize, String>>>,
character_cell_size: &Rc<RefCell<Option<SizeInPixels>>>,
- connected_clients: &Rc<RefCell<HashSet<ClientId>>>,
+ connected_clients: &Rc<RefCell<HashMap<ClientId, bool>>>,
style: &Style,
display_area: &Rc<RefCell<Size>>, // includes all panes (including eg. the status bar and tab bar in the default layout)
tiled_panes: &'a mut TiledPanes,
@@ -352,7 +352,7 @@ impl<'a> LayoutApplier<'a> {
self.terminal_emulator_color_codes.clone(),
self.link_handler.clone(),
self.character_cell_size.clone(),
- self.connected_clients.borrow().iter().copied().collect(),
+ self.connected_clients.borrow().keys().copied().collect(),
self.style,
layout.run.clone(),
self.debug,
@@ -401,7 +401,7 @@ impl<'a> LayoutApplier<'a> {
self.terminal_emulator_color_codes.clone(),
self.link_handler.clone(),
self.character_cell_size.clone(),
- self.connected_clients.borrow().iter().copied().collect(),
+ self.connected_clients.borrow().keys().copied().collect(),
self.style,
floating_pane_layout.run.clone(),
self.debug,
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index f1232594..fd6e110a 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -827,7 +827,7 @@ impl Tab {
&self.terminal_emulator_colors,
&self.terminal_emulator_color_codes,
&self.character_cell_size,
- &self.connected_clients,
+ &self.connected_clients_in_app,
&self.style,
&self.display_area,
&mut self.tiled_panes,
@@ -897,7 +897,7 @@ impl Tab {
&self.terminal_emulator_colors,
&self.terminal_emulator_color_codes,
&self.character_cell_size,
- &self.connected_clients,
+ &self.connected_clients_in_app,
&self.style,
&self.display_area,
&mut self.tiled_panes,
@@ -935,7 +935,7 @@ impl Tab {
&self.terminal_emulator_colors,
&self.terminal_emulator_color_codes,
&self.character_cell_size,
- &self.connected_clients,
+ &self.connected_clients_in_app,
&self.style,
&self.display_area,
&mut self.tiled_panes,
@@ -1384,7 +1384,11 @@ impl Tab {
self.terminal_emulator_color_codes.clone(),
self.link_handler.clone(),
self.character_cell_size.clone(),
- self.connected_clients.borrow().iter().copied().collect(),
+ self.connected_clients_in_app
+ .borrow()
+ .keys()
+ .copied()
+ .collect(),
self.style,
invoked_with,
self.debug,
@@ -1485,7 +1489,11 @@ impl Tab {
self.terminal_emulator_color_codes.clone(),
self.link_handler.clone(),
self.character_cell_size.clone(),
- self.connected_clients.borrow().iter().copied().collect(),
+ self.connected_clients_in_app
+ .borrow()
+ .keys()
+ .copied()
+ .collect(),
self.style,
invoked_with,
self.debug,
@@ -1576,7 +1584,11 @@ impl Tab {
self.terminal_emulator_color_codes.clone(),
self.link_handler.clone(),
self.character_cell_size.clone(),
- self.connected_clients.borrow().iter().copied().collect(),
+ self.connected_clients_in_app
+ .borrow()
+ .keys()
+ .copied()
+ .collect(),
self.style,
invoked_with,
self.debug,
@@ -1708,7 +1720,11 @@ impl Tab {
self.terminal_emulator_color_codes.clone(),
self.link_handler.clone(),
self.character_cell_size.clone(),
- self.connected_clients.borrow().iter().copied().collect(),
+ self.connected_clients_in_app
+ .borrow()
+ .keys()
+ .copied()
+ .collect(),
self.style,
invoked_with,
self.debug,
@@ -1950,7 +1966,11 @@ impl Tab {
self.terminal_emulator_color_codes.clone(),
self.link_handler.clone(),
self.character_cell_size.clone(),
- self.connected_clients.borrow().iter().copied().collect(),
+ self.connected_clients_in_app
+ .borrow()
+ .keys()
+ .copied()
+ .collect(),
self.style,
run,
self.debug,
diff --git a/zellij-server/src/ui/loading_indication.rs b/zellij-server/src/ui/loading_indication.rs
index c61662ff..f5b7afe6 100644
--- a/zellij-server/src/ui/loading_indication.rs
+++ b/zellij-server/src/ui/loading_indication.rs
@@ -1,38 +1,29 @@
use std::fmt::{Display, Error, Formatter};
+use std::time::Instant;
use zellij_utils::{
data::{PaletteColor, Styling},
errors::prelude::*,
};
-#[derive(Debug, Clone)]
-pub enum LoadingStatus {
- InProgress,
- Success,
- NotFound,
-}
-
#[derive(Debug, Clone, Default)]
pub struct LoadingIndication {
pub ended: bool,
- loading_from_memory: Option<LoadingStatus>,
- loading_from_hd_cache: Option<LoadingStatus>,
- compiling: Option<LoadingStatus>,
- starting_plugin: Option<LoadingStatus>,
- writing_plugin_to_cache: Option<LoadingStatus>,
- cloning_plugin_for_other_clients: Option<LoadingStatus>,
error: Option<String>,
animation_offset: usize,
plugin_name: String,
terminal_emulator_colors: Option<Styling>,
override_previous_error: bool,
+ started_at: Option<Instant>,
}
impl LoadingIndication {
pub fn new(plugin_name: String) -> Self {
+ let started_at = Some(Instant::now());
LoadingIndication {
plugin_name,
animation_offset: 0,
+ started_at,
..Default::default()
}
}
@@ -59,51 +50,6 @@ impl LoadingIndication {
}
}
}
- pub fn indicate_loading_plugin_from_memory(&mut self) {
- self.loading_from_memory = Some(LoadingStatus::InProgress);
- }
- pub fn indicate_loading_plugin_from_memory_success(&mut self) {
- self.loading_from_memory = Some(LoadingStatus::Success);
- }
- pub fn indicate_loading_plugin_from_memory_notfound(&mut self) {
- self.loading_from_memory = Some(LoadingStatus::NotFound);
- }
- pub fn indicate_loading_plugin_from_hd_cache(&mut self) {
- self.loading_from_hd_cache = Some(LoadingStatus::InProgress);
- }
- pub fn indicate_loading_plugin_from_hd_cache_success(&mut self) {
- self.loading_from_hd_cache = Some(LoadingStatus::Success);
- }
- pub fn indicate_loading_plugin_from_hd_cache_notfound(&mut self) {
- self.loading_from_hd_cache = Some(LoadingStatus::NotFound);
- }
- pub fn indicate_compiling_plugin(&mut self) {
- self.compiling = Some(LoadingStatus::InProgress);
- }
- pub fn indicate_compiling_plugin_success(&mut self) {
- self.compiling = Some(LoadingStatus::Success);
- }
- pub fn indicate_starting_plugin(&mut self) {
- self.starting_plugin = Some(LoadingStatus::InProgress);
- }
- pub fn indicate_starting_plugin_success(&mut self) {
- self.starting_plugin = Some(LoadingStatus::Success);
- }
- pub fn indicate_writing_plugin_to_cache(&mut self) {
- self.writing_plugin_to_cache = Some(LoadingStatus::InProgress);
- }
- pub fn indicate_writing_plugin_to_cache_success(&mut self) {
- self.writing_plugin_to_cache = Some(LoadingStatus::Success);
- }
- pub fn indicate_cloning_plugin_for_other_clients(&mut self) {
- self.cloning_plugin_for_other_clients = Some(LoadingStatus::InProgress);
- }
- pub fn indicate_cloning_plugin_for_other_clients_success(&mut self) {
- self.cloning_plugin_for_other_clients = Some(LoadingStatus::Success);
- }
- pub fn end(&mut self) {
- self.ended = true;
- }
pub fn progress_animation_offset(&mut self) {
if self.animation_offset == 3 {
self.animation_offset = 0;
@@ -120,14 +66,6 @@ impl LoadingIndication {
pub fn override_previous_error(&mut self) {
self.override_previous_error = true;
}
- fn started_loading(&self) -> bool {
- self.loading_from_memory.is_some()
- || self.loading_from_hd_cache.is_some()
- || self.compiling.is_some()
- || self.starting_plugin.is_some()
- || self.writing_plugin_to_cache.is_some()
- || self.cloning_plugin_for_other_clients.is_some()
- }
}
macro_rules! style {
@@ -147,29 +85,13 @@ impl Display for LoadingIndication {
},
None => ansi_term::Style::new(),
};
- let green = match self.terminal_emulator_colors {
- Some(terminal_emulator_colors) => {
- style!(terminal_emulator_colors.exit_code_success.base).bold()
- },
- None => ansi_term::Style::new(),
- };
- let yellow = match self.terminal_emulator_colors {
- Some(terminal_emulator_colors) => {
- style!(terminal_emulator_colors.exit_code_error.emphasis_0).bold()
- },
- None => ansi_term::Style::new(),
- };
let red = match self.terminal_emulator_colors {
Some(terminal_emulator_colors) => {
style!(terminal_emulator_colors.exit_code_error.base).bold()
},
None => ansi_term::Style::new(),
};
- let bold = ansi_term::Style::new().bold().italic();
let plugin_name = &self.plugin_name;
- let success = green.paint("SUCCESS");
- let failure = red.paint("FAILED");
- let not_found = yellow.paint("NOT FOUND");
let add_dots = |stringified: &mut String| {
for _ in 0..self.animation_offset {
stringified.push('.');
@@ -177,108 +99,7 @@ impl Display for LoadingIndication {
stringified.push(' ');
};
let mut stringified = String::new();
- let loading_text = "Loading";
- let loading_from_memory_text = "Attempting to load from memory";
- let loading_from_hd_cache_text = "Attempting to load from cache";
- let compiling_text = "Compiling WASM";
- let starting_plugin_text = "Starting";
- let writing_plugin_to_cache_text = "Writing to cache";
- let cloning_plugin_for_other_clients_text = "Cloning for other clients";
- if self.started_loading() {
- stringified.push_str(&format!("{} {}...", loading_text, cyan.paint(plugin_name)));
- } else {
- stringified.push_str(&format!(
- "{} {}",
- bold.paint(loading_text),
- cyan.italic().paint(plugin_name)
- ));
- add_dots(&mut stringified);
- }
- match self.loading_from_memory {
- Some(LoadingStatus::InProgress) => {
- stringified.push_str(&format!("\n\r{}", bold.paint(loading_from_memory_text)));
- add_dots(&mut stringified);
- },
- Some(LoadingStatus::Success) => {
- stringified.push_str(&format!("\n\r{loading_from_memory_text}... {success}"));
- },
- Some(LoadingStatus::NotFound) => {
- stringified.push_str(&format!("\n\r{loading_from_memory_text}... {not_found}"));
- },
- None => {},
- }
- match self.loading_from_hd_cache {
- Some(LoadingStatus::InProgress) => {
- stringified.push_str(&format!("\n\r{}", bold.paint(loading_from_hd_cache_text)));
- add_dots(&mut stringified);
- },
- Some(LoadingStatus::Success) => {
- stringified.push_str(&format!("\n\r{loading_from_hd_cache_text}... {success}"));
- },
- Some(LoadingStatus::NotFound) => {
- stringified.push_str(&format!("\n\r{loading_from_hd_cache_text}... {not_found}"));
- },
- None => {},
- }
- match self.compiling {
- Some(LoadingStatus::InProgress) => {
- stringified.push_str(&format!("\n\r{}", bold.paint(compiling_text)));
- add_dots(&mut stringified);
- },
- Some(LoadingStatus::Success) => {
- stringified.push_str(&format!("\n\r{compiling_text}... {success}"));
- },
- Some(LoadingStatus::NotFound) => {
- stringified.push_str(&format!("\n\r{compiling_text}... {failure}"));
- },
- None => {},
- }
- match self.starting_plugin {
- Some(LoadingStatus::InProgress) => {
- stringified.push_str(&format!("\n\r{}", bold.paint(starting_plugin_text)));
- add_dots(&mut stringified);
- },
- Some(LoadingStatus::Success) => {
- stringified.push_str(&format!("\n\r{starting_plugin_text}... {success}"));
- },
- Some(LoadingStatus::NotFound) => {
- stringified.push_str(&format!("\n\r{starting_plugin_text}... {failure}"));
- },
- None => {},
- }
- match self.writing_plugin_to_cache {
- Some(LoadingStatus::InProgress) => {
- stringified.push_str(&format!("\n\r{}", bold.paint(writing_plugin_to_cache_text)));
- add_dots(&mut stringified);
- },
- Some(LoadingStatus::Success) => {
- stringified.push_str(&format!("\n\r{writing_plugin_to_cache_text}... {success}"));
- },
- Some(LoadingStatus::NotFound) => {
- stringified.push_str(&format!("\n\r{writing_plugin_to_cache_text}... {failure}"));
- },
- None => {},
- }
- match self.cloning_plugin_for_other_clients {
- Some(LoadingStatus::InProgress) => {
- stringified.push_str(&format!(
- "\n\r{}",
- bold.paint(cloning_plugin_for_other_clients_text)
- ));
- add_dots(&mut stringified);
- },
- Some(LoadingStatus::Success) => {
- stringified.push_str(&format!(
- "\n\r{cloning_plugin_for_other_clients_text}... {success}"
- ));
- },
- Some(LoadingStatus::NotFound) => {
- stringified.push_str(&format!(
- "\n\r{cloning_plugin_for_other_clients_text}... {failure}"
- ));
- },
- None => {},
- }
+
if let Some(error_text) = &self.error {
stringified.push_str(&format!(
"\n\r{} {}",
@@ -292,7 +113,20 @@ impl Display for LoadingIndication {
red.bold()
.paint("ERROR IN PLUGIN - check logs for more info")
));
+ } else {
+ let loading_text = "Loading";
+ stringified.push_str(&format!("{} {}", loading_text, cyan.paint(plugin_name)));
+ add_dots(&mut stringified);
+ }
+ if self
+ .started_at
+ .map(|s| s.elapsed() > std::time::Duration::from_millis(400))
+ .unwrap_or(true)
+ || self.error.is_some()
+ {
+ write!(f, "{}", stringified)
+ } else {
+ Ok(())
}
- write!(f, "{}", stringified)
}
}
diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-2.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-2.snap
index aef09fe2..3f88587b 100644
--- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-2.snap
+++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-2.snap
@@ -1,6 +1,5 @@
---
source: zellij-server/src/./unit/screen_tests.rs
-assertion_line: 3512
expression: "format!(\"{}\", snapshot)"
---
00 (C): ┌ tiled_pane ──────────────────────────────────────────────────────────────────┐
@@ -9,7 +8,7 @@ expression: "format!(\"{}\", snapshot)"
03 (C): │ │
04 (C): │ │
05 (C): │ ┌ floating_plugin_pane_to_eject ───────┐ │
-06 (C): │ │Loading file:/path/to/fake/plugin │ │
+06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ │ │
@@ -23,4 +22,3 @@ expression: "format!(\"{}\", snapshot)"
17 (C): │ │
18 (C): │ │
19 (C): └──────────────────────────────────────────────────────────────────────────────┘
-
diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-3.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-3.snap
index 2c160dd8..9f6c79b0 100644
--- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-3.snap
+++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-3.snap
@@ -1,6 +1,5 @@
---
source: zellij-server/src/./unit/screen_tests.rs
-assertion_line: 3516
expression: "format!(\"{}\", snapshot)"
---
00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────┐
@@ -9,7 +8,7 @@ expression: "format!(\"{}\", snapshot)"
03 (C): │ │
04 (C): │ │
05 (C): │ ┌ floating_plugin_pane_to_eject ───────┐ │
-06 (C): │ │Loading file:/path/to/fake/plugin │ │
+06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ │ │
@@ -23,4 +22,3 @@ expression: "format!(\"{}\", snapshot)"
17 (C): │ │
18 (C): │ │
19 (C): └──────────────────────────────────────────────────────────────────────────────┘
-
diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-4.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-4.snap
index 2c160dd8..9f6c79b0 100644
--- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-4.snap
+++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-4.snap
@@ -1,6 +1,5 @@
---
source: zellij-server/src/./unit/screen_tests.rs
-assertion_line: 3516
expression: "format!(\"{}\", snapshot)"
---
00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────┐
@@ -9,7 +8,7 @@ expression: "format!(\"{}\", snapshot)"
03 (C): │ │
04 (C): │ │
05 (C): │ ┌ floating_plugin_pane_to_eject ───────┐ │
-06 (C): │ │Loading file:/path/to/fake/plugin │ │
+06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ │ │
@@ -23,4 +22,3 @@ expression: "format!(\"{}\", snapshot)"
17 (C): │ │
18 (C): │ │
19 (C): └──────────────────────────────────────────────────────────────────────────────┘
-
diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-6.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-6.snap
index 2c160dd8..9f6c79b0 100644
--- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-6.snap
+++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-6.snap
@@ -1,6 +1,5 @@
---
source: zellij-server/src/./unit/screen_tests.rs
-assertion_line: 3516
expression: "format!(\"{}\", snapshot)"
---
00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────┐
@@ -9,7 +8,7 @@ expression: "format!(\"{}\", snapshot)"
03 (C): │ │
04 (C): │ │
05 (C): │ ┌ floating_plugin_pane_to_eject ───────┐ │
-06 (C): │ │Loading file:/path/to/fake/plugin │ │
+06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ │ │
@@ -23,4 +22,3 @@ expression: "format!(\"{}\", snapshot)"
17 (C): │ │
18 (C): │ │
19 (C): └──────────────────────────────────────────────────────────────────────────────┘
-
diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-7.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-7.snap
index 2c160dd8..9f6c79b0 100644
--- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-7.snap
+++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-7.snap
@@ -1,6 +1,5 @@
---
source: zellij-server/src/./unit/screen_tests.rs
-assertion_line: 3516
expression: "format!(\"{}\", snapshot)"
---
00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────┐
@@ -9,7 +8,7 @@ expression: "format!(\"{}\", snapshot)"
03 (C): │ │
04 (C): │ │
05 (C): │ ┌ floating_plugin_pane_to_eject ───────┐ │
-06 (C): │ │Loading file:/path/to/fake/plugin │ │
+06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ │ │
@@ -23,4 +22,3 @@ expression: "format!(\"{}\", snapshot)"
17 (C): │ │
18 (C): │ │
19 (C): └──────────────────────────────────────────────────────────────────────────────┘
-
diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab.snap
index aef09fe2..3f88587b 100644
--- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab.snap
+++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab.snap
@@ -1,6 +1,5 @@
---
source: zellij-server/src/./unit/screen_tests.rs
-assertion_line: 3512
expression: "format!(\"{}\", snapshot)"
---
00 (C): ┌ tiled_pane ──────────────────────────────────────────────────────────────────┐
@@ -9,7 +8,7 @@ expression: "format!(\"{}\", snapshot)"
03 (C): │ │
04 (C): │ │
05 (C): │ ┌ floating_plugin_pane_to_eject ───────┐ │
-06 (C): │ │Loading file:/path/to/fake/plugin │ │
+06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ │ │
@@ -23,4 +22,3 @@ expression: "format!(\"{}\", snapshot)"
17 (C): │ │
18 (C): │ │
19 (C): └──────────────────────────────────────────────────────────────────────────────┘
-
diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-3.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-3.snap
index b938c7bc..aeadfedd 100644
--- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-3.snap
+++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-3.snap
@@ -1,10 +1,9 @@
---
source: zellij-server/src/./unit/screen_tests.rs
-assertion_line: 3097
expression: "format!(\"{}\", snapshot)"
---
00 (C): ┌ plugin_pane_to_break_free ───────────────────────────────────────────────────┐
-01 (C): │Loading file:/path/to/fake/plugin │
+01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ │
@@ -23,4 +22,3 @@ expression: "format!(\"{}\", snapshot)"
17 (C): │ │
18 (C): │ │
19 (C): └──────────────────────────────────────────────────────────────────────────────┘
-
diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-4.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-4.snap
index a55bc4d2..aeadfedd 100644
--- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-4.snap
+++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-4.snap
@@ -1,10 +1,9 @@
---
source: zellij-server/src/./unit/screen_tests.rs
-assertion_line: 3429
expression: "format!(\"{}\", snapshot)"
---
00 (C): ┌ plugin_pane_to_break_free ───────────────────────────────────────────────────┐
-01 (C): │Loading file:/path/to/fake/plugin │
+01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ │
@@ -23,4 +22,3 @@ expression: "format!(\"{}\", snapshot)"
17 (C): │ │
18 (C): │ │
19 (C): └──────────────────────────────────────────────────────────────────────────────┘
-
diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab.snap
index a2011a8f..966f0f69 100644
--- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab.snap
+++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab.snap
@@ -1,10 +1,9 @@
---
source: zellij-server/src/./unit/screen_tests.rs
-assertion_line: 3097
expression: "format!(\"{}\", snapshot)"
---
00 (C): ┌ plugin_pane_to_break_free ───────────┐┌ pane_to_stay ────────────────────────┐
-01 (C): │Loading file:/path/to/fake/plugin ││ │
+01 (C): │ ││ │
02 (C): │ ││ │
03 (C): │ ││ │
04 (C): │ ││ │
@@ -23,4 +22,3 @@ expression: "format!(\"{}\", snapshot)"
17 (C): │ ││ │
18 (C): │ ││ │
19 (C): └──────────────────────────────────────┘└──────────────────────────────────────┘
-