add web terminal emulator (closes #6)
This commit is contained in:
		
							parent
							
								
									62c40358a2
								
							
						
					
					
						commit
						ac709e66f5
					
				
					 15 changed files with 586 additions and 87 deletions
				
			
		
							
								
								
									
										160
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										160
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -76,7 +76,7 @@ version = "1.0.0"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "windows-sys",
 | 
			
		||||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -86,7 +86,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		|||
checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anstyle",
 | 
			
		||||
 "windows-sys",
 | 
			
		||||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -250,7 +250,7 @@ dependencies = [
 | 
			
		|||
 "js-sys",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "windows-targets",
 | 
			
		||||
 "windows-targets 0.48.5",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -334,12 +334,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "errno"
 | 
			
		||||
version = "0.3.7"
 | 
			
		||||
version = "0.3.8"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8"
 | 
			
		||||
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
 "windows-sys",
 | 
			
		||||
 "windows-sys 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -483,7 +483,7 @@ version = "0.5.5"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "windows-sys",
 | 
			
		||||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -653,9 +653,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "libc"
 | 
			
		||||
version = "0.2.150"
 | 
			
		||||
version = "0.2.151"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
 | 
			
		||||
checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "linux-raw-sys"
 | 
			
		||||
| 
						 | 
				
			
			@ -663,6 +663,16 @@ version = "0.4.11"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "lock_api"
 | 
			
		||||
version = "0.4.11"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "autocfg",
 | 
			
		||||
 "scopeguard",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "log"
 | 
			
		||||
version = "0.4.20"
 | 
			
		||||
| 
						 | 
				
			
			@ -704,7 +714,7 @@ checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
 | 
			
		|||
dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
 "wasi",
 | 
			
		||||
 "windows-sys",
 | 
			
		||||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -772,6 +782,29 @@ version = "0.1.1"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "parking_lot"
 | 
			
		||||
version = "0.12.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "lock_api",
 | 
			
		||||
 "parking_lot_core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "parking_lot_core"
 | 
			
		||||
version = "0.9.9"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "libc",
 | 
			
		||||
 "redox_syscall",
 | 
			
		||||
 "smallvec",
 | 
			
		||||
 "windows-targets 0.48.5",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "percent-encoding"
 | 
			
		||||
version = "2.3.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -912,6 +945,7 @@ dependencies = [
 | 
			
		|||
 "prost",
 | 
			
		||||
 "regex",
 | 
			
		||||
 "reqwest",
 | 
			
		||||
 "rustix",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "sysinfo",
 | 
			
		||||
| 
						 | 
				
			
			@ -1045,15 +1079,16 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rustix"
 | 
			
		||||
version = "0.38.24"
 | 
			
		||||
version = "0.38.28"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234"
 | 
			
		||||
checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "bitflags 2.4.1",
 | 
			
		||||
 "errno",
 | 
			
		||||
 "itoa",
 | 
			
		||||
 "libc",
 | 
			
		||||
 "linux-raw-sys",
 | 
			
		||||
 "windows-sys",
 | 
			
		||||
 "windows-sys 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -1068,6 +1103,12 @@ version = "1.0.15"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "scopeguard"
 | 
			
		||||
version = "1.2.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde"
 | 
			
		||||
version = "1.0.192"
 | 
			
		||||
| 
						 | 
				
			
			@ -1161,7 +1202,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		|||
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
 "windows-sys",
 | 
			
		||||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -1232,7 +1273,7 @@ dependencies = [
 | 
			
		|||
 "fastrand",
 | 
			
		||||
 "redox_syscall",
 | 
			
		||||
 "rustix",
 | 
			
		||||
 "windows-sys",
 | 
			
		||||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -1271,11 +1312,12 @@ dependencies = [
 | 
			
		|||
 "libc",
 | 
			
		||||
 "mio",
 | 
			
		||||
 "num_cpus",
 | 
			
		||||
 "parking_lot",
 | 
			
		||||
 "pin-project-lite",
 | 
			
		||||
 "signal-hook-registry",
 | 
			
		||||
 "socket2 0.5.5",
 | 
			
		||||
 "tokio-macros",
 | 
			
		||||
 "windows-sys",
 | 
			
		||||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -1654,7 +1696,7 @@ version = "0.51.1"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "windows-targets",
 | 
			
		||||
 "windows-targets 0.48.5",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -1663,7 +1705,16 @@ version = "0.48.0"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "windows-targets",
 | 
			
		||||
 "windows-targets 0.48.5",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows-sys"
 | 
			
		||||
version = "0.52.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "windows-targets 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -1672,13 +1723,28 @@ version = "0.48.5"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "windows_aarch64_gnullvm",
 | 
			
		||||
 "windows_aarch64_msvc",
 | 
			
		||||
 "windows_i686_gnu",
 | 
			
		||||
 "windows_i686_msvc",
 | 
			
		||||
 "windows_x86_64_gnu",
 | 
			
		||||
 "windows_x86_64_gnullvm",
 | 
			
		||||
 "windows_x86_64_msvc",
 | 
			
		||||
 "windows_aarch64_gnullvm 0.48.5",
 | 
			
		||||
 "windows_aarch64_msvc 0.48.5",
 | 
			
		||||
 "windows_i686_gnu 0.48.5",
 | 
			
		||||
 "windows_i686_msvc 0.48.5",
 | 
			
		||||
 "windows_x86_64_gnu 0.48.5",
 | 
			
		||||
 "windows_x86_64_gnullvm 0.48.5",
 | 
			
		||||
 "windows_x86_64_msvc 0.48.5",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows-targets"
 | 
			
		||||
version = "0.52.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "windows_aarch64_gnullvm 0.52.0",
 | 
			
		||||
 "windows_aarch64_msvc 0.52.0",
 | 
			
		||||
 "windows_i686_gnu 0.52.0",
 | 
			
		||||
 "windows_i686_msvc 0.52.0",
 | 
			
		||||
 "windows_x86_64_gnu 0.52.0",
 | 
			
		||||
 "windows_x86_64_gnullvm 0.52.0",
 | 
			
		||||
 "windows_x86_64_msvc 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -1687,42 +1753,84 @@ version = "0.48.5"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_aarch64_gnullvm"
 | 
			
		||||
version = "0.52.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_aarch64_msvc"
 | 
			
		||||
version = "0.48.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_aarch64_msvc"
 | 
			
		||||
version = "0.52.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_i686_gnu"
 | 
			
		||||
version = "0.48.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_i686_gnu"
 | 
			
		||||
version = "0.52.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_i686_msvc"
 | 
			
		||||
version = "0.48.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_i686_msvc"
 | 
			
		||||
version = "0.52.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_x86_64_gnu"
 | 
			
		||||
version = "0.48.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_x86_64_gnu"
 | 
			
		||||
version = "0.52.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_x86_64_gnullvm"
 | 
			
		||||
version = "0.48.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_x86_64_gnullvm"
 | 
			
		||||
version = "0.52.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_x86_64_msvc"
 | 
			
		||||
version = "0.48.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_x86_64_msvc"
 | 
			
		||||
version = "0.52.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "winreg"
 | 
			
		||||
version = "0.50.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -1730,5 +1838,5 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		|||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "windows-sys",
 | 
			
		||||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,10 +13,11 @@ once_cell = "1.18.0"
 | 
			
		|||
prost = "0.12.1"
 | 
			
		||||
regex = "1.10.2"
 | 
			
		||||
reqwest = { version = "0.11.18", features = ["blocking", "json"], default-features = false }
 | 
			
		||||
rustix = { version = "0.38.28", features = ["fs", "process", "pty", "stdio", "termios"] }
 | 
			
		||||
serde = { version = "1.0.173", features = ["derive"] }
 | 
			
		||||
serde_json = "1.0.103"
 | 
			
		||||
sysinfo = { version = "0.29.2", default-features = false }
 | 
			
		||||
tokio = { version = "1.28.2", features = ["rt-multi-thread", "io-util", "process", "macros", "signal"] }
 | 
			
		||||
tokio = { version = "1.28.2", features = ["full"] }
 | 
			
		||||
tokio-stream = { version = "0.1.14", features = ["net", "sync"] }
 | 
			
		||||
tokio-util = { version = "0.7.10", features = ["codec"] }
 | 
			
		||||
tonic = { version = "0.10.2" }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ pub mod config;
 | 
			
		|||
pub mod debian;
 | 
			
		||||
pub mod health;
 | 
			
		||||
pub mod info;
 | 
			
		||||
pub mod pty;
 | 
			
		||||
pub mod self_update;
 | 
			
		||||
pub mod server;
 | 
			
		||||
pub mod task;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										166
									
								
								agent/src/pty.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								agent/src/pty.rs
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,166 @@
 | 
			
		|||
use std::{io, task::ready};
 | 
			
		||||
 | 
			
		||||
use rustix::{
 | 
			
		||||
    fd::OwnedFd,
 | 
			
		||||
    fs::{fcntl_getfl, fcntl_setfl, OFlags},
 | 
			
		||||
    process::{ioctl_tiocsctty, setsid},
 | 
			
		||||
    pty::{grantpt, ioctl_tiocgptpeer, openpt, unlockpt, OpenptFlags},
 | 
			
		||||
    stdio::{dup2_stderr, dup2_stdin, dup2_stdout},
 | 
			
		||||
    termios::{tcsetwinsize, Winsize},
 | 
			
		||||
};
 | 
			
		||||
use tokio::{
 | 
			
		||||
    io::{unix::AsyncFd, AsyncRead, AsyncWrite},
 | 
			
		||||
    process::Child,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct Pty {
 | 
			
		||||
    fd: AsyncFd<OwnedFd>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Pty {
 | 
			
		||||
    pub fn open() -> io::Result<Self> {
 | 
			
		||||
        let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)?;
 | 
			
		||||
        grantpt(&master)?;
 | 
			
		||||
        unlockpt(&master)?;
 | 
			
		||||
 | 
			
		||||
        // Set nonblocking
 | 
			
		||||
        let flags = fcntl_getfl(&master)?;
 | 
			
		||||
        fcntl_setfl(&master, flags | OFlags::NONBLOCK)?;
 | 
			
		||||
 | 
			
		||||
        let fd = AsyncFd::new(master)?;
 | 
			
		||||
 | 
			
		||||
        Ok(Self { fd })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn child(&self) -> io::Result<PtyChild> {
 | 
			
		||||
        // NOTE: Linux v4.13 and above
 | 
			
		||||
        let fd = ioctl_tiocgptpeer(&self.fd, OpenptFlags::RDWR | OpenptFlags::NOCTTY)?;
 | 
			
		||||
        let child = PtyChild { fd };
 | 
			
		||||
 | 
			
		||||
        Ok(child)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn resize_window(&self, rows: u16, cols: u16) -> io::Result<()> {
 | 
			
		||||
        let winsize = Winsize {
 | 
			
		||||
            ws_row: rows,
 | 
			
		||||
            ws_col: cols,
 | 
			
		||||
            ws_xpixel: 0,
 | 
			
		||||
            ws_ypixel: 0,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        tcsetwinsize(&self.fd, winsize)?;
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn try_clone(&self) -> io::Result<Pty> {
 | 
			
		||||
        let fd = self.fd.get_ref().try_clone()?;
 | 
			
		||||
 | 
			
		||||
        Ok(Pty {
 | 
			
		||||
            fd: AsyncFd::new(fd)?,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AsyncRead for Pty {
 | 
			
		||||
    fn poll_read(
 | 
			
		||||
        self: std::pin::Pin<&mut Self>,
 | 
			
		||||
        cx: &mut std::task::Context<'_>,
 | 
			
		||||
        buf: &mut tokio::io::ReadBuf<'_>,
 | 
			
		||||
    ) -> std::task::Poll<io::Result<()>> {
 | 
			
		||||
        loop {
 | 
			
		||||
            let mut guard = ready!(self.fd.poll_read_ready(cx)?);
 | 
			
		||||
 | 
			
		||||
            match guard.try_io(|inner| {
 | 
			
		||||
                let fd = inner.get_ref();
 | 
			
		||||
                let n = rustix::io::read(fd, buf.initialize_unfilled())?;
 | 
			
		||||
                buf.advance(n);
 | 
			
		||||
 | 
			
		||||
                Ok(())
 | 
			
		||||
            }) {
 | 
			
		||||
                Ok(result) => return std::task::Poll::Ready(result),
 | 
			
		||||
                Err(_would_block) => continue,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AsyncWrite for Pty {
 | 
			
		||||
    fn poll_write(
 | 
			
		||||
        self: std::pin::Pin<&mut Self>,
 | 
			
		||||
        cx: &mut std::task::Context<'_>,
 | 
			
		||||
        buf: &[u8],
 | 
			
		||||
    ) -> std::task::Poll<Result<usize, io::Error>> {
 | 
			
		||||
        loop {
 | 
			
		||||
            let mut guard = ready!(self.fd.poll_write_ready(cx))?;
 | 
			
		||||
 | 
			
		||||
            match guard.try_io(|inner| Ok(rustix::io::write(inner.get_ref(), buf)?)) {
 | 
			
		||||
                Ok(result) => return std::task::Poll::Ready(result),
 | 
			
		||||
                Err(_would_block) => continue,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn poll_flush(
 | 
			
		||||
        self: std::pin::Pin<&mut Self>,
 | 
			
		||||
        _cx: &mut std::task::Context<'_>,
 | 
			
		||||
    ) -> std::task::Poll<Result<(), io::Error>> {
 | 
			
		||||
        std::task::Poll::Ready(Ok(()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn poll_shutdown(
 | 
			
		||||
        self: std::pin::Pin<&mut Self>,
 | 
			
		||||
        _cx: &mut std::task::Context<'_>,
 | 
			
		||||
    ) -> std::task::Poll<Result<(), io::Error>> {
 | 
			
		||||
        std::task::Poll::Ready(Ok(()))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct PtyChild {
 | 
			
		||||
    fd: OwnedFd,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PtyChild {
 | 
			
		||||
    pub fn login_tty(&self) -> io::Result<()> {
 | 
			
		||||
        setsid()?;
 | 
			
		||||
        ioctl_tiocsctty(&self.fd)?;
 | 
			
		||||
        dup2_stdin(&self.fd)?;
 | 
			
		||||
        dup2_stdout(&self.fd)?;
 | 
			
		||||
        dup2_stderr(&self.fd)?;
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn open_shell(pty_child: PtyChild) -> io::Result<Child> {
 | 
			
		||||
    let mut cmd = tokio::process::Command::new("/bin/bash");
 | 
			
		||||
 | 
			
		||||
    unsafe {
 | 
			
		||||
        cmd.pre_exec(move || {
 | 
			
		||||
            pty_child.login_tty()?;
 | 
			
		||||
            Ok(())
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    cmd.spawn()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod test {
 | 
			
		||||
    use rustix::fd::AsRawFd;
 | 
			
		||||
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    #[tokio::test]
 | 
			
		||||
    async fn can_open_pty() {
 | 
			
		||||
        let pty = Pty::open().unwrap();
 | 
			
		||||
        let child = pty.child().unwrap();
 | 
			
		||||
 | 
			
		||||
        let master_fd = pty.fd.get_ref().as_raw_fd();
 | 
			
		||||
        let child_fd = child.fd.as_raw_fd();
 | 
			
		||||
 | 
			
		||||
        assert!(master_fd != child_fd);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +1,20 @@
 | 
			
		|||
use std::{pin::Pin, process::Stdio, sync::Mutex};
 | 
			
		||||
 | 
			
		||||
use tokio::process::Command;
 | 
			
		||||
use tokio::{io::AsyncWriteExt, process::Command};
 | 
			
		||||
use tokio_stream::{
 | 
			
		||||
    wrappers::{ReceiverStream, WatchStream},
 | 
			
		||||
    Stream, StreamExt,
 | 
			
		||||
};
 | 
			
		||||
use tokio_util::codec::{BytesCodec, FramedRead};
 | 
			
		||||
use tonic::{Request, Response, Status};
 | 
			
		||||
use tonic::{Request, Response, Status, Streaming};
 | 
			
		||||
 | 
			
		||||
use crate::{debian, health::HealthMonitor, info::Info, task::TaskBuilder};
 | 
			
		||||
use crate::{
 | 
			
		||||
    debian,
 | 
			
		||||
    health::HealthMonitor,
 | 
			
		||||
    info::Info,
 | 
			
		||||
    pty::{open_shell, Pty},
 | 
			
		||||
    task::TaskBuilder,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::proto::*;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -168,4 +174,51 @@ impl agent_server::Agent for AgentService<'static> {
 | 
			
		|||
 | 
			
		||||
        Ok(Response::new(Box::pin(stream)))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    type TerminalStream = Pin<Box<dyn Stream<Item = Result<TerminalResponse, Status>> + Send>>;
 | 
			
		||||
 | 
			
		||||
    async fn terminal(
 | 
			
		||||
        &self,
 | 
			
		||||
        req: Request<Streaming<TerminalRequest>>,
 | 
			
		||||
    ) -> AgentResult<Self::TerminalStream> {
 | 
			
		||||
        let mut in_stream = req.into_inner();
 | 
			
		||||
        let mut pty = Pty::open()?;
 | 
			
		||||
        let pty_clone = pty.try_clone()?;
 | 
			
		||||
        let pty_child = pty.child()?;
 | 
			
		||||
        let mut child = open_shell(pty_child)?;
 | 
			
		||||
 | 
			
		||||
        tokio::spawn(async move {
 | 
			
		||||
            // TODO: Handle errors inside here
 | 
			
		||||
            while let Some(result) = in_stream.next().await {
 | 
			
		||||
                match result {
 | 
			
		||||
                    Ok(req) => {
 | 
			
		||||
                        if let Some(resize) = req.resize {
 | 
			
		||||
                            pty.resize_window(resize.rows as u16, resize.cols as u16)
 | 
			
		||||
                                .unwrap();
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        pty.write_all(&req.input[..]).await.unwrap();
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(err) => {
 | 
			
		||||
                        // Log and ignore the error...
 | 
			
		||||
                        tracing::warn!(%err, "received an incoming stream error");
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // TODO: Maybe there's a more graceful way to stop the process?
 | 
			
		||||
            child.kill().await.unwrap();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let out_stream = FramedRead::new(pty_clone, BytesCodec::new()).map(|inner| {
 | 
			
		||||
            inner
 | 
			
		||||
                .map(|b| TerminalResponse { output: b.to_vec() })
 | 
			
		||||
                .map_err(|err| {
 | 
			
		||||
                    tracing::error!(%err, "read error on pseudoterminal");
 | 
			
		||||
                    Status::internal("terminal read error")
 | 
			
		||||
                })
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        Ok(Response::new(Box::pin(out_stream)))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
@import "tailwindcss/base";
 | 
			
		||||
@import "tailwindcss/components";
 | 
			
		||||
@import "tailwindcss/utilities";
 | 
			
		||||
@import "xterm";
 | 
			
		||||
 | 
			
		||||
/* This file is for your main application CSS */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,27 +2,52 @@
 | 
			
		|||
// to get started and then uncomment the line below.
 | 
			
		||||
// import "./user_socket.js"
 | 
			
		||||
 | 
			
		||||
// You can include dependencies in two ways.
 | 
			
		||||
//
 | 
			
		||||
// The simplest option is to put them in assets/vendor and
 | 
			
		||||
// import them using relative paths:
 | 
			
		||||
//
 | 
			
		||||
//     import "../vendor/some-package.js"
 | 
			
		||||
//
 | 
			
		||||
// Alternatively, you can `npm install some-package --prefix assets` and import
 | 
			
		||||
// them using a path starting with the package name:
 | 
			
		||||
//
 | 
			
		||||
//     import "some-package"
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import "phoenix_html"
 | 
			
		||||
import { Socket } from "phoenix"
 | 
			
		||||
import { LiveSocket } from "phoenix_live_view"
 | 
			
		||||
import topbar from "../vendor/topbar"
 | 
			
		||||
import Alpine from "alpinejs"
 | 
			
		||||
import { Terminal } from "xterm"
 | 
			
		||||
import { FitAddon } from "@xterm/addon-fit"
 | 
			
		||||
 | 
			
		||||
Alpine.start()
 | 
			
		||||
window.Alpine = Alpine
 | 
			
		||||
let Hooks = {}
 | 
			
		||||
 | 
			
		||||
// TODO: Move this code and xterm into a separate generated file, and load that file only when needed
 | 
			
		||||
Hooks.Terminal = {
 | 
			
		||||
  mounted() {
 | 
			
		||||
    const term = new Terminal({
 | 
			
		||||
      fontFamily: '"Courier New", "DejaVu Sans Mono", "Everson Mono", monospace',
 | 
			
		||||
      fontSize: 13,
 | 
			
		||||
      convertEol: true,
 | 
			
		||||
      theme: {
 | 
			
		||||
        background: "#24273a",
 | 
			
		||||
        foreground: "#cad3f5",
 | 
			
		||||
        cursor: "#f4dbd6",
 | 
			
		||||
        black: "#494D64",
 | 
			
		||||
        red: "#ed8796",
 | 
			
		||||
        green: "#a6da95",
 | 
			
		||||
        yellow: "#eed49f",
 | 
			
		||||
        blue: "#8aadf4",
 | 
			
		||||
        magenta: "#f5bde6",
 | 
			
		||||
        cyan: "#8bd5ca",
 | 
			
		||||
        white: "#b8c0e0",
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const fitAddon = new FitAddon()
 | 
			
		||||
    const resizeObserver = new ResizeObserver(() => fitAddon.fit())
 | 
			
		||||
 | 
			
		||||
    term.loadAddon(fitAddon)
 | 
			
		||||
    term.open(this.el)
 | 
			
		||||
    term.onData(data => this.pushEventTo(this.el, "data_event", data))
 | 
			
		||||
    term.onResize(resize => this.pushEventTo(this.el, "resize_event", resize))
 | 
			
		||||
 | 
			
		||||
    this.handleEvent("data", payload => term.write(payload.data || ""))
 | 
			
		||||
 | 
			
		||||
    fitAddon.fit()
 | 
			
		||||
    resizeObserver.observe(this.el)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
 | 
			
		||||
let liveSocket = new LiveSocket("/live", Socket, {
 | 
			
		||||
| 
						 | 
				
			
			@ -33,11 +58,12 @@ let liveSocket = new LiveSocket("/live", Socket, {
 | 
			
		|||
        window.Alpine.clone(from, to)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
  hooks: Hooks,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// Show progress bar on live navigation and form submits
 | 
			
		||||
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
 | 
			
		||||
topbar.config({ barColors: { 0: "#29d" }, shaDowColor: "rgba(0, 0, 0, .3)" })
 | 
			
		||||
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
 | 
			
		||||
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -50,3 +76,5 @@ liveSocket.connect()
 | 
			
		|||
// >> liveSocket.disableLatencySim()
 | 
			
		||||
window.liveSocket = liveSocket
 | 
			
		||||
 | 
			
		||||
Alpine.start()
 | 
			
		||||
window.Alpine = Alpine
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										17
									
								
								app/assets/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										17
									
								
								app/assets/package-lock.json
									
									
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -5,7 +5,9 @@
 | 
			
		|||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "alpinejs": "^3.13.3"
 | 
			
		||||
        "@xterm/addon-fit": "^0.9.0-beta.1",
 | 
			
		||||
        "alpinejs": "^3.13.3",
 | 
			
		||||
        "xterm": "^5.3.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@vue/reactivity": {
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +23,14 @@
 | 
			
		|||
      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
 | 
			
		||||
      "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@xterm/addon-fit": {
 | 
			
		||||
      "version": "0.9.0-beta.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.9.0-beta.1.tgz",
 | 
			
		||||
      "integrity": "sha512-HmGRUMMamUpQYuQBF2VP1LJ0xzqF85LMFfpaNu84t1Tsrl1lPKJWtqX9FDZ22Rf5q6bnKdbj44TRVAUHgDRbLA==",
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "xterm": "^5.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/alpinejs": {
 | 
			
		||||
      "version": "3.13.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.3.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +38,11 @@
 | 
			
		|||
      "dependencies": {
 | 
			
		||||
        "@vue/reactivity": "~3.1.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/xterm": {
 | 
			
		||||
      "version": "5.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg=="
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,7 @@
 | 
			
		|||
{
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "alpinejs": "^3.13.3"
 | 
			
		||||
    "@xterm/addon-fit": "^0.9.0-beta.1",
 | 
			
		||||
    "alpinejs": "^3.13.3",
 | 
			
		||||
    "xterm": "^5.3.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -131,6 +131,16 @@ defmodule Prymn.Agents do
 | 
			
		|||
  def sys_update(%Agent{} = agent, request) when is_map(request),
 | 
			
		||||
    do: sys_update(agent, struct(SysUpdateRequest, request))
 | 
			
		||||
 | 
			
		||||
  def terminal(%Agent{} = agent) do
 | 
			
		||||
    # TODO: Find a better solve for bi-directional GRPC stream
 | 
			
		||||
    with {:ok, channel} <- get_channel(agent),
 | 
			
		||||
         stream <- Stub.terminal(channel) do
 | 
			
		||||
      stream
 | 
			
		||||
    else
 | 
			
		||||
      {:error, error} -> {:error, error}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_channel(%Agent{} = agent) do
 | 
			
		||||
    case start_connection(agent.host_address) do
 | 
			
		||||
      {:ok, pid} -> {:ok, Connection.get_channel(pid)}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,7 @@ defmodule Prymn.Application do
 | 
			
		|||
      {Finch, name: Prymn.Finch},
 | 
			
		||||
      {Oban, Application.fetch_env!(:prymn, Oban)},
 | 
			
		||||
      Prymn.Agents.Supervisor,
 | 
			
		||||
      {Task.Supervisor, name: Prymn.TaskSupervisor},
 | 
			
		||||
      PrymnWeb.Endpoint
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ defmodule PrymnWeb.SystemInfo do
 | 
			
		|||
 | 
			
		||||
  require Logger
 | 
			
		||||
  alias Phoenix.LiveView.AsyncResult
 | 
			
		||||
  alias PrymnProto.Prymn.SysInfoResponse
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def update(assigns, socket) do
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +60,7 @@ defmodule PrymnWeb.SystemInfo do
 | 
			
		|||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_async(:get_sys_info, {:ok, {:ok, sys_info}}, socket) do
 | 
			
		||||
  def handle_async(:get_sys_info, {:ok, %SysInfoResponse{} = sys_info}, socket) do
 | 
			
		||||
    %{sys_info: sys_info_result, agent: agent} = socket.assigns
 | 
			
		||||
 | 
			
		||||
    {:noreply,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										120
									
								
								app/lib/prymn_web/components/terminal.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								app/lib/prymn_web/components/terminal.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,120 @@
 | 
			
		|||
defmodule PrymnWeb.Terminal do
 | 
			
		||||
  use PrymnWeb, :live_component
 | 
			
		||||
 | 
			
		||||
  alias PrymnProto.Prymn.TerminalRequest
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def mount(socket) do
 | 
			
		||||
    {:ok, assign(socket, :open, false)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def update(assigns, socket) do
 | 
			
		||||
    socket =
 | 
			
		||||
      if assigns[:data],
 | 
			
		||||
        do: push_event(socket, "data", %{"data" => assigns[:data]}),
 | 
			
		||||
        else: socket
 | 
			
		||||
 | 
			
		||||
    {:ok, assign(socket, assigns)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def render(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div>
 | 
			
		||||
      <PrymnWeb.Button.primary
 | 
			
		||||
        :if={not @open}
 | 
			
		||||
        type="button"
 | 
			
		||||
        phx-target={@myself}
 | 
			
		||||
        phx-click="open_terminal"
 | 
			
		||||
      >
 | 
			
		||||
        Open Terminal
 | 
			
		||||
      </PrymnWeb.Button.primary>
 | 
			
		||||
      <PrymnWeb.Button.primary
 | 
			
		||||
        :if={@open}
 | 
			
		||||
        type="button"
 | 
			
		||||
        phx-target={@myself}
 | 
			
		||||
        phx-click="close_terminal"
 | 
			
		||||
      >
 | 
			
		||||
        Close Terminal
 | 
			
		||||
      </PrymnWeb.Button.primary>
 | 
			
		||||
      <div :if={@open} class="mt-2 bg-black p-2">
 | 
			
		||||
        <div phx-hook="Terminal" id="terminal" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event("open_terminal", _params, socket) do
 | 
			
		||||
    agent = Prymn.Agents.from_server(socket.assigns.server)
 | 
			
		||||
    pid = self()
 | 
			
		||||
 | 
			
		||||
    Task.Supervisor.start_child(Prymn.TaskSupervisor, fn ->
 | 
			
		||||
      # FIXME: Have to wrap this in a Task because gun sends unsolicited messages
 | 
			
		||||
      # to calling process
 | 
			
		||||
      stream = Prymn.Agents.terminal(agent)
 | 
			
		||||
 | 
			
		||||
      {:ok, mux_pid} =
 | 
			
		||||
        Task.Supervisor.start_child(Prymn.TaskSupervisor, fn -> receive_loop(stream) end)
 | 
			
		||||
 | 
			
		||||
      send_update(pid, PrymnWeb.Terminal, id: "terminal", mux_pid: mux_pid, open: true)
 | 
			
		||||
 | 
			
		||||
      case GRPC.Stub.recv(stream, timeout: :infinity) do
 | 
			
		||||
        {:ok, stream} ->
 | 
			
		||||
          Enum.map(stream, fn
 | 
			
		||||
            {:ok, %{output: data}} ->
 | 
			
		||||
              send(mux_pid, :data)
 | 
			
		||||
              send_update(pid, PrymnWeb.Terminal, id: "terminal", data: data)
 | 
			
		||||
 | 
			
		||||
            {:error, _err} ->
 | 
			
		||||
              send_update(pid, PrymnWeb.Terminal, id: "terminal", open: false)
 | 
			
		||||
          end)
 | 
			
		||||
 | 
			
		||||
        {:error, error} ->
 | 
			
		||||
          dbg(error)
 | 
			
		||||
      end
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    {:noreply, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event("close_terminal", _params, socket) do
 | 
			
		||||
    send(socket.assigns.mux_pid, :close)
 | 
			
		||||
    {:noreply, assign(socket, :open, false)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event("data_event", data, socket) when is_binary(data) do
 | 
			
		||||
    send(socket.assigns.mux_pid, {:data_event, data})
 | 
			
		||||
    {:noreply, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event("resize_event", %{"cols" => cols, "rows" => rows}, socket) do
 | 
			
		||||
    send(socket.assigns.mux_pid, {:resize_event, rows, cols})
 | 
			
		||||
    {:noreply, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp receive_loop(stream) do
 | 
			
		||||
    receive do
 | 
			
		||||
      {:data_event, data} ->
 | 
			
		||||
        GRPC.Stub.send_request(stream, %TerminalRequest{input: data})
 | 
			
		||||
        receive_loop(stream)
 | 
			
		||||
 | 
			
		||||
      {:resize_event, rows, cols} ->
 | 
			
		||||
        GRPC.Stub.send_request(stream, %TerminalRequest{
 | 
			
		||||
          resize: %TerminalRequest.Resize{rows: rows, cols: cols}
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        receive_loop(stream)
 | 
			
		||||
 | 
			
		||||
      :data ->
 | 
			
		||||
        receive_loop(stream)
 | 
			
		||||
 | 
			
		||||
      :close ->
 | 
			
		||||
        GRPC.Stub.send_request(stream, %TerminalRequest{input: ""}, end_stream: true)
 | 
			
		||||
    after
 | 
			
		||||
      120_000 ->
 | 
			
		||||
        GRPC.Stub.send_request(stream, %TerminalRequest{input: ""}, end_stream: true)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -71,20 +71,18 @@ defmodule PrymnWeb.ServerLive.Show do
 | 
			
		|||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div :if={@server.status == :registered} class="my-10">
 | 
			
		||||
      <form phx-change="change_dry_run">
 | 
			
		||||
        <.input type="checkbox" name="dry_run" value={@dry_run} label="Enable dry-run operations" />
 | 
			
		||||
      </form>
 | 
			
		||||
      <div :if={@server.status == :registered} class="my-10 space-y-5 divide-y">
 | 
			
		||||
        <.live_component
 | 
			
		||||
          id={"system_info-#{@server.name}"}
 | 
			
		||||
          module={PrymnWeb.SystemInfo}
 | 
			
		||||
          agent={assigns[:agent]}
 | 
			
		||||
        />
 | 
			
		||||
        <section class="mt-4">
 | 
			
		||||
          <form phx-change="change_dry_run">
 | 
			
		||||
            <.input type="checkbox" name="dry_run" value={@dry_run} label="Enable dry-run operations" />
 | 
			
		||||
          </form>
 | 
			
		||||
        </section>
 | 
			
		||||
        <section class="mt-4">
 | 
			
		||||
          <h2 class="border-b border-solid border-gray-500 pb-1 text-2xl font-medium">System</h2>
 | 
			
		||||
          <p class="mt-4">
 | 
			
		||||
        <section>
 | 
			
		||||
          <h2 class="my-5 text-xl">System</h2>
 | 
			
		||||
          <p>
 | 
			
		||||
            Updates: <%= 0 %> pending updates.
 | 
			
		||||
            <Button.primary type="button" class="ml-4" phx-click="system_update">
 | 
			
		||||
              Update now
 | 
			
		||||
| 
						 | 
				
			
			@ -94,32 +92,11 @@ defmodule PrymnWeb.ServerLive.Show do
 | 
			
		|||
            </p>
 | 
			
		||||
          </p>
 | 
			
		||||
        </section>
 | 
			
		||||
        <section class="mt-4">
 | 
			
		||||
          <h2 class="border-b border-solid border-gray-500 pb-1 text-2xl font-medium">
 | 
			
		||||
            Backups
 | 
			
		||||
        <section>
 | 
			
		||||
          <h2 class="my-5 text-xl">
 | 
			
		||||
            Terminal
 | 
			
		||||
          </h2>
 | 
			
		||||
          <.table id="backups" rows={[%{date: "2023-10-11"}, %{date: "2023-10-10"}]}>
 | 
			
		||||
            <:col :let={backup} label="Date"><%= backup.date %></:col>
 | 
			
		||||
            <:action>
 | 
			
		||||
              <Button.primary>Restore</Button.primary>
 | 
			
		||||
            </:action>
 | 
			
		||||
          </.table>
 | 
			
		||||
        </section>
 | 
			
		||||
        <section class="mt-4">
 | 
			
		||||
          <h2 class="border-b border-solid border-gray-500 pb-1 text-2xl font-medium">
 | 
			
		||||
            Manage Services
 | 
			
		||||
          </h2>
 | 
			
		||||
          <.table
 | 
			
		||||
            id="services"
 | 
			
		||||
            rows={[%{name: "mariadb", status: "Active"}, %{name: "php8.0", status: "Disabled"}]}
 | 
			
		||||
          >
 | 
			
		||||
            <:col :let={service} label="Service"><%= service.name %></:col>
 | 
			
		||||
            <:col :let={service} label="Status"><%= service.status %></:col>
 | 
			
		||||
            <:action>
 | 
			
		||||
              <Button.primary>Activate</Button.primary>
 | 
			
		||||
              <Button.secondary>Deactivate</Button.secondary>
 | 
			
		||||
            </:action>
 | 
			
		||||
          </.table>
 | 
			
		||||
          <.live_component id="terminal" module={PrymnWeb.Terminal} server={@server} />
 | 
			
		||||
        </section>
 | 
			
		||||
      </div>
 | 
			
		||||
      <.back navigate={~p"/servers"}>Back to servers</.back>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,9 +69,24 @@ message SysUpdateResponse {
 | 
			
		|||
    int32 progress = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message TerminalRequest {
 | 
			
		||||
    message Resize {
 | 
			
		||||
        uint32 rows = 1;
 | 
			
		||||
        uint32 cols = 2;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    bytes input = 1;
 | 
			
		||||
    optional Resize resize = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message TerminalResponse {
 | 
			
		||||
    bytes output = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
service Agent {
 | 
			
		||||
    rpc Health(google.protobuf.Empty) returns (stream HealthResponse);
 | 
			
		||||
    rpc Exec(ExecRequest) returns (stream ExecResponse);
 | 
			
		||||
    rpc Terminal(stream TerminalRequest) returns (stream TerminalResponse);
 | 
			
		||||
    rpc GetSysInfo(google.protobuf.Empty) returns (SysInfoResponse);
 | 
			
		||||
    rpc SysUpdate(SysUpdateRequest) returns (stream SysUpdateResponse);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue