diff options
| author | Aram Drevekenin <aram@poor.dev> | 2025-10-21 14:36:30 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-21 14:36:30 +0200 |
| commit | 889bd06f447f9d50ee46b8efc7d9970d3693cb96 (patch) | |
| tree | 171fa657faccf4b4ce7124847977626826a2a750 | |
| parent | Give plugins the ability to read pane contents (#4465) (diff) | |
| download | sl0ppij-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
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) @@ -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", -] @@ -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(|®istered_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): └──────────────────────────────────────┘└──────────────────────────────────────┘ - |
