1use std::fmt;
4use std::fs::{self, File, Metadata, OpenOptions};
5use std::io::{self, Read, Write};
6use std::num::NonZeroU32;
7use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
8use std::os::unix::net::SocketAddr;
9use std::path::{Component, Path, PathBuf};
10use std::sync::atomic::{AtomicU64, Ordering};
11
12use serde::{Deserialize, Serialize};
13use sha2::{Digest, Sha256};
14
15use super::runtime::{self, Endpoint};
16
17const MOST_BYTES: u64 = 65_536;
18static TEMPORARIES: AtomicU64 = AtomicU64::new(0);
19
20#[derive(Debug)]
21pub struct Refusal {
22 pub code: &'static str,
23 detail: String,
24}
25
26impl Refusal {
27 pub(super) fn new(code: &'static str, detail: impl fmt::Display) -> Self {
28 Self {
29 code,
30 detail: detail.to_string(),
31 }
32 }
33}
34
35impl fmt::Display for Refusal {
36 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37 write!(formatter, "{}: {}", self.code, self.detail)
38 }
39}
40
41impl std::error::Error for Refusal {}
42
43#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
44#[serde(deny_unknown_fields)]
45pub struct Entry {
46 pub name: String,
47 pub socket: PathBuf,
48 pub data_dir: PathBuf,
49 pub pid: NonZeroU32,
50 pub version: String,
51}
52
53impl Entry {
54 pub fn current(endpoint: &Endpoint) -> Result<Self, Refusal> {
59 let data_dir = fs::canonicalize(&endpoint.data_dir)
60 .map_err(|error| Refusal::new("registry_publication_failed", error))?;
61 if !fs::metadata(&data_dir)
62 .map_err(|error| Refusal::new("registry_publication_failed", error))?
63 .is_dir()
64 {
65 return Err(Refusal::new(
66 "registry_publication_failed",
67 "data location is not a directory",
68 ));
69 }
70 let pid = NonZeroU32::new(std::process::id())
71 .ok_or_else(|| Refusal::new("registry_publication_failed", "process id is zero"))?;
72 let entry = Self {
73 name: endpoint.name.clone(),
74 socket: endpoint.socket.clone(),
75 data_dir,
76 pid,
77 version: env!("CARGO_PKG_VERSION").to_owned(),
78 };
79 entry
80 .validate()
81 .map_err(|error| Refusal::new("registry_publication_failed", error))?;
82 Ok(entry)
83 }
84
85 fn validate(&self) -> Result<(), &'static str> {
86 if self.name.is_empty()
87 || self.name.len() > 128
88 || matches!(self.name.as_str(), "." | "..")
89 || self.name.contains(['/', '\\', '\0'])
90 {
91 return Err("invalid logical store name");
92 }
93 if !valid_path(&self.socket) || SocketAddr::from_pathname(&self.socket).is_err() {
94 return Err("invalid Unix endpoint");
95 }
96 if !valid_path(&self.data_dir)
97 || self.data_dir.file_name().is_none()
98 || self
99 .data_dir
100 .components()
101 .any(|part| !matches!(part, Component::RootDir | Component::Normal(_)))
102 {
103 return Err("invalid canonical data directory");
104 }
105 if self.version.is_empty()
106 || self.version.len() > 128
107 || !self
108 .version
109 .bytes()
110 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'+'))
111 {
112 return Err("invalid haematite version");
113 }
114 Ok(())
115 }
116}
117
118fn valid_path(path: &Path) -> bool {
119 path.is_absolute() && path.to_str().is_some_and(|value| !value.contains('\0'))
120}
121
122fn entry_path(directory: &Path, name: &str) -> PathBuf {
123 const HEX: &[u8; 16] = b"0123456789abcdef";
124 let digest = Sha256::digest(name.as_bytes());
125 let mut filename = String::with_capacity(69);
126 for byte in digest {
127 filename.push(char::from(HEX[usize::from(byte >> 4)]));
128 filename.push(char::from(HEX[usize::from(byte & 15)]));
129 }
130 filename.push_str(".json");
131 directory.join(filename)
132}
133
134fn private_directory(directory: &Path) -> io::Result<Metadata> {
135 let metadata = fs::symlink_metadata(directory)?;
136 if !directory.is_absolute()
137 || !metadata.is_dir()
138 || metadata.permissions().mode() & 0o7777 != 0o700
139 {
140 return Err(io::Error::new(
141 io::ErrorKind::PermissionDenied,
142 "registry directory must be a non-symlink absolute mode-0700 directory",
143 ));
144 }
145 Ok(metadata)
146}
147
148pub fn read(path: &Path) -> Result<Entry, Refusal> {
153 let refused = |error: &dyn fmt::Display| {
154 Refusal::new(
155 "invalid_registry_entry",
156 format!("{}: {error}", path.display()),
157 )
158 };
159 let parent = path
160 .parent()
161 .ok_or_else(|| refused(&"entry has no parent"))?;
162 let directory = private_directory(parent).map_err(|error| refused(&error))?;
163 let named = fs::symlink_metadata(path).map_err(|error| refused(&error))?;
164 if !named.is_file() {
165 return Err(refused(&"entry is not a regular file"));
166 }
167 let mut file = File::open(path).map_err(|error| refused(&error))?;
168 let metadata = file.metadata().map_err(|error| refused(&error))?;
169 if !metadata.is_file()
170 || metadata.uid() != directory.uid()
171 || metadata.permissions().mode() & 0o7777 != 0o600
172 || metadata.len() > MOST_BYTES
173 {
174 return Err(refused(
175 &"entry must be an owned mode-0600 regular file within the size bound",
176 ));
177 }
178 let mut bytes = Vec::new();
179 Read::by_ref(&mut file)
180 .take(MOST_BYTES + 1)
181 .read_to_end(&mut bytes)
182 .map_err(|error| refused(&error))?;
183 if u64::try_from(bytes.len()).map_err(|error| refused(&error))? > MOST_BYTES {
184 return Err(refused(&"entry exceeds the size bound"));
185 }
186 let entry: Entry = serde_json::from_slice(&bytes).map_err(|error| refused(&error))?;
187 entry.validate().map_err(|error| refused(&error))?;
188 if path != entry_path(parent, &entry.name) {
189 return Err(refused(&"entry name does not match its filename"));
190 }
191 Ok(entry)
192}
193
194pub fn read_directory(directory: &Path) -> Result<Vec<Entry>, Refusal> {
199 private_directory(directory).map_err(|error| {
200 Refusal::new(
201 "registry_unavailable",
202 format!("{}: {error}", directory.display()),
203 )
204 })?;
205 let entries = fs::read_dir(directory).map_err(|error| {
206 Refusal::new(
207 "registry_unavailable",
208 format!("{}: {error}", directory.display()),
209 )
210 })?;
211 let mut records = Vec::new();
212 for entry in entries {
213 let entry = entry.map_err(|error| Refusal::new("registry_unavailable", error))?;
214 let path = entry.path();
215 if path
216 .extension()
217 .is_some_and(|extension| extension == "json")
218 {
219 records.push(read(&path)?);
220 }
221 }
222 records.sort_by(|left, right| left.name.cmp(&right.name));
223 Ok(records)
224}
225
226struct Prepared {
227 temporary: Option<PathBuf>,
228 destination: PathBuf,
229 identity: (u64, u64),
230 entry: Entry,
231}
232
233impl Prepared {
234 fn new(directory: &Path, entry: Entry) -> Result<Self, Refusal> {
235 let error = |cause: &dyn fmt::Display| {
236 Refusal::new(
237 "registry_publication_failed",
238 format!("{}: {cause}", directory.display()),
239 )
240 };
241 entry.validate().map_err(|cause| error(&cause))?;
242 let owner = private_directory(directory)
243 .map_err(|cause| error(&cause))?
244 .uid();
245 let bytes = serde_json::to_vec(&entry).map_err(|cause| error(&cause))?;
246 if u64::try_from(bytes.len()).map_err(|cause| error(&cause))? > MOST_BYTES {
247 return Err(error(&"entry exceeds size bound"));
248 }
249 let sequence = TEMPORARIES
250 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
251 value.checked_add(1)
252 })
253 .map_err(|cause| error(&cause))?;
254 let temporary = directory.join(format!(".entry-{}-{sequence}", std::process::id()));
255 let mut file = OpenOptions::new()
256 .write(true)
257 .create_new(true)
258 .mode(0o600)
259 .open(&temporary)
260 .map_err(|cause| error(&cause))?;
261 let metadata = match file.metadata() {
262 Ok(metadata) => metadata,
263 Err(cause) => {
264 drop(file);
265 return Err(match fs::remove_file(&temporary) {
266 Ok(()) => error(&cause),
267 Err(cleanup) => error(&format!(
268 "metadata: {cause}; registry_cleanup_failed: {cleanup}"
269 )),
270 });
271 }
272 };
273 let mut prepared = Self {
274 temporary: Some(temporary),
275 destination: entry_path(directory, &entry.name),
276 identity: (metadata.dev(), metadata.ino()),
277 entry,
278 };
279 let written = if metadata.uid() == owner
280 && metadata.is_file()
281 && metadata.permissions().mode() & 0o7777 == 0o600
282 {
283 file.write_all(&bytes)
284 } else {
285 Err(io::Error::new(
286 io::ErrorKind::PermissionDenied,
287 "temporary entry ownership or mode is invalid",
288 ))
289 };
290 drop(file);
291 if let Err(cause) = written {
292 return Err(match prepared.clean_temporary() {
293 Ok(()) => error(&cause),
294 Err(cleanup) => error(&format!("write: {cause}; {cleanup}")),
295 });
296 }
297 Ok(prepared)
298 }
299
300 fn clean_temporary(&mut self) -> Result<(), Refusal> {
301 if let Some(path) = self.temporary.take() {
302 remove_same(&path, self.identity)?;
303 }
304 Ok(())
305 }
306
307 fn publish(mut self) -> Result<Publication, Refusal> {
308 let temporary = self.temporary.as_ref().ok_or_else(|| {
309 Refusal::new("registry_publication_failed", "temporary entry is absent")
310 })?;
311 if let Err(cause) = fs::rename(temporary, &self.destination) {
312 return Err(match self.clean_temporary() {
313 Ok(()) => Refusal::new("registry_publication_failed", cause),
314 Err(cleanup) => Refusal::new(
315 "registry_publication_failed",
316 format!("rename: {cause}; {cleanup}"),
317 ),
318 });
319 }
320 self.temporary.take();
321 Ok(Publication {
323 path: self.destination.clone(),
324 identity: self.identity,
325 entry: self.entry.clone(),
326 active: true,
327 })
328 }
329}
330
331impl Drop for Prepared {
332 fn drop(&mut self) {
333 if let Err(error) = self.clean_temporary() {
334 eprintln!("{error}");
335 }
336 }
337}
338
339fn remove_same(path: &Path, identity: (u64, u64)) -> Result<(), Refusal> {
340 let refused = |cause: &dyn fmt::Display| {
341 Refusal::new(
342 "registry_cleanup_failed",
343 format!("{}: {cause}", path.display()),
344 )
345 };
346 let metadata = fs::symlink_metadata(path).map_err(|cause| refused(&cause))?;
347 if !metadata.is_file() || (metadata.dev(), metadata.ino()) != identity {
348 return Err(refused(&"owned entry was replaced"));
349 }
350 fs::remove_file(path).map_err(|cause| refused(&cause))
351}
352
353pub(super) struct Publication {
354 path: PathBuf,
355 identity: (u64, u64),
356 entry: Entry,
357 active: bool,
358}
359
360impl Publication {
361 pub(super) fn publish(endpoint: &Endpoint) -> Result<Self, Refusal> {
362 let namespace = namespace(endpoint)
363 .map_err(|cause| Refusal::new("registry_publication_failed", cause))?;
364 let directory = namespace.socket.parent().ok_or_else(|| {
365 Refusal::new(
366 "registry_publication_failed",
367 "runtime endpoint has no parent",
368 )
369 })?;
370 Self::publish_in(directory, Entry::current(endpoint)?)
371 }
372
373 pub(super) fn publish_in(directory: &Path, entry: Entry) -> Result<Self, Refusal> {
374 Prepared::new(directory, entry)?.publish()
375 }
376
377 pub(super) fn finish(&mut self) -> Result<(), Refusal> {
378 if !self.active {
379 return Ok(());
380 }
381 self.active = false;
382 let actual =
383 read(&self.path).map_err(|cause| Refusal::new("registry_cleanup_failed", cause))?;
384 if actual != self.entry {
385 return Err(Refusal::new(
386 "registry_cleanup_failed",
387 "owned entry contents changed",
388 ));
389 }
390 remove_same(&self.path, self.identity)
391 }
392}
393
394impl Drop for Publication {
395 fn drop(&mut self) {
396 if let Err(error) = self.finish() {
397 eprintln!("{error}");
398 }
399 }
400}
401
402fn namespace(endpoint: &Endpoint) -> Result<Endpoint, runtime::Refusal> {
403 #[cfg(test)]
404 if let Some((root, uid)) = super::cli::endpoint_tests::input() {
405 return runtime::resolve_input(
406 &endpoint.data_dir,
407 Some(&endpoint.name),
408 None,
409 false,
410 &root,
411 uid,
412 );
413 }
414 runtime::resolve(&endpoint.data_dir, Some(&endpoint.name), None, false)
415}
416
417#[cfg(test)]
418mod tests {
419 use std::error::Error;
420 use std::fs;
421 use std::io::{self, BufRead, Read, Write};
422 use std::os::unix::fs::PermissionsExt;
423 use std::path::Path;
424 use std::process::{Child, ChildStdout, Command, Stdio};
425
426 use super::{Entry, Prepared, Publication, entry_path, read, read_directory};
427 use crate::serve::cli::endpoint_tests::{Fixture, with_fixture};
428 use crate::serve::runtime::Endpoint;
429 use crate::serve::transport::Listener;
430
431 type TestResult = Result<(), Box<dyn Error>>;
432
433 fn endpoint(root: &Path, name: &str) -> Endpoint {
434 Endpoint {
435 name: name.to_owned(),
436 socket: root.join(format!("{name}.sock")),
437 data_dir: root.join("database"),
438 }
439 }
440
441 fn directory(fixture: &Fixture) -> Result<std::path::PathBuf, io::Error> {
442 let directory = fixture.directory.path().join("haem");
443 fs::create_dir(&directory)?;
444 fs::set_permissions(&directory, fs::Permissions::from_mode(0o700))?;
445 Ok(directory)
446 }
447
448 #[test]
449 fn publication_precedes_readiness_and_contains_the_five_fields() -> TestResult {
450 with_fixture(|fixture| {
451 let directory = directory(fixture)?;
452 let endpoint = endpoint(fixture.directory.path(), "ready");
453 let mut listener = Listener::bind(&endpoint.socket)?;
454 listener.publish_in(&directory, Entry::current(&endpoint)?)?;
455 let entry = read(&entry_path(&directory, &endpoint.name))?;
456 assert_eq!(entry.name, endpoint.name);
457 assert_eq!(entry.socket, endpoint.socket);
458 assert_eq!(entry.data_dir, fs::canonicalize(&fixture.data_dir)?);
459 assert_eq!(entry.pid.get(), std::process::id());
460 assert_eq!(entry.version, env!("CARGO_PKG_VERSION"));
461 let path = entry_path(&directory, &endpoint.name);
462 let value: serde_json::Value = serde_json::from_slice(&fs::read(&path)?)?;
463 assert_eq!(value.as_object().ok_or("entry is not an object")?.len(), 5);
464 assert_eq!(fs::metadata(&path)?.permissions().mode() & 0o7777, 0o600);
465 listener.finish()?;
466 assert!(!path.try_exists()?);
467 assert!(Listener::bind(&endpoint.socket).is_err());
468 drop(listener);
469 drop(Listener::bind(&endpoint.socket)?);
470 Ok(())
471 })
472 }
473
474 #[test]
475 fn a_reader_sees_the_complete_prior_entry_until_atomic_rename() -> TestResult {
476 with_fixture(|fixture| {
477 let directory = directory(fixture)?;
478 let endpoint = endpoint(fixture.directory.path(), "atomic");
479 let prior = Entry::current(&endpoint)?;
480 let path = entry_path(&directory, &prior.name);
481 fs::write(&path, serde_json::to_vec(&prior)?)?;
482 fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
483 let mut replacement = prior.clone();
484 replacement.version = "0.1.1".to_owned();
485 let pending = Prepared::new(&directory, replacement.clone())?;
486 assert_eq!(read(&path)?, prior);
487 let mut publication = pending.publish()?;
488 assert_eq!(read(&path)?, replacement);
489 assert_eq!(read_directory(&directory)?, vec![replacement]);
490 publication.finish()?;
491 Ok(())
492 })
493 }
494
495 #[test]
496 fn clean_exit_removes_only_our_entry_and_kill_leaves_it_complete() -> TestResult {
497 with_fixture(|fixture| {
498 let directory = directory(fixture)?;
499 let other = Entry::current(&endpoint(fixture.directory.path(), "other"))?;
500 let mut other_owner = Publication::publish_in(&directory, other.clone())?;
501 let child_path = entry_path(&directory, "child");
502 let child_socket = fixture.directory.path().join("child.sock");
503 let mut clean = Process::start(fixture.directory.path())?;
504 let clean_entry = read(&child_path)?;
505 assert_eq!(clean_entry.pid.get(), clean.pid()?);
506 assert!(Listener::bind(&child_socket).is_err());
507 clean.finish()?;
508 assert!(!child_path.try_exists()?);
509 assert_eq!(read(&entry_path(&directory, "other"))?, other);
510 drop(Listener::bind(&child_socket)?);
511 let mut killed = Process::start(fixture.directory.path())?;
512 let last = read(&child_path)?;
513 killed.kill()?;
514 assert_eq!(read(&child_path)?, last);
515 drop(Listener::bind(&child_socket)?);
516 assert_eq!(read(&entry_path(&directory, "other"))?, other);
517 other_owner.finish()?;
518 Ok(())
519 })
520 }
521
522 #[test]
523 fn publication_and_cleanup_failures_are_named_and_preserve_replacements() -> TestResult {
524 with_fixture(|fixture| {
525 let directory = directory(fixture)?;
526 let endpoint = endpoint(fixture.directory.path(), "failure");
527 let entry = Entry::current(&endpoint)?;
528 let path = entry_path(&directory, &entry.name);
529 fs::create_dir(&path)?;
530 let mut listener = Listener::bind(&endpoint.socket)?;
531 let mut ready = false;
532 let result = listener.publish_in(&directory, entry.clone()).map(|()| {
533 ready = true;
534 });
535 let refused = result.err().ok_or("publication unexpectedly succeeded")?;
536 assert_eq!(refused.code, "registry_publication_failed");
537 assert!(!ready);
538 assert!(path.is_dir());
539 assert_eq!(fs::read_dir(&directory)?.count(), 1);
540 fs::remove_dir(&path)?;
541 listener.publish_in(&directory, entry)?;
542 let saved = directory.join("saved");
543 fs::rename(&path, &saved)?;
544 fs::create_dir(&path)?;
545 let refused = listener.finish().err().ok_or("replacement was removed")?;
546 assert_eq!(refused.code, "registry_cleanup_failed");
547 assert!(path.is_dir());
548 assert!(saved.try_exists()?);
549 fs::remove_dir(&path)?;
550 fs::rename(&saved, &path)?;
551 fs::remove_file(&path)?;
552 Ok(())
553 })
554 }
555
556 #[test]
557 fn malformed_entries_are_refused_at_the_reader_boundary() -> TestResult {
558 with_fixture(|fixture| {
559 let directory = directory(fixture)?;
560 let entry = Entry::current(&endpoint(fixture.directory.path(), "validate"))?;
561 let path = entry_path(&directory, &entry.name);
562 let valid = serde_json::to_value(entry)?;
563 for (field, replacement) in [
564 ("name", serde_json::json!("../escape")),
565 ("name", serde_json::json!("another")),
566 ("socket", serde_json::json!("relative.sock")),
567 ("data_dir", serde_json::json!("relative")),
568 ("pid", serde_json::json!(0)),
569 ("pid", serde_json::json!("1")),
570 ("version", serde_json::json!("")),
571 ("extra", serde_json::json!(true)),
572 ] {
573 let mut value = valid.clone();
574 value[field] = replacement;
575 fs::write(&path, serde_json::to_vec(&value)?)?;
576 fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
577 let refused = read(&path).err().ok_or("invalid entry was accepted")?;
578 assert_eq!(refused.code, "invalid_registry_entry");
579 assert!(refused.to_string().contains(&path.display().to_string()));
580 }
581 fs::write(&path, b"{")?;
582 assert_eq!(
583 read(&path).err().ok_or("partial entry was accepted")?.code,
584 "invalid_registry_entry"
585 );
586 Ok(())
587 })
588 }
589
590 #[test]
591 fn missing_namespace_is_named_and_the_next_publication_recreates_it() -> TestResult {
592 with_fixture(|fixture| {
593 let directory = fixture.directory.path().join("haem");
594 assert_eq!(
595 read_directory(&directory)
596 .err()
597 .ok_or("missing namespace was accepted")?
598 .code,
599 "registry_unavailable"
600 );
601 let endpoint = endpoint(fixture.directory.path(), "recreate");
602 let mut listener = Listener::bind(&endpoint.socket)?;
603 listener.publish(&endpoint)?;
604 assert_eq!(read_directory(&directory)?.len(), 1);
605 listener.finish()?;
606 Ok(())
607 })
608 }
609
610 #[test]
611 fn explicit_socket_cannot_skip_a_refused_namespace_lookup() -> TestResult {
612 with_fixture(|fixture| {
613 let endpoint = endpoint(fixture.directory.path(), "explicit");
614 let mut listener = Listener::bind(&endpoint.socket)?;
615 fs::set_permissions(fixture.directory.path(), fs::Permissions::from_mode(0o500))?;
616 let result = listener.publish(&endpoint);
617 fs::set_permissions(fixture.directory.path(), fs::Permissions::from_mode(0o700))?;
618 let refused = result.err().ok_or("invalid namespace was accepted")?;
619 assert_eq!(refused.code, "registry_publication_failed");
620 assert!(refused.to_string().contains("invalid_runtime_location"));
621 assert!(!fixture.directory.path().join("haem").try_exists()?);
622 Ok(())
623 })
624 }
625
626 #[test]
627 fn registry_child() -> TestResult {
628 let Some(root) = std::env::var_os("HAEM_REGISTRY_TEST_CHILD") else {
629 return Ok(());
630 };
631 let root = Path::new(&root);
632 let endpoint = endpoint(root, "child");
633 let mut listener = Listener::bind(&endpoint.socket)?;
634 listener.publish_in(&root.join("haem"), Entry::current(&endpoint)?)?;
635 println!("haematite-registry-ready");
636 io::stdout().flush()?;
637 let mut command = String::new();
638 io::stdin().read_line(&mut command)?;
639 if command != "finish\n" {
640 return Err("child did not receive its finish signal".into());
641 }
642 listener.finish()?;
643 Ok(())
644 }
645
646 struct Process {
647 child: Option<Child>,
648 output: io::BufReader<ChildStdout>,
649 }
650
651 impl Process {
652 fn start(root: &Path) -> Result<Self, Box<dyn Error>> {
653 let mut child = Command::new(std::env::current_exe()?)
654 .args([
655 "--exact",
656 "serve::registry::tests::registry_child",
657 "--nocapture",
658 ])
659 .env("HAEM_REGISTRY_TEST_CHILD", root)
660 .stdin(Stdio::piped())
661 .stdout(Stdio::piped())
662 .stderr(Stdio::piped())
663 .spawn()?;
664 let Some(output) = child.stdout.take() else {
665 let kill = child.kill();
666 let wait = child.wait();
667 return Err(format!(
668 "child stdout is absent; cleanup kill: {kill:?}; cleanup wait: {wait:?}"
669 )
670 .into());
671 };
672 let mut process = Self {
673 child: Some(child),
674 output: io::BufReader::new(output),
675 };
676 loop {
677 let mut line = String::new();
678 match process.output.read_line(&mut line) {
679 Ok(0) => {
680 return Err(match process.wait(true) {
681 Ok(()) => "child exited before readiness".into(),
682 Err(error) => format!("child exited before readiness: {error}").into(),
683 });
684 }
685 Ok(_) if line.contains("haematite-registry-ready") => return Ok(process),
686 Ok(_) => {}
687 Err(error) => return Err(format!("read child readiness: {error}").into()),
688 }
689 }
690 }
691
692 fn pid(&self) -> Result<u32, Box<dyn Error>> {
693 Ok(self.child.as_ref().ok_or("child already exited")?.id())
694 }
695
696 fn finish(&mut self) -> TestResult {
697 let child = self.child.as_mut().ok_or("child already exited")?;
698 child
699 .stdin
700 .take()
701 .ok_or("child stdin is absent")?
702 .write_all(b"finish\n")?;
703 self.wait(true)
704 }
705
706 fn kill(&mut self) -> TestResult {
707 self.child.as_mut().ok_or("child already exited")?.kill()?;
708 self.wait(false)
709 }
710
711 fn wait(&mut self, clean: bool) -> TestResult {
712 let child = self.child.as_mut().ok_or("child already exited")?;
713 let status = child.wait()?;
714 let mut errors = String::new();
715 child
716 .stderr
717 .take()
718 .ok_or("child stderr is absent")?
719 .read_to_string(&mut errors)?;
720 self.child.take();
721 if clean && !status.success() {
722 return Err(format!("child exited {status}: {errors}").into());
723 }
724 if !clean && status.success() {
725 return Err("killed child exited successfully".into());
726 }
727 if !errors.is_empty() {
728 return Err(format!("child stderr: {errors}").into());
729 }
730 Ok(())
731 }
732 }
733
734 impl Drop for Process {
735 fn drop(&mut self) {
736 if let Some(child) = self.child.as_mut() {
737 match child.try_wait() {
738 Ok(Some(_)) => {}
739 Ok(None) => {
740 if let Err(error) = child.kill() {
741 eprintln!("registry child cleanup kill: {error}");
742 }
743 if let Err(error) = child.wait() {
744 eprintln!("registry child cleanup wait: {error}");
745 }
746 }
747 Err(error) => {
748 eprintln!("registry child cleanup status: {error}");
749 if let Err(error) = child.kill() {
750 eprintln!("registry child cleanup kill: {error}");
751 }
752 if let Err(error) = child.wait() {
753 eprintln!("registry child cleanup wait: {error}");
754 }
755 }
756 }
757 if let Some(mut stderr) = child.stderr.take() {
758 let mut errors = String::new();
759 match stderr.read_to_string(&mut errors) {
760 Ok(_) if !errors.is_empty() => {
761 eprintln!("registry child cleanup stderr: {errors}");
762 }
763 Ok(_) => {}
764 Err(error) => eprintln!("registry child cleanup stderr read: {error}"),
765 }
766 }
767 }
768 }
769 }
770}