Skip to content

Commit 4e4d018

Browse files
Abstract out disk-writing utilities from FilesystemPersister
1 parent 75107c9 commit 4e4d018

File tree

2 files changed

+166
-149
lines changed

2 files changed

+166
-149
lines changed

lightning-persister/src/lib.rs

Lines changed: 17 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
mod util;
2+
13
extern crate lightning;
24
extern crate bitcoin;
35
extern crate libc;
46

57
use bitcoin::hashes::hex::ToHex;
8+
use crate::util::DiskWriteable;
69
use lightning::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateErr};
710
use lightning::chain::channelmonitor;
811
use lightning::chain::keysinterface::ChannelKeys;
912
use lightning::chain::transaction::OutPoint;
1013
use lightning::util::ser::Writeable;
1114
use std::fs;
1215
use std::io::Error;
13-
use std::path::{Path, PathBuf};
1416

1517
#[cfg(test)]
1618
use {
@@ -22,9 +24,6 @@ use {
2224
std::io::Cursor
2325
};
2426

25-
#[cfg(not(target_os = "windows"))]
26-
use std::os::unix::io::AsRawFd;
27-
2827
/// FilesystemPersister persists channel data on disk, where each channel's
2928
/// data is stored in a file named after its funding outpoint.
3029
///
@@ -41,13 +40,9 @@ pub struct FilesystemPersister {
4140
path_to_channel_data: String,
4241
}
4342

44-
trait DiskWriteable {
45-
fn write(&self, writer: &mut fs::File) -> Result<(), Error>;
46-
}
47-
4843
impl<ChanSigner: ChannelKeys> DiskWriteable for ChannelMonitor<ChanSigner> {
49-
fn write(&self, writer: &mut fs::File) -> Result<(), Error> {
50-
Writeable::write(self, writer)
44+
fn write_to_file(&self, writer: &mut fs::File) -> Result<(), Error> {
45+
self.write(writer)
5146
}
5247
}
5348

@@ -60,41 +55,6 @@ impl FilesystemPersister {
6055
}
6156
}
6257

63-
fn get_full_filepath(&self, funding_txo: OutPoint) -> String {
64-
let mut path = PathBuf::from(&self.path_to_channel_data);
65-
path.push(format!("{}_{}", funding_txo.txid.to_hex(), funding_txo.index));
66-
path.to_str().unwrap().to_string()
67-
}
68-
69-
// Utility to write a file to disk.
70-
fn write_channel_data(&self, funding_txo: OutPoint, monitor: &dyn DiskWriteable) -> std::io::Result<()> {
71-
fs::create_dir_all(&self.path_to_channel_data)?;
72-
// Do a crazy dance with lots of fsync()s to be overly cautious here...
73-
// We never want to end up in a state where we've lost the old data, or end up using the
74-
// old data on power loss after we've returned.
75-
// The way to atomically write a file on Unix platforms is:
76-
// open(tmpname), write(tmpfile), fsync(tmpfile), close(tmpfile), rename(), fsync(dir)
77-
let filename = self.get_full_filepath(funding_txo);
78-
let tmp_filename = format!("{}.tmp", filename.clone());
79-
80-
{
81-
// Note that going by rust-lang/rust@d602a6b, on MacOS it is only safe to use
82-
// rust stdlib 1.36 or higher.
83-
let mut f = fs::File::create(&tmp_filename)?;
84-
monitor.write(&mut f)?;
85-
f.sync_all()?;
86-
}
87-
fs::rename(&tmp_filename, &filename)?;
88-
// Fsync the parent directory on Unix.
89-
#[cfg(not(target_os = "windows"))]
90-
{
91-
let path = Path::new(&filename).parent().unwrap();
92-
let dir_file = fs::OpenOptions::new().read(true).open(path)?;
93-
unsafe { libc::fsync(dir_file.as_raw_fd()); }
94-
}
95-
Ok(())
96-
}
97-
9858
#[cfg(test)]
9959
fn load_channel_data<Keys: KeysInterface>(&self, keys: &Keys) ->
10060
Result<HashMap<OutPoint, ChannelMonitor<Keys::ChanKeySigner>>, ChannelMonitorUpdateErr> {
@@ -132,28 +92,18 @@ impl FilesystemPersister {
13292

13393
impl<ChanSigner: ChannelKeys + Send + Sync> channelmonitor::Persist<ChanSigner> for FilesystemPersister {
13494
fn persist_new_channel(&self, funding_txo: OutPoint, monitor: &ChannelMonitor<ChanSigner>) -> Result<(), ChannelMonitorUpdateErr> {
135-
self.write_channel_data(funding_txo, monitor)
95+
let filename = format!("{}_{}", funding_txo.txid.to_hex(), funding_txo.index);
96+
util::write_to_file(self.path_to_channel_data.clone(), filename, monitor)
13697
.map_err(|_| ChannelMonitorUpdateErr::PermanentFailure)
13798
}
13899

139100
fn update_persisted_channel(&self, funding_txo: OutPoint, _update: &ChannelMonitorUpdate, monitor: &ChannelMonitor<ChanSigner>) -> Result<(), ChannelMonitorUpdateErr> {
140-
self.write_channel_data(funding_txo, monitor)
101+
let filename = format!("{}_{}", funding_txo.txid.to_hex(), funding_txo.index);
102+
util::write_to_file(self.path_to_channel_data.clone(), filename, monitor)
141103
.map_err(|_| ChannelMonitorUpdateErr::PermanentFailure)
142104
}
143105
}
144106

145-
#[cfg(test)]
146-
impl Drop for FilesystemPersister {
147-
fn drop(&mut self) {
148-
// We test for invalid directory names, so it's OK if directory removal
149-
// fails.
150-
match fs::remove_dir_all(&self.path_to_channel_data) {
151-
Err(e) => println!("Failed to remove test persister directory: {}", e),
152-
_ => {}
153-
}
154-
}
155-
}
156-
157107
#[cfg(test)]
158108
mod tests {
159109
extern crate lightning;
@@ -162,29 +112,29 @@ mod tests {
162112
use bitcoin::blockdata::block::{Block, BlockHeader};
163113
use bitcoin::hashes::hex::FromHex;
164114
use bitcoin::Txid;
165-
use DiskWriteable;
166-
use Error;
167115
use lightning::chain::channelmonitor::{Persist, ChannelMonitorUpdateErr};
168116
use lightning::chain::transaction::OutPoint;
169117
use lightning::{check_closed_broadcast, check_added_monitors};
170118
use lightning::ln::features::InitFeatures;
171119
use lightning::ln::functional_test_utils::*;
172120
use lightning::ln::msgs::ErrorAction;
173121
use lightning::util::events::{MessageSendEventsProvider, MessageSendEvent};
174-
use lightning::util::ser::Writer;
175122
use lightning::util::test_utils;
176123
use std::fs;
177-
use std::io;
178124
#[cfg(target_os = "windows")]
179125
use {
180126
lightning::get_event_msg,
181127
lightning::ln::msgs::ChannelMessageHandler,
182128
};
183129

184-
struct TestWriteable{}
185-
impl DiskWriteable for TestWriteable {
186-
fn write(&self, writer: &mut fs::File) -> Result<(), Error> {
187-
writer.write_all(&[42; 1])
130+
impl Drop for FilesystemPersister {
131+
fn drop(&mut self) {
132+
// We test for invalid directory names, so it's OK if directory removal
133+
// fails.
134+
match fs::remove_dir_all(&self.path_to_channel_data) {
135+
Err(e) => println!("Failed to remove test persister directory: {}", e),
136+
_ => {}
137+
}
188138
}
189139
}
190140

@@ -256,88 +206,6 @@ mod tests {
256206
check_persisted_data!(11);
257207
}
258208

259-
// Test that if the persister's path to channel data is read-only, writing
260-
// data to it fails. Windows ignores the read-only flag for folders, so this
261-
// test is Unix-only.
262-
#[cfg(not(target_os = "windows"))]
263-
#[test]
264-
fn test_readonly_dir() {
265-
let persister = FilesystemPersister::new("test_readonly_dir_persister".to_string());
266-
let test_writeable = TestWriteable{};
267-
let test_txo = OutPoint {
268-
txid: Txid::from_hex("8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be").unwrap(),
269-
index: 0
270-
};
271-
// Create the persister's directory and set it to read-only.
272-
let path = &persister.path_to_channel_data;
273-
fs::create_dir_all(path).unwrap();
274-
let mut perms = fs::metadata(path).unwrap().permissions();
275-
perms.set_readonly(true);
276-
fs::set_permissions(path, perms).unwrap();
277-
match persister.write_channel_data(test_txo, &test_writeable) {
278-
Err(e) => assert_eq!(e.kind(), io::ErrorKind::PermissionDenied),
279-
_ => panic!("Unexpected error message")
280-
}
281-
}
282-
283-
// Test failure to rename in the process of atomically creating a channel
284-
// monitor's file. We induce this failure by making the `tmp` file a
285-
// directory.
286-
// Explanation: given "from" = the file being renamed, "to" = the
287-
// renamee that already exists: Windows should fail because it'll fail
288-
// whenever "to" is a directory, and Unix should fail because if "from" is a
289-
// file, then "to" is also required to be a file.
290-
#[test]
291-
fn test_rename_failure() {
292-
let persister = FilesystemPersister::new("test_rename_failure".to_string());
293-
let test_writeable = TestWriteable{};
294-
let txid_hex = "8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be";
295-
let outp_idx = 0;
296-
let test_txo = OutPoint {
297-
txid: Txid::from_hex(txid_hex).unwrap(),
298-
index: outp_idx,
299-
};
300-
// Create the channel data file and make it a directory.
301-
let path = &persister.path_to_channel_data;
302-
fs::create_dir_all(format!("{}/{}_{}", path, txid_hex, outp_idx)).unwrap();
303-
match persister.write_channel_data(test_txo, &test_writeable) {
304-
Err(e) => {
305-
#[cfg(not(target_os = "windows"))]
306-
assert_eq!(e.kind(), io::ErrorKind::Other);
307-
#[cfg(target_os = "windows")]
308-
assert_eq!(e.kind(), io::ErrorKind::PermissionDenied);
309-
}
310-
_ => panic!("Unexpected error message")
311-
}
312-
}
313-
314-
// Test failure to create the temporary file in the persistence process.
315-
// We induce this failure by having the temp file already exist and be a
316-
// directory.
317-
#[test]
318-
fn test_tmp_file_creation_failure() {
319-
let persister = FilesystemPersister::new("test_tmp_file_creation_failure".to_string());
320-
let test_writeable = TestWriteable{};
321-
let txid_hex = "8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be";
322-
let outp_idx = 0;
323-
let test_txo = OutPoint {
324-
txid: Txid::from_hex(txid_hex).unwrap(),
325-
index: outp_idx,
326-
};
327-
// Create the tmp file and make it a directory.
328-
let path = &persister.path_to_channel_data;
329-
fs::create_dir_all(format!("{}/{}_{}.tmp", path, txid_hex, outp_idx)).unwrap();
330-
match persister.write_channel_data(test_txo, &test_writeable) {
331-
Err(e) => {
332-
#[cfg(not(target_os = "windows"))]
333-
assert_eq!(e.kind(), io::ErrorKind::Other);
334-
#[cfg(target_os = "windows")]
335-
assert_eq!(e.kind(), io::ErrorKind::PermissionDenied);
336-
}
337-
_ => panic!("Unexpected error message")
338-
}
339-
}
340-
341209
// Test that if the persister's path to channel data is read-only, writing a
342210
// monitor to it results in the persister returning a PermanentFailure.
343211
// Windows ignores the read-only flag for folders, so this test is Unix-only.

lightning-persister/src/util.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
use std::fs;
2+
use std::path::{Path, PathBuf};
3+
4+
#[cfg(not(target_os = "windows"))]
5+
use std::os::unix::io::AsRawFd;
6+
7+
pub(crate) trait DiskWriteable {
8+
fn write_to_file(&self, writer: &mut fs::File) -> Result<(), std::io::Error>;
9+
}
10+
11+
pub fn get_full_filepath(filepath: String, filename: String) -> String {
12+
let mut path = PathBuf::from(filepath);
13+
path.push(filename);
14+
path.to_str().unwrap().to_string()
15+
}
16+
17+
#[allow(bare_trait_objects)]
18+
pub(crate) fn write_to_file<D: DiskWriteable>(path: String, filename: String, data: &D) -> std::io::Result<()> {
19+
fs::create_dir_all(path.clone())?;
20+
// Do a crazy dance with lots of fsync()s to be overly cautious here...
21+
// We never want to end up in a state where we've lost the old data, or end up using the
22+
// old data on power loss after we've returned.
23+
// The way to atomically write a file on Unix platforms is:
24+
// open(tmpname), write(tmpfile), fsync(tmpfile), close(tmpfile), rename(), fsync(dir)
25+
let filename_with_path = get_full_filepath(path, filename);
26+
let tmp_filename = format!("{}.tmp", filename_with_path);
27+
28+
{
29+
// Note that going by rust-lang/rust@d602a6b, on MacOS it is only safe to use
30+
// rust stdlib 1.36 or higher.
31+
let mut f = fs::File::create(&tmp_filename)?;
32+
data.write_to_file(&mut f)?;
33+
f.sync_all()?;
34+
}
35+
fs::rename(&tmp_filename, &filename_with_path)?;
36+
// Fsync the parent directory on Unix.
37+
#[cfg(not(target_os = "windows"))]
38+
{
39+
let path = Path::new(&filename_with_path).parent().unwrap();
40+
let dir_file = fs::OpenOptions::new().read(true).open(path)?;
41+
unsafe { libc::fsync(dir_file.as_raw_fd()); }
42+
}
43+
Ok(())
44+
}
45+
46+
#[cfg(test)]
47+
mod tests {
48+
use super::{DiskWriteable, get_full_filepath, write_to_file};
49+
use std::fs;
50+
use std::io;
51+
use std::io::Write;
52+
53+
struct TestWriteable{}
54+
impl DiskWriteable for TestWriteable {
55+
fn write_to_file(&self, writer: &mut fs::File) -> Result<(), io::Error> {
56+
writer.write_all(&[42; 1])
57+
}
58+
}
59+
60+
// Test that if the persister's path to channel data is read-only, writing
61+
// data to it fails. Windows ignores the read-only flag for folders, so this
62+
// test is Unix-only.
63+
#[cfg(not(target_os = "windows"))]
64+
#[test]
65+
fn test_readonly_dir() {
66+
let test_writeable = TestWriteable{};
67+
let filename = "test_readonly_dir_persister_filename".to_string();
68+
let path = "test_readonly_dir_persister_dir";
69+
fs::create_dir_all(path.to_string()).unwrap();
70+
let mut perms = fs::metadata(path.to_string()).unwrap().permissions();
71+
perms.set_readonly(true);
72+
fs::set_permissions(path.to_string(), perms).unwrap();
73+
match write_to_file(path.to_string(), filename, &test_writeable) {
74+
Err(e) => assert_eq!(e.kind(), io::ErrorKind::PermissionDenied),
75+
_ => panic!("Unexpected error message")
76+
}
77+
}
78+
79+
// Test failure to rename in the process of atomically creating a channel
80+
// monitor's file. We induce this failure by making the `tmp` file a
81+
// directory.
82+
// Explanation: given "from" = the file being renamed, "to" = the
83+
// renamee that already exists: Windows should fail because it'll fail
84+
// whenever "to" is a directory, and Unix should fail because if "from" is a
85+
// file, then "to" is also required to be a file.
86+
#[test]
87+
fn test_rename_failure() {
88+
let test_writeable = TestWriteable{};
89+
let filename = "test_rename_failure_filename";
90+
let path = "test_rename_failure_dir";
91+
// Create the channel data file and make it a directory.
92+
fs::create_dir_all(get_full_filepath(path.to_string(), filename.to_string())).unwrap();
93+
match write_to_file(path.to_string(), filename.to_string(), &test_writeable) {
94+
Err(e) => {
95+
#[cfg(not(target_os = "windows"))]
96+
assert_eq!(e.kind(), io::ErrorKind::Other);
97+
#[cfg(target_os = "windows")]
98+
assert_eq!(e.kind(), io::ErrorKind::PermissionDenied);
99+
}
100+
_ => panic!("Unexpected error message")
101+
}
102+
fs::remove_dir_all(path).unwrap();
103+
}
104+
105+
#[test]
106+
fn test_diskwriteable_failure() {
107+
struct FailingWriteable {}
108+
impl DiskWriteable for FailingWriteable {
109+
fn write_to_file(&self, _writer: &mut fs::File) -> Result<(), std::io::Error> {
110+
Err(std::io::Error::new(std::io::ErrorKind::Other, "expected failure"))
111+
}
112+
}
113+
114+
let filename = "test_diskwriteable_failure";
115+
let path = "test_diskwriteable_failure_dir";
116+
let test_writeable = FailingWriteable{};
117+
match write_to_file(path.to_string(), filename.to_string(), &test_writeable) {
118+
Err(e) => {
119+
assert_eq!(e.kind(), std::io::ErrorKind::Other);
120+
assert_eq!(e.get_ref().unwrap().to_string(), "expected failure");
121+
},
122+
_ => panic!("unexpected result")
123+
}
124+
fs::remove_dir_all(path).unwrap();
125+
}
126+
127+
// Test failure to create the temporary file in the persistence process.
128+
// We induce this failure by having the temp file already exist and be a
129+
// directory.
130+
#[test]
131+
fn test_tmp_file_creation_failure() {
132+
let test_writeable = TestWriteable{};
133+
let filename = "test_tmp_file_creation_failure_filename".to_string();
134+
let path = "test_tmp_file_creation_failure_dir".to_string();
135+
136+
// Create the tmp file and make it a directory.
137+
let tmp_path = get_full_filepath(path.clone(), format!("{}.tmp", filename.clone()));
138+
fs::create_dir_all(tmp_path).unwrap();
139+
match write_to_file(path, filename, &test_writeable) {
140+
Err(e) => {
141+
#[cfg(not(target_os = "windows"))]
142+
assert_eq!(e.kind(), io::ErrorKind::Other);
143+
#[cfg(target_os = "windows")]
144+
assert_eq!(e.kind(), io::ErrorKind::PermissionDenied);
145+
}
146+
_ => panic!("Unexpected error message")
147+
}
148+
}
149+
}

0 commit comments

Comments
 (0)