This repository has no description
0

Configure Feed

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

core / shuttle / src / session.rs
8.2 kB 245 lines
1use crate::cache::{CacheUploadManager, ReadCacheProxy, WriteCacheProxy}; 2use crate::command::Spec; 3use crate::dns_proxy::DnsProxy; 4use crate::exec; 5use crate::nix_config::{self, SYSTEMCTL_EXECUTABLE}; 6use crate::on_payload; 7use crate::protocol::{self, Message, v1}; 8use crate::{activation, command}; 9use anyhow::{Context, Result, bail}; 10use std::time::Duration; 11use tokio::io::{AsyncWrite, BufReader}; 12use tokio::sync::mpsc::{self, Sender}; 13use tokio::task::{JoinError, JoinSet}; 14use tokio_vsock::{VsockAddr, VsockStream}; 15use tracing::{info, warn}; 16 17pub async fn run(host_cid: u32, port: u32) -> Result<()> { 18 let mut conn = VsockStream::connect(VsockAddr::new(host_cid, port)) 19 .await 20 .with_context(|| format!("dial host vsock cid={host_cid} port={port}"))?; 21 22 send_hello(&mut conn).await?; 23 24 let (reader_conn, writer_conn) = tokio::io::split(conn); 25 let (out_tx, out_rx) = mpsc::channel::<Message>(256); 26 let writer = tokio::spawn(async move { writer_loop(writer_conn, out_rx).await }); 27 let mut reader = BufReader::new(reader_conn); 28 29 let init = match protocol::read_message(&mut reader).await? { 30 Some(Message { 31 init: Some(init), .. 32 }) => init, 33 Some(other) => bail!("expected init, got {}", protocol::kind(&other)), 34 None => bail!("read init: EOF"), 35 }; 36 info!(job_id = %init.job_id, "received init"); 37 38 let read_proxy = ReadCacheProxy::start(host_cid, init.cache_read_proxy_port) 39 .await 40 .context("start read cache proxy")?; 41 let write_proxy = WriteCacheProxy::start(host_cid, init.cache_upload_proxy_port) 42 .await 43 .context("start write cache proxy")?; 44 let _dns_proxy = DnsProxy::start(host_cid, init.dns_proxy_port) 45 .await 46 .context("start dns proxy")?; 47 let _cache_cfg = nix_config::configure( 48 &init, 49 read_proxy.as_ref().map(ReadCacheProxy::url).unwrap_or(""), 50 ) 51 .await 52 .context("configure nix cache")?; 53 let uploader = CacheUploadManager::start( 54 write_proxy.as_ref().map(WriteCacheProxy::url).unwrap_or(""), 55 out_tx.clone(), 56 ) 57 .await 58 .context("start cache upload manager")?; 59 60 let mut tasks = JoinSet::new(); 61 let read_result: Result<()> = loop { 62 tokio::select! { 63 read = protocol::read_message(&mut reader) => match read { 64 Ok(Some(msg)) => spawn_message_task(&mut tasks, host_cid, msg, &out_tx, uploader.clone()), 65 Ok(None) => break Ok(()), 66 Err(error) => break Err(error).context("read message"), 67 }, 68 Some(result) = tasks.join_next(), if !tasks.is_empty() => { 69 log_task_result(result, false); 70 } 71 } 72 }; 73 74 tasks.abort_all(); 75 while let Some(result) = tasks.join_next().await { 76 log_task_result(result, true); 77 } 78 79 drop(out_tx); 80 let _ = writer.await; 81 read_result?; 82 Ok(()) 83} 84 85fn spawn_message_task( 86 tasks: &mut JoinSet<()>, 87 host_cid: u32, 88 msg: Message, 89 out_tx: &Sender<Message>, 90 uploader: Option<CacheUploadManager>, 91) { 92 let kind = protocol::kind(&msg); 93 let handle = on_payload!(msg, { 94 activate_config => tasks.spawn(activation::run(msg.id, activate_config, out_tx.clone())), 95 exec_start => tasks.spawn(exec::run(msg.id, exec_start, out_tx.clone(), host_cid)), 96 cache_drain => tasks.spawn(run_cache_drain(msg.id, cache_drain, out_tx.clone(), uploader)), 97 poweroff => tasks.spawn(run_poweroff(msg.id, poweroff, out_tx.clone())), 98 }); 99 if handle.is_none() { 100 warn!(kind, "ignoring unsupported message"); 101 } 102} 103 104fn log_task_result(result: Result<(), JoinError>, shutting_down: bool) { 105 match result { 106 Ok(()) => {} 107 Err(error) if shutting_down && error.is_cancelled() => {} 108 Err(error) => warn!(%error, "session handler task failed"), 109 } 110} 111 112async fn writer_loop<W>(mut conn: W, mut rx: mpsc::Receiver<Message>) 113where 114 W: AsyncWrite + Unpin, 115{ 116 while let Some(msg) = rx.recv().await { 117 if let Err(error) = protocol::write_message(&mut conn, &msg).await { 118 warn!(%error, "failed to write protocol message"); 119 break; 120 } 121 } 122} 123 124async fn send_hello(conn: &mut VsockStream) -> Result<()> { 125 let boot_id = tokio::fs::read_to_string("/proc/sys/kernel/random/boot_id") 126 .await 127 .unwrap_or_default() 128 .trim() 129 .to_owned(); 130 let nix_version = nix_config::nix_version().await; 131 132 let hello_payload = v1::Hello { 133 protocol_version: protocol::PROTOCOL_VERSION, 134 agent_version: env!("CARGO_PKG_VERSION").to_string(), 135 boot_id: boot_id.clone(), 136 nix_version: nix_version.clone(), 137 }; 138 info!( 139 protocol = hello_payload.protocol_version, 140 version = %hello_payload.agent_version, 141 boot = %hello_payload.boot_id, 142 nix = %hello_payload.nix_version, 143 "sent hello" 144 ); 145 let hello = Message { 146 id: "hello".to_owned(), 147 hello: Some(hello_payload), 148 ..Default::default() 149 }; 150 151 protocol::write_message(conn, &hello) 152 .await 153 .context("send hello")?; 154 Ok(()) 155} 156 157async fn run_cache_drain( 158 id: String, 159 req: v1::CacheDrain, 160 out: Sender<Message>, 161 uploader: Option<CacheUploadManager>, 162) { 163 let timeout = 164 (req.timeout_seconds > 0).then(|| Duration::from_secs(u64::from(req.timeout_seconds))); 165 let stats = match uploader.as_ref() { 166 Some(uploader) => uploader.drain(timeout).await, 167 None => Default::default(), 168 }; 169 170 if let Some(error) = &stats.last_error { 171 warn!( 172 %id, 173 pending = stats.pending, 174 active = stats.active, 175 uploaded = stats.uploaded, 176 failed = stats.failed, 177 %error, 178 "cache drain completed with error" 179 ); 180 } else { 181 info!( 182 %id, 183 uploaded = stats.uploaded, 184 failed = stats.failed, 185 "cache drain completed" 186 ); 187 } 188 189 let result = Message { 190 id, 191 cache_drain_result: Some(v1::CacheDrainResult { 192 error: protocol::error_or_empty(stats.last_error), 193 cache_queued: stats.pending, 194 cache_active: stats.active, 195 cache_uploaded: stats.uploaded, 196 cache_failed: stats.failed, 197 }), 198 ..Default::default() 199 }; 200 let _ = out.send(result).await; 201} 202 203async fn run_poweroff(id: String, _req: v1::Poweroff, out: Sender<Message>) { 204 let result = Message { 205 id, 206 poweroff_result: Some(v1::PoweroffResult { 207 error: String::new(), 208 }), 209 ..Default::default() 210 }; 211 let _ = out.send(result).await; 212 213 tokio::spawn(async move { 214 tokio::time::sleep(Duration::from_millis(100)).await; 215 216 // prefer a clean shutdown through the init system when one is around 217 // (systemd on NixOS, busybox/openrc elsewhere), then fall back to the 218 // raw reboot(2) syscall on minimal guests 219 for poweroff in [SYSTEMCTL_EXECUTABLE, "/sbin/poweroff", "/usr/sbin/poweroff"] { 220 if !std::path::Path::new(poweroff).exists() { 221 continue; 222 } 223 let mut spec = Spec::new(poweroff).timeout(Duration::from_secs(5)); 224 if poweroff == SYSTEMCTL_EXECUTABLE { 225 spec = spec.args(["poweroff"]); 226 } 227 match command::run_capture(spec).await { 228 Ok(output) if output.success() => return, 229 Ok(output) => warn!( 230 %poweroff, 231 exit_code = output.exit.exit_code, 232 error = ?output.exit.error, 233 output = %output.combined_lossy(), 234 "poweroff command failed" 235 ), 236 Err(error) => warn!(%poweroff, %error, "poweroff command failed"), 237 } 238 } 239 240 // only ever returns on failure 241 let error = 242 nix::sys::reboot::reboot(nix::sys::reboot::RebootMode::RB_POWER_OFF).unwrap_err(); 243 warn!(%error, "reboot(RB_POWER_OFF) syscall failed"); 244 }); 245}