1#[cfg(unix)]
13pub(crate) mod control;
14#[cfg(windows)]
15#[path = "attach/windows.rs"]
16pub(crate) mod control;
17
18use std::ffi::OsString;
24use std::future::Future;
25use std::io::Error;
26use std::path::{Path, PathBuf};
27use std::pin::Pin;
28use std::process::Stdio;
29use std::sync::Arc;
30use sha2::{Digest, Sha256};
32use tokio::io::{AsyncRead, AsyncWrite};
33use tokio::process::{Child, Command};
34use crate::StreamDeclaration;
36use crate::inventory::{Declaration, DeclarationError, Facility};
37
38#[cfg(windows)]
41const CREATE_NO_WINDOW: u32 = 0x0800_0000;
42
43type EndWork = Pin<Box<dyn Future<Output = std::io::Result<()>> + Send>>;
48type EndStart = Box<dyn FnOnce() -> EndWork + Send>;
49
50#[must_use = "retain the ending until physical settlement succeeds"]
53pub struct Ending {
54 receipt: Box<dyn End>,
55}
56
57struct Terminal {
58 start: Option<EndStart>,
59 work: Option<EndWork>,
60 result: Option<Result<(), Arc<Error>>>,
61}
62
63struct ProcessEnd {
64 child: Child,
65 signaled: bool,
66 completed: bool,
67}
68
69pub struct Attached {
70 pub(super) control: Option<crate::transport::Io>,
71 pub(super) write: Box<dyn AsyncWrite + Send + Unpin>,
72 pub(super) read: Box<dyn AsyncRead + Send + Unpin>,
73 pub(super) diagnostics: Option<Box<dyn AsyncRead + Send + Unpin>>,
74 pub(super) end: Option<Ending>,
75}
76
77#[derive(Clone, Debug)]
78pub struct Executable {
79 program: PathBuf,
80 declaration: Declaration,
81 sha256: [u8; 32],
82 arguments: Vec<OsString>,
83 directory: Option<PathBuf>,
84 environment: Vec<(OsString, OsString)>,
85}
86
87#[derive(Debug, thiserror::Error)]
90pub enum ExecutableError {
91 #[error("the sidecar executable `{path}` is not an absolute path")]
92 Relative { path: PathBuf },
93
94 #[error("the sidecar executable `{path}` cannot be read: {source}")]
95 Unreadable { path: PathBuf, source: std::io::Error },
96
97 #[error("the sidecar executable `{path}` is not a file")]
98 NotFile { path: PathBuf },
99
100 #[error("the sidecar executable `{path}` has no valid declaration: {source}")]
101 Declaration {
102 path: PathBuf,
103
104 #[source]
105 source: Box<DeclarationError>,
106 },
107}
108
109pub trait End: Send + 'static {
112 fn settle(&mut self) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>>;
113}
114
115pub trait Attach: Clone + Send + Sync + 'static {
116 type Error: std::fmt::Display + Send + 'static;
117
118 fn owner(&self) -> &str;
119
120 fn publications(&self) -> Vec<StreamDeclaration> {
121 Vec::new()
122 }
123
124 fn observers(&self) -> Vec<crate::inventory::observers::Declaration> {
125 Vec::new()
126 }
127
128 fn operations(&self) -> harmos_domain::Result<Vec<crate::operations::OperationDefinition>> {
129 Ok(Vec::new())
130 }
131
132 fn operation_contracts(&self) -> harmos_domain::Result<Option<Vec<crate::operations::Contract>>> {
133 Ok(None)
134 }
135
136 fn required_contracts(&self) -> harmos_domain::Result<Option<Vec<crate::inventory::contracts::Required>>> {
137 Ok(None)
138 }
139
140 fn sdk_lifecycle(&self) -> harmos_domain::Result<Option<[harmos_domain::model::operations::lifecycle::Descriptor; 2]>> {
141 Ok(None)
142 }
143
144 fn observe_contract(&self) -> harmos_domain::Result<Option<crate::operations::Contract>> {
145 Ok(None)
146 }
147
148 fn listener_declarations(&self) -> harmos_domain::Result<Option<Vec<harmos_domain::listeners::Definition>>> {
149 Ok(None)
150 }
151
152 fn attach(&self) -> impl Future<Output = Result<Attached, Self::Error>> + Send;
153}
154
155impl Ending {
158 pub fn new(receipt: impl End) -> Self {
159 Self { receipt: Box::new(receipt) }
160 }
161
162 pub fn terminal<F, U>(end: F) -> Self
163 where
164 F: FnOnce() -> U + Send + 'static,
165 U: Future<Output = std::io::Result<()>> + Send + 'static,
166 {
167 Self::new(Terminal {
168 start: Some(Box::new(move || Box::pin(end()))),
169 work: None,
170 result: None,
171 })
172 }
173
174 pub async fn settle(&mut self) -> std::io::Result<()> {
175 self.receipt.settle().await
176 }
177}
178
179impl End for Terminal {
180 fn settle(&mut self) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>> {
181 Box::pin(async move {
182 if let Some(result) = &self.result {
183 return result.clone().map_err(Error::other);
184 }
185 if self.work.is_none() {
186 let start = self.start.take().ok_or_else(|| Error::other("terminal ending construction did not return"))?;
187 self.work = Some(start());
188 }
189 let result = self.work.as_mut().expect("original terminal work is retained").await;
190 self.work = None;
191 self.result = Some(result.map_err(Arc::new));
192 self.result.as_ref().expect("terminal outcome is retained").clone().map_err(Error::other)
193 })
194 }
195}
196
197impl End for ProcessEnd {
198 fn settle(&mut self) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>> {
199 Box::pin(async move {
200 if self.completed {
201 return Ok(());
202 }
203 if !self.signaled {
204 self.child.start_kill()?;
205 self.signaled = true;
206 }
207 self.child.wait().await?;
208 self.completed = true;
209 Ok(())
210 })
211 }
212}
213
214impl Attached {
215 pub fn new(write: impl AsyncWrite + Send + Unpin + 'static, read: impl AsyncRead + Send + Unpin + 'static) -> Self {
216 Self {
217 control: None,
218 write: Box::new(write),
219 read: Box::new(read),
220 diagnostics: None,
221 end: None,
222 }
223 }
224
225 pub fn control(mut self, channel: crate::transport::Io) -> Self {
226 self.control = Some(channel);
227 self
228 }
229
230 pub fn diagnostics(mut self, read: impl AsyncRead + Send + Unpin + 'static) -> Self {
231 self.diagnostics = Some(Box::new(read));
232 self
233 }
234
235 pub fn ended_by<F, U>(self, end: F) -> Self
236 where
237 F: FnOnce() -> U + Send + 'static,
238 U: Future<Output = ()> + Send + 'static,
239 {
240 self.ending(Ending::terminal(move || async move {
241 end().await;
242 Ok(())
243 }))
244 }
245
246 pub fn ending(mut self, ending: Ending) -> Self {
247 self.end = Some(ending);
248 self
249 }
250
251 #[allow(clippy::type_complexity)]
252 pub fn into_parts(
253 self,
254 ) -> (
255 Box<dyn AsyncWrite + Send + Unpin>,
256 Box<dyn AsyncRead + Send + Unpin>,
257 Option<crate::transport::Io>,
258 Option<Box<dyn AsyncRead + Send + Unpin>>,
259 Option<Ending>,
260 ) {
261 (self.write, self.read, self.control, self.diagnostics, self.end)
262 }
263}
264
265impl Executable {
266 pub fn at(path: impl AsRef<Path>) -> Result<Self, ExecutableError> {
267 let program = path.as_ref().to_path_buf();
268 if !program.is_absolute() {
269 return Err(ExecutableError::Relative { path: program });
270 }
271 let held = std::fs::metadata(&program).map_err(|source| ExecutableError::Unreadable { path: program.clone(), source })?;
272 if !held.is_file() {
273 return Err(ExecutableError::NotFile { path: program });
274 }
275 let bytes = std::fs::read(&program).map_err(|source| ExecutableError::Unreadable { path: program.clone(), source })?;
276 let declaration = Declaration::inspect(&bytes, Facility::Sidecar).map_err(|source| ExecutableError::Declaration {
277 path: program.clone(),
278 source: Box::new(source),
279 })?;
280 Ok(Self {
281 program,
282 declaration,
283 sha256: Sha256::digest(&bytes).into(),
284 arguments: Vec::new(),
285 directory: None,
286 environment: Vec::new(),
287 })
288 }
289
290 pub fn sha256(&self) -> [u8; 32] {
291 self.sha256
292 }
293
294 pub fn declaration(&self) -> &Declaration {
295 &self.declaration
296 }
297
298 pub fn path(&self) -> &Path {
299 &self.program
300 }
301
302 pub fn arguments(&self) -> &[OsString] {
303 &self.arguments
304 }
305
306 pub fn working_directory(&self) -> Option<&Path> {
307 self.directory.as_deref()
308 }
309
310 pub fn environment(&self) -> &[(OsString, OsString)] {
311 &self.environment
312 }
313
314 pub fn argument(mut self, argument: impl Into<OsString>) -> Self {
315 self.arguments.push(argument.into());
316 self
317 }
318
319 pub fn directory(mut self, path: impl Into<PathBuf>) -> Self {
320 self.directory = Some(path.into());
321 self
322 }
323
324 pub fn variable(mut self, name: impl Into<OsString>, value: impl Into<OsString>) -> Self {
325 self.environment.push((name.into(), value.into()));
326 self
327 }
328}
329
330impl Attach for Executable {
331 type Error = Error;
332
333 fn owner(&self) -> &str {
334 &self.declaration.id
335 }
336
337 fn publications(&self) -> Vec<StreamDeclaration> {
338 self.declaration.streams.clone()
339 }
340
341 fn observers(&self) -> Vec<crate::inventory::observers::Declaration> {
342 self.declaration.observers.clone()
343 }
344
345 fn operations(&self) -> harmos_domain::Result<Vec<crate::operations::OperationDefinition>> {
346 Ok(self.declaration.operations.clone())
347 }
348
349 fn operation_contracts(&self) -> harmos_domain::Result<Option<Vec<crate::operations::Contract>>> {
350 Ok(Some(self.declaration.contracts.provides.clone()))
351 }
352
353 fn required_contracts(&self) -> harmos_domain::Result<Option<Vec<crate::inventory::contracts::Required>>> {
354 Ok(Some(self.declaration.contracts.requires.clone()))
355 }
356
357 fn sdk_lifecycle(&self) -> harmos_domain::Result<Option<[harmos_domain::model::operations::lifecycle::Descriptor; 2]>> {
358 Ok(Some(self.declaration.contracts.lifecycle.clone()))
359 }
360
361 fn observe_contract(&self) -> harmos_domain::Result<Option<crate::operations::Contract>> {
362 Ok(self.declaration.observe_contract())
363 }
364
365 fn listener_declarations(&self) -> harmos_domain::Result<Option<Vec<harmos_domain::listeners::Definition>>> {
366 Ok(Some(self.declaration.listeners.clone()))
367 }
368
369 async fn attach(&self) -> Result<Attached, Error> {
370 let mut command = command(&self.program, &self.arguments, self.directory.as_deref(), &self.environment)?;
371
372 #[cfg(any(unix, windows))]
373 let control = control::prepare(&mut command)?;
374 let mut child = command.spawn()?;
375 tracing::debug!(target: "harmos::carrier", provider = %self.declaration.id, remote_pid = child.id().expect("a spawned child has its process id"), "sidecar attached");
376 drop(command);
377 #[cfg(windows)]
378 let control = control.io(child.id().expect("a spawned child has its process id"));
379 let write = child.stdin.take().ok_or_else(|| Error::other("the sidecar was started without a standard input"))?;
380 let read = child.stdout.take().ok_or_else(|| Error::other("the sidecar was started without a standard output"))?;
381 let diagnostics = child.stderr.take();
382
383 let ending = Ending::new(ProcessEnd { child, signaled: false, completed: false });
384 let mut attached = Attached::new(write, read).ending(ending);
385 #[cfg(any(unix, windows))]
386 {
387 attached = attached.control(control);
388 }
389 if let Some(diagnostics) = diagnostics {
390 attached = attached.diagnostics(diagnostics);
391 }
392 Ok(attached)
393 }
394}
395
396fn command(
401 program: &Path, arguments: &[OsString], directory: Option<&Path>, environment: &[(OsString, OsString)],
402) -> Result<Command, Error> {
403 let mut command = Command::new(program);
404 command.args(arguments).env_clear().stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).kill_on_drop(true);
405 #[cfg(windows)]
406 {
407 command.creation_flags(CREATE_NO_WINDOW);
408 command.env("SystemRoot", system_root()?);
409 let temporary = std::env::temp_dir();
410 command.env("TMP", &temporary).env("TEMP", &temporary);
411 }
412 if !cfg!(windows)
413 && let Some(tmpdir) = std::env::var_os("TMPDIR").filter(|value| !value.is_empty())
414 {
415 command.env("TMPDIR", tmpdir);
416 }
417 command.envs(environment.iter().cloned());
418 if let Some(directory) = directory {
419 command.current_dir(directory);
420 }
421
422 Ok(command)
423}
424
425#[cfg(windows)]
426fn system_root() -> Result<OsString, Error> {
427 use std::os::windows::ffi::OsStringExt;
428
429 use windows_sys::Win32::System::SystemInformation::GetSystemWindowsDirectoryW;
430
431 if let Some(root) = std::env::var_os("SystemRoot").filter(|value| !value.is_empty()) {
432 return Ok(root);
433 }
434
435 let mut buffer = vec![0_u16; 260];
436 loop {
437 #[allow(unsafe_code)]
440 let length = unsafe { GetSystemWindowsDirectoryW(buffer.as_mut_ptr(), buffer.len() as u32) };
441 if length == 0 {
442 return Err(Error::last_os_error());
443 }
444 if (length as usize) < buffer.len() {
445 return Ok(OsString::from_wide(&buffer[..length as usize]));
446 }
447 buffer.resize(length as usize, 0);
448 }
449}
450
451#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn command_configuration_preserved() {
459 let arguments = [OsString::from("--plugin")];
460 let directory = Path::new("work");
461 let environment = [
462 (OsString::from("HARMOS_EXPLICIT"), OsString::from("kept")),
463 (OsString::from("TMPDIR"), OsString::from("chosen-unix-temp")),
464 (OsString::from("TMP"), OsString::from("chosen-windows-temp")),
465 (OsString::from("TEMP"), OsString::from("chosen-windows-temp")),
466 ];
467 let configured = command(Path::new("sidecar"), &arguments, Some(directory), &environment).unwrap();
468 let configured = configured.as_std();
469 assert_eq!(configured.get_program(), "sidecar");
470 assert_eq!(configured.get_args().collect::<Vec<_>>(), [arguments[0].as_os_str()]);
471 assert_eq!(configured.get_current_dir(), Some(directory));
472 let variables = configured.get_envs().collect::<std::collections::BTreeMap<_, _>>();
473 for (name, value) in &environment {
474 assert_eq!(variables.get(name.as_os_str()), Some(&Some(value.as_os_str())));
475 }
476 assert!(!variables.contains_key(std::ffi::OsStr::new("PATH")));
477 #[cfg(windows)]
478 assert!(variables.contains_key(std::ffi::OsStr::new("SystemRoot")));
479 }
480}