From 942c652fc2feb3254bec7915d86f4e41a947f71a Mon Sep 17 00:00:00 2001 From: Ana Hobden Date: Wed, 9 Nov 2022 15:19:32 -0800 Subject: [PATCH] Handle signals and user stdin more gracefully --- Cargo.lock | 1 + Cargo.toml | 4 +- .../common/configure_nix_daemon_service.rs | 71 +++++--- src/action/common/create_group.rs | 53 +++--- src/action/common/create_user.rs | 162 ++++++++++-------- src/action/common/setup_default_profile.rs | 2 + src/action/darwin/bootstrap_volume.rs | 16 +- src/action/darwin/create_synthetic_objects.rs | 12 +- src/action/darwin/create_volume.rs | 38 ++-- src/action/darwin/enable_ownership.rs | 6 +- .../darwin/kickstart_launchctl_service.rs | 3 +- src/action/darwin/unmount_volume.rs | 6 +- src/action/linux/start_systemd_unit.rs | 14 +- src/action/linux/systemd_sysext_merge.rs | 22 ++- src/cli/mod.rs | 31 ++++ src/cli/subcommand/install.rs | 22 ++- src/cli/subcommand/uninstall.rs | 6 +- src/error.rs | 2 + src/lib.rs | 2 + src/plan.rs | 36 +++- src/planner/darwin/multi.rs | 16 +- 21 files changed, 354 insertions(+), 171 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1f2f05..6bd6589 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1906,6 +1906,7 @@ dependencies = [ "memchr", "mio", "num_cpus", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", diff --git a/Cargo.toml b/Cargo.toml index 2f9e5fe..1ef1f80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ crossterm = { version = "0.25.0", features = ["event-stream"] } eyre = "0.6.8" futures = "0.3.24" glob = "0.3.0" -nix = { version = "0.25.0", features = ["user", "fs"], default-features = false } +nix = { version = "0.25.0", features = ["user", "fs", "process"], default-features = false } owo-colors = { version = "3.5.0", features = [ "supports-colors" ] } reqwest = { version = "0.11.11", default-features = false, features = ["rustls-tls", "stream"] } serde = { version = "1.0.144", features = ["derive"] } @@ -32,7 +32,7 @@ tar = "0.4.38" target-lexicon = "0.12.4" tempdir = { version = "0.3.7"} thiserror = "1.0.33" -tokio = { version = "1.21.0", features = ["time", "io-std", "process", "fs", "tracing", "rt-multi-thread", "macros", "io-util"] } +tokio = { version = "1.21.0", features = ["time", "io-std", "process", "fs", "signal", "tracing", "rt-multi-thread", "macros", "io-util", "parking_lot"] } tokio-util = { version = "0.7", features = ["io"] } tracing = { version = "0.1.36", features = [ "valuable" ] } tracing-error = "0.2.0" diff --git a/src/action/common/configure_nix_daemon_service.rs b/src/action/common/configure_nix_daemon_service.rs index 5e810fd..ba2c76a 100644 --- a/src/action/common/configure_nix_daemon_service.rs +++ b/src/action/common/configure_nix_daemon_service.rs @@ -95,7 +95,8 @@ impl Action for ConfigureNixDaemonService { execute_command( Command::new("launchctl") .arg("load") - .arg(DARWIN_NIX_DAEMON_DEST), + .arg(DARWIN_NIX_DAEMON_DEST) + .stdin(std::process::Stdio::null()), ) .await .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; @@ -116,22 +117,37 @@ impl Action for ConfigureNixDaemonService { execute_command( Command::new("systemd-tmpfiles") .arg("--create") - .arg("--prefix=/nix/var/nix"), + .arg("--prefix=/nix/var/nix") + .stdin(std::process::Stdio::null()), ) .await .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; - execute_command(Command::new("systemctl").arg("link").arg(SERVICE_SRC)) - .await - .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; + execute_command( + Command::new("systemctl") + .arg("link") + .arg(SERVICE_SRC) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; - execute_command(Command::new("systemctl").arg("link").arg(SOCKET_SRC)) - .await - .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; + execute_command( + Command::new("systemctl") + .arg("link") + .arg(SOCKET_SRC) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; - execute_command(Command::new("systemctl").arg("daemon-reload")) - .await - .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; + execute_command( + Command::new("systemctl") + .arg("daemon-reload") + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; }, }; @@ -181,18 +197,27 @@ impl Action for ConfigureNixDaemonService { .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; }, _ => { - execute_command(Command::new("systemctl").args(["disable", SOCKET_SRC])) - .await - .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; + execute_command( + Command::new("systemctl") + .args(["disable", SOCKET_SRC]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; - execute_command(Command::new("systemctl").args(["disable", SERVICE_SRC])) - .await - .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; + execute_command( + Command::new("systemctl") + .args(["disable", SERVICE_SRC]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; execute_command( Command::new("systemd-tmpfiles") .arg("--remove") - .arg("--prefix=/nix/var/nix"), + .arg("--prefix=/nix/var/nix") + .stdin(std::process::Stdio::null()), ) .await .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; @@ -202,9 +227,13 @@ impl Action for ConfigureNixDaemonService { .boxed() })?; - execute_command(Command::new("systemctl").arg("daemon-reload")) - .await - .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; + execute_command( + Command::new("systemctl") + .arg("daemon-reload") + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?; }, }; diff --git a/src/action/common/create_group.rs b/src/action/common/create_group.rs index 2ac3971..1ef282f 100644 --- a/src/action/common/create_group.rs +++ b/src/action/common/create_group.rs @@ -70,25 +70,28 @@ impl Action for CreateGroup { patch: _, } | OperatingSystem::Darwin => { - execute_command(Command::new("/usr/sbin/dseditgroup").args([ - "-o", - "create", - "-r", - "Nix build group for nix-daemon", - "-i", - &format!("{gid}"), - name.as_str(), - ])) + execute_command( + Command::new("/usr/sbin/dseditgroup") + .args([ + "-o", + "create", + "-r", + "Nix build group for nix-daemon", + "-i", + &format!("{gid}"), + name.as_str(), + ]) + .stdin(std::process::Stdio::null()), + ) .await .map_err(|e| CreateGroupError::Command(e).boxed())?; }, _ => { - execute_command(Command::new("groupadd").args([ - "-g", - &gid.to_string(), - "--system", - &name, - ])) + execute_command( + Command::new("groupadd") + .args(["-g", &gid.to_string(), "--system", &name]) + .stdin(std::process::Stdio::null()), + ) .await .map_err(|e| CreateGroupError::Command(e).boxed())?; }, @@ -141,18 +144,22 @@ impl Action for CreateGroup { patch: _, } | OperatingSystem::Darwin => { - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-delete", - &format!("/Groups/{name}"), - ])) + execute_command( + Command::new("/usr/bin/dscl") + .args([".", "-delete", &format!("/Groups/{name}")]) + .stdin(std::process::Stdio::null()), + ) .await .map_err(|e| CreateGroupError::Command(e).boxed())?; }, _ => { - execute_command(Command::new("groupdel").arg(&name)) - .await - .map_err(|e| CreateGroupError::Command(e).boxed())?; + execute_command( + Command::new("groupdel") + .arg(&name) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| CreateGroupError::Command(e).boxed())?; }, }; diff --git a/src/action/common/create_user.rs b/src/action/common/create_user.rs index 40a6086..041a1f9 100644 --- a/src/action/common/create_user.rs +++ b/src/action/common/create_user.rs @@ -81,47 +81,63 @@ impl Action for CreateUser { patch: _, } | OperatingSystem::Darwin => { - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-create", - &format!("/Users/{name}"), - ])) + execute_command( + Command::new("/usr/bin/dscl") + .args([".", "-create", &format!("/Users/{name}")]) + .stdin(std::process::Stdio::null()), + ) .await .map_err(|e| CreateUserError::Command(e).boxed())?; - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-create", - &format!("/Users/{name}"), - "UniqueID", - &format!("{uid}"), - ])) + execute_command( + Command::new("/usr/bin/dscl") + .args([ + ".", + "-create", + &format!("/Users/{name}"), + "UniqueID", + &format!("{uid}"), + ]) + .stdin(std::process::Stdio::null()), + ) .await .map_err(|e| CreateUserError::Command(e).boxed())?; - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-create", - &format!("/Users/{name}"), - "PrimaryGroupID", - &format!("{gid}"), - ])) + execute_command( + Command::new("/usr/bin/dscl") + .args([ + ".", + "-create", + &format!("/Users/{name}"), + "PrimaryGroupID", + &format!("{gid}"), + ]) + .stdin(std::process::Stdio::null()), + ) .await .map_err(|e| CreateUserError::Command(e).boxed())?; - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-create", - &format!("/Users/{name}"), - "NFSHomeDirectory", - "/var/empty", - ])) + execute_command( + Command::new("/usr/bin/dscl") + .args([ + ".", + "-create", + &format!("/Users/{name}"), + "NFSHomeDirectory", + "/var/empty", + ]) + .stdin(std::process::Stdio::null()), + ) .await .map_err(|e| CreateUserError::Command(e).boxed())?; - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-create", - &format!("/Users/{name}"), - "UserShell", - "/sbin/nologin", - ])) + execute_command( + Command::new("/usr/bin/dscl") + .args([ + ".", + "-create", + &format!("/Users/{name}"), + "UserShell", + "/sbin/nologin", + ]) + .stdin(std::process::Stdio::null()), + ) .await .map_err(|e| CreateUserError::Command(e).boxed())?; execute_command( @@ -132,17 +148,16 @@ impl Action for CreateUser { &format!("/Groups/{groupname}"), "GroupMembership", ]) - .arg(&name), + .arg(&name) + .stdin(std::process::Stdio::null()), ) .await .map_err(|e| CreateUserError::Command(e).boxed())?; - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-create", - &format!("/Users/{name}"), - "IsHidden", - "1", - ])) + execute_command( + Command::new("/usr/bin/dscl") + .args([".", "-create", &format!("/Users/{name}"), "IsHidden", "1"]) + .stdin(std::process::Stdio::null()), + ) .await .map_err(|e| CreateUserError::Command(e).boxed())?; execute_command( @@ -152,31 +167,36 @@ impl Action for CreateUser { .arg(&name) .arg("-t") .arg(&name) - .arg(groupname), + .arg(groupname) + .stdin(std::process::Stdio::null()), ) .await .map_err(|e| CreateUserError::Command(e).boxed())?; }, _ => { - execute_command(Command::new("useradd").args([ - "--home-dir", - "/var/empty", - "--comment", - &format!("\"Nix build user\""), - "--gid", - &gid.to_string(), - "--groups", - &gid.to_string(), - "--no-user-group", - "--system", - "--shell", - "/sbin/nologin", - "--uid", - &uid.to_string(), - "--password", - "\"!\"", - &name.to_string(), - ])) + execute_command( + Command::new("useradd") + .args([ + "--home-dir", + "/var/empty", + "--comment", + &format!("\"Nix build user\""), + "--gid", + &gid.to_string(), + "--groups", + &gid.to_string(), + "--no-user-group", + "--system", + "--shell", + "/sbin/nologin", + "--uid", + &uid.to_string(), + "--password", + "\"!\"", + &name.to_string(), + ]) + .stdin(std::process::Stdio::null()), + ) .await .map_err(|e| CreateUserError::Command(e).boxed())?; }, @@ -235,18 +255,22 @@ impl Action for CreateUser { patch: _, } | OperatingSystem::Darwin => { - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-delete", - &format!("/Users/{name}"), - ])) + execute_command( + Command::new("/usr/bin/dscl") + .args([".", "-delete", &format!("/Users/{name}")]) + .stdin(std::process::Stdio::null()), + ) .await .map_err(|e| CreateUserError::Command(e).boxed())?; }, _ => { - execute_command(Command::new("userdel").args([&name.to_string()])) - .await - .map_err(|e| CreateUserError::Command(e).boxed())?; + execute_command( + Command::new("userdel") + .args([&name.to_string()]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| CreateUserError::Command(e).boxed())?; }, }; diff --git a/src/action/common/setup_default_profile.rs b/src/action/common/setup_default_profile.rs index 38618f3..0fcec42 100644 --- a/src/action/common/setup_default_profile.rs +++ b/src/action/common/setup_default_profile.rs @@ -99,6 +99,7 @@ impl Action for SetupDefaultProfile { .arg(&nix_pkg) .arg("-i") .arg(&nss_ca_cert_pkg) + .stdin(std::process::Stdio::null()) .env( "HOME", dirs::home_dir().ok_or_else(|| SetupDefaultProfileError::NoRootHome.boxed())?, @@ -139,6 +140,7 @@ impl Action for SetupDefaultProfile { "NIX_SSL_CERT_FILE", "/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt", ); + command.stdin(std::process::Stdio::null()); execute_command(&mut command) .await diff --git a/src/action/darwin/bootstrap_volume.rs b/src/action/darwin/bootstrap_volume.rs index ecdb447..5d9b9b3 100644 --- a/src/action/darwin/bootstrap_volume.rs +++ b/src/action/darwin/bootstrap_volume.rs @@ -55,15 +55,16 @@ impl Action for BootstrapVolume { execute_command( Command::new("launchctl") .args(["bootstrap", "system"]) - .arg(path), + .arg(path) + .stdin(std::process::Stdio::null()), ) .await .map_err(|e| BootstrapVolumeError::Command(e).boxed())?; - execute_command(Command::new("launchctl").args([ - "kickstart", - "-k", - "system/org.nixos.darwin-store", - ])) + execute_command( + Command::new("launchctl") + .args(["kickstart", "-k", "system/org.nixos.darwin-store"]) + .stdin(std::process::Stdio::null()), + ) .await .map_err(|e| BootstrapVolumeError::Command(e).boxed())?; @@ -97,7 +98,8 @@ impl Action for BootstrapVolume { execute_command( Command::new("launchctl") .args(["bootout", "system"]) - .arg(path), + .arg(path) + .stdin(std::process::Stdio::null()), ) .await .map_err(|e| BootstrapVolumeError::Command(e).boxed())?; diff --git a/src/action/darwin/create_synthetic_objects.rs b/src/action/darwin/create_synthetic_objects.rs index 94a43e4..7e45833 100644 --- a/src/action/darwin/create_synthetic_objects.rs +++ b/src/action/darwin/create_synthetic_objects.rs @@ -44,13 +44,15 @@ impl Action for CreateSyntheticObjects { // Yup we literally call both and ignore the error! Reasoning: https://github.com/NixOS/nix/blob/95331cb9c99151cbd790ceb6ddaf49fc1c0da4b3/scripts/create-darwin-volume.sh#L261 execute_command( Command::new("/System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util") - .arg("-t"), + .arg("-t") + .stdin(std::process::Stdio::null()), ) .await .ok(); // Deliberate execute_command( Command::new("/System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util") - .arg("-B"), + .arg("-B") + .stdin(std::process::Stdio::null()), ) .await .ok(); // Deliberate @@ -83,13 +85,15 @@ impl Action for CreateSyntheticObjects { // Yup we literally call both and ignore the error! Reasoning: https://github.com/NixOS/nix/blob/95331cb9c99151cbd790ceb6ddaf49fc1c0da4b3/scripts/create-darwin-volume.sh#L261 execute_command( Command::new("/System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util") - .arg("-t"), + .arg("-t") + .stdin(std::process::Stdio::null()), ) .await .ok(); // Deliberate execute_command( Command::new("/System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util") - .arg("-B"), + .arg("-B") + .stdin(std::process::Stdio::null()), ) .await .ok(); // Deliberate diff --git a/src/action/darwin/create_volume.rs b/src/action/darwin/create_volume.rs index 49a7899..263d872 100644 --- a/src/action/darwin/create_volume.rs +++ b/src/action/darwin/create_volume.rs @@ -69,18 +69,22 @@ impl Action for CreateVolume { } tracing::debug!("Creating volume"); - execute_command(Command::new("/usr/sbin/diskutil").args([ - "apfs", - "addVolume", - &format!("{}", disk.display()), - if !*case_sensitive { - "APFS" - } else { - "Case-sensitive APFS" - }, - name, - "-nomount", - ])) + execute_command( + Command::new("/usr/sbin/diskutil") + .args([ + "apfs", + "addVolume", + &format!("{}", disk.display()), + if !*case_sensitive { + "APFS" + } else { + "Case-sensitive APFS" + }, + name, + "-nomount", + ]) + .stdin(std::process::Stdio::null()), + ) .await .map_err(|e| CreateVolumeError::Command(e).boxed())?; @@ -122,9 +126,13 @@ impl Action for CreateVolume { } tracing::debug!("Deleting volume"); - execute_command(Command::new("/usr/sbin/diskutil").args(["apfs", "deleteVolume", name])) - .await - .map_err(|e| CreateVolumeError::Command(e).boxed())?; + execute_command( + Command::new("/usr/sbin/diskutil") + .args(["apfs", "deleteVolume", name]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| CreateVolumeError::Command(e).boxed())?; tracing::trace!("Deleted volume"); *action_state = ActionState::Completed; diff --git a/src/action/darwin/enable_ownership.rs b/src/action/darwin/enable_ownership.rs index 3e627ea..3c2202d 100644 --- a/src/action/darwin/enable_ownership.rs +++ b/src/action/darwin/enable_ownership.rs @@ -58,7 +58,8 @@ impl Action for EnableOwnership { let buf = execute_command( Command::new("/usr/sbin/diskutil") .args(["info", "-plist"]) - .arg(&path), + .arg(&path) + .stdin(std::process::Stdio::null()), ) .await .unwrap() @@ -72,7 +73,8 @@ impl Action for EnableOwnership { execute_command( Command::new("/usr/sbin/diskutil") .arg("enableOwnership") - .arg(path), + .arg(path) + .stdin(std::process::Stdio::null()), ) .await .map_err(|e| EnableOwnershipError::Command(e).boxed())?; diff --git a/src/action/darwin/kickstart_launchctl_service.rs b/src/action/darwin/kickstart_launchctl_service.rs index c98eed8..a99c962 100644 --- a/src/action/darwin/kickstart_launchctl_service.rs +++ b/src/action/darwin/kickstart_launchctl_service.rs @@ -55,7 +55,8 @@ impl Action for KickstartLaunchctlService { Command::new("launchctl") .arg("kickstart") .arg("-k") - .arg(unit), + .arg(unit) + .stdin(std::process::Stdio::null()), ) .await .map_err(|e| KickstartLaunchctlServiceError::Command(e).boxed())?; diff --git a/src/action/darwin/unmount_volume.rs b/src/action/darwin/unmount_volume.rs index 834cea3..b7e93e8 100644 --- a/src/action/darwin/unmount_volume.rs +++ b/src/action/darwin/unmount_volume.rs @@ -66,7 +66,8 @@ impl Action for UnmountVolume { execute_command( Command::new("/usr/sbin/diskutil") .args(["unmount", "force"]) - .arg(name), + .arg(name) + .stdin(std::process::Stdio::null()), ) .await .map_err(|e| UnmountVolumeError::Command(e).boxed())?; @@ -108,7 +109,8 @@ impl Action for UnmountVolume { execute_command( Command::new(" /usr/sbin/diskutil") .args(["unmount", "force"]) - .arg(name), + .arg(name) + .stdin(std::process::Stdio::null()), ) .await .map_err(|e| UnmountVolumeError::Command(e).boxed())?; diff --git a/src/action/linux/start_systemd_unit.rs b/src/action/linux/start_systemd_unit.rs index 7f36f57..1c4d635 100644 --- a/src/action/linux/start_systemd_unit.rs +++ b/src/action/linux/start_systemd_unit.rs @@ -55,7 +55,8 @@ impl Action for StartSystemdUnit { Command::new("systemctl") .arg("enable") .arg("--now") - .arg(format!("{unit}")), + .arg(format!("{unit}")) + .stdin(std::process::Stdio::null()), ) .await .map_err(|e| StartSystemdUnitError::Command(e).boxed())?; @@ -90,9 +91,14 @@ impl Action for StartSystemdUnit { tracing::debug!("Stopping systemd unit"); // TODO(@Hoverbear): Handle proxy vars - execute_command(Command::new("systemctl").arg("stop").arg(format!("{unit}"))) - .await - .map_err(|e| StartSystemdUnitError::Command(e).boxed())?; + execute_command( + Command::new("systemctl") + .arg("stop") + .arg(format!("{unit}")) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| StartSystemdUnitError::Command(e).boxed())?; tracing::trace!("Stopped systemd unit"); *action_state = ActionState::Completed; diff --git a/src/action/linux/systemd_sysext_merge.rs b/src/action/linux/systemd_sysext_merge.rs index 332192f..cda994d 100644 --- a/src/action/linux/systemd_sysext_merge.rs +++ b/src/action/linux/systemd_sysext_merge.rs @@ -57,9 +57,14 @@ impl Action for SystemdSysextMerge { } tracing::debug!("Merging systemd-sysext"); - execute_command(Command::new("systemd-sysext").arg("merge").arg(device)) - .await - .map_err(|e| SystemdSysextMergeError::Command(e).boxed())?; + execute_command( + Command::new("systemd-sysext") + .arg("merge") + .arg(device) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| SystemdSysextMergeError::Command(e).boxed())?; tracing::trace!("Merged systemd-sysext"); *action_state = ActionState::Completed; @@ -94,9 +99,14 @@ impl Action for SystemdSysextMerge { tracing::debug!("Unmrging systemd-sysext"); // TODO(@Hoverbear): Handle proxy vars - execute_command(Command::new("systemd-sysext").arg("unmerge").arg(device)) - .await - .map_err(|e| SystemdSysextMergeError::Command(e).boxed())?; + execute_command( + Command::new("systemd-sysext") + .arg("unmerge") + .arg(device) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| SystemdSysextMergeError::Command(e).boxed())?; tracing::trace!("Unmerged systemd-sysext"); *action_state = ActionState::Completed; diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 44b704b..bff5a36 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -3,6 +3,8 @@ pub(crate) mod subcommand; use clap::Parser; use std::process::ExitCode; +use tokio::sync::broadcast::{Receiver, Sender}; +use tokio_util::sync::CancellationToken; use self::subcommand::HarmonicSubcommand; @@ -40,3 +42,32 @@ impl CommandExecute for HarmonicCli { } } } + +pub(crate) async fn signal_channel() -> eyre::Result<(Sender<()>, Receiver<()>)> { + let (sender, reciever) = tokio::sync::broadcast::channel(100); + + let sender_cloned = sender.clone(); + let _guard = tokio::spawn(async move { + let mut ctrl_c = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()) + .expect("failed to install signal handler"); + + let mut terminate = + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler"); + + loop { + tokio::select! { + Some(()) = ctrl_c.recv() => { + tracing::warn!("Got SIGINT signal"); + sender_cloned.send(()).ok(); + }, + Some(()) = terminate.recv() => { + tracing::warn!("Got SIGTERM signal"); + sender_cloned.send(()).ok(); + }, + } + } + }); + + Ok((sender, reciever)) +} diff --git a/src/cli/subcommand/install.rs b/src/cli/subcommand/install.rs index 38f1fc8..313d382 100644 --- a/src/cli/subcommand/install.rs +++ b/src/cli/subcommand/install.rs @@ -1,8 +1,9 @@ use std::{path::PathBuf, process::ExitCode}; -use crate::BuiltinPlanner; +use crate::{cli::signal_channel, BuiltinPlanner, HarmonicError}; use clap::{ArgAction, Parser}; use eyre::{eyre, WrapErr}; +use tokio_util::sync::CancellationToken; use crate::{cli::CommandExecute, interaction}; @@ -59,12 +60,21 @@ impl CommandExecute for Install { } } - if let Err(err) = plan.install().await { - tracing::error!("{:?}", eyre!(err)); - if !interaction::confirm(plan.describe_revert(explain)).await? { - interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await; + let (tx, rx1) = signal_channel().await?; + + if let Err(err) = plan.install(rx1).await { + match err { + HarmonicError::Cancelled => {}, + err => { + tracing::error!("{:?}", eyre!(err)); + if !interaction::confirm(plan.describe_revert(explain)).await? { + interaction::clean_exit_with_message("Okay, didn't do anything! Bye!") + .await; + } + let rx2 = tx.subscribe(); + plan.revert(rx2).await? + }, } - plan.revert().await? } Ok(ExitCode::SUCCESS) diff --git a/src/cli/subcommand/uninstall.rs b/src/cli/subcommand/uninstall.rs index af242d7..4ffdfff 100644 --- a/src/cli/subcommand/uninstall.rs +++ b/src/cli/subcommand/uninstall.rs @@ -1,6 +1,6 @@ use std::{path::PathBuf, process::ExitCode}; -use crate::InstallPlan; +use crate::{cli::signal_channel, InstallPlan}; use clap::{ArgAction, Parser}; use eyre::WrapErr; @@ -48,7 +48,9 @@ impl CommandExecute for Uninstall { } } - plan.revert().await?; + let (tx, rx) = signal_channel().await?; + + plan.revert(rx).await?; // TODO(@hoverbear): It would be so nice to catch errors and offer the user a way to keep going... // However that will require being able to link error -> step and manually setting that step as `Uncompleted`. diff --git a/src/error.rs b/src/error.rs index ed6a605..2c7c66b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,4 +12,6 @@ pub enum HarmonicError { RecordingReceipt(PathBuf, #[source] std::io::Error), #[error(transparent)] SerializingReceipt(serde_json::Error), + #[error("Cancelled by user")] + Cancelled, } diff --git a/src/lib.rs b/src/lib.rs index 3cfdf03..22f4981 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,8 @@ use tokio::process::Command; #[tracing::instrument(skip_all, fields(command = %format!("{:?}", command.as_std())))] async fn execute_command(command: &mut Command) -> Result { + // TODO(@hoverbear): When tokio releases past 1.21.2, add a process group https://github.com/DeterminateSystems/harmonic/issues/41#issuecomment-1309513073 + tracing::trace!("Executing"); let command_str = format!("{:?}", command.as_std()); let output = command.output().await?; diff --git a/src/plan.rs b/src/plan.rs index c661787..7ed21db 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; use crossterm::style::Stylize; +use tokio::sync::broadcast::Receiver; +use tokio_util::sync::CancellationToken; use crate::{ action::{Action, ActionDescription}, @@ -74,16 +76,31 @@ impl InstallPlan { } #[tracing::instrument(skip_all)] - pub async fn install(&mut self) -> Result<(), HarmonicError> { + pub async fn install( + &mut self, + cancel_channel: impl Into>>, + ) -> Result<(), HarmonicError> { let Self { actions, planner: _, } = self; + let mut cancel_channel = cancel_channel.into(); // This is **deliberately sequential**. // Actions which are parallelizable are represented by "group actions" like CreateUsers // The plan itself represents the concept of the sequence of stages. for action in actions { + if let Some(ref mut cancel_channel) = cancel_channel { + if cancel_channel.try_recv() + != Err(tokio::sync::broadcast::error::TryRecvError::Empty) + { + if let Err(err) = write_receipt(self.clone()).await { + tracing::error!("Error saving receipt: {:?}", err); + } + return Err(HarmonicError::Cancelled); + } + } + if let Err(err) = action.execute().await { if let Err(err) = write_receipt(self.clone()).await { tracing::error!("Error saving receipt: {:?}", err); @@ -140,16 +157,31 @@ impl InstallPlan { } #[tracing::instrument(skip_all)] - pub async fn revert(&mut self) -> Result<(), HarmonicError> { + pub async fn revert( + &mut self, + cancel_channel: impl Into>>, + ) -> Result<(), HarmonicError> { let Self { actions, planner: _, } = self; + let mut cancel_channel = cancel_channel.into(); // This is **deliberately sequential**. // Actions which are parallelizable are represented by "group actions" like CreateUsers // The plan itself represents the concept of the sequence of stages. for action in actions.iter_mut().rev() { + if let Some(ref mut cancel_channel) = cancel_channel { + if cancel_channel.try_recv() + != Err(tokio::sync::broadcast::error::TryRecvError::Empty) + { + if let Err(err) = write_receipt(self.clone()).await { + tracing::error!("Error saving receipt: {:?}", err); + } + return Err(HarmonicError::Cancelled); + } + } + if let Err(err) = action.revert().await { if let Err(err) = write_receipt(self.clone()).await { tracing::error!("Error saving receipt: {:?}", err); diff --git a/src/planner/darwin/multi.rs b/src/planner/darwin/multi.rs index 08da5eb..d614b55 100644 --- a/src/planner/darwin/multi.rs +++ b/src/planner/darwin/multi.rs @@ -32,10 +32,14 @@ pub struct DarwinMulti { } async fn default_root_disk() -> Result { - let buf = execute_command(Command::new("/usr/sbin/diskutil").args(["info", "-plist", "/"])) - .await - .unwrap() - .stdout; + let buf = execute_command( + Command::new("/usr/sbin/diskutil") + .args(["info", "-plist", "/"]) + .stdin(std::process::Stdio::null()), + ) + .await + .unwrap() + .stdout; let the_plist: DiskUtilOutput = plist::from_reader(Cursor::new(buf))?; Ok(the_plist.parent_whole_disk) @@ -60,7 +64,9 @@ impl Planner for DarwinMulti { root_disk @ Some(_) => root_disk, None => { let buf = execute_command( - Command::new("/usr/sbin/diskutil").args(["info", "-plist", "/"]), + Command::new("/usr/sbin/diskutil") + .args(["info", "-plist", "/"]) + .stdin(std::process::Stdio::null()), ) .await .unwrap()