Skip to content

Commit 2a313f0

Browse files
committed
Refactor internal code to simplify logic in termination handlers.
Refactors the global container state to store whether the container exists and current logic to terminate the container if it exists. By coupling the two, it simplifies safe handling and modification of this global state. Since we have a signal handler, these functions may be called asynchronously, therefore any global state may be modified, rendering the current state invalid if not appropriately guarded. Coupling the two ensures that our logic is sound.
1 parent e2c2180 commit 2a313f0

File tree

5 files changed

+292
-242
lines changed

5 files changed

+292
-242
lines changed

src/bin/commands/containers.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ fn get_cross_volumes(
319319
engine: &docker::Engine,
320320
msg_info: &mut MessageInfo,
321321
) -> cross::Result<Vec<String>> {
322-
use cross::docker::remote::VOLUME_PREFIX;
322+
use cross::docker::VOLUME_PREFIX;
323323
let stdout = docker::subcommand(engine, "volume")
324324
.arg("list")
325325
.args(["--format", "{{.Name}}"])
@@ -400,7 +400,7 @@ pub fn create_persistent_volume(
400400
let container = dirs.unique_container_identifier(&toolchain.host().target)?;
401401
let volume = dirs.unique_toolchain_identifier()?;
402402

403-
if docker::remote::volume_exists(engine, &volume, msg_info)? {
403+
if docker::volume_exists(engine, &volume, msg_info)? {
404404
eyre::bail!("Error: volume {volume} already exists.");
405405
}
406406

@@ -409,18 +409,18 @@ pub fn create_persistent_volume(
409409
.run_and_get_status(msg_info, false)?;
410410

411411
// stop the container if it's already running
412-
let state = docker::remote::container_state(engine, &container, msg_info)?;
412+
let state = docker::container_state(engine, &container, msg_info)?;
413413
if !state.is_stopped() {
414414
msg_info.warn(format_args!("container {container} was running."))?;
415-
docker::remote::container_stop_default(engine, &container, msg_info)?;
415+
docker::container_stop_default(engine, &container, msg_info)?;
416416
}
417417
if state.exists() {
418418
msg_info.warn(format_args!("container {container} was exited."))?;
419-
docker::remote::container_rm(engine, &container, msg_info)?;
419+
docker::container_rm(engine, &container, msg_info)?;
420420
}
421421

422422
// create a dummy running container to copy data over
423-
let mount_prefix = docker::remote::MOUNT_PREFIX;
423+
let mount_prefix = docker::MOUNT_PREFIX;
424424
let mut docker = docker::subcommand(engine, "run");
425425
docker.args(["--name", &container]);
426426
docker.arg("--rm");
@@ -440,7 +440,7 @@ pub fn create_persistent_volume(
440440
docker.args(["sh", "-c", "sleep infinity"]);
441441
}
442442
// store first, since failing to non-existing container is fine
443-
docker::remote::create_container_deleter(engine.clone(), container.clone());
443+
docker::Container::create(engine.clone(), container.clone())?;
444444
docker.run_and_get_status(msg_info, false)?;
445445

446446
docker::remote::copy_volume_container_xargo(
@@ -467,7 +467,7 @@ pub fn create_persistent_volume(
467467
msg_info,
468468
)?;
469469

470-
docker::remote::drop_container(is_tty, msg_info);
470+
docker::Container::finish_static(is_tty, msg_info);
471471

472472
Ok(())
473473
}
@@ -486,11 +486,11 @@ pub fn remove_persistent_volume(
486486
let dirs = docker::ToolchainDirectories::assemble(&mount_finder, toolchain)?;
487487
let volume = dirs.unique_toolchain_identifier()?;
488488

489-
if !docker::remote::volume_exists(engine, &volume, msg_info)? {
489+
if !docker::volume_exists(engine, &volume, msg_info)? {
490490
eyre::bail!("Error: volume {volume} does not exist.");
491491
}
492492

493-
docker::remote::volume_rm(engine, &volume, msg_info)?;
493+
docker::volume_rm(engine, &volume, msg_info)?;
494494

495495
Ok(())
496496
}
@@ -499,7 +499,7 @@ fn get_cross_containers(
499499
engine: &docker::Engine,
500500
msg_info: &mut MessageInfo,
501501
) -> cross::Result<Vec<String>> {
502-
use cross::docker::remote::VOLUME_PREFIX;
502+
use cross::docker::VOLUME_PREFIX;
503503
let stdout = docker::subcommand(engine, "ps")
504504
.arg("-a")
505505
.args(["--format", "{{.Names}}: {{.State}}"])
@@ -533,7 +533,7 @@ pub fn remove_all_containers(
533533
// cannot fail, formatted as {{.Names}}: {{.State}}
534534
let (name, state) = container.split_once(':').unwrap();
535535
let name = name.trim();
536-
let state = docker::remote::ContainerState::new(state.trim())?;
536+
let state = docker::ContainerState::new(state.trim())?;
537537
if state.is_stopped() {
538538
stopped.push(name);
539539
} else {

src/docker/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pub mod custom;
1+
pub(crate) mod custom;
22
mod engine;
33
mod image;
44
mod local;

src/docker/remote.rs

Lines changed: 6 additions & 224 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
use std::collections::BTreeMap;
22
use std::io::{self, BufRead, Read, Write};
33
use std::path::{Path, PathBuf};
4-
use std::process::{Command, ExitStatus, Output};
5-
use std::sync::atomic::{AtomicBool, Ordering};
4+
use std::process::{Command, ExitStatus};
65
use std::{env, fs, time};
76

87
use eyre::Context;
@@ -14,80 +13,9 @@ use crate::errors::Result;
1413
use crate::extensions::CommandExt;
1514
use crate::file::{self, PathExt, ToUtf8};
1615
use crate::rustc::{self, QualifiedToolchain, VersionMetaExt};
17-
use crate::shell::{ColorChoice, MessageInfo, Stream, Verbosity};
16+
use crate::shell::{MessageInfo, Stream};
17+
use crate::temp;
1818
use crate::TargetTriple;
19-
use crate::{temp, OutputExt};
20-
21-
// the mount directory for the data volume.
22-
pub const MOUNT_PREFIX: &str = "/cross";
23-
// the prefix used when naming volumes
24-
pub const VOLUME_PREFIX: &str = "cross-";
25-
// default timeout to stop a container (in seconds)
26-
pub const DEFAULT_TIMEOUT: u32 = 2;
27-
// instant kill in case of a non-graceful exit
28-
pub const NO_TIMEOUT: u32 = 0;
29-
30-
// we need to specify drops for the containers, but we
31-
// also need to ensure the drops are called on a
32-
// termination handler. we use an atomic bool to ensure
33-
// that the drop only gets called once, even if we have
34-
// the signal handle invoked multiple times or it fails.
35-
pub(crate) static mut CONTAINER: Option<DeleteContainer> = None;
36-
pub(crate) static mut CONTAINER_EXISTS: AtomicBool = AtomicBool::new(false);
37-
38-
// it's unlikely that we ever need to erase a line in the destructors,
39-
// and it's better than keep global state everywhere, or keeping a ref
40-
// cell which could have already deleted a line
41-
pub(crate) struct DeleteContainer(Engine, String, u32, ColorChoice, Verbosity);
42-
43-
impl Drop for DeleteContainer {
44-
fn drop(&mut self) {
45-
// SAFETY: safe, since guarded by a thread-safe atomic swap.
46-
unsafe {
47-
if CONTAINER_EXISTS.swap(false, Ordering::SeqCst) {
48-
let mut msg_info = MessageInfo::new(self.3, self.4);
49-
container_stop(&self.0, &self.1, self.2, &mut msg_info).ok();
50-
container_rm(&self.0, &self.1, &mut msg_info).ok();
51-
}
52-
}
53-
}
54-
}
55-
56-
#[derive(Debug, PartialEq, Eq)]
57-
pub enum ContainerState {
58-
Created,
59-
Running,
60-
Paused,
61-
Restarting,
62-
Dead,
63-
Exited,
64-
DoesNotExist,
65-
}
66-
67-
impl ContainerState {
68-
pub fn new(state: &str) -> Result<Self> {
69-
match state {
70-
"created" => Ok(ContainerState::Created),
71-
"running" => Ok(ContainerState::Running),
72-
"paused" => Ok(ContainerState::Paused),
73-
"restarting" => Ok(ContainerState::Restarting),
74-
"dead" => Ok(ContainerState::Dead),
75-
"exited" => Ok(ContainerState::Exited),
76-
"" => Ok(ContainerState::DoesNotExist),
77-
_ => eyre::bail!("unknown container state: got {state}"),
78-
}
79-
}
80-
81-
#[must_use]
82-
pub fn is_stopped(&self) -> bool {
83-
matches!(self, Self::Exited | Self::DoesNotExist)
84-
}
85-
86-
#[must_use]
87-
pub fn exists(&self) -> bool {
88-
!matches!(self, Self::DoesNotExist)
89-
}
90-
}
9119

9220
#[derive(Debug, Clone)]
9321
enum VolumeId {
@@ -101,50 +29,12 @@ enum VolumeId {
10129
// commands while the container is cleaning up.
10230
macro_rules! bail_container_exited {
10331
() => {{
104-
if !container_exists() {
32+
if !Container::exists_static() {
10533
eyre::bail!("container already exited due to signal");
10634
}
10735
}};
10836
}
10937

110-
pub fn create_container_deleter(engine: Engine, container: String) {
111-
// SAFETY: safe, since single-threaded execution.
112-
unsafe {
113-
CONTAINER_EXISTS.store(true, Ordering::Relaxed);
114-
CONTAINER = Some(DeleteContainer(
115-
engine,
116-
container,
117-
NO_TIMEOUT,
118-
ColorChoice::Never,
119-
Verbosity::Quiet,
120-
));
121-
}
122-
}
123-
124-
pub fn drop_container(is_tty: bool, msg_info: &mut MessageInfo) {
125-
// SAFETY: safe, since single-threaded execution.
126-
unsafe {
127-
// relax the no-timeout and lack of output
128-
if let Some(container) = &mut CONTAINER {
129-
if is_tty {
130-
container.2 = DEFAULT_TIMEOUT;
131-
}
132-
container.3 = msg_info.color_choice;
133-
container.4 = msg_info.verbosity;
134-
}
135-
CONTAINER = None;
136-
}
137-
}
138-
139-
fn container_exists() -> bool {
140-
// SAFETY: safe, not mutating an atomic bool
141-
// this can be more relaxed: just used to ensure
142-
// that we don't make unnecessary calls, which are
143-
// safe even if executed, after we've signaled a
144-
// drop to our container.
145-
unsafe { CONTAINER_EXISTS.load(Ordering::Relaxed) }
146-
}
147-
14838
fn subcommand_or_exit(engine: &Engine, cmd: &str) -> Result<Command> {
14939
bail_container_exited!();
15040
Ok(subcommand(engine, cmd))
@@ -706,114 +596,6 @@ fn copy_volume_container_project(
706596
Ok(())
707597
}
708598

709-
fn run_and_get_status(
710-
engine: &Engine,
711-
args: &[&str],
712-
msg_info: &mut MessageInfo,
713-
) -> Result<ExitStatus> {
714-
command(engine)
715-
.args(args)
716-
.run_and_get_status(msg_info, true)
717-
}
718-
719-
fn run_and_get_output(
720-
engine: &Engine,
721-
args: &[&str],
722-
msg_info: &mut MessageInfo,
723-
) -> Result<Output> {
724-
command(engine).args(args).run_and_get_output(msg_info)
725-
}
726-
727-
pub fn volume_create(
728-
engine: &Engine,
729-
volume: &str,
730-
msg_info: &mut MessageInfo,
731-
) -> Result<ExitStatus> {
732-
run_and_get_status(engine, &["volume", "create", volume], msg_info)
733-
}
734-
735-
pub fn volume_rm(engine: &Engine, volume: &str, msg_info: &mut MessageInfo) -> Result<ExitStatus> {
736-
run_and_get_status(engine, &["volume", "rm", volume], msg_info)
737-
}
738-
739-
pub fn volume_exists(engine: &Engine, volume: &str, msg_info: &mut MessageInfo) -> Result<bool> {
740-
run_and_get_output(engine, &["volume", "inspect", volume], msg_info)
741-
.map(|output| output.status.success())
742-
}
743-
744-
pub fn existing_volumes(
745-
engine: &Engine,
746-
toolchain: &QualifiedToolchain,
747-
msg_info: &mut MessageInfo,
748-
) -> Result<Vec<String>> {
749-
let list = run_and_get_output(
750-
engine,
751-
&[
752-
"volume",
753-
"list",
754-
"--format",
755-
"{{.Name}}",
756-
"--filter",
757-
&format!("name=^{VOLUME_PREFIX}{}", toolchain),
758-
],
759-
msg_info,
760-
)?
761-
.stdout()?;
762-
763-
if list.is_empty() {
764-
Ok(vec![])
765-
} else {
766-
Ok(list.split('\n').map(ToOwned::to_owned).collect())
767-
}
768-
}
769-
770-
pub fn container_stop(
771-
engine: &Engine,
772-
container: &str,
773-
timeout: u32,
774-
msg_info: &mut MessageInfo,
775-
) -> Result<ExitStatus> {
776-
run_and_get_status(
777-
engine,
778-
&["stop", container, "--time", &timeout.to_string()],
779-
msg_info,
780-
)
781-
}
782-
783-
pub fn container_stop_default(
784-
engine: &Engine,
785-
container: &str,
786-
msg_info: &mut MessageInfo,
787-
) -> Result<ExitStatus> {
788-
// we want a faster timeout, since this might happen in signal
789-
// handler. our containers normally clean up pretty fast, it's
790-
// only without a pseudo-tty that they don't.
791-
container_stop(engine, container, DEFAULT_TIMEOUT, msg_info)
792-
}
793-
794-
// if stop succeeds without a timeout, this can have a spurious error
795-
// that is, if the container no longer exists. just silence this.
796-
pub fn container_rm(
797-
engine: &Engine,
798-
container: &str,
799-
msg_info: &mut MessageInfo,
800-
) -> Result<ExitStatus> {
801-
run_and_get_output(engine, &["rm", container], msg_info).map(|output| output.status)
802-
}
803-
804-
pub fn container_state(
805-
engine: &Engine,
806-
container: &str,
807-
msg_info: &mut MessageInfo,
808-
) -> Result<ContainerState> {
809-
let stdout = command(engine)
810-
.args(["ps", "-a"])
811-
.args(["--filter", &format!("name={container}")])
812-
.args(["--format", "{{.State}}"])
813-
.run_and_get_stdout(msg_info)?;
814-
ContainerState::new(stdout.trim())
815-
}
816-
817599
impl QualifiedToolchain {
818600
pub fn unique_toolchain_identifier(&self) -> Result<String> {
819601
// try to get the commit hash for the currently toolchain, if possible
@@ -980,7 +762,7 @@ pub(crate) fn run(
980762
}
981763

982764
// store first, since failing to non-existing container is fine
983-
create_container_deleter(engine.clone(), container.clone());
765+
Container::create(engine.clone(), container.clone())?;
984766
docker.run_and_get_status(msg_info, true)?;
985767

986768
// 4. copy all mounted volumes over
@@ -1228,7 +1010,7 @@ symlink_recurse \"${{prefix}}\"
12281010
.map_err::<eyre::ErrReport, _>(Into::into)?;
12291011
}
12301012

1231-
drop_container(is_tty, msg_info);
1013+
Container::finish_static(is_tty, msg_info);
12321014

12331015
status
12341016
}

0 commit comments

Comments
 (0)