A focused Docker Compose management web application.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: switch to using pty for spawned processes

Brooke 03c28bc4 1ed10e16

+222 -64
+166 -13
Cargo.lock
··· 283 283 284 284 [[package]] 285 285 name = "bitflags" 286 + version = "1.3.2" 287 + source = "registry+https://github.com/rust-lang/crates.io-index" 288 + checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 289 + 290 + [[package]] 291 + name = "bitflags" 286 292 version = "2.11.0" 287 293 source = "registry+https://github.com/rust-lang/crates.io-index" 288 294 checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" ··· 831 837 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 832 838 833 839 [[package]] 840 + name = "downcast-rs" 841 + version = "1.2.1" 842 + source = "registry+https://github.com/rust-lang/crates.io-index" 843 + checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" 844 + 845 + [[package]] 834 846 name = "dunce" 835 847 version = "1.0.5" 836 848 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 955 967 version = "2.3.0" 956 968 source = "registry+https://github.com/rust-lang/crates.io-index" 957 969 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 970 + 971 + [[package]] 972 + name = "filedescriptor" 973 + version = "0.8.3" 974 + source = "registry+https://github.com/rust-lang/crates.io-index" 975 + checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" 976 + dependencies = [ 977 + "libc", 978 + "thiserror 1.0.69", 979 + "winapi", 980 + ] 958 981 959 982 [[package]] 960 983 name = "find-msvc-tools" ··· 1658 1681 ] 1659 1682 1660 1683 [[package]] 1684 + name = "ioctl-rs" 1685 + version = "0.1.6" 1686 + source = "registry+https://github.com/rust-lang/crates.io-index" 1687 + checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d" 1688 + dependencies = [ 1689 + "libc", 1690 + ] 1691 + 1692 + [[package]] 1661 1693 name = "ipnet" 1662 1694 version = "2.11.0" 1663 1695 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1776 1808 source = "registry+https://github.com/rust-lang/crates.io-index" 1777 1809 checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" 1778 1810 dependencies = [ 1779 - "bitflags", 1811 + "bitflags 2.11.0", 1780 1812 "libc", 1781 1813 "redox_syscall 0.7.1", 1782 1814 ] ··· 1854 1886 "log", 1855 1887 "luminary-macros", 1856 1888 "password-auth", 1889 + "portable-pty", 1857 1890 "rand_chacha 0.3.1", 1858 1891 "salvo", 1859 1892 "serde", ··· 1899 1932 version = "2.8.0" 1900 1933 source = "registry+https://github.com/rust-lang/crates.io-index" 1901 1934 checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" 1935 + 1936 + [[package]] 1937 + name = "memoffset" 1938 + version = "0.6.5" 1939 + source = "registry+https://github.com/rust-lang/crates.io-index" 1940 + checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 1941 + dependencies = [ 1942 + "autocfg", 1943 + ] 1902 1944 1903 1945 [[package]] 1904 1946 name = "mime" ··· 1975 2017 source = "registry+https://github.com/rust-lang/crates.io-index" 1976 2018 checksum = "22f9786d56d972959e1408b6a93be6af13b9c1392036c5c1fafa08a1b0c6ee87" 1977 2019 dependencies = [ 1978 - "bitflags", 2020 + "bitflags 2.11.0", 1979 2021 "byteorder", 1980 2022 "derive_builder", 1981 2023 "getset", ··· 2000 2042 2001 2043 [[package]] 2002 2044 name = "nix" 2045 + version = "0.25.1" 2046 + source = "registry+https://github.com/rust-lang/crates.io-index" 2047 + checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" 2048 + dependencies = [ 2049 + "autocfg", 2050 + "bitflags 1.3.2", 2051 + "cfg-if", 2052 + "libc", 2053 + "memoffset", 2054 + "pin-utils", 2055 + ] 2056 + 2057 + [[package]] 2058 + name = "nix" 2003 2059 version = "0.31.1" 2004 2060 source = "registry+https://github.com/rust-lang/crates.io-index" 2005 2061 checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" 2006 2062 dependencies = [ 2007 - "bitflags", 2063 + "bitflags 2.11.0", 2008 2064 "cfg-if", 2009 2065 "cfg_aliases", 2010 2066 "libc", ··· 2279 2335 ] 2280 2336 2281 2337 [[package]] 2338 + name = "portable-pty" 2339 + version = "0.8.1" 2340 + source = "registry+https://github.com/rust-lang/crates.io-index" 2341 + checksum = "806ee80c2a03dbe1a9fb9534f8d19e4c0546b790cde8fd1fea9d6390644cb0be" 2342 + dependencies = [ 2343 + "anyhow", 2344 + "bitflags 1.3.2", 2345 + "downcast-rs", 2346 + "filedescriptor", 2347 + "lazy_static", 2348 + "libc", 2349 + "log", 2350 + "nix 0.25.1", 2351 + "serial", 2352 + "shared_library", 2353 + "shell-words", 2354 + "winapi", 2355 + "winreg", 2356 + ] 2357 + 2358 + [[package]] 2282 2359 name = "potential_utf" 2283 2360 version = "0.1.4" 2284 2361 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2542 2619 source = "registry+https://github.com/rust-lang/crates.io-index" 2543 2620 checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 2544 2621 dependencies = [ 2545 - "bitflags", 2622 + "bitflags 2.11.0", 2546 2623 ] 2547 2624 2548 2625 [[package]] ··· 2551 2628 source = "registry+https://github.com/rust-lang/crates.io-index" 2552 2629 checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" 2553 2630 dependencies = [ 2554 - "bitflags", 2631 + "bitflags 2.11.0", 2555 2632 ] 2556 2633 2557 2634 [[package]] ··· 2767 2844 source = "registry+https://github.com/rust-lang/crates.io-index" 2768 2845 checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" 2769 2846 dependencies = [ 2770 - "bitflags", 2847 + "bitflags 2.11.0", 2771 2848 "errno", 2772 2849 "libc", 2773 2850 "linux-raw-sys", ··· 3050 3127 "mime-infer", 3051 3128 "multer", 3052 3129 "multimap", 3053 - "nix", 3130 + "nix 0.31.1", 3054 3131 "parking_lot", 3055 3132 "percent-encoding", 3056 3133 "pin-project", ··· 3157 3234 source = "registry+https://github.com/rust-lang/crates.io-index" 3158 3235 checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" 3159 3236 dependencies = [ 3160 - "bitflags", 3237 + "bitflags 2.11.0", 3161 3238 "core-foundation", 3162 3239 "core-foundation-sys", 3163 3240 "libc", ··· 3304 3381 "ryu", 3305 3382 "serde", 3306 3383 "unsafe-libyaml", 3384 + ] 3385 + 3386 + [[package]] 3387 + name = "serial" 3388 + version = "0.4.0" 3389 + source = "registry+https://github.com/rust-lang/crates.io-index" 3390 + checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86" 3391 + dependencies = [ 3392 + "serial-core", 3393 + "serial-unix", 3394 + "serial-windows", 3395 + ] 3396 + 3397 + [[package]] 3398 + name = "serial-core" 3399 + version = "0.4.0" 3400 + source = "registry+https://github.com/rust-lang/crates.io-index" 3401 + checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581" 3402 + dependencies = [ 3403 + "libc", 3404 + ] 3405 + 3406 + [[package]] 3407 + name = "serial-unix" 3408 + version = "0.4.0" 3409 + source = "registry+https://github.com/rust-lang/crates.io-index" 3410 + checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7" 3411 + dependencies = [ 3412 + "ioctl-rs", 3413 + "libc", 3414 + "serial-core", 3415 + "termios", 3416 + ] 3417 + 3418 + [[package]] 3419 + name = "serial-windows" 3420 + version = "0.4.0" 3421 + source = "registry+https://github.com/rust-lang/crates.io-index" 3422 + checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162" 3423 + dependencies = [ 3424 + "libc", 3425 + "serial-core", 3307 3426 ] 3308 3427 3309 3428 [[package]] ··· 3338 3457 ] 3339 3458 3340 3459 [[package]] 3460 + name = "shared_library" 3461 + version = "0.1.9" 3462 + source = "registry+https://github.com/rust-lang/crates.io-index" 3463 + checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" 3464 + dependencies = [ 3465 + "lazy_static", 3466 + "libc", 3467 + ] 3468 + 3469 + [[package]] 3470 + name = "shell-words" 3471 + version = "1.1.1" 3472 + source = "registry+https://github.com/rust-lang/crates.io-index" 3473 + checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" 3474 + 3475 + [[package]] 3341 3476 name = "shlex" 3342 3477 version = "1.3.0" 3343 3478 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3513 3648 dependencies = [ 3514 3649 "atoi", 3515 3650 "base64", 3516 - "bitflags", 3651 + "bitflags 2.11.0", 3517 3652 "byteorder", 3518 3653 "bytes", 3519 3654 "crc", ··· 3556 3691 dependencies = [ 3557 3692 "atoi", 3558 3693 "base64", 3559 - "bitflags", 3694 + "bitflags 2.11.0", 3560 3695 "byteorder", 3561 3696 "crc", 3562 3697 "dotenvy", ··· 3714 3849 ] 3715 3850 3716 3851 [[package]] 3852 + name = "termios" 3853 + version = "0.2.2" 3854 + source = "registry+https://github.com/rust-lang/crates.io-index" 3855 + checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" 3856 + dependencies = [ 3857 + "libc", 3858 + ] 3859 + 3860 + [[package]] 3717 3861 name = "thiserror" 3718 3862 version = "1.0.69" 3719 3863 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3944 4088 source = "registry+https://github.com/rust-lang/crates.io-index" 3945 4089 checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 3946 4090 dependencies = [ 3947 - "bitflags", 4091 + "bitflags 2.11.0", 3948 4092 "bytes", 3949 4093 "futures-util", 3950 4094 "http", ··· 4347 4491 source = "registry+https://github.com/rust-lang/crates.io-index" 4348 4492 checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" 4349 4493 dependencies = [ 4350 - "bitflags", 4494 + "bitflags 2.11.0", 4351 4495 "hashbrown 0.15.5", 4352 4496 "indexmap", 4353 4497 "semver", ··· 4780 4924 ] 4781 4925 4782 4926 [[package]] 4927 + name = "winreg" 4928 + version = "0.10.1" 4929 + source = "registry+https://github.com/rust-lang/crates.io-index" 4930 + checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 4931 + dependencies = [ 4932 + "winapi", 4933 + ] 4934 + 4935 + [[package]] 4783 4936 name = "wit-bindgen" 4784 4937 version = "0.51.0" 4785 4938 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4837 4990 checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" 4838 4991 dependencies = [ 4839 4992 "anyhow", 4840 - "bitflags", 4993 + "bitflags 2.11.0", 4841 4994 "indexmap", 4842 4995 "log", 4843 4996 "serde",
+1
packages/node/Cargo.toml
··· 11 11 password-auth = "1.0.0" 12 12 async-stream = "0.3.6" 13 13 rand_chacha = "0.3.1" 14 + portable-pty = "0.8" 14 15 futures-util = "0.3" 15 16 json-patch = "4.1.0" 16 17 serde-saphyr = "0.0"
+1 -1
packages/node/src/core/action.rs
··· 62 62 args.push(service); 63 63 } 64 64 65 - let mut stream = self.cli(&project, args)?; 65 + let mut stream = self.run_pty(&project, args)?; 66 66 let sender = self.create_log_sender(project.clone()).await; 67 67 68 68 while let Some(bytes) = stream.next().await {
+51 -47
packages/node/src/core/engine.rs
··· 1 1 //! The core engine of the Luminary application, containing shared state and configuration. 2 2 3 - use std::{collections::HashMap, path::Path, process::Stdio, sync::Arc}; 3 + use std::{collections::HashMap, io::Read, path::Path, sync::Arc}; 4 4 5 5 use async_stream::stream; 6 6 use bollard::Docker; 7 7 use bytes::Bytes; 8 8 use eyre::{Context, Result, bail}; 9 9 use futures_util::{StreamExt, stream::BoxStream}; 10 + use log::error; 10 11 use luminary_macros::wrap_err; 11 - use tokio::{ 12 - io::AsyncReadExt, 13 - process::Command, 14 - sync::{Mutex, RwLock, broadcast}, 15 - }; 12 + use portable_pty::{CommandBuilder, PtySize, native_pty_system}; 13 + use tokio::sync::{Mutex, RwLock, broadcast, mpsc}; 16 14 17 15 use crate::{ 18 16 configuration::LuminaryConfiguration, 19 17 core::{LuminaryProjectList, ProjectLogChannel}, 18 + }; 19 + 20 + const PTY_SIZE: PtySize = PtySize { 21 + rows: 40, 22 + cols: 80, 23 + pixel_width: 18, 24 + pixel_height: 18, 20 25 }; 21 26 22 27 /// The core engine of the Luminary application, containing shared state and configuration. ··· 66 71 } 67 72 } 68 73 69 - /// Spawns a `docker compose` process and returns a stream of raw bytes merging both stdout and stderr 70 - pub(super) fn cli<'a>( 74 + /// Spawns a `docker compose` process and returns a stream of raw bytes from its output. 75 + pub(super) fn run_pty<'a>( 71 76 &self, 72 77 name: &'a str, 73 78 args: impl IntoIterator<Item = &'a str>, ··· 78 83 bail!("Project '{}' does not exist", name); 79 84 } 80 85 81 - let mut child = Command::new("docker") 82 - .current_dir(path) 83 - .arg("compose") 84 - .args(["--ansi", "always"]) 85 - .args(args) 86 - .stdout(Stdio::piped()) 87 - .stderr(Stdio::piped()) 88 - .spawn() 89 - .wrap_err("Failed to spawn docker compose process")?; 86 + let pty = native_pty_system() 87 + .openpty(PTY_SIZE) 88 + .map_err(|e| eyre::eyre!("Failed to open PTY: {e}"))?; 90 89 91 - let mut stdout = child.stdout.take().expect("stdout was piped"); 92 - let mut stderr = child.stderr.take().expect("stderr was piped"); 90 + let mut command = CommandBuilder::new("docker"); 91 + command.cwd(path); 92 + command.arg("compose"); 93 + command.args(args); 93 94 94 - Ok(stream! { 95 - let mut stdout_buf = vec![0u8; 4096]; 96 - let mut stderr_buf = vec![0u8; 4096]; 97 - let mut stdout_done = false; 98 - let mut stderr_done = false; 95 + let mut child = pty 96 + .slave 97 + .spawn_command(command) 98 + .map_err(|err| eyre::eyre!("Failed to spawn docker compose process: {}", err))?; 99 99 100 - while !stdout_done || !stderr_done { 101 - tokio::select! { 102 - result = stdout.read(&mut stdout_buf), if !stdout_done => { 103 - match result { 104 - Ok(0) => stdout_done = true, 105 - Ok(n) => yield Ok(Bytes::copy_from_slice(&stdout_buf[..n])), 106 - Err(e) => { 107 - yield Err(eyre::eyre!(e).wrap_err("Failed to read stdout")); 108 - stdout_done = true; 109 - } 110 - } 111 - } 112 - result = stderr.read(&mut stderr_buf), if !stderr_done => { 113 - match result { 114 - Ok(0) => stderr_done = true, 115 - Ok(n) => yield Ok(Bytes::copy_from_slice(&stderr_buf[..n])), 116 - Err(e) => { 117 - yield Err(eyre::eyre!(e).wrap_err("Failed to read stderr")); 118 - stderr_done = true; 119 - } 120 - } 100 + let mut reader = pty 101 + .master 102 + .try_clone_reader() 103 + .map_err(|err| eyre::eyre!("Failed to create PTY output reader: {}", err))?; 104 + 105 + // Close the slave so reader recieves EOF 106 + drop(pty.slave); 107 + 108 + let (sender, mut reciever) = mpsc::channel::<Result<Bytes>>(64); 109 + 110 + tokio::task::spawn_blocking(move || { 111 + let mut buf = vec![0u8; 4096]; 112 + 113 + loop { 114 + match reader.read(&mut buf) { 115 + Err(err) => error!("Error reading from PTY: {}", err), 116 + Ok(0) => break, 117 + Ok(n) => { 118 + sender.blocking_send(Ok(Bytes::copy_from_slice(&buf[..n]))).ok(); 121 119 } 122 120 } 123 121 } 124 122 125 - child.wait().await.ok(); 123 + child.wait().ok(); 124 + }); 125 + 126 + return Ok(stream! { 127 + while let Some(chunk) = reciever.recv().await { 128 + yield chunk; 129 + } 126 130 } 127 - .boxed()) 131 + .boxed()); 128 132 } 129 133 }
+1 -1
packages/node/src/core/logs.rs
··· 87 87 debug!("Starting logs stream for project '{}'...", project); 88 88 // Spawn docker compose process, yielding logs as they are recieved 89 89 match this 90 - .cli(&project, ["logs", "-f"]) 90 + .run_pty(&project, ["logs", "-f"]) 91 91 .wrap_err("Failed to start docker compose logs process") 92 92 { 93 93 Err(err) => error!("{}", eyre_fmt!(err)),
+2 -2
packages/node/src/core/project.rs
··· 150 150 151 151 if recreate { 152 152 // Use manual command as action is currently "patching" 153 - let mut stream = self.cli(&from, vec!["down", "--remove-orphans"])?; 153 + let mut stream = self.run_pty(&from, vec!["down", "--remove-orphans"])?; 154 154 while let Some(_) = stream.next().await {} 155 155 }; 156 156 ··· 159 159 .wrap_err("Failed to rename project directory")?; 160 160 161 161 if recreate { 162 - let mut stream = self.cli(&to, vec!["up", "-d"])?; 162 + let mut stream = self.run_pty(&to, vec!["up", "-d"])?; 163 163 while let Some(_) = stream.next().await {} 164 164 } 165 165