From 3d8369219056190372fbc622f972faac8a3cc06c Mon Sep 17 00:00:00 2001 From: Stackrats Date: Wed, 14 Oct 2026 19:04:37 +0700 Subject: [PATCH 1/7] Let KDE draw the window's titlebar on Wayland MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Plasma under Wayland the titlebar's minimise, maximise and close did nothing — no hover, no click — and the bar stood taller than every other window's. Neither was GTK's doing: tao 0.35 replaced the window's titlebar on Wayland with its own GTK header bar, wrapped in an event box set above its children, so every pointer event landed on the box and none reached the buttons. Dragging still worked only because the window handles that itself. And being a header bar, it asked for client-side decorations, so KWin never drew its own. tao 0.36 (tauri-apps/tao#1218) dropped that header bar and leaves the choice to GTK, which asks for server-side decorations through KDE's protocol wherever the compositor offers it. Tauri 2.12 carries tao 0.37, so this moves Tauri to 2.12.3 and @tauri-apps/api with it — the release build refuses mismatched Rust and JS minors. Run against this Plasma session with WAYLAND_DEBUG, 0.7.4 sends request_mode(1), client-side; this build sends request_mode(2) and KWin answers mode(2), drawing a Breeze titlebar over a window whose geometry is now the content alone. Desktops that tile still lose the titlebar via linux_env::wants_no_titlebar, whose comment claimed GTK3 had no server-side decoration support on Wayland at all — it does, and the comment now says who draws the bar where. --- Cargo.lock | 364 ++++++++---------- clients/desktop-vue/package.json | 2 +- .../desktop-vue/src-tauri/src/linux_env.rs | 7 +- pnpm-lock.yaml | 28 +- 4 files changed, 189 insertions(+), 212 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7908c86..55b973e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,15 +25,15 @@ dependencies = [ [[package]] name = "alloc-no-stdlib" -version = "2.0.4" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" [[package]] name = "alloc-stdlib" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" dependencies = [ "alloc-no-stdlib", ] @@ -298,6 +298,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bit-set" version = "0.8.0" @@ -413,9 +419,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.4" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -424,9 +430,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.3" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -533,12 +539,13 @@ dependencies = [ [[package]] name = "cargo_toml" -version = "0.22.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9" dependencies = [ + "semver", "serde", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", ] [[package]] @@ -559,13 +566,13 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfb" -version = "0.7.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" dependencies = [ - "byteorder", "fnv", "uuid", + "web-time", ] [[package]] @@ -823,9 +830,9 @@ dependencies = [ [[package]] name = "cssparser" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" dependencies = [ "cssparser-macros", "dtoa-short", @@ -836,29 +843,19 @@ dependencies = [ [[package]] name = "cssparser-macros" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" dependencies = [ "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "ctor" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" -dependencies = [ - "ctor-proc-macro", - "dtor", -] - -[[package]] -name = "ctor-proc-macro" -version = "0.0.7" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" [[package]] name = "darling" @@ -974,6 +971,15 @@ dependencies = [ "dirs-sys 0.5.0", ] +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys 0.5.0", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -1054,9 +1060,9 @@ dependencies = [ [[package]] name = "dom_query" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" dependencies = [ "bit-set", "cssparser", @@ -1091,21 +1097,6 @@ dependencies = [ "dtoa", ] -[[package]] -name = "dtor" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" -dependencies = [ - "dtor-proc-macro", -] - -[[package]] -name = "dtor-proc-macro" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" - [[package]] name = "dunce" version = "1.0.5" @@ -1836,9 +1827,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "html5ever" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" dependencies = [ "log", "markup5ever", @@ -2169,9 +2160,9 @@ dependencies = [ [[package]] name = "infer" -version = "0.19.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" dependencies = [ "cfb", ] @@ -2294,21 +2285,21 @@ dependencies = [ [[package]] name = "json-patch" -version = "3.0.1" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" dependencies = [ "jsonptr", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.19", ] [[package]] name = "jsonptr" -version = "0.6.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" dependencies = [ "serde", "serde_json", @@ -2316,13 +2307,12 @@ dependencies = [ [[package]] name = "keyboard-types" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" dependencies = [ "bitflags 2.13.1", "serde", - "unicode-segmentation", ] [[package]] @@ -2463,9 +2453,9 @@ dependencies = [ [[package]] name = "markup5ever" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" dependencies = [ "log", "tendril", @@ -2755,9 +2745,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.19.3" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996" dependencies = [ "crossbeam-channel", "dpi", @@ -2789,6 +2779,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -2916,6 +2912,7 @@ dependencies = [ "objc2", "objc2-core-foundation", "objc2-foundation", + "objc2-quartz-core", ] [[package]] @@ -3961,9 +3958,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "selectors" -version = "0.36.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" dependencies = [ "bitflags 2.13.1", "cssparser", @@ -4373,9 +4370,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "swift-rs" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9" dependencies = [ "base64 0.21.7", "serde", @@ -4449,9 +4446,9 @@ dependencies = [ [[package]] name = "tao" -version = "0.35.3" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400" dependencies = [ "bitflags 2.13.1", "block2", @@ -4469,20 +4466,20 @@ dependencies = [ "libc", "log", "ndk", + "ndk-context", "ndk-sys", "objc2", "objc2-app-kit", "objc2-foundation", "objc2-ui-kit", - "once_cell", "parking_lot", "percent-encoding", "raw-window-handle", "tao-macros", "unicode-segmentation", "url", - "windows", - "windows-core 0.61.2", + "windows 0.62.2", + "windows-core 0.62.2", "windows-version", "x11-dl", ] @@ -4506,14 +4503,14 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.11.5" +version = "2.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +checksum = "c404ecf956ff241bf6b39dd688614724e1bf1f63255abb4fc46d1c40371672ea" dependencies = [ "anyhow", "bytes", "cookie", - "dirs 6.0.0", + "dirs 7.0.0", "dunce", "embed_plist", "getrandom 0.3.4", @@ -4554,18 +4551,18 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.62.2", ] [[package]] name = "tauri-build" -version = "2.6.3" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +checksum = "2eea0b6302f82c574b21af6fc6ae6e86183b9280853c22621d0abf0936911ffc" dependencies = [ "anyhow", "cargo_toml", - "dirs 6.0.0", + "dirs 7.0.0", "glob", "heck 0.5.0", "json-patch", @@ -4580,16 +4577,16 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.6.3" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +checksum = "02f468f9a9bcd404f57e5c191d91024e3de953d4b610f78c2907e7110615cb1c" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "brotli", "ico", "json-patch", "plist", - "png 0.17.16", + "png 0.18.1", "proc-macro2", "quote", "semver", @@ -4607,9 +4604,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.6.3" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +checksum = "131bffec939b20642380317f8403a33c8888489925e14bb965ceaff0d949b1cb" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -4750,9 +4747,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.11.3" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413" dependencies = [ "cookie", "dpi", @@ -4770,14 +4767,14 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.62.2", ] [[package]] name = "tauri-runtime-wry" -version = "2.11.4" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22" dependencies = [ "gtk", "http", @@ -4795,7 +4792,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.62.2", "wry", ] @@ -4829,9 +4826,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.9.3" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee" dependencies = [ "anyhow", "brotli", @@ -4883,7 +4880,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade" dependencies = [ "thiserror 2.0.19", - "windows", + "windows 0.61.3", "windows-version", ] @@ -5076,21 +5073,6 @@ dependencies = [ "toml_edit 0.20.2", ] -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap 2.14.0", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.15", -] - [[package]] name = "toml" version = "1.1.4+spec-1.1.0" @@ -5115,15 +5097,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -5262,12 +5235,12 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72" dependencies = [ "crossbeam-channel", - "dirs 6.0.0", + "dirs 7.0.0", "libappindicator", "muda", "objc2", @@ -5341,47 +5314,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - [[package]] name = "unicode-ident" version = "1.0.24" @@ -5415,13 +5347,13 @@ dependencies = [ [[package]] name = "urlpattern" -version = "0.3.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" dependencies = [ + "icu_properties", "regex", "serde", - "unic-ucd-ident", "url", ] @@ -5676,16 +5608,14 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.38.2" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", - "windows-core 0.61.2", - "windows-implement", - "windows-interface", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] @@ -5701,13 +5631,13 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.38.2" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" dependencies = [ "thiserror 2.0.19", - "windows", - "windows-core 0.61.2", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] @@ -5743,16 +5673,17 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "window-vibrancy" -version = "0.6.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580" dependencies = [ "objc2", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation", + "objc2-quartz-core", "raw-window-handle", - "windows-sys 0.59.0", + "windows-sys 0.61.2", "windows-version", ] @@ -5762,11 +5693,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -5778,6 +5721,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -5812,7 +5764,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -5859,6 +5822,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-registry" version = "0.5.3" @@ -6008,6 +5981,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -6164,12 +6146,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - [[package]] name = "winnow" version = "1.0.4" @@ -6212,15 +6188,15 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wry" -version = "0.55.1" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0" dependencies = [ "base64 0.22.1", "block2", "cookie", "crossbeam-channel", - "dirs 6.0.0", + "dirs 7.0.0", "dom_query", "dpi", "dunce", @@ -6248,8 +6224,8 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", - "windows-core 0.61.2", + "windows 0.62.2", + "windows-core 0.62.2", "windows-version", "x11-dl", ] diff --git a/clients/desktop-vue/package.json b/clients/desktop-vue/package.json index 74d2334..4df6524 100644 --- a/clients/desktop-vue/package.json +++ b/clients/desktop-vue/package.json @@ -9,7 +9,7 @@ "check": "vp check" }, "dependencies": { - "@tauri-apps/api": "^2", + "@tauri-apps/api": "^2.12.3", "@tauri-apps/plugin-autostart": "^2", "@tauri-apps/plugin-dialog": "^2.7.2", "@tauri-apps/plugin-notification": "^2", diff --git a/clients/desktop-vue/src-tauri/src/linux_env.rs b/clients/desktop-vue/src-tauri/src/linux_env.rs index f68e9bd..732fbfc 100644 --- a/clients/desktop-vue/src-tauri/src/linux_env.rs +++ b/clients/desktop-vue/src-tauri/src/linux_env.rs @@ -79,9 +79,10 @@ pub fn reallocate_webview(window: &tauri::WebviewWindow) { /// Whether the window should carry no titlebar of its own. /// -/// Running as native Wayland, GTK draws one — "Mast", minimise, maximise, -/// close — because GTK3 cannot ask the compositor to. It has no server-side -/// decoration support on Wayland whatsoever; `GTK_CSD=0`, which the first +/// Running as native Wayland, who draws the titlebar is the compositor's +/// call. GTK3 asks for server-side decorations through KDE's protocol, and +/// where that is offered — KWin offers it — the compositor draws its own; +/// everywhere else GTK draws one client-side. `GTK_CSD=0`, which the first /// version of this set, affects X11 only, which is why the bar never showed /// under XWayland and why that variable did nothing here. /// diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ccef7e5..fdf1799 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: clients/desktop-vue: dependencies: '@tauri-apps/api': - specifier: ^2 - version: 2.11.1 + specifier: ^2.12.3 + version: 2.12.3 '@tauri-apps/plugin-autostart': specifier: ^2 version: 2.5.1 @@ -551,8 +551,8 @@ packages: peerDependencies: vue: ^2.7.0 || ^3.0.0 - '@tauri-apps/api@2.11.1': - resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==} + '@tauri-apps/api@2.12.3': + resolution: {integrity: sha512-9s0PKw0p4r3kFq92jhpivYSp+cSht15o0gXpZxuQd0+dV9KCexEVkNaLELVoLdJWyoH4KlfQo5h3W2LIM8siJg==} '@tauri-apps/plugin-autostart@2.5.1': resolution: {integrity: sha512-zS/xx7yzveCcotkA+8TqkI2lysmG2wvQXv2HGAVExITmnFfHAdj1arGsbbfs3o6EktRHf6l34pJxc3YGG2mg7w==} @@ -1928,19 +1928,19 @@ snapshots: '@tanstack/virtual-core': 3.17.7 vue: 3.5.40(typescript@5.9.3) - '@tauri-apps/api@2.11.1': {} + '@tauri-apps/api@2.12.3': {} '@tauri-apps/plugin-autostart@2.5.1': dependencies: - '@tauri-apps/api': 2.11.1 + '@tauri-apps/api': 2.12.3 '@tauri-apps/plugin-dialog@2.7.2': dependencies: - '@tauri-apps/api': 2.11.1 + '@tauri-apps/api': 2.12.3 '@tauri-apps/plugin-notification@2.3.3': dependencies: - '@tauri-apps/api': 2.11.1 + '@tauri-apps/api': 2.12.3 '@testing-library/dom@10.4.1': dependencies: @@ -1976,11 +1976,11 @@ snapshots: vite: '@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3)' vue: 3.5.40(typescript@5.9.3) - '@vitest/browser-preview@4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))(vitest@4.1.10(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3)))': + '@vitest/browser-preview@4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))(vitest@4.1.10)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/browser': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))(vitest@4.1.10(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))) + '@vitest/browser': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))(vitest@4.1.10) vitest: 4.1.10(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3)) transitivePeerDependencies: - bufferutil @@ -1988,7 +1988,7 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))(vitest@4.1.10(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3)))': + '@vitest/browser@4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))(vitest@4.1.10)': dependencies: '@blazediff/core': 1.9.1 '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3)) @@ -2706,8 +2706,8 @@ snapshots: dependencies: '@oxc-project/types': 0.141.0 '@oxlint/plugins': 1.73.0 - '@vitest/browser': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))(vitest@4.1.10(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))) - '@vitest/browser-preview': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))(vitest@4.1.10(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))) + '@vitest/browser': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))(vitest@4.1.10) + '@vitest/browser-preview': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))(vitest@4.1.10) '@vitest/expect': 4.1.10 '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3)) '@vitest/pretty-format': 4.1.10 @@ -2783,7 +2783,7 @@ snapshots: vite: '@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3)' why-is-node-running: 2.3.0 optionalDependencies: - '@vitest/browser-preview': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))(vitest@4.1.10(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))) + '@vitest/browser-preview': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(jiti@2.7.0)(typescript@5.9.3))(vitest@4.1.10) transitivePeerDependencies: - msw From f0176f05131e366504e208209236479ab5cfa559 Mon Sep 17 00:00:00 2001 From: Stackrats Date: Wed, 14 Oct 2026 19:05:29 +0700 Subject: [PATCH 2/7] Mast v0.7.5 --- Cargo.lock | 30 +++++++++---------- Cargo.toml | 2 +- clients/desktop-vue/src-tauri/tauri.conf.json | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55b973e..93063f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2464,7 +2464,7 @@ dependencies = [ [[package]] name = "mast-cli" -version = "0.7.4" +version = "0.7.5" dependencies = [ "clap", "futures", @@ -2481,7 +2481,7 @@ dependencies = [ [[package]] name = "mast-client" -version = "0.7.4" +version = "0.7.5" dependencies = [ "async-trait", "futures", @@ -2491,7 +2491,7 @@ dependencies = [ [[package]] name = "mast-client-ipc" -version = "0.7.4" +version = "0.7.5" dependencies = [ "async-trait", "futures", @@ -2505,7 +2505,7 @@ dependencies = [ [[package]] name = "mast-client-local" -version = "0.7.4" +version = "0.7.5" dependencies = [ "async-trait", "futures", @@ -2520,7 +2520,7 @@ dependencies = [ [[package]] name = "mast-compose" -version = "0.7.4" +version = "0.7.5" dependencies = [ "mast-docker", "mast-yaml-edit", @@ -2536,7 +2536,7 @@ dependencies = [ [[package]] name = "mast-contract" -version = "0.7.4" +version = "0.7.5" dependencies = [ "serde", "serde_json", @@ -2545,7 +2545,7 @@ dependencies = [ [[package]] name = "mast-daemon" -version = "0.7.4" +version = "0.7.5" dependencies = [ "async-trait", "futures", @@ -2565,7 +2565,7 @@ dependencies = [ [[package]] name = "mast-desktop" -version = "0.7.4" +version = "0.7.5" dependencies = [ "futures", "gdkx11", @@ -2595,7 +2595,7 @@ dependencies = [ [[package]] name = "mast-diagnostics" -version = "0.7.4" +version = "0.7.5" dependencies = [ "mast-laravel", "rusqlite", @@ -2605,7 +2605,7 @@ dependencies = [ [[package]] name = "mast-docker" -version = "0.7.4" +version = "0.7.5" dependencies = [ "async-trait", "bollard", @@ -2619,7 +2619,7 @@ dependencies = [ [[package]] name = "mast-engine" -version = "0.7.4" +version = "0.7.5" dependencies = [ "async-trait", "futures", @@ -2647,7 +2647,7 @@ dependencies = [ [[package]] name = "mast-laravel" -version = "0.7.4" +version = "0.7.5" dependencies = [ "mast-docker", "tempfile", @@ -2657,7 +2657,7 @@ dependencies = [ [[package]] name = "mast-project" -version = "0.7.4" +version = "0.7.5" dependencies = [ "dirs 6.0.0", "serde", @@ -2670,7 +2670,7 @@ dependencies = [ [[package]] name = "mast-registry" -version = "0.7.4" +version = "0.7.5" dependencies = [ "reqwest 0.12.28", "serde", @@ -2681,7 +2681,7 @@ dependencies = [ [[package]] name = "mast-yaml-edit" -version = "0.7.4" +version = "0.7.5" dependencies = [ "saphyr", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 4fe8eb0..8a0faa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ ] [workspace.package] -version = "0.7.4" +version = "0.7.5" edition = "2024" [workspace.dependencies] diff --git a/clients/desktop-vue/src-tauri/tauri.conf.json b/clients/desktop-vue/src-tauri/tauri.conf.json index 678cd3a..9d36cb7 100644 --- a/clients/desktop-vue/src-tauri/tauri.conf.json +++ b/clients/desktop-vue/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Mast", - "version": "0.7.4", + "version": "0.7.5", "identifier": "dev.mast.desktop", "build": { "beforeDevCommand": "vp run --filter mast-desktop-ui dev", From 50f75f27dd6fdec4f2be9dcf95ff35d257957c78 Mon Sep 17 00:00:00 2001 From: Stackrats Date: Wed, 14 Oct 2026 19:12:40 +0700 Subject: [PATCH 3/7] Take async-trait 0.1.92 for Rust 1.99's clippy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's stable toolchain moved to Rust 1.99, whose new double_must_use lint fires on the #[must_use] async-trait 0.1.91 puts on every generated method — six errors at RuntimeAdapter's attribute, none of them in code written here, and enough to stop clippy before it reached any crate that depends on mast-docker. async-trait 0.1.92 exists to resolve exactly that lint (dtolnay/async-trait#303). With it, clippy 1.99 runs clean across the whole workspace with -D warnings. --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93063f4..603f9b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,9 +231,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", @@ -848,7 +848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" dependencies = [ "quote", - "syn 3.0.3", + "syn 2.0.119", ] [[package]] From db72b0257de9a4b89e39df4b72d5d932f2a3bc12 Mon Sep 17 00:00:00 2001 From: Stackrats Date: Wed, 14 Oct 2026 19:18:01 +0700 Subject: [PATCH 4/7] Stop Sail commands as their own user, not as root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stopping a project command that runs through Sail — `sail artisan dev`, `sail npm run dev` — reported success and stopped nothing. The stop script finds the command's processes by the MAST_COMMAND_RUN marker in /proc//environ, and it ran as root. Reading another user's environ takes CAP_SYS_PTRACE, which Docker's default capability set leaves out, so as root every one of the command's sail-owned processes was unreadable, the marker was never found, and the script exited 0. Since nothing was flagged as left behind, the next Start, Restart or auto-start launched a second copy beside the first. On a real project that left two `artisan dev` stacks in one container — two Horizon masters, two queue listeners, two schedulers running every task twice — and the second stack's Vite, failing on the port the first still held, deleted public/hot on its way out, so pages fell back to an old build while the command showed as running. The stop now runs as the user the command was started as, which can read and signal all of its own processes. The existing test ran the script as the processes' owner and so could never see this; a new Docker-gated test runs the real stop argv against an Alpine container with the command owned by uid 1000. Against the root stop it fails with the command still running; with this change it stops the command and leaves another run's process alone. --- engine/mast-engine/src/custom_command.rs | 140 ++++++++++++++++++++++- engine/mast-engine/tests/engine_tests.rs | 4 +- 2 files changed, 140 insertions(+), 4 deletions(-) diff --git a/engine/mast-engine/src/custom_command.rs b/engine/mast-engine/src/custom_command.rs index 664dc97..65230a4 100644 --- a/engine/mast-engine/src/custom_command.rs +++ b/engine/mast-engine/src/custom_command.rs @@ -120,6 +120,11 @@ pub(crate) fn prepare( let mut argv = exec_prefix(dir, invocation, &service, &app_user, Some(&marker))?; argv.extend(command); + // Stop as the user the command ran as, never as root. Reading another + // user's /proc//environ takes CAP_SYS_PTRACE, which Docker's default + // capability set leaves out — so root sees none of the command's + // processes, finds no marker, and reports a clean stop over a command that + // is still running. The command's own user can read and signal all of it. let mut stop_argv = match container { // Keep the original identity if the project directory moves or the // Compose configuration is edited while the command is running. @@ -127,10 +132,10 @@ pub(crate) fn prepare( "docker".into(), "exec".into(), "-u".into(), - "root".into(), + app_user.clone(), container.into(), ], - None => exec_prefix(dir, invocation, &service, "root", None)?, + None => exec_prefix(dir, invocation, &service, &app_user, None)?, }; stop_argv.extend(["sh".into(), "-c".into(), stop_script(&marker)]); Ok(Some(ManagedCommand { argv, stop_argv })) @@ -312,7 +317,7 @@ mod tests { "docker", "exec", "-u", - "root", + "developer", "original-container", "sh", "-c" @@ -370,4 +375,133 @@ mod tests { ); unrelated.kill().await.unwrap(); } + + async fn docker(args: &[&str]) -> mast_docker::CommandOutput { + let argv: Vec = std::iter::once("docker") + .chain(args.iter().copied()) + .map(String::from) + .collect(); + mast_docker::run_command( + &argv, + None, + &[], + std::time::Duration::from_secs(120), + 64 * 1024, + ) + .await + .expect("docker ran") + } + + async fn docker_usable() -> bool { + if std::env::var_os("MAST_SKIP_DOCKER_TESTS").is_some() { + return false; + } + mast_docker::run_command( + &[ + "docker".into(), + "info".into(), + "--format".into(), + "{{.OSType}}".into(), + ], + None, + &[], + std::time::Duration::from_secs(10), + 16 * 1024, + ) + .await + .map(|out| out.success() && out.stdout.trim() == "linux") + .unwrap_or(false) + } + + struct RemoveContainer(String); + + impl Drop for RemoveContainer { + fn drop(&mut self) { + let _ = std::process::Command::new("docker") + .args(["rm", "-f", &self.0]) + .output(); + } + } + + // The test above runs the stop script as the user that owns the + // processes, which is the case that always worked. Sail's are owned by + // `sail`, inside a container where root lacks CAP_SYS_PTRACE and cannot + // read their environment — so this drives the real stop argv against a + // real container, with the command owned by a non-root user. + #[cfg(unix)] + #[tokio::test] + async fn stop_reaches_a_non_root_command_inside_a_container() { + if !docker_usable().await { + eprintln!("skipping: no usable docker daemon"); + return; + } + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let name = format!("mast-it-stop-{nonce}"); + let _cleanup = RemoveContainer(name.clone()); + let started = docker(&[ + "run", + "-d", + "--name", + &name, + "alpine:latest", + "sleep", + "600", + ]) + .await; + assert!(started.success(), "{}", started.stderr); + + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join(".env"), "APP_USER=1000\n").unwrap(); + let run = prepare( + dir.path(), + None, + "app", + Some(&name), + &tail(&["npm", "run", "dev"]), + OperationId(1), + ) + .unwrap() + .unwrap(); + let marker = run + .argv + .iter() + .find(|arg| arg.starts_with("MAST_COMMAND_RUN=")) + .unwrap() + .clone(); + // What `sail exec -T -u -e ` leaves running, plus a + // neighbour from another run that must survive. + let other = format!("{marker}-other"); + for (tag, seconds) in [(&marker, "300"), (&other, "301")] { + let exec = docker(&[ + "exec", "-d", "-u", "1000", "-e", tag, &name, "sleep", seconds, + ]) + .await; + assert!(exec.success(), "{}", exec.stderr); + } + + let stopped = mast_docker::run_command( + &run.stop_argv, + None, + &[], + std::time::Duration::from_secs(30), + 64 * 1024, + ) + .await + .unwrap(); + assert!(stopped.success(), "{}", stopped.stderr); + let ps = docker(&["exec", &name, "ps", "-o", "args"]).await; + assert!( + !ps.stdout.contains("sleep 300"), + "the command is still running after a successful stop:\n{}", + ps.stdout + ); + assert!( + ps.stdout.contains("sleep 301"), + "another run's process was stopped:\n{}", + ps.stdout + ); + } } diff --git a/engine/mast-engine/tests/engine_tests.rs b/engine/mast-engine/tests/engine_tests.rs index 358a58d..0720e8b 100644 --- a/engine/mast-engine/tests/engine_tests.rs +++ b/engine/mast-engine/tests/engine_tests.rs @@ -3566,8 +3566,10 @@ async fn failed_sail_cleanup_must_succeed_before_a_later_command_can_run() { std::fs::write(project.join(".env"), "APP_USER=sail\n").unwrap(); std::fs::create_dir_all(project.join("vendor/bin")).unwrap(); let sail = project.join("vendor/bin/sail"); + // Cleanup runs as the same user as the command; it is the one exec that + // carries no `-e MAST_COMMAND_RUN=` marker. std::fs::write(&sail, - "#!/bin/sh\nif [ \"$4\" = root ]; then\n [ -f allow-cleanup ] && exit 0\n echo 'cleanup deliberately blocked' >&2\n exit 1\nfi\necho launched >> launches.txt\necho ready\nexec sleep 60\n", + "#!/bin/sh\nif [ \"$5\" != -e ]; then\n [ -f allow-cleanup ] && exit 0\n echo 'cleanup deliberately blocked' >&2\n exit 1\nfi\necho launched >> launches.txt\necho ready\nexec sleep 60\n", ).unwrap(); std::fs::set_permissions(&sail, std::fs::Permissions::from_mode(0o755)).unwrap(); let engine = test_engine(tmp.path(), Arc::new(FakeConnector(FakeAdapter::new()))); From 0da4b125898981806503c9298f7d08fb0dc9f0be Mon Sep 17 00:00:00 2001 From: Stackrats Date: Wed, 14 Oct 2026 19:20:42 +0700 Subject: [PATCH 5/7] Mast v0.7.6 --- Cargo.lock | 30 +++++++++---------- Cargo.toml | 2 +- clients/desktop-vue/src-tauri/tauri.conf.json | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 603f9b3..2f4d5d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2464,7 +2464,7 @@ dependencies = [ [[package]] name = "mast-cli" -version = "0.7.5" +version = "0.7.6" dependencies = [ "clap", "futures", @@ -2481,7 +2481,7 @@ dependencies = [ [[package]] name = "mast-client" -version = "0.7.5" +version = "0.7.6" dependencies = [ "async-trait", "futures", @@ -2491,7 +2491,7 @@ dependencies = [ [[package]] name = "mast-client-ipc" -version = "0.7.5" +version = "0.7.6" dependencies = [ "async-trait", "futures", @@ -2505,7 +2505,7 @@ dependencies = [ [[package]] name = "mast-client-local" -version = "0.7.5" +version = "0.7.6" dependencies = [ "async-trait", "futures", @@ -2520,7 +2520,7 @@ dependencies = [ [[package]] name = "mast-compose" -version = "0.7.5" +version = "0.7.6" dependencies = [ "mast-docker", "mast-yaml-edit", @@ -2536,7 +2536,7 @@ dependencies = [ [[package]] name = "mast-contract" -version = "0.7.5" +version = "0.7.6" dependencies = [ "serde", "serde_json", @@ -2545,7 +2545,7 @@ dependencies = [ [[package]] name = "mast-daemon" -version = "0.7.5" +version = "0.7.6" dependencies = [ "async-trait", "futures", @@ -2565,7 +2565,7 @@ dependencies = [ [[package]] name = "mast-desktop" -version = "0.7.5" +version = "0.7.6" dependencies = [ "futures", "gdkx11", @@ -2595,7 +2595,7 @@ dependencies = [ [[package]] name = "mast-diagnostics" -version = "0.7.5" +version = "0.7.6" dependencies = [ "mast-laravel", "rusqlite", @@ -2605,7 +2605,7 @@ dependencies = [ [[package]] name = "mast-docker" -version = "0.7.5" +version = "0.7.6" dependencies = [ "async-trait", "bollard", @@ -2619,7 +2619,7 @@ dependencies = [ [[package]] name = "mast-engine" -version = "0.7.5" +version = "0.7.6" dependencies = [ "async-trait", "futures", @@ -2647,7 +2647,7 @@ dependencies = [ [[package]] name = "mast-laravel" -version = "0.7.5" +version = "0.7.6" dependencies = [ "mast-docker", "tempfile", @@ -2657,7 +2657,7 @@ dependencies = [ [[package]] name = "mast-project" -version = "0.7.5" +version = "0.7.6" dependencies = [ "dirs 6.0.0", "serde", @@ -2670,7 +2670,7 @@ dependencies = [ [[package]] name = "mast-registry" -version = "0.7.5" +version = "0.7.6" dependencies = [ "reqwest 0.12.28", "serde", @@ -2681,7 +2681,7 @@ dependencies = [ [[package]] name = "mast-yaml-edit" -version = "0.7.5" +version = "0.7.6" dependencies = [ "saphyr", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 8a0faa9..b34092b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ ] [workspace.package] -version = "0.7.5" +version = "0.7.6" edition = "2024" [workspace.dependencies] diff --git a/clients/desktop-vue/src-tauri/tauri.conf.json b/clients/desktop-vue/src-tauri/tauri.conf.json index 9d36cb7..be173be 100644 --- a/clients/desktop-vue/src-tauri/tauri.conf.json +++ b/clients/desktop-vue/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Mast", - "version": "0.7.5", + "version": "0.7.6", "identifier": "dev.mast.desktop", "build": { "beforeDevCommand": "vp run --filter mast-desktop-ui dev", From 2f11178824fd483ff2c20c4e60bf7a63057ee651 Mon Sep 17 00:00:00 2001 From: Stackrats Date: Wed, 14 Oct 2026 19:44:38 +0700 Subject: [PATCH 6/7] Find a Sail command's PHP processes when stopping it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stopping as the command's own user still missed the processes that matter most. Sail's PHP binary carries a file capability (cap_net_bind_service, so `artisan serve` can bind port 80), and running a binary with file capabilities makes the process non-dumpable: its /proc//environ passes to root, readable by no one without CAP_SYS_PTRACE — not even `sail`. So the environment marker only ever found the sh and node processes around a command. For `sail artisan dev`, stopping those would have orphaned Horizon, the scheduler, the queue listener, Reverb and Pail; for a command that is PHP from the start, such as `sail artisan horizon`, it found nothing at all. Each run now starts through a small sh wrapper that records its PID and start time in /tmp/mast-runs/ inside the container, then execs the command, so the PID is the command's own. The stop takes the recorded process (if that PID still has the recorded start time, so a reused number is never signalled), anything carrying the run's marker, and every descendant of either, read from /proc//stat, which anyone can read. A completed stop removes the record. The Docker test now starts its runs through their real argv, wrapper included, with a setgid copy of busybox standing in for Sail's PHP — a non-dumpable process whose environment its own user cannot read — and a child under it. Matching by environment alone, it fails with both still running; with the record and the tree it stops them and leaves another run alone. Dry-run in a real Sail container against a leftover `artisan dev` stack, it finds the 22 marked processes and the 7 PHP processes beneath them that the marker cannot see, and nothing else. --- engine/mast-engine/src/custom_command.rs | 247 ++++++++++++++++++----- 1 file changed, 192 insertions(+), 55 deletions(-) diff --git a/engine/mast-engine/src/custom_command.rs b/engine/mast-engine/src/custom_command.rs index 65230a4..f327bfa 100644 --- a/engine/mast-engine/src/custom_command.rs +++ b/engine/mast-engine/src/custom_command.rs @@ -1,6 +1,7 @@ //! Ownership for executable Sail commands. Killing the host Docker client -//! does not stop its container process, so mark the process and descendants -//! with a unique environment value and clean up only that marked family. +//! does not stop its container process, so each run records its command's +//! PID inside the container and marks the process and descendants with a +//! unique environment value, and cleanup stops only that family. use std::path::{Path, PathBuf}; use std::sync::atomic::Ordering; @@ -102,7 +103,8 @@ pub(crate) fn prepare( .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_nanos(); - let marker = format!("MAST_COMMAND_RUN={}-{}-{nonce}", std::process::id(), op.0); + let run = format!("{}-{}-{nonce}", std::process::id(), op.0); + let marker = format!("MAST_COMMAND_RUN={run}"); let env = sail_environment(dir, std::env::var("APP_ENV").ok().as_deref()); let app_user = env .get("APP_USER") @@ -119,6 +121,13 @@ pub(crate) fn prepare( let container = container.filter(|_| service == app_service); let mut argv = exec_prefix(dir, invocation, &service, &app_user, Some(&marker))?; + argv.extend([ + "sh".into(), + "-c".into(), + RUN_WRAPPER.into(), + "mast-run".into(), + format!("{RUN_DIR}/{run}"), + ]); argv.extend(command); // Stop as the user the command ran as, never as root. Reading another // user's /proc//environ takes CAP_SYS_PTRACE, which Docker's default @@ -137,10 +146,26 @@ pub(crate) fn prepare( ], None => exec_prefix(dir, invocation, &service, &app_user, None)?, }; - stop_argv.extend(["sh".into(), "-c".into(), stop_script(&marker)]); + stop_argv.extend(["sh".into(), "-c".into(), stop_script(&run)]); Ok(Some(ManagedCommand { argv, stop_argv })) } +/// Where each run records its command's PID, inside the container. In /tmp, +/// so the record goes with the container — as does every process it names. +const RUN_DIR: &str = "/tmp/mast-runs"; + +/// Record the command's own PID and start time, then become the command. +/// +/// The environment marker alone cannot find every process. Sail's PHP binary +/// carries a file capability (`cap_net_bind_service`, so `artisan serve` can +/// bind port 80), which makes every PHP process non-dumpable, and the environ +/// of a non-dumpable process is readable by no one without CAP_SYS_PTRACE — +/// not even its own user. `exec` keeps the PID, so the record names the +/// command itself; the start time tells it apart from an unrelated process +/// that later reuses the number. The wrapper's own comm is `sh`, so field 22 +/// of its stat is the start time without parsing around a spaced name. +const RUN_WRAPPER: &str = r#"mkdir -p "${1%/*}" 2>/dev/null; echo "$$ $(cut -d' ' -f22 /proc/$$/stat)" > "$1" 2>/dev/null; shift; exec "$@""#; + fn sail_environment( dir: &Path, app_env: Option<&str>, @@ -189,28 +214,56 @@ fn exec_prefix( Ok(argv) } -fn stop_script(marker: &str) -> String { - // This value is generated above, never supplied by a user. Matching the - // entire environment entry excludes similar command lines and other Mast - // runs. Re-scan during shutdown to catch descendants spawned by a parent - // while its first termination signal was being delivered. - assert!( - marker - .chars() - .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '=')) - ); +fn stop_script(run: &str) -> String { + // The run id is generated above, never supplied by a user. A run's + // processes are the command the wrapper recorded (if that PID still has + // the recorded start time), anything carrying the run's exact marker, and + // every descendant of either — found through /proc//stat, which + // anyone can read, because the PHP processes under a command can be read + // through nothing else. Matching the entire environment entry excludes + // similar command lines and other Mast runs. Re-scan during shutdown to + // catch descendants spawned while the first signals were being delivered. + assert!(run.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')); format!( - r#"marker='{marker}'; stopped=''; remaining=10 + r#"marker='MAST_COMMAND_RUN={run}'; record='{RUN_DIR}/{run}'; tracked=''; stopped=''; remaining=10 +if {{ read -r pid start < "$record" && read -r stat < "/proc/$pid/stat"; }} 2>/dev/null; then + rest=${{stat##*) }}; set -- $rest + [ "${{20}}" = "$start" ] && tracked=$pid +fi while :; do - alive='' - for path in /proc/[0-9]*/environ; do - pid=${{path#/proc/}}; pid=${{pid%/environ}} + table='' + for path in /proc/[0-9]*/stat; do + pid=${{path#/proc/}}; pid=${{pid%/stat}} [ "$pid" = "$$" ] && continue - tr '\0' '\n' 2>/dev/null < "$path" | grep -Fx "$marker" >/dev/null || continue + {{ read -r stat < "$path"; }} 2>/dev/null || continue + rest=${{stat##*) }}; set -- $rest + case "$1" in Z|X) continue;; esac + table="$table$pid $2 +" + case " $tracked " in *" $pid "*) continue;; esac + tr '\0' '\n' 2>/dev/null < "/proc/$pid/environ" | grep -Fx "$marker" >/dev/null && tracked="$tracked $pid" + done + grown=1 + while [ -n "$grown" ]; do + grown='' + while read -r pid ppid; do + [ -n "$pid" ] || continue + case " $tracked " in *" $pid "*) continue;; esac + case " $tracked " in *" $ppid "*) tracked="$tracked $pid"; grown=1;; esac + done </dev/null; stopped="$stopped $pid";; esac done - [ -z "$alive" ] && exit 0 + tracked=$alive + if [ -z "$alive" ]; then rm -f "$record"; exit 0; fi [ "$remaining" -gt 0 ] || break remaining=$((remaining - 1)); sleep 1 done @@ -309,8 +362,22 @@ mod tests { .unwrap() .unwrap(); assert_eq!(&run.argv[1..6], ["exec", "-T", "-u", "developer", "-e"]); - assert!(run.argv[6].starts_with("MAST_COMMAND_RUN=")); - assert_eq!(&run.argv[7..], ["app", "npm", "run", "dev"]); + let run_id = run.argv[6].strip_prefix("MAST_COMMAND_RUN=").unwrap(); + let record = format!("/tmp/mast-runs/{run_id}"); + assert_eq!( + &run.argv[7..], + [ + "app", + "sh", + "-c", + RUN_WRAPPER, + "mast-run", + &record, + "npm", + "run", + "dev" + ] + ); assert_eq!( &run.stop_argv[..7], [ @@ -324,6 +391,7 @@ mod tests { ] ); assert!(run.stop_argv[7].contains(&run.argv[6])); + assert!(run.stop_argv[7].contains(&record)); let later = prepare( dir.path(), None, @@ -357,7 +425,7 @@ mod tests { .kill_on_drop(true) .spawn() .unwrap(); - let script = stop_script(&format!("MAST_COMMAND_RUN={token}")); + let script = stop_script(&token); let output = tokio::process::Command::new("sh") .args(["-c", &script]) .output() @@ -423,14 +491,34 @@ mod tests { } } - // The test above runs the stop script as the user that owns the - // processes, which is the case that always worked. Sail's are owned by - // `sail`, inside a container where root lacks CAP_SYS_PTRACE and cannot - // read their environment — so this drives the real stop argv against a - // real container, with the command owned by a non-root user. + /// Start a prepared run in a plain container the way `sail exec` would: + /// the run's user, marker and recording wrapper, with `command` standing + /// in for the Sail executable. + async fn start_in(container: &str, run: &ManagedCommand, command: &[&str]) { + let service = run.argv.iter().position(|arg| arg == "app").unwrap(); + let mut argv: Vec<&str> = vec!["exec", "-d"]; + argv.extend(run.argv[3..service].iter().map(String::as_str)); + argv.push(container); + argv.extend( + run.argv[service + 1..service + 6] + .iter() + .map(String::as_str), + ); + argv.extend(command); + let exec = docker(&argv).await; + assert!(exec.success(), "{}", exec.stderr); + } + + // Sail's PHP processes run as `sail` and are non-dumpable — the PHP binary + // carries a file capability — so their environment is readable by neither + // root nor `sail`, and the marker cannot find them. A setgid copy of + // busybox stands in for that binary: running it makes a process + // non-dumpable the same way. (It must keep the name `busybox`, which is + // how busybox knows to take the applet from its first argument.) Both runs start through their real argv, + // wrapper included, and one is stopped through its real stop argv. #[cfg(unix)] #[tokio::test] - async fn stop_reaches_a_non_root_command_inside_a_container() { + async fn stop_reaches_command_processes_no_one_can_read() { if !docker_usable().await { eprintln!("skipping: no usable docker daemon"); return; @@ -452,38 +540,85 @@ mod tests { ]) .await; assert!(started.success(), "{}", started.stderr); + let setgid = docker(&[ + "exec", + &name, + "sh", + "-c", + "mkdir /tmp/setgid && cp /bin/busybox /tmp/setgid/busybox \\ + && chgrp daemon /tmp/setgid/busybox && chmod 2755 /tmp/setgid/busybox", + ]) + .await; + assert!(setgid.success(), "{}", setgid.stderr); let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join(".env"), "APP_USER=1000\n").unwrap(); - let run = prepare( - dir.path(), - None, - "app", - Some(&name), - &tail(&["npm", "run", "dev"]), - OperationId(1), - ) - .unwrap() - .unwrap(); - let marker = run - .argv - .iter() - .find(|arg| arg.starts_with("MAST_COMMAND_RUN=")) + let prepared = |op| { + prepare( + dir.path(), + None, + "app", + Some(&name), + &tail(&["npm", "run", "dev"]), + OperationId(op), + ) .unwrap() - .clone(); - // What `sail exec -T -u -e ` leaves running, plus a - // neighbour from another run that must survive. - let other = format!("{marker}-other"); - for (tag, seconds) in [(&marker, "300"), (&other, "301")] { - let exec = docker(&[ - "exec", "-d", "-u", "1000", "-e", tag, &name, "sleep", seconds, - ]) - .await; - assert!(exec.success(), "{}", exec.stderr); + .unwrap() + }; + let ours = prepared(1); + let theirs = prepared(2); + // A command with a child, as `artisan dev` has, beside another run. + start_in( + &name, + &ours, + &[ + "/tmp/setgid/busybox", + "sh", + "-c", + "/tmp/setgid/busybox sleep 300 & wait", + ], + ) + .await; + start_in(&name, &theirs, &["/tmp/setgid/busybox", "sleep", "301"]).await; + let record = + ours.argv[ours.argv.iter().position(|arg| arg == "mast-run").unwrap() + 1].clone(); + + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10); + loop { + let ps = docker(&["exec", &name, "ps", "-o", "args"]).await; + let recorded = docker(&["exec", &name, "test", "-s", &record]).await; + if ps.stdout.contains("busybox sleep 300") + && ps.stdout.contains("sleep 301") + && recorded.success() + { + break; + } + assert!( + std::time::Instant::now() < deadline, + "runs did not start:\n{}", + ps.stdout + ); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; } + // The case this guards: the command's own user cannot read its + // environment, so only the record and the process tree can find it. + let unreadable = docker(&[ + "exec", + "-u", + "1000", + &name, + "sh", + "-c", + &format!("read -r pid start < {record} && ! cat /proc/$pid/environ >/dev/null 2>&1"), + ]) + .await; + assert!( + unreadable.success(), + "the command's environment should be unreadable" + ); let stopped = mast_docker::run_command( - &run.stop_argv, + &ours.stop_argv, None, &[], std::time::Duration::from_secs(30), @@ -495,7 +630,7 @@ mod tests { let ps = docker(&["exec", &name, "ps", "-o", "args"]).await; assert!( !ps.stdout.contains("sleep 300"), - "the command is still running after a successful stop:\n{}", + "the command or its child is still running after a successful stop:\n{}", ps.stdout ); assert!( @@ -503,5 +638,7 @@ mod tests { "another run's process was stopped:\n{}", ps.stdout ); + let kept = docker(&["exec", &name, "test", "-e", &record]).await; + assert!(!kept.success(), "a completed stop removes the run's record"); } } From 976ac878be064f2c6bcd27b5b0e0e040c8dedef9 Mon Sep 17 00:00:00 2001 From: Stackrats Date: Wed, 14 Oct 2026 19:51:39 +0700 Subject: [PATCH 7/7] Name the command in its run record MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A record now reads `pid start name`, so one left behind by an earlier Mast session — a run this process never started and has no operation for — can still say which command it was, not just which PID. The name is reduced to a single field (anything outside [A-Za-z0-9._-] becomes `_`), and the stop reads only the first two fields, so anything added after them later cannot shift what it compares. --- engine/mast-engine/src/custom_command.rs | 54 +++++++++++++++++++++--- engine/mast-engine/src/project_ops.rs | 2 +- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/engine/mast-engine/src/custom_command.rs b/engine/mast-engine/src/custom_command.rs index f327bfa..8c9f9bb 100644 --- a/engine/mast-engine/src/custom_command.rs +++ b/engine/mast-engine/src/custom_command.rs @@ -91,6 +91,7 @@ pub(crate) fn prepare( invocation: Option<&ComposeInvocation>, app_service: &str, container: Option<&str>, + name: &str, tail: &[String], op: OperationId, ) -> Result, ErrorInfo> { @@ -127,6 +128,7 @@ pub(crate) fn prepare( RUN_WRAPPER.into(), "mast-run".into(), format!("{RUN_DIR}/{run}"), + record_name(name), ]); argv.extend(command); // Stop as the user the command ran as, never as root. Reading another @@ -154,7 +156,7 @@ pub(crate) fn prepare( /// so the record goes with the container — as does every process it names. const RUN_DIR: &str = "/tmp/mast-runs"; -/// Record the command's own PID and start time, then become the command. +/// Record the command's own PID, start time and name, then become it. /// /// The environment marker alone cannot find every process. Sail's PHP binary /// carries a file capability (`cap_net_bind_service`, so `artisan serve` can @@ -162,9 +164,32 @@ const RUN_DIR: &str = "/tmp/mast-runs"; /// of a non-dumpable process is readable by no one without CAP_SYS_PTRACE — /// not even its own user. `exec` keeps the PID, so the record names the /// command itself; the start time tells it apart from an unrelated process -/// that later reuses the number. The wrapper's own comm is `sh`, so field 22 -/// of its stat is the start time without parsing around a spaced name. -const RUN_WRAPPER: &str = r#"mkdir -p "${1%/*}" 2>/dev/null; echo "$$ $(cut -d' ' -f22 /proc/$$/stat)" > "$1" 2>/dev/null; shift; exec "$@""#; +/// that later reuses the number; the name lets a record outlive the Mast +/// session that wrote it and still say what it was. The wrapper's own comm is +/// `sh`, so field 22 of its stat is the start time without parsing around a +/// spaced name. +const RUN_WRAPPER: &str = r#"mkdir -p "${1%/*}" 2>/dev/null; echo "$$ $(cut -d' ' -f22 /proc/$$/stat) $2" > "$1" 2>/dev/null; shift 2; exec "$@""#; + +/// A command name as one field of the record: anything outside a plain +/// identifier becomes `_`, so the record stays three space-separated words. +fn record_name(name: &str) -> String { + let clean: String = name + .chars() + .take(64) + .map(|c| { + if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') { + c + } else { + '_' + } + }) + .collect(); + if clean.is_empty() { + "command".into() + } else { + clean + } +} fn sail_environment( dir: &Path, @@ -226,7 +251,7 @@ fn stop_script(run: &str) -> String { assert!(run.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')); format!( r#"marker='MAST_COMMAND_RUN={run}'; record='{RUN_DIR}/{run}'; tracked=''; stopped=''; remaining=10 -if {{ read -r pid start < "$record" && read -r stat < "/proc/$pid/stat"; }} 2>/dev/null; then +if {{ read -r pid start _ < "$record" && read -r stat < "/proc/$pid/stat"; }} 2>/dev/null; then rest=${{stat##*) }}; set -- $rest [ "${{20}}" = "$start" ] && tracked=$pid fi @@ -300,6 +325,15 @@ mod tests { assert_eq!(sail_environment(dir.path(), None)["APP_USER"], "default"); } + #[test] + fn record_names_stay_one_field() { + assert_eq!(record_name("dev"), "dev"); + assert_eq!(record_name("queue:work high"), "queue_work_high"); + assert_eq!(record_name("vite.dev-2"), "vite.dev-2"); + assert_eq!(record_name(""), "command"); + assert_eq!(record_name(&"x".repeat(100)).len(), 64); + } + #[test] fn only_known_sail_executables_are_wrapped() { assert_eq!( @@ -356,6 +390,7 @@ mod tests { None, "app", Some("original-container"), + "dev server", &tail(&["npm", "run", "dev"]), OperationId(7), ) @@ -373,6 +408,7 @@ mod tests { RUN_WRAPPER, "mast-run", &record, + "dev_server", "npm", "run", "dev" @@ -397,6 +433,7 @@ mod tests { None, "app", Some("original-container"), + "dev server", &tail(&["npm", "run", "dev"]), OperationId(7), ) @@ -500,7 +537,7 @@ mod tests { argv.extend(run.argv[3..service].iter().map(String::as_str)); argv.push(container); argv.extend( - run.argv[service + 1..service + 6] + run.argv[service + 1..service + 7] .iter() .map(String::as_str), ); @@ -559,6 +596,7 @@ mod tests { None, "app", Some(&name), + "dev", &tail(&["npm", "run", "dev"]), OperationId(op), ) @@ -600,6 +638,10 @@ mod tests { ); tokio::time::sleep(std::time::Duration::from_millis(100)).await; } + let fields = docker(&["exec", &name, "cat", &record]).await; + let fields: Vec<&str> = fields.stdout.split_whitespace().collect(); + assert_eq!(fields.len(), 3, "record is `pid start name`: {fields:?}"); + assert_eq!(fields[2], "dev"); // The case this guards: the command's own user cannot read its // environment, so only the record and the process tree can find it. let unreadable = docker(&[ diff --git a/engine/mast-engine/src/project_ops.rs b/engine/mast-engine/src/project_ops.rs index 32a8595..6c12e59 100644 --- a/engine/mast-engine/src/project_ops.rs +++ b/engine/mast-engine/src/project_ops.rs @@ -873,7 +873,7 @@ impl Engine { let app_service = app_service_of(&path); let container = self.process_container_id(project, &app_service); if let Some(managed) = crate::custom_command::prepare( - &path, invocation.as_ref(), &app_service, container.as_deref(), &argv[1..], op, + &path, invocation.as_ref(), &app_service, container.as_deref(), name, &argv[1..], op, )? { argv = managed.argv; stop_argv = Some(managed.stop_argv);