diff --git a/cmd/spindle-microvm-run/main_linux.go b/cmd/spindle-microvm-run/main_linux.go index 2d37fff7..c6988151 100644 --- a/cmd/spindle-microvm-run/main_linux.go +++ b/cmd/spindle-microvm-run/main_linux.go @@ -220,7 +220,7 @@ func runMicroVMRunDev(ctx context.Context, cmd *cli.Command) error { } defer dnsProxy.Close() - session := microvm.NewAgentSession(conn, logger) + session := microvm.NewAgentSession(conn, vm.CID(), logger) initCtx, cancelInit := context.WithTimeout(ctx, 30*time.Second) defer cancelInit() diff --git a/shuttle/src/activation.rs b/shuttle/src/activation.rs index d5ae31b2..f966a28d 100644 --- a/shuttle/src/activation.rs +++ b/shuttle/src/activation.rs @@ -201,11 +201,12 @@ async fn run_streaming_stderr( OutKind::Stdout => stdout.extend_from_slice(&event.data), OutKind::Stderr => { stderr.extend_from_slice(&event.data); - let data = String::from_utf8_lossy(&event.data).into_owned(); let _ = out .send(Message { id: id.to_owned(), - exec_stderr: Some(v1::ExecStderr { data }), + exec_stderr: Some(v1::ExecStderr { + data: event.data.into(), + }), ..Default::default() }) .await; diff --git a/shuttle/src/command.rs b/shuttle/src/command.rs index 64514be5..5a571df7 100644 --- a/shuttle/src/command.rs +++ b/shuttle/src/command.rs @@ -8,7 +8,7 @@ use std::path::PathBuf; use std::process::Stdio; use std::time::Duration; use tokio::io::{AsyncRead, AsyncReadExt}; -use tokio::process::{Child, Command}; +use tokio::process::{Child, ChildStdin, Command}; use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio::task::JoinHandle; use tracing::warn; @@ -22,6 +22,7 @@ pub struct Spec { pub timeout: Option, pub uid: Option, pub gid: Option, + pub piped_stdin: bool, } impl Spec { @@ -34,6 +35,7 @@ impl Spec { timeout: None, uid: None, gid: None, + piped_stdin: false, } } @@ -79,6 +81,11 @@ impl Spec { self.gid = Some(gid); self } + + pub fn piped_stdin(mut self) -> Self { + self.piped_stdin = true; + self + } } #[derive(Clone, Debug)] @@ -120,6 +127,7 @@ pub struct OutData { } pub struct StreamingCommand { + pub stdin: Option, events: Receiver, exit: JoinHandle>, } @@ -174,6 +182,7 @@ fn read_oom_kill_count() -> u32 { pub fn spawn_streaming(mut spec: Spec) -> Result { let oom_kill_before = read_oom_kill_count(); let mut child = spawn(&mut spec)?; + let stdin = child.stdin.take(); let stdout = child.stdout.take().context("stdout pipe missing")?; let stderr = child.stderr.take().context("stderr pipe missing")?; @@ -194,6 +203,7 @@ pub fn spawn_streaming(mut spec: Spec) -> Result { }); Ok(StreamingCommand { + stdin, events: events_rx, exit, }) @@ -205,6 +215,9 @@ fn spawn(spec: &mut Spec) -> Result { .envs(spec.env.iter().map(|(key, value)| (key, value))) .stdout(Stdio::piped()) .stderr(Stdio::piped()); + if spec.piped_stdin { + cmd.stdin(Stdio::piped()); + } if let Some(cwd) = &spec.cwd { cmd.current_dir(cwd); diff --git a/shuttle/src/exec.rs b/shuttle/src/exec.rs index b1868444..8641ed80 100644 --- a/shuttle/src/exec.rs +++ b/shuttle/src/exec.rs @@ -4,12 +4,15 @@ use nix::unistd::{Group, User}; use std::ffi::OsString; use std::path::PathBuf; use std::time::Duration; +use tokio::io::AsyncWriteExt; use tokio::sync::mpsc::Sender; +use tokio_vsock::{VsockAddr, VsockStream}; use tracing::{info, warn}; const DEFAULT_USER: &str = "spindle-workflow"; +const VSOCK_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); -pub async fn run(id: String, req: v1::ExecStart, out: Sender) { +pub async fn run(id: String, req: v1::ExecStart, out: Sender, host_cid: u32) { let send_exit = async |exit_code: i32, error: Option, timed_out: bool| { let msg = Message { id: id.clone(), @@ -57,6 +60,29 @@ pub async fn run(id: String, req: v1::ExecStart, out: Sender) { .envs(env) .envs(parse_env(&req.env)) .run_as(run_as.uid, run_as.gid); + + if req.stdio_vsock_port == 0 { + send_exit( + 1, + Some("exec is missing a stdio vsock port".to_owned()), + false, + ) + .await; + return; + } + let addr = VsockAddr::new(host_cid, req.stdio_vsock_port); + let conn = match tokio::time::timeout(VSOCK_CONNECT_TIMEOUT, VsockStream::connect(addr)).await { + Ok(Ok(conn)) => conn, + Ok(Err(error)) => { + send_exit(1, Some(format!("dial host stdio port: {error}")), false).await; + return; + } + Err(_) => { + send_exit(1, Some("dial host stdio port: timed out".to_owned()), false).await; + return; + } + }; + spec = spec.piped_stdin(); if !req.cwd.is_empty() { spec = spec.cwd(req.cwd.clone()); } @@ -76,30 +102,47 @@ pub async fn run(id: String, req: v1::ExecStart, out: Sender) { "starting exec" ); - let cmd = match command::spawn_streaming(spec) { + let mut cmd = match command::spawn_streaming(spec) { Ok(cmd) => cmd, Err(err) => { send_exit(127, Some(err.to_string()), false).await; return; } }; + + let (mut conn_reader, mut conn_writer) = tokio::io::split(conn); + // dropping this also closes the socket if the exec gets cancelled + let mut child_stdin = cmd.stdin.take().expect("piped_stdin"); + let _stdin_pump = AbortOnDrop(tokio::spawn(async move { + let _ = tokio::io::copy(&mut conn_reader, &mut child_stdin).await; + })); + let (mut events, exit_task) = cmd.into_parts(); + let mut stdio_error = None; while let Some(event) = events.recv().await { - let data = String::from_utf8_lossy(&event.data).into_owned(); - let output = match event.kind { - OutKind::Stdout => Message { - id: id.clone(), - exec_stdout: Some(v1::ExecStdout { data }), - ..Default::default() - }, - OutKind::Stderr => Message { - id: id.clone(), - exec_stderr: Some(v1::ExecStderr { data }), - ..Default::default() - }, - }; - let _ = out.send(output).await; + match event.kind { + OutKind::Stdout => { + if let Err(error) = conn_writer.write_all(&event.data).await { + stdio_error = Some(format!("forward stdout to host: {error}")); + // stop these before their pipes fill and block the child + events.close(); + break; + } + } + OutKind::Stderr => { + let _ = out + .send(Message { + id: id.clone(), + exec_stderr: Some(v1::ExecStderr { + data: event.data.into(), + }), + ..Default::default() + }) + .await; + } + } } + // the child might keep reading stdin after it closes stdout let exit = match exit_task .await .unwrap_or_else(|error| Err(anyhow::anyhow!("command supervisor failed: {error}"))) @@ -111,9 +154,23 @@ pub async fn run(id: String, req: v1::ExecStart, out: Sender) { } }; + // losing stdout fails the exec even if the child exited cleanly + if let Some(error) = stdio_error { + send_exit(1, Some(error), false).await; + return; + } send_exit(exit.exit_code, exit.error, exit.timed_out).await } +// aborts the task when its exec goes away +struct AbortOnDrop(tokio::task::JoinHandle<()>); + +impl Drop for AbortOnDrop { + fn drop(&mut self) { + self.0.abort(); + } +} + #[derive(Clone, Debug)] struct ResolvedUser { name: String, diff --git a/shuttle/src/gen/file_descriptor_set.bin b/shuttle/src/gen/file_descriptor_set.bin index e89630f2b2a2a231d14694f57becb7075029bda4..cf34612b32a148a436d849803af071b0f5277314 100644 GIT binary patch delta 37259 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 diff --git a/shuttle/src/gen/spindle/agent/v1/spindle.agent.v1.rs b/shuttle/src/gen/spindle/agent/v1/spindle.agent.v1.rs index 04a12079..18a55e9c 100644 --- a/shuttle/src/gen/spindle/agent/v1/spindle.agent.v1.rs +++ b/shuttle/src/gen/spindle/agent/v1/spindle.agent.v1.rs @@ -36,16 +36,13 @@ pub struct ExecStart { pub user: ::prost::alloc::string::String, #[prost(uint32, tag = "5")] pub timeout_seconds: u32, -} -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct ExecStdout { - #[prost(string, tag = "1")] - pub data: ::prost::alloc::string::String, + #[prost(uint32, tag = "6")] + pub stdio_vsock_port: u32, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ExecStderr { - #[prost(string, tag = "1")] - pub data: ::prost::alloc::string::String, + #[prost(bytes = "bytes", tag = "1")] + pub data: ::prost::bytes::Bytes, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ExecExit { @@ -122,8 +119,6 @@ pub struct Message { pub init: ::core::option::Option, #[prost(message, optional, tag = "4")] pub exec_start: ::core::option::Option, - #[prost(message, optional, tag = "5")] - pub exec_stdout: ::core::option::Option, #[prost(message, optional, tag = "6")] pub exec_stderr: ::core::option::Option, #[prost(message, optional, tag = "7")] diff --git a/shuttle/src/protocol.rs b/shuttle/src/protocol.rs index 979e314d..26701c1a 100644 --- a/shuttle/src/protocol.rs +++ b/shuttle/src/protocol.rs @@ -33,7 +33,6 @@ impl_reflect!( Hello, Init, ExecStart, - ExecStdout, ExecStderr, ExecExit, ActivateConfig, @@ -46,7 +45,7 @@ impl_reflect!( Message, ); -pub const PROTOCOL_VERSION: u32 = 1; +pub const PROTOCOL_VERSION: u32 = 2; pub const DEFAULT_PORT: u32 = 10240; pub const MAX_MESSAGE_BYTES: usize = 1024 * 1024; @@ -69,7 +68,6 @@ pub fn kind(msg: &Message) -> &'static str { hello => "hello", init => "init", exec_start => "exec_start", - exec_stdout => "exec_stdout", exec_stderr => "exec_stderr", exec_exit => "exec_exit", activate_config => "activate_config", diff --git a/shuttle/src/session.rs b/shuttle/src/session.rs index 4743cc53..7a3ae443 100644 --- a/shuttle/src/session.rs +++ b/shuttle/src/session.rs @@ -61,7 +61,7 @@ pub async fn run(host_cid: u32, port: u32) -> Result<()> { let read_result: Result<()> = loop { tokio::select! { read = protocol::read_message(&mut reader) => match read { - Ok(Some(msg)) => spawn_message_task(&mut tasks, msg, &out_tx, uploader.clone()), + Ok(Some(msg)) => spawn_message_task(&mut tasks, host_cid, msg, &out_tx, uploader.clone()), Ok(None) => break Ok(()), Err(error) => break Err(error).context("read message"), }, @@ -84,6 +84,7 @@ pub async fn run(host_cid: u32, port: u32) -> Result<()> { fn spawn_message_task( tasks: &mut JoinSet<()>, + host_cid: u32, msg: Message, out_tx: &Sender, uploader: Option, @@ -91,7 +92,7 @@ fn spawn_message_task( let kind = protocol::kind(&msg); let handle = on_payload!(msg, { activate_config => tasks.spawn(activation::run(msg.id, activate_config, out_tx.clone())), - exec_start => tasks.spawn(exec::run(msg.id, exec_start, out_tx.clone())), + exec_start => tasks.spawn(exec::run(msg.id, exec_start, out_tx.clone(), host_cid)), cache_drain => tasks.spawn(run_cache_drain(msg.id, cache_drain, out_tx.clone(), uploader)), poweroff => tasks.spawn(run_poweroff(msg.id, poweroff, out_tx.clone())), }); diff --git a/spindle/agentproto/gen/agent.pb.go b/spindle/agentproto/gen/agent.pb.go index f4a02799..8620d8f4 100644 --- a/spindle/agentproto/gen/agent.pb.go +++ b/spindle/agentproto/gen/agent.pb.go @@ -173,6 +173,7 @@ type ExecStart struct { Cwd string `protobuf:"bytes,3,opt,name=cwd,proto3" json:"cwd,omitempty"` User string `protobuf:"bytes,4,opt,name=user,proto3" json:"user,omitempty"` TimeoutSeconds uint32 `protobuf:"varint,5,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` + StdioVsockPort uint32 `protobuf:"varint,6,opt,name=stdio_vsock_port,json=stdioVsockPort,proto3" json:"stdio_vsock_port,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -242,60 +243,23 @@ func (x *ExecStart) GetTimeoutSeconds() uint32 { return 0 } -type ExecStdout struct { - state protoimpl.MessageState `protogen:"open.v1"` - Data string `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ExecStdout) Reset() { - *x = ExecStdout{} - mi := &file_spindle_agent_v1_agent_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ExecStdout) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ExecStdout) ProtoMessage() {} - -func (x *ExecStdout) ProtoReflect() protoreflect.Message { - mi := &file_spindle_agent_v1_agent_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ExecStdout.ProtoReflect.Descriptor instead. -func (*ExecStdout) Descriptor() ([]byte, []int) { - return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{3} -} - -func (x *ExecStdout) GetData() string { +func (x *ExecStart) GetStdioVsockPort() uint32 { if x != nil { - return x.Data + return x.StdioVsockPort } - return "" + return 0 } type ExecStderr struct { state protoimpl.MessageState `protogen:"open.v1"` - Data string `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ExecStderr) Reset() { *x = ExecStderr{} - mi := &file_spindle_agent_v1_agent_proto_msgTypes[4] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -307,7 +271,7 @@ func (x *ExecStderr) String() string { func (*ExecStderr) ProtoMessage() {} func (x *ExecStderr) ProtoReflect() protoreflect.Message { - mi := &file_spindle_agent_v1_agent_proto_msgTypes[4] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -320,14 +284,14 @@ func (x *ExecStderr) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecStderr.ProtoReflect.Descriptor instead. func (*ExecStderr) Descriptor() ([]byte, []int) { - return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{4} + return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{3} } -func (x *ExecStderr) GetData() string { +func (x *ExecStderr) GetData() []byte { if x != nil { return x.Data } - return "" + return nil } type ExecExit struct { @@ -343,7 +307,7 @@ type ExecExit struct { func (x *ExecExit) Reset() { *x = ExecExit{} - mi := &file_spindle_agent_v1_agent_proto_msgTypes[5] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -355,7 +319,7 @@ func (x *ExecExit) String() string { func (*ExecExit) ProtoMessage() {} func (x *ExecExit) ProtoReflect() protoreflect.Message { - mi := &file_spindle_agent_v1_agent_proto_msgTypes[5] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -368,7 +332,7 @@ func (x *ExecExit) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecExit.ProtoReflect.Descriptor instead. func (*ExecExit) Descriptor() ([]byte, []int) { - return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{5} + return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{4} } func (x *ExecExit) GetExitCode() int32 { @@ -405,7 +369,7 @@ type ActivateConfig struct { func (x *ActivateConfig) Reset() { *x = ActivateConfig{} - mi := &file_spindle_agent_v1_agent_proto_msgTypes[6] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -417,7 +381,7 @@ func (x *ActivateConfig) String() string { func (*ActivateConfig) ProtoMessage() {} func (x *ActivateConfig) ProtoReflect() protoreflect.Message { - mi := &file_spindle_agent_v1_agent_proto_msgTypes[6] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -430,7 +394,7 @@ func (x *ActivateConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ActivateConfig.ProtoReflect.Descriptor instead. func (*ActivateConfig) Descriptor() ([]byte, []int) { - return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{6} + return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{5} } func (x *ActivateConfig) GetConfigKey() string { @@ -479,7 +443,7 @@ type ActivateConfigResult struct { func (x *ActivateConfigResult) Reset() { *x = ActivateConfigResult{} - mi := &file_spindle_agent_v1_agent_proto_msgTypes[7] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -491,7 +455,7 @@ func (x *ActivateConfigResult) String() string { func (*ActivateConfigResult) ProtoMessage() {} func (x *ActivateConfigResult) ProtoReflect() protoreflect.Message { - mi := &file_spindle_agent_v1_agent_proto_msgTypes[7] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -504,7 +468,7 @@ func (x *ActivateConfigResult) ProtoReflect() protoreflect.Message { // Deprecated: Use ActivateConfigResult.ProtoReflect.Descriptor instead. func (*ActivateConfigResult) Descriptor() ([]byte, []int) { - return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{7} + return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{6} } func (x *ActivateConfigResult) GetConfigKey() string { @@ -538,7 +502,7 @@ type BuiltPaths struct { func (x *BuiltPaths) Reset() { *x = BuiltPaths{} - mi := &file_spindle_agent_v1_agent_proto_msgTypes[8] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -550,7 +514,7 @@ func (x *BuiltPaths) String() string { func (*BuiltPaths) ProtoMessage() {} func (x *BuiltPaths) ProtoReflect() protoreflect.Message { - mi := &file_spindle_agent_v1_agent_proto_msgTypes[8] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -563,7 +527,7 @@ func (x *BuiltPaths) ProtoReflect() protoreflect.Message { // Deprecated: Use BuiltPaths.ProtoReflect.Descriptor instead. func (*BuiltPaths) Descriptor() ([]byte, []int) { - return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{8} + return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{7} } func (x *BuiltPaths) GetPaths() []string { @@ -589,7 +553,7 @@ type CacheDrain struct { func (x *CacheDrain) Reset() { *x = CacheDrain{} - mi := &file_spindle_agent_v1_agent_proto_msgTypes[9] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -601,7 +565,7 @@ func (x *CacheDrain) String() string { func (*CacheDrain) ProtoMessage() {} func (x *CacheDrain) ProtoReflect() protoreflect.Message { - mi := &file_spindle_agent_v1_agent_proto_msgTypes[9] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -614,7 +578,7 @@ func (x *CacheDrain) ProtoReflect() protoreflect.Message { // Deprecated: Use CacheDrain.ProtoReflect.Descriptor instead. func (*CacheDrain) Descriptor() ([]byte, []int) { - return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{9} + return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{8} } func (x *CacheDrain) GetTimeoutSeconds() uint32 { @@ -637,7 +601,7 @@ type CacheDrainResult struct { func (x *CacheDrainResult) Reset() { *x = CacheDrainResult{} - mi := &file_spindle_agent_v1_agent_proto_msgTypes[10] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -649,7 +613,7 @@ func (x *CacheDrainResult) String() string { func (*CacheDrainResult) ProtoMessage() {} func (x *CacheDrainResult) ProtoReflect() protoreflect.Message { - mi := &file_spindle_agent_v1_agent_proto_msgTypes[10] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -662,7 +626,7 @@ func (x *CacheDrainResult) ProtoReflect() protoreflect.Message { // Deprecated: Use CacheDrainResult.ProtoReflect.Descriptor instead. func (*CacheDrainResult) Descriptor() ([]byte, []int) { - return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{10} + return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{9} } func (x *CacheDrainResult) GetError() string { @@ -708,7 +672,7 @@ type Poweroff struct { func (x *Poweroff) Reset() { *x = Poweroff{} - mi := &file_spindle_agent_v1_agent_proto_msgTypes[11] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -720,7 +684,7 @@ func (x *Poweroff) String() string { func (*Poweroff) ProtoMessage() {} func (x *Poweroff) ProtoReflect() protoreflect.Message { - mi := &file_spindle_agent_v1_agent_proto_msgTypes[11] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -733,7 +697,7 @@ func (x *Poweroff) ProtoReflect() protoreflect.Message { // Deprecated: Use Poweroff.ProtoReflect.Descriptor instead. func (*Poweroff) Descriptor() ([]byte, []int) { - return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{11} + return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{10} } type PoweroffResult struct { @@ -745,7 +709,7 @@ type PoweroffResult struct { func (x *PoweroffResult) Reset() { *x = PoweroffResult{} - mi := &file_spindle_agent_v1_agent_proto_msgTypes[12] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -757,7 +721,7 @@ func (x *PoweroffResult) String() string { func (*PoweroffResult) ProtoMessage() {} func (x *PoweroffResult) ProtoReflect() protoreflect.Message { - mi := &file_spindle_agent_v1_agent_proto_msgTypes[12] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -770,7 +734,7 @@ func (x *PoweroffResult) ProtoReflect() protoreflect.Message { // Deprecated: Use PoweroffResult.ProtoReflect.Descriptor instead. func (*PoweroffResult) Descriptor() ([]byte, []int) { - return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{12} + return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{11} } func (x *PoweroffResult) GetError() string { @@ -786,7 +750,6 @@ type Message struct { Hello *Hello `protobuf:"bytes,2,opt,name=hello,proto3" json:"hello,omitempty"` Init *Init `protobuf:"bytes,3,opt,name=init,proto3" json:"init,omitempty"` ExecStart *ExecStart `protobuf:"bytes,4,opt,name=exec_start,json=execStart,proto3" json:"exec_start,omitempty"` - ExecStdout *ExecStdout `protobuf:"bytes,5,opt,name=exec_stdout,json=execStdout,proto3" json:"exec_stdout,omitempty"` ExecStderr *ExecStderr `protobuf:"bytes,6,opt,name=exec_stderr,json=execStderr,proto3" json:"exec_stderr,omitempty"` ExecExit *ExecExit `protobuf:"bytes,7,opt,name=exec_exit,json=execExit,proto3" json:"exec_exit,omitempty"` ActivateConfig *ActivateConfig `protobuf:"bytes,8,opt,name=activate_config,json=activateConfig,proto3" json:"activate_config,omitempty"` @@ -802,7 +765,7 @@ type Message struct { func (x *Message) Reset() { *x = Message{} - mi := &file_spindle_agent_v1_agent_proto_msgTypes[13] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -814,7 +777,7 @@ func (x *Message) String() string { func (*Message) ProtoMessage() {} func (x *Message) ProtoReflect() protoreflect.Message { - mi := &file_spindle_agent_v1_agent_proto_msgTypes[13] + mi := &file_spindle_agent_v1_agent_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -827,7 +790,7 @@ func (x *Message) ProtoReflect() protoreflect.Message { // Deprecated: Use Message.ProtoReflect.Descriptor instead. func (*Message) Descriptor() ([]byte, []int) { - return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{13} + return file_spindle_agent_v1_agent_proto_rawDescGZIP(), []int{12} } func (x *Message) GetId() string { @@ -858,13 +821,6 @@ func (x *Message) GetExecStart() *ExecStart { return nil } -func (x *Message) GetExecStdout() *ExecStdout { - if x != nil { - return x.ExecStdout - } - return nil -} - func (x *Message) GetExecStderr() *ExecStderr { if x != nil { return x.ExecStderr @@ -944,19 +900,17 @@ const file_spindle_agent_v1_agent_proto_rawDesc = "" + "\x19cache_trusted_public_keys\x18\x02 \x03(\tR\x16cacheTrustedPublicKeys\x121\n" + "\x15cache_read_proxy_port\x18\x03 \x01(\rR\x12cacheReadProxyPort\x125\n" + "\x17cache_upload_proxy_port\x18\x04 \x01(\rR\x14cacheUploadProxyPort\x12$\n" + - "\x0edns_proxy_port\x18\x05 \x01(\rR\fdnsProxyPort\"\x80\x01\n" + + "\x0edns_proxy_port\x18\x05 \x01(\rR\fdnsProxyPort\"\xaa\x01\n" + "\tExecStart\x12\x12\n" + "\x04argv\x18\x01 \x03(\tR\x04argv\x12\x10\n" + "\x03env\x18\x02 \x03(\tR\x03env\x12\x10\n" + "\x03cwd\x18\x03 \x01(\tR\x03cwd\x12\x12\n" + "\x04user\x18\x04 \x01(\tR\x04user\x12'\n" + - "\x0ftimeout_seconds\x18\x05 \x01(\rR\x0etimeoutSeconds\" \n" + - "\n" + - "ExecStdout\x12\x12\n" + - "\x04data\x18\x01 \x01(\tR\x04data\" \n" + + "\x0ftimeout_seconds\x18\x05 \x01(\rR\x0etimeoutSeconds\x12(\n" + + "\x10stdio_vsock_port\x18\x06 \x01(\rR\x0estdioVsockPort\" \n" + "\n" + "ExecStderr\x12\x12\n" + - "\x04data\x18\x01 \x01(\tR\x04data\"Z\n" + + "\x04data\x18\x01 \x01(\fR\x04data\"Z\n" + "\bExecExit\x12\x1b\n" + "\texit_code\x18\x01 \x01(\x05R\bexitCode\x12\x14\n" + "\x05error\x18\x02 \x01(\tR\x05error\x12\x1b\n" + @@ -990,15 +944,13 @@ const file_spindle_agent_v1_agent_proto_rawDesc = "" + "\n" + "\bPoweroff\"&\n" + "\x0ePoweroffResult\x12\x14\n" + - "\x05error\x18\x01 \x01(\tR\x05error\"\xa8\b\n" + + "\x05error\x18\x01 \x01(\tR\x05error\"\xe8\a\n" + "\aMessage\x12\x17\n" + "\x02id\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x02id\x12-\n" + "\x05hello\x18\x02 \x01(\v2\x17.spindle.agent.v1.HelloR\x05hello\x12*\n" + "\x04init\x18\x03 \x01(\v2\x16.spindle.agent.v1.InitR\x04init\x12:\n" + "\n" + "exec_start\x18\x04 \x01(\v2\x1b.spindle.agent.v1.ExecStartR\texecStart\x12=\n" + - "\vexec_stdout\x18\x05 \x01(\v2\x1c.spindle.agent.v1.ExecStdoutR\n" + - "execStdout\x12=\n" + "\vexec_stderr\x18\x06 \x01(\v2\x1c.spindle.agent.v1.ExecStderrR\n" + "execStderr\x127\n" + "\texec_exit\x18\a \x01(\v2\x1a.spindle.agent.v1.ExecExitR\bexecExit\x12I\n" + @@ -1011,12 +963,11 @@ const file_spindle_agent_v1_agent_proto_rawDesc = "" + "cacheDrain\x12P\n" + "\x12cache_drain_result\x18\f \x01(\v2\".spindle.agent.v1.CacheDrainResultR\x10cacheDrainResult\x126\n" + "\bpoweroff\x18\r \x01(\v2\x1a.spindle.agent.v1.PoweroffR\bpoweroff\x12I\n" + - "\x0fpoweroff_result\x18\x0e \x01(\v2 .spindle.agent.v1.PoweroffResultR\x0epoweroffResult:\xb9\x01\xbaH\xb5\x01\"\xb2\x01\n" + + "\x0fpoweroff_result\x18\x0e \x01(\v2 .spindle.agent.v1.PoweroffResultR\x0epoweroffResult:\xac\x01\xbaH\xa8\x01\"\xa5\x01\n" + "\x05hello\n" + "\x04init\n" + "\n" + "exec_start\n" + - "\vexec_stdout\n" + "\vexec_stderr\n" + "\texec_exit\n" + "\x0factivate_config\n" + @@ -1025,7 +976,7 @@ const file_spindle_agent_v1_agent_proto_rawDesc = "" + "\vcache_drain\n" + "\x12cache_drain_result\n" + "\bpoweroff\n" + - "\x0fpoweroff_result\x10\x01B1Z/tangled.org/core/spindle/agentproto/gen;agentv1b\x06proto3" + "\x0fpoweroff_result\x10\x01J\x04\b\x05\x10\x06J\x04\b\x0f\x10\x10B1Z/tangled.org/core/spindle/agentproto/gen;agentv1b\x06proto3" var ( file_spindle_agent_v1_agent_proto_rawDescOnce sync.Once @@ -1039,42 +990,40 @@ func file_spindle_agent_v1_agent_proto_rawDescGZIP() []byte { return file_spindle_agent_v1_agent_proto_rawDescData } -var file_spindle_agent_v1_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_spindle_agent_v1_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_spindle_agent_v1_agent_proto_goTypes = []any{ (*Hello)(nil), // 0: spindle.agent.v1.Hello (*Init)(nil), // 1: spindle.agent.v1.Init (*ExecStart)(nil), // 2: spindle.agent.v1.ExecStart - (*ExecStdout)(nil), // 3: spindle.agent.v1.ExecStdout - (*ExecStderr)(nil), // 4: spindle.agent.v1.ExecStderr - (*ExecExit)(nil), // 5: spindle.agent.v1.ExecExit - (*ActivateConfig)(nil), // 6: spindle.agent.v1.ActivateConfig - (*ActivateConfigResult)(nil), // 7: spindle.agent.v1.ActivateConfigResult - (*BuiltPaths)(nil), // 8: spindle.agent.v1.BuiltPaths - (*CacheDrain)(nil), // 9: spindle.agent.v1.CacheDrain - (*CacheDrainResult)(nil), // 10: spindle.agent.v1.CacheDrainResult - (*Poweroff)(nil), // 11: spindle.agent.v1.Poweroff - (*PoweroffResult)(nil), // 12: spindle.agent.v1.PoweroffResult - (*Message)(nil), // 13: spindle.agent.v1.Message + (*ExecStderr)(nil), // 3: spindle.agent.v1.ExecStderr + (*ExecExit)(nil), // 4: spindle.agent.v1.ExecExit + (*ActivateConfig)(nil), // 5: spindle.agent.v1.ActivateConfig + (*ActivateConfigResult)(nil), // 6: spindle.agent.v1.ActivateConfigResult + (*BuiltPaths)(nil), // 7: spindle.agent.v1.BuiltPaths + (*CacheDrain)(nil), // 8: spindle.agent.v1.CacheDrain + (*CacheDrainResult)(nil), // 9: spindle.agent.v1.CacheDrainResult + (*Poweroff)(nil), // 10: spindle.agent.v1.Poweroff + (*PoweroffResult)(nil), // 11: spindle.agent.v1.PoweroffResult + (*Message)(nil), // 12: spindle.agent.v1.Message } var file_spindle_agent_v1_agent_proto_depIdxs = []int32{ 0, // 0: spindle.agent.v1.Message.hello:type_name -> spindle.agent.v1.Hello 1, // 1: spindle.agent.v1.Message.init:type_name -> spindle.agent.v1.Init 2, // 2: spindle.agent.v1.Message.exec_start:type_name -> spindle.agent.v1.ExecStart - 3, // 3: spindle.agent.v1.Message.exec_stdout:type_name -> spindle.agent.v1.ExecStdout - 4, // 4: spindle.agent.v1.Message.exec_stderr:type_name -> spindle.agent.v1.ExecStderr - 5, // 5: spindle.agent.v1.Message.exec_exit:type_name -> spindle.agent.v1.ExecExit - 6, // 6: spindle.agent.v1.Message.activate_config:type_name -> spindle.agent.v1.ActivateConfig - 7, // 7: spindle.agent.v1.Message.activate_config_result:type_name -> spindle.agent.v1.ActivateConfigResult - 8, // 8: spindle.agent.v1.Message.built_paths:type_name -> spindle.agent.v1.BuiltPaths - 9, // 9: spindle.agent.v1.Message.cache_drain:type_name -> spindle.agent.v1.CacheDrain - 10, // 10: spindle.agent.v1.Message.cache_drain_result:type_name -> spindle.agent.v1.CacheDrainResult - 11, // 11: spindle.agent.v1.Message.poweroff:type_name -> spindle.agent.v1.Poweroff - 12, // 12: spindle.agent.v1.Message.poweroff_result:type_name -> spindle.agent.v1.PoweroffResult - 13, // [13:13] is the sub-list for method output_type - 13, // [13:13] is the sub-list for method input_type - 13, // [13:13] is the sub-list for extension type_name - 13, // [13:13] is the sub-list for extension extendee - 0, // [0:13] is the sub-list for field type_name + 3, // 3: spindle.agent.v1.Message.exec_stderr:type_name -> spindle.agent.v1.ExecStderr + 4, // 4: spindle.agent.v1.Message.exec_exit:type_name -> spindle.agent.v1.ExecExit + 5, // 5: spindle.agent.v1.Message.activate_config:type_name -> spindle.agent.v1.ActivateConfig + 6, // 6: spindle.agent.v1.Message.activate_config_result:type_name -> spindle.agent.v1.ActivateConfigResult + 7, // 7: spindle.agent.v1.Message.built_paths:type_name -> spindle.agent.v1.BuiltPaths + 8, // 8: spindle.agent.v1.Message.cache_drain:type_name -> spindle.agent.v1.CacheDrain + 9, // 9: spindle.agent.v1.Message.cache_drain_result:type_name -> spindle.agent.v1.CacheDrainResult + 10, // 10: spindle.agent.v1.Message.poweroff:type_name -> spindle.agent.v1.Poweroff + 11, // 11: spindle.agent.v1.Message.poweroff_result:type_name -> spindle.agent.v1.PoweroffResult + 12, // [12:12] is the sub-list for method output_type + 12, // [12:12] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_spindle_agent_v1_agent_proto_init() } @@ -1088,7 +1037,7 @@ func file_spindle_agent_v1_agent_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_spindle_agent_v1_agent_proto_rawDesc), len(file_spindle_agent_v1_agent_proto_rawDesc)), NumEnums: 0, - NumMessages: 14, + NumMessages: 13, NumExtensions: 0, NumServices: 0, }, diff --git a/spindle/agentproto/protocol.go b/spindle/agentproto/protocol.go index ac12e909..4104c300 100644 --- a/spindle/agentproto/protocol.go +++ b/spindle/agentproto/protocol.go @@ -13,7 +13,7 @@ import ( ) const ( - ProtocolVersion = 1 + ProtocolVersion = 2 DefaultPort = 10240 MaxMessageBytes = 1024 * 1024 ) diff --git a/spindle/agentproto/spindle/agent/v1/agent.proto b/spindle/agentproto/spindle/agent/v1/agent.proto index 6b3d4dc2..353d1383 100644 --- a/spindle/agentproto/spindle/agent/v1/agent.proto +++ b/spindle/agentproto/spindle/agent/v1/agent.proto @@ -27,14 +27,11 @@ message ExecStart { string cwd = 3; string user = 4; uint32 timeout_seconds = 5; -} - -message ExecStdout { - string data = 1; + uint32 stdio_vsock_port = 6; } message ExecStderr { - string data = 1; + bytes data = 1; } message ExecExit { @@ -85,9 +82,9 @@ message PoweroffResult { message Message { option (buf.validate.message).oneof = { fields: [ - "hello", "init", "exec_start", "exec_stdout", "exec_stderr", "exec_exit", - "activate_config", "activate_config_result", "built_paths", "cache_drain", - "cache_drain_result", "poweroff", "poweroff_result" + "hello", "init", "exec_start", "exec_stderr", + "exec_exit", "activate_config", "activate_config_result", "built_paths", + "cache_drain", "cache_drain_result", "poweroff", "poweroff_result" ], required: true }; @@ -97,7 +94,6 @@ message Message { Hello hello = 2; Init init = 3; ExecStart exec_start = 4; - ExecStdout exec_stdout = 5; ExecStderr exec_stderr = 6; ExecExit exec_exit = 7; ActivateConfig activate_config = 8; @@ -107,4 +103,7 @@ message Message { CacheDrainResult cache_drain_result = 12; Poweroff poweroff = 13; PoweroffResult poweroff_result = 14; + + // old framed stdio fields + reserved 5, 15; } diff --git a/spindle/engines/microvm/README.md b/spindle/engines/microvm/README.md index fb9eb925..cea962b3 100644 --- a/spindle/engines/microvm/README.md +++ b/spindle/engines/microvm/README.md @@ -179,8 +179,9 @@ store path, if we've built this exact base + config combo before) and the agent builds and activates it before the user steps run. Afterwards, each step is sent as an exec request (`$shell -lc ` as an unprivileged workflow user in `/workspace/repo`, with workflow/step environment and unlocked secrets), and -stdout/stderr stream back as messages until an exit message arrives. Timeouts -are cooperative: we derive a deadline from the workflow timeout and ship it to +stdout streams back raw over a dedicated per-exec vsock connection (the +agent dials it; stdin rides the same socket), while stderr and the exit +status stay on the control channel. Timeouts are cooperative: we derive a deadline from the workflow timeout and ship it to the guest, with a little grace on the host side so the guest gets to report the timeout itself. While a step runs we also watch for the VM crashing, if it does we tail the serial (and qemu) logs into the step's stderr so you get something diff --git a/spindle/engines/microvm/agent.go b/spindle/engines/microvm/agent.go index 9204b5b2..cbf05e0e 100644 --- a/spindle/engines/microvm/agent.go +++ b/spindle/engines/microvm/agent.go @@ -107,21 +107,24 @@ func (h *agentHub) acceptLoop() { type AgentExec struct { *agentv1.ExecStart ID string + Stdin io.Reader Stdout io.Writer Stderr io.Writer } type AgentSession struct { conn net.Conn + cid uint32 enc *agentproto.Encoder dec *agentproto.Decoder l *slog.Logger mu sync.Mutex } -func NewAgentSession(conn net.Conn, l *slog.Logger) *AgentSession { +func NewAgentSession(conn net.Conn, cid uint32, l *slog.Logger) *AgentSession { return &AgentSession{ conn: conn, + cid: cid, enc: agentproto.NewEncoder(conn), dec: agentproto.NewDecoder(conn), l: l, @@ -140,6 +143,9 @@ func (s *AgentSession) Init(ctx context.Context, init *agentv1.Init) error { if helloPayload == nil { return fmt.Errorf("expected agent hello, got nil") } + if helloPayload.ProtocolVersion != agentproto.ProtocolVersion { + return fmt.Errorf("agent protocol version %d, want %d (stale guest image?)", helloPayload.ProtocolVersion, agentproto.ProtocolVersion) + } s.l.Info("agent connected", "protocol", helloPayload.ProtocolVersion, "version", helloPayload.AgentVersion, "boot", helloPayload.BootId, "nix", helloPayload.NixVersion) if err := s.enc.Encode(&agentproto.Message{ @@ -163,6 +169,13 @@ func (s *AgentSession) Exec(ctx context.Context, exec AgentExec) (int, error) { exec.ExecStart.TimeoutSeconds = timeoutSeconds(ctx, guestTimeoutGrace) } + ln, port, err := listenRandomVsockPort(ctx) + if err != nil { + return 0, fmt.Errorf("listen for exec stdio: %w", err) + } + defer ln.Close() + exec.ExecStart.StdioVsockPort = port + if err := s.enc.Encode(&agentproto.Message{ Id: exec.ID, ExecStart: exec.ExecStart, @@ -170,27 +183,37 @@ func (s *AgentSession) Exec(ctx context.Context, exec AgentExec) (int, error) { return 0, fmt.Errorf("send exec_start: %w", err) } + filtered := &cidFilteredVsockListener{Listener: ln, cid: s.cid, logger: s.l} + stdioDone := make(chan error, 1) + go func() { + stdioDone <- pumpStdio(ctx, filtered, exec.Stdin, exec.Stdout) + }() + for { msg, err := s.decode(ctx) if err != nil { + ln.Close() + <-stdioDone return 0, err } if msg.BuiltPaths == nil && msg.Id != exec.ID { continue } - if p := msg.ExecStdout; p != nil { - _, _ = io.WriteString(exec.Stdout, p.Data) - } else if p := msg.ExecStderr; p != nil { - _, _ = io.WriteString(exec.Stderr, p.Data) - } else if p := msg.BuiltPaths; p != nil { + if p := msg.BuiltPaths; p != nil { // s.l.Debug("guest built paths", "reason", p.Reason, "count", len(p.Paths)) + } else if p := msg.ExecStderr; p != nil { + _, _ = exec.Stderr.Write(p.Data) } else if p := msg.ExecExit; p != nil { var err error if p.Error != "" { s.l.Warn("guest exec error", "id", msg.Id, "error", p.Error) err = fmt.Errorf("guest exec error: %s", p.Error) } + ln.Close() // wake Accept if the guest never dialed + if err := <-stdioDone; err != nil { + return 0, err + } if p.TimedOut { return int(p.ExitCode), errGuestTimedOut } @@ -199,6 +222,49 @@ func (s *AgentSession) Exec(ctx context.Context, exec AgentExec) (int, error) { } } +func pumpStdio(ctx context.Context, ln net.Listener, stdin io.Reader, stdout io.Writer) error { + conn, err := ln.Accept() + if err != nil { + if errors.Is(err, net.ErrClosed) { + return nil // exec ended before dialing + } + return fmt.Errorf("accept guest stdio connection: %w", err) + } + vsockConn, ok := conn.(*vsock.Conn) + if !ok { + conn.Close() + return fmt.Errorf("guest connection is not a vsock connection") + } + defer vsockConn.Close() + stop := context.AfterFunc(ctx, func() { vsockConn.Close() }) + defer stop() + + stdinDone := make(chan error, 1) + go func() { + stdinDone <- writeStdin(vsockConn, stdin) + }() + + if _, err := io.Copy(stdout, conn); err != nil { + vsockConn.Close() + <-stdinDone + return fmt.Errorf("read guest stdout: %w", err) + } + if err := <-stdinDone; err != nil { + return fmt.Errorf("write guest stdin: %w", err) + } + return nil +} + +// send stdin EOF without closing stdout +func writeStdin(conn *vsock.Conn, r io.Reader) error { + if r != nil { + if _, err := io.Copy(conn, r); err != nil { + return err + } + } + return conn.CloseWrite() +} + func (s *AgentSession) ActivateConfig(ctx context.Context, id string, req *agentv1.ActivateConfig, out io.Writer) (*agentv1.ActivateConfigResult, error) { s.mu.Lock() defer s.mu.Unlock() @@ -229,11 +295,7 @@ func (s *AgentSession) ActivateConfig(ctx context.Context, id string, req *agent // s.l.Debug("guest built paths", "reason", p.Reason, "count", len(p.Paths)) } else if p := msg.ExecStderr; p != nil { if out != nil { - _, _ = io.WriteString(out, p.Data) - } - } else if p := msg.ExecStdout; p != nil { - if out != nil { - _, _ = io.WriteString(out, p.Data) + _, _ = out.Write(p.Data) } } else if p := msg.ActivateConfigResult; p != nil { if p.Error != "" { diff --git a/spindle/engines/microvm/engine.go b/spindle/engines/microvm/engine.go index 3136ecc0..7bccb4b9 100644 --- a/spindle/engines/microvm/engine.go +++ b/spindle/engines/microvm/engine.go @@ -310,7 +310,7 @@ func (e *Engine) SetupWorkflow(ctx context.Context, wid models.WorkflowId, wf *m return err } - agentSession := NewAgentSession(conn, l) + agentSession := NewAgentSession(conn, cid, l) initCtx, cancelInit := context.WithTimeout(ctx, agentHandshakeTimeout) defer cancelInit() if err := agentSession.Init(initCtx, &agentv1.Init{