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::Definition>> {
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<[crate::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 { start: Some(Box::new(move || Box::pin(end()))), work: None, result: None })
168 }
169
170 pub async fn settle(&mut self) -> std::io::Result<()> {
171 self.receipt.settle().await
172 }
173}
174
175impl End for Terminal {
176 fn settle(&mut self) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>> {
177 Box::pin(async move {
178 if let Some(result) = &self.result {
179 return result.clone().map_err(Error::other);
180 }
181 if self.work.is_none() {
182 let start = self.start.take().ok_or_else(|| Error::other("terminal ending construction did not return"))?;
183 self.work = Some(start());
184 }
185 let result = self.work.as_mut().expect("original terminal work is retained").await;
186 self.work = None;
187 self.result = Some(result.map_err(Arc::new));
188 self.result.as_ref().expect("terminal outcome is retained").clone().map_err(Error::other)
189 })
190 }
191}
192
193impl End for ProcessEnd {
194 fn settle(&mut self) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>> {
195 Box::pin(async move {
196 if self.completed {
197 return Ok(());
198 }
199 if !self.signaled {
200 self.child.start_kill()?;
201 self.signaled = true;
202 }
203 self.child.wait().await?;
204 self.completed = true;
205 Ok(())
206 })
207 }
208}
209
210impl Attached {
211 pub fn new(write: impl AsyncWrite + Send + Unpin + 'static, read: impl AsyncRead + Send + Unpin + 'static) -> Self {
212 Self { control: None, write: Box::new(write), read: Box::new(read), diagnostics: None, end: None }
213 }
214
215 pub fn control(mut self, channel: crate::transport::Io) -> Self {
216 self.control = Some(channel);
217 self
218 }
219
220 pub fn diagnostics(mut self, read: impl AsyncRead + Send + Unpin + 'static) -> Self {
221 self.diagnostics = Some(Box::new(read));
222 self
223 }
224
225 pub fn ended_by<F, U>(self, end: F) -> Self
226 where
227 F: FnOnce() -> U + Send + 'static,
228 U: Future<Output = ()> + Send + 'static,
229 {
230 self.ending(Ending::terminal(move || async move {
231 end().await;
232 Ok(())
233 }))
234 }
235
236 pub fn ending(mut self, ending: Ending) -> Self {
237 self.end = Some(ending);
238 self
239 }
240
241 #[allow(clippy::type_complexity)]
242 pub fn into_parts(
243 self,
244 ) -> (
245 Box<dyn AsyncWrite + Send + Unpin>,
246 Box<dyn AsyncRead + Send + Unpin>,
247 Option<crate::transport::Io>,
248 Option<Box<dyn AsyncRead + Send + Unpin>>,
249 Option<Ending>,
250 ) {
251 (self.write, self.read, self.control, self.diagnostics, self.end)
252 }
253}
254
255impl Executable {
256 pub fn at(path: impl AsRef<Path>) -> Result<Self, ExecutableError> {
257 let program = path.as_ref().to_path_buf();
258 if !program.is_absolute() {
259 return Err(ExecutableError::Relative { path: program });
260 }
261 let held = std::fs::metadata(&program).map_err(|source| ExecutableError::Unreadable { path: program.clone(), source })?;
262 if !held.is_file() {
263 return Err(ExecutableError::NotFile { path: program });
264 }
265 let bytes = std::fs::read(&program).map_err(|source| ExecutableError::Unreadable { path: program.clone(), source })?;
266 let declaration = Declaration::inspect(&bytes, Facility::Sidecar)
267 .map_err(|source| ExecutableError::Declaration { path: program.clone(), source: Box::new(source) })?;
268 Ok(Self {
269 program,
270 declaration,
271 sha256: Sha256::digest(&bytes).into(),
272 arguments: Vec::new(),
273 directory: None,
274 environment: Vec::new(),
275 })
276 }
277
278 pub fn sha256(&self) -> [u8; 32] {
279 self.sha256
280 }
281
282 pub fn declaration(&self) -> &Declaration {
283 &self.declaration
284 }
285
286 pub fn path(&self) -> &Path {
287 &self.program
288 }
289
290 pub fn arguments(&self) -> &[OsString] {
291 &self.arguments
292 }
293
294 pub fn working_directory(&self) -> Option<&Path> {
295 self.directory.as_deref()
296 }
297
298 pub fn environment(&self) -> &[(OsString, OsString)] {
299 &self.environment
300 }
301
302 pub fn argument(mut self, argument: impl Into<OsString>) -> Self {
303 self.arguments.push(argument.into());
304 self
305 }
306
307 pub fn directory(mut self, path: impl Into<PathBuf>) -> Self {
308 self.directory = Some(path.into());
309 self
310 }
311
312 pub fn variable(mut self, name: impl Into<OsString>, value: impl Into<OsString>) -> Self {
313 self.environment.push((name.into(), value.into()));
314 self
315 }
316}
317
318impl Attach for Executable {
319 type Error = Error;
320
321 fn owner(&self) -> &str {
322 &self.declaration.id
323 }
324
325 fn publications(&self) -> Vec<StreamDeclaration> {
326 self.declaration.streams.clone()
327 }
328
329 fn observers(&self) -> Vec<crate::inventory::observers::Declaration> {
330 self.declaration.observers.clone()
331 }
332
333 fn operations(&self) -> harmos_domain::Result<Vec<crate::operations::Definition>> {
334 Ok(self.declaration.operations.clone())
335 }
336
337 fn operation_contracts(&self) -> harmos_domain::Result<Option<Vec<crate::operations::Contract>>> {
338 Ok(Some(self.declaration.contracts.provides.clone()))
339 }
340
341 fn required_contracts(&self) -> harmos_domain::Result<Option<Vec<crate::inventory::contracts::Required>>> {
342 Ok(Some(self.declaration.contracts.requires.clone()))
343 }
344
345 fn sdk_lifecycle(&self) -> harmos_domain::Result<Option<[crate::operations::lifecycle::Descriptor; 2]>> {
346 Ok(Some(self.declaration.contracts.lifecycle.clone()))
347 }
348
349 fn observe_contract(&self) -> harmos_domain::Result<Option<crate::operations::Contract>> {
350 Ok(self.declaration.observe_contract())
351 }
352
353 fn listener_declarations(&self) -> harmos_domain::Result<Option<Vec<harmos_domain::listeners::Definition>>> {
354 Ok(Some(self.declaration.listeners.clone()))
355 }
356
357 async fn attach(&self) -> Result<Attached, Error> {
358 let mut command = command(&self.program, &self.arguments, self.directory.as_deref(), &self.environment)?;
359
360 #[cfg(any(unix, windows))]
361 let control = control::prepare(&mut command)?;
362 let mut child = command.spawn()?;
363 drop(command);
364 #[cfg(windows)]
365 let control = control.io(child.id().expect("a spawned child has its process id"));
366 let write = child.stdin.take().ok_or_else(|| Error::other("the sidecar was started without a standard input"))?;
367 let read = child.stdout.take().ok_or_else(|| Error::other("the sidecar was started without a standard output"))?;
368 let diagnostics = child.stderr.take();
369
370 let ending = Ending::new(ProcessEnd { child, signaled: false, completed: false });
371 let mut attached = Attached::new(write, read).ending(ending);
372 #[cfg(any(unix, windows))]
373 {
374 attached = attached.control(control);
375 }
376 if let Some(diagnostics) = diagnostics {
377 attached = attached.diagnostics(diagnostics);
378 }
379 Ok(attached)
380 }
381}
382
383fn command(
388 program: &Path, arguments: &[OsString], directory: Option<&Path>, environment: &[(OsString, OsString)],
389) -> Result<Command, Error> {
390 let mut command = Command::new(program);
391 command.args(arguments).env_clear().stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).kill_on_drop(true);
392 #[cfg(windows)]
393 {
394 command.creation_flags(CREATE_NO_WINDOW);
395 command.env("SystemRoot", system_root()?);
396 let temporary = std::env::temp_dir();
397 command.env("TMP", &temporary).env("TEMP", &temporary);
398 }
399 if !cfg!(windows)
400 && let Some(tmpdir) = std::env::var_os("TMPDIR").filter(|value| !value.is_empty())
401 {
402 command.env("TMPDIR", tmpdir);
403 }
404 command.envs(environment.iter().cloned());
405 if let Some(directory) = directory {
406 command.current_dir(directory);
407 }
408
409 Ok(command)
410}
411
412#[cfg(windows)]
413fn system_root() -> Result<OsString, Error> {
414 use std::os::windows::ffi::OsStringExt;
415
416 use windows_sys::Win32::System::SystemInformation::GetSystemWindowsDirectoryW;
417
418 if let Some(root) = std::env::var_os("SystemRoot").filter(|value| !value.is_empty()) {
419 return Ok(root);
420 }
421
422 let mut buffer = vec![0_u16; 260];
423 loop {
424 #[allow(unsafe_code)]
427 let length = unsafe { GetSystemWindowsDirectoryW(buffer.as_mut_ptr(), buffer.len() as u32) };
428 if length == 0 {
429 return Err(Error::last_os_error());
430 }
431 if (length as usize) < buffer.len() {
432 return Ok(OsString::from_wide(&buffer[..length as usize]));
433 }
434 buffer.resize(length as usize, 0);
435 }
436}
437
438#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn command_configuration_preserved() {
446 let arguments = [OsString::from("--plugin")];
447 let directory = Path::new("work");
448 let environment = [
449 (OsString::from("HARMOS_EXPLICIT"), OsString::from("kept")),
450 (OsString::from("TMPDIR"), OsString::from("chosen-unix-temp")),
451 (OsString::from("TMP"), OsString::from("chosen-windows-temp")),
452 (OsString::from("TEMP"), OsString::from("chosen-windows-temp")),
453 ];
454 let configured = command(Path::new("sidecar"), &arguments, Some(directory), &environment).unwrap();
455 let configured = configured.as_std();
456 assert_eq!(configured.get_program(), "sidecar");
457 assert_eq!(configured.get_args().collect::<Vec<_>>(), [arguments[0].as_os_str()]);
458 assert_eq!(configured.get_current_dir(), Some(directory));
459 let variables = configured.get_envs().collect::<std::collections::BTreeMap<_, _>>();
460 for (name, value) in &environment {
461 assert_eq!(variables.get(name.as_os_str()), Some(&Some(value.as_os_str())));
462 }
463 assert!(!variables.contains_key(std::ffi::OsStr::new("PATH")));
464 #[cfg(windows)]
465 assert!(variables.contains_key(std::ffi::OsStr::new("SystemRoot")));
466 }
467}