Skip to content

Commit aee8c75

Browse files
committed
bug: corruption when killed while writing
1 parent 0b6cc3c commit aee8c75

File tree

4 files changed

+192
-4
lines changed

4 files changed

+192
-4
lines changed

crates/cargo-util/src/paths.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,19 @@ pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()>
180180
.with_context(|| format!("failed to write `{}`", path.display()))
181181
}
182182

183+
/// Writes a file to disk atomically.
184+
///
185+
/// write_atomic uses tempfile::persist to accomplish atomic writes.
186+
pub fn write_atomic<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
187+
let path = path.as_ref();
188+
let mut tmp = TempFileBuilder::new()
189+
.prefix(path.file_name().unwrap())
190+
.tempfile_in(path.parent().unwrap())?;
191+
tmp.write_all(contents.as_ref())?;
192+
tmp.persist(path)?;
193+
Ok(())
194+
}
195+
183196
/// Equivalent to [`write()`], but does not write anything if the file contents
184197
/// are identical to the given contents.
185198
pub fn write_if_changed<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
@@ -775,6 +788,29 @@ fn exclude_from_time_machine(path: &Path) {
775788
#[cfg(test)]
776789
mod tests {
777790
use super::join_paths;
791+
use super::write;
792+
use super::write_atomic;
793+
794+
#[test]
795+
fn write_works() {
796+
let original_contents = "[dependencies]\nfoo = 0.1.0";
797+
798+
let tmpdir = tempfile::tempdir().unwrap();
799+
let path = tmpdir.path().join("Cargo.toml");
800+
write(&path, original_contents).unwrap();
801+
let contents = std::fs::read_to_string(&path).unwrap();
802+
assert_eq!(contents, original_contents);
803+
}
804+
#[test]
805+
fn write_atomic_works() {
806+
let original_contents = "[dependencies]\nfoo = 0.1.0";
807+
808+
let tmpdir = tempfile::tempdir().unwrap();
809+
let path = tmpdir.path().join("Cargo.toml");
810+
write_atomic(&path, original_contents).unwrap();
811+
let contents = std::fs::read_to_string(&path).unwrap();
812+
assert_eq!(contents, original_contents);
813+
}
778814

779815
#[test]
780816
fn join_paths_lists_paths_on_error() {

src/bin/cargo/commands/remove.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,10 @@ fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> {
270270
}
271271

272272
if is_modified {
273-
cargo_util::paths::write(workspace.root_manifest(), manifest.to_string().as_bytes())?;
273+
cargo_util::paths::write_atomic(
274+
workspace.root_manifest(),
275+
manifest.to_string().as_bytes(),
276+
)?;
274277
}
275278

276279
Ok(())

src/cargo/util/toml_mut/manifest.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ impl LocalManifest {
296296
let s = self.manifest.data.to_string();
297297
let new_contents_bytes = s.as_bytes();
298298

299-
cargo_util::paths::write(&self.path, new_contents_bytes)
299+
cargo_util::paths::write_atomic(&self.path, new_contents_bytes)
300300
}
301301

302302
/// Lookup a dependency.

tests/testsuite/death.rs

Lines changed: 151 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
//! Tests for ctrl-C handling.
22
3+
use cargo_test_support::{project, slow_cpu_multiplier};
34
use std::fs;
45
use std::io::{self, Read};
56
use std::net::TcpListener;
67
use std::process::{Child, Stdio};
78
use std::thread;
8-
9-
use cargo_test_support::{project, slow_cpu_multiplier};
9+
use std::time;
1010

1111
#[cargo_test]
1212
fn ctrl_c_kills_everyone() {
@@ -87,6 +87,155 @@ fn ctrl_c_kills_everyone() {
8787
);
8888
}
8989

90+
#[cargo_test]
91+
fn kill_cargo_add_never_corrupts_cargo_toml() {
92+
cargo_test_support::registry::init();
93+
cargo_test_support::registry::Package::new("my-package", "0.1.1+my-package").publish();
94+
95+
let with_dependency = r#"
96+
[package]
97+
name = "foo"
98+
version = "0.0.1"
99+
authors = []
100+
101+
[dependencies]
102+
my-package = "0.1.1"
103+
"#;
104+
let without_dependency = r#"
105+
[package]
106+
name = "foo"
107+
version = "0.0.1"
108+
authors = []
109+
"#;
110+
111+
for sleep_time_ms in [30, 60, 90] {
112+
let p = project()
113+
.file("Cargo.toml", without_dependency)
114+
.file("src/lib.rs", "")
115+
.build();
116+
117+
let mut cargo = p.cargo("add").arg("my-package").build_command();
118+
cargo
119+
.stdin(Stdio::piped())
120+
.stdout(Stdio::piped())
121+
.stderr(Stdio::piped());
122+
123+
let mut child = cargo.spawn().unwrap();
124+
125+
thread::sleep(time::Duration::from_millis(sleep_time_ms));
126+
127+
assert!(child.kill().is_ok());
128+
assert!(child.wait().is_ok());
129+
130+
// check the Cargo.toml
131+
let contents = fs::read(p.root().join("Cargo.toml")).unwrap();
132+
133+
// not empty
134+
assert_ne!(
135+
contents, b"",
136+
"Cargo.toml is empty, and should not be at {} milliseconds",
137+
sleep_time_ms
138+
);
139+
140+
// We should have the original Cargo.toml or the new one, nothing else.
141+
if std::str::from_utf8(&contents)
142+
.unwrap()
143+
.contains("[dependencies]")
144+
{
145+
assert_eq!(
146+
std::str::from_utf8(&contents).unwrap(),
147+
with_dependency,
148+
"Cargo.toml is with_dependency after add at {} milliseconds",
149+
sleep_time_ms
150+
);
151+
} else {
152+
assert_eq!(
153+
std::str::from_utf8(&contents).unwrap(),
154+
without_dependency,
155+
"Cargo.toml is without_dependency after add at {} milliseconds",
156+
sleep_time_ms
157+
);
158+
}
159+
}
160+
}
161+
162+
#[cargo_test]
163+
fn kill_cargo_remove_never_corrupts_cargo_toml() {
164+
let with_dependency = r#"
165+
[package]
166+
name = "foo"
167+
version = "0.0.1"
168+
authors = []
169+
build = "build.rs"
170+
171+
[dependencies]
172+
bar = "0.0.1"
173+
"#;
174+
let without_dependency = r#"
175+
[package]
176+
name = "foo"
177+
version = "0.0.1"
178+
authors = []
179+
build = "build.rs"
180+
"#;
181+
182+
// This test depends on killing the cargo process at the right time to cause a failed write.
183+
// Note that we're iterating and using the index as time in ms to sleep before killing the cargo process.
184+
// If it is working correctly, we never fail, but can't hang out here all day...
185+
// So we'll just run it a few times and hope for the best.
186+
for sleep_time_ms in [30, 60, 90] {
187+
// new basic project with a single dependency
188+
let p = project()
189+
.file("Cargo.toml", with_dependency)
190+
.file("src/lib.rs", "")
191+
.build();
192+
193+
// run cargo remove the dependency
194+
let mut cargo = p.cargo("remove").arg("bar").build_command();
195+
cargo
196+
.stdin(Stdio::piped())
197+
.stdout(Stdio::piped())
198+
.stderr(Stdio::piped());
199+
200+
let mut child = cargo.spawn().unwrap();
201+
202+
thread::sleep(time::Duration::from_millis(sleep_time_ms));
203+
204+
assert!(child.kill().is_ok());
205+
assert!(child.wait().is_ok());
206+
207+
// check the Cargo.toml
208+
let contents = fs::read(p.root().join("Cargo.toml")).unwrap();
209+
210+
// not empty
211+
assert_ne!(
212+
contents, b"",
213+
"Cargo.toml is empty, and should not be at {} milliseconds",
214+
sleep_time_ms
215+
);
216+
217+
// We should have the original Cargo.toml or the new one, nothing else.
218+
if std::str::from_utf8(&contents)
219+
.unwrap()
220+
.contains("[dependencies]")
221+
{
222+
assert_eq!(
223+
std::str::from_utf8(&contents).unwrap(),
224+
with_dependency,
225+
"Cargo.toml is not the same as the original at {} milliseconds",
226+
sleep_time_ms
227+
);
228+
} else {
229+
assert_eq!(
230+
std::str::from_utf8(&contents).unwrap(),
231+
without_dependency,
232+
"Cargo.toml is not the same as expected at {} milliseconds",
233+
sleep_time_ms
234+
);
235+
}
236+
}
237+
}
238+
90239
#[cfg(unix)]
91240
pub fn ctrl_c(child: &mut Child) {
92241
let r = unsafe { libc::kill(-(child.id() as i32), libc::SIGINT) };

0 commit comments

Comments
 (0)