mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 16:56:59 +01:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2b5916b99 | ||
|
|
4626ce17a7 | ||
|
|
630f316fe6 | ||
|
|
801681f747 | ||
|
|
e7382f2c7e | ||
|
|
7046f0f3ef | ||
|
|
37e8dae61b | ||
|
|
c52f2098f2 | ||
|
|
f4a2674b9c | ||
|
|
347861bbc1 | ||
|
|
c57763b07f | ||
|
|
846dfd8d14 | ||
|
|
2ae99de844 | ||
|
|
69ff216e7d | ||
|
|
b3ea564c6b | ||
|
|
463c97a14d | ||
|
|
176112a9cb | ||
|
|
639ca359fa |
No files matched your search
Generated
+10
-7
@@ -4380,9 +4380,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.45"
|
version = "0.23.46"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -4933,7 +4933,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.3.4",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
@@ -5249,9 +5249,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typed-path"
|
name = "typed-path"
|
||||||
version = "0.12.3"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
|
checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
@@ -6113,15 +6113,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zip"
|
name = "zip"
|
||||||
version = "8.6.0"
|
version = "9.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
|
checksum = "3f5b58784007ff6e5f280ec82c8596686a3b9b5dd5836c63288d4479f84728b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
"getrandom 0.4.3",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
"nix",
|
||||||
"typed-path",
|
"typed-path",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
"zopfli",
|
"zopfli",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ ignore = "0.4"
|
|||||||
sha2 = "0.11"
|
sha2 = "0.11"
|
||||||
# Google Takeout exports are zip sets; the importer reads their central
|
# Google Takeout exports are zip sets; the importer reads their central
|
||||||
# directories and streams entries out without extracting to disk first.
|
# directories and streams entries out without extracting to disk first.
|
||||||
zip = { version = "8", default-features = false, features = ["deflate"] }
|
zip = { version = "9", default-features = false, features = ["deflate"] }
|
||||||
# Proton's photo duplicate detection is keyed on the plaintext SHA-1, so an
|
# Proton's photo duplicate detection is keyed on the plaintext SHA-1, so an
|
||||||
# import has to speak SHA-1 whatever its age.
|
# import has to speak SHA-1 whatever its age.
|
||||||
sha1 = "0.11"
|
sha1 = "0.11"
|
||||||
|
|||||||
@@ -2527,14 +2527,30 @@ fn mount_once(mountpoint: Option<PathBuf>) -> Result<pdfs_fuse::MountOutcome> {
|
|||||||
"could not save the refreshed session tokens; the next start may need `pdfs login`"
|
"could not save the refreshed session tokens; the next start may need `pdfs login`"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Dropping the runtime waits for every blocking task, however long; a call
|
release_runtime(rt, matches!(result, Ok(pdfs_fuse::MountOutcome::Shutdown)));
|
||||||
// still stuck on Drive would hold the stop until systemd kills it (B165).
|
|
||||||
rt.shutdown_timeout(std::time::Duration::from_secs(2));
|
|
||||||
|
|
||||||
let outcome = result.context("mount failed")?;
|
let outcome = result.context("mount failed")?;
|
||||||
Ok(outcome)
|
Ok(outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Let go of a mount's runtime once the mount has ended.
|
||||||
|
///
|
||||||
|
/// On a stop the process exits right after, so the runtime is left running
|
||||||
|
/// for it to end with. Shutting it down first broke every thread the mount
|
||||||
|
/// does not join, such as a size lookup or a profile backup a request started
|
||||||
|
/// during the stop: its next call to Drive panicked with "A Tokio 1.x context
|
||||||
|
/// was found, but it is being shutdown" (`docs/BUGS.md` B276). A mount that
|
||||||
|
/// is about to be tried again does shut it down, because a new runtime
|
||||||
|
/// replaces it. Dropping it would wait for every blocking task, however long;
|
||||||
|
/// a call still stuck on Drive would hold the retry (B165).
|
||||||
|
fn release_runtime(rt: tokio::runtime::Runtime, exiting: bool) {
|
||||||
|
if exiting {
|
||||||
|
std::mem::forget(rt);
|
||||||
|
} else {
|
||||||
|
rt.shutdown_timeout(std::time::Duration::from_secs(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn cmd_pin(path: PathBuf) -> Result<()> {
|
fn cmd_pin(path: PathBuf) -> Result<()> {
|
||||||
match control_request(CtlRequest::Pin {
|
match control_request(CtlRequest::Pin {
|
||||||
path: path_arg(&path)?,
|
path: path_arg(&path)?,
|
||||||
@@ -4146,6 +4162,47 @@ fn control_request(req: CtlRequest) -> Result<CtlResponse> {
|
|||||||
.with_context(|| format!("talk to mount daemon at {}", socket.display()))
|
.with_context(|| format!("talk to mount daemon at {}", socket.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod release_runtime_tests {
|
||||||
|
use super::*;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// A thread the mount never joined still reaches Drive after a stop. It
|
||||||
|
/// used to panic on its first timer once the runtime was shut down.
|
||||||
|
#[test]
|
||||||
|
fn a_thread_left_running_at_a_stop_can_still_use_the_runtime() {
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
let handle = rt.handle().clone();
|
||||||
|
let (released_tx, released_rx) = std::sync::mpsc::channel();
|
||||||
|
let straggler = std::thread::spawn(move || {
|
||||||
|
released_rx.recv().unwrap();
|
||||||
|
handle.block_on(async {
|
||||||
|
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||||
|
7
|
||||||
|
})
|
||||||
|
});
|
||||||
|
release_runtime(rt, true);
|
||||||
|
released_tx.send(()).unwrap();
|
||||||
|
assert_eq!(straggler.join().expect("the thread must not panic"), 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_mount_tried_again_shuts_its_runtime_down() {
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
|
||||||
|
rt.spawn(async move {
|
||||||
|
let _done = done_tx;
|
||||||
|
std::future::pending::<()>().await;
|
||||||
|
});
|
||||||
|
release_runtime(rt, false);
|
||||||
|
// The task is dropped with the runtime, and its sender with it.
|
||||||
|
assert_eq!(
|
||||||
|
done_rx.recv_timeout(Duration::from_secs(5)),
|
||||||
|
Err(std::sync::mpsc::RecvTimeoutError::Disconnected)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod pause_tests {
|
mod pause_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -621,10 +621,20 @@ impl ContentCache {
|
|||||||
for entry in rd.flatten() {
|
for entry in rd.flatten() {
|
||||||
let name = entry.file_name();
|
let name = entry.file_name();
|
||||||
let Some(name) = name.to_str() else { continue };
|
let Some(name) = name.to_str() else { continue };
|
||||||
|
// A `.meta` tags the blob named like it without the suffix. One
|
||||||
|
// whose blob is gone is what an eviction leaves between its two
|
||||||
|
// removals, by a crash or by a store of the same uid landing in
|
||||||
|
// between, and nothing else would ever remove it.
|
||||||
|
if let Some(blob) = name.strip_suffix(".meta") {
|
||||||
|
if !dir.join(blob).exists() {
|
||||||
|
let _ = std::fs::remove_file(entry.path());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// `.geom` is a block-geometry sidecar (see `GeometryMeta`), tens
|
// `.geom` is a block-geometry sidecar (see `GeometryMeta`), tens
|
||||||
// of bytes and not a cached block; like `.meta` it is neither
|
// of bytes and not a cached block; like `.meta` it is neither
|
||||||
// budgeted nor evictable on its own.
|
// budgeted nor evictable on its own.
|
||||||
if name.ends_with(".meta") || name.ends_with(".geom") {
|
if name.ends_with(".geom") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// A `.tmp` is a staging file from a store that never completed
|
// A `.tmp` is a staging file from a store that never completed
|
||||||
@@ -2250,6 +2260,28 @@ mod tests {
|
|||||||
assert!(total <= 8, "budget enforced after reconcile, total={total}");
|
assert!(total <= 8, "budget enforced after reconcile, total={total}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An eviction removes the blob, then its tag. A crash in between, or a
|
||||||
|
/// store of the same uid landing in between, left the tag on its own, and
|
||||||
|
/// nothing removed it.
|
||||||
|
#[test]
|
||||||
|
fn a_tag_without_its_blob_is_swept_at_open() {
|
||||||
|
let dir = TempDir::new();
|
||||||
|
let content = dir.path().join("content");
|
||||||
|
let pins = dir.path().join("pins.json");
|
||||||
|
let db = Arc::new(Db::open_in_memory().unwrap());
|
||||||
|
{
|
||||||
|
let c = ContentCache::open(content.clone(), pins.clone(), 0, db.clone()).unwrap();
|
||||||
|
c.store(&uid("kept"), 1, 4, b"kept").unwrap();
|
||||||
|
c.store(&uid("gone"), 1, 4, b"gone").unwrap();
|
||||||
|
std::fs::remove_file(c.blob_path(&uid("gone"))).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let c = ContentCache::open(content, pins, 0, db).unwrap();
|
||||||
|
assert!(!c.meta_path(&uid("gone")).exists());
|
||||||
|
assert!(c.meta_path(&uid("kept")).exists());
|
||||||
|
assert_eq!(c.read_range(&uid("kept"), 1, 4, 0, 4).unwrap(), b"kept");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stale_metadata_is_a_miss() {
|
fn stale_metadata_is_a_miss() {
|
||||||
let (c, _d) = cache();
|
let (c, _d) = cache();
|
||||||
|
|||||||
@@ -1332,8 +1332,8 @@ mod tests {
|
|||||||
));
|
));
|
||||||
let file = std::fs::File::create(&path).unwrap();
|
let file = std::fs::File::create(&path).unwrap();
|
||||||
let mut zip = zip::ZipWriter::new(file);
|
let mut zip = zip::ZipWriter::new(file);
|
||||||
let options: zip::write::FileOptions<'_, ()> =
|
let options = zip::write::SimpleFileOptions::default()
|
||||||
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
.compression_method(zip::CompressionMethod::Stored);
|
||||||
for (name, body) in files {
|
for (name, body) in files {
|
||||||
zip.start_file(*name, options).unwrap();
|
zip.start_file(*name, options).unwrap();
|
||||||
zip.write_all(body).unwrap();
|
zip.write_all(body).unwrap();
|
||||||
|
|||||||
@@ -1281,6 +1281,13 @@ impl Core {
|
|||||||
sent: (&sent.0, &sent.1),
|
sent: (&sent.0, &sent.1),
|
||||||
landed: (&home.to_string(), &name),
|
landed: (&home.to_string(), &name),
|
||||||
};
|
};
|
||||||
|
// Counted from the op's retirement until the bytes are cached and the
|
||||||
|
// tree has the real uid, as an upload is. Uncounted, the queue read
|
||||||
|
// empty while that still ran: a delete in between took an open file
|
||||||
|
// for closed and dropped what its handle reads, and a script that
|
||||||
|
// waited for an empty queue read the file before it was cached
|
||||||
|
// (`docs/BUGS.md` B257).
|
||||||
|
let _landing = LandingUpload::begin(&self.landing_uploads);
|
||||||
let newer = match self.retire_create(op, uploaded, &landing, &local, &real)? {
|
let newer = match self.retire_create(op, uploaded, &landing, &local, &real)? {
|
||||||
CreateRetired::Withdrawn { trash } => {
|
CreateRetired::Withdrawn { trash } => {
|
||||||
return self.trash_withdrawn_create(op, trash, &local, &real, &name);
|
return self.trash_withdrawn_create(op, trash, &local, &real, &name);
|
||||||
|
|||||||
@@ -2376,7 +2376,7 @@ impl ProtonFs {
|
|||||||
// regardless — the file is gone from their point of view the moment the
|
// regardless — the file is gone from their point of view the moment the
|
||||||
// command returns. The same when the network goes
|
// command returns. The same when the network goes
|
||||||
// away under the remote call below.
|
// away under the remote call below.
|
||||||
let queue_it = |reply: ReplyEmpty| match self.core.queue_trash(&uid, name) {
|
let queue_it = |reply: ReplyEmpty| match self.core.queue_trash(&uid, name, open_now) {
|
||||||
Ok(()) => reply.ok(),
|
Ok(()) => reply.ok(),
|
||||||
Err(e) => reply.error(e),
|
Err(e) => reply.error(e),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3999,7 +3999,12 @@ impl Core {
|
|||||||
/// the cached ones, as when the trash is sent at once: a partial write
|
/// the cached ones, as when the trash is sent at once: a partial write
|
||||||
/// fills its gaps from them, offline too (`docs/BUGS.md` B186). The final
|
/// fills its gaps from them, offline too (`docs/BUGS.md` B186). The final
|
||||||
/// close or the trash landing, whichever comes last, lets go of them.
|
/// close or the trash landing, whichever comes last, lets go of them.
|
||||||
fn queue_trash(&self, uid: &NodeUid, name: &str) -> Result<(), Errno> {
|
///
|
||||||
|
/// `open` is whether the caller found the file open. A create that landed
|
||||||
|
/// under a delete is trashed by its real uid before the tree has it, so
|
||||||
|
/// asking the tree alone took an open file for closed (`docs/BUGS.md`
|
||||||
|
/// B257).
|
||||||
|
fn queue_trash(&self, uid: &NodeUid, name: &str, open: bool) -> Result<(), Errno> {
|
||||||
self.require_uid_writable(uid)?;
|
self.require_uid_writable(uid)?;
|
||||||
// The bytes below are about to be discarded, and a drain worker may be
|
// The bytes below are about to be discarded, and a drain worker may be
|
||||||
// reading them onto the wire right now. Stop it first: with several
|
// reading them onto the wire right now. Stop it first: with several
|
||||||
@@ -4014,7 +4019,7 @@ impl Core {
|
|||||||
error!(%uid, error = %e, "queueing trash failed");
|
error!(%uid, error = %e, "queueing trash failed");
|
||||||
Errno::EIO
|
Errno::EIO
|
||||||
})?;
|
})?;
|
||||||
let open_now = self.is_open_anywhere(uid);
|
let open_now = open || self.is_open_anywhere(uid);
|
||||||
if open_now {
|
if open_now {
|
||||||
let read_from = self.pending_blob(uid);
|
let read_from = self.pending_blob(uid);
|
||||||
self.release_dropped_below(
|
self.release_dropped_below(
|
||||||
@@ -5383,9 +5388,12 @@ impl Core {
|
|||||||
/// `unlink` under an open handle. Its queued ops go now, or the drain would
|
/// `unlink` under an open handle. Its queued ops go now, or the drain would
|
||||||
/// upload to the node after it was trashed (`docs/BUGS.md` B106).
|
/// upload to the node after it was trashed (`docs/BUGS.md` B106).
|
||||||
fn remove_replaced(&self, ino: u64, uid: &NodeUid, name: &str) -> Result<(), Errno> {
|
fn remove_replaced(&self, ino: u64, uid: &NodeUid, name: &str) -> Result<(), Errno> {
|
||||||
|
// Asked under the uid the tree has now: a landed create's real uid is
|
||||||
|
// not in the tree yet (`docs/BUGS.md` B257).
|
||||||
|
let open = self.is_open_anywhere(uid);
|
||||||
let landed;
|
let landed;
|
||||||
let uid = if is_local_uid(uid) {
|
let uid = if is_local_uid(uid) {
|
||||||
if self.is_open_anywhere(uid) {
|
if open {
|
||||||
self.withdraw_queued_ops(uid)?;
|
self.withdraw_queued_ops(uid)?;
|
||||||
} else {
|
} else {
|
||||||
self.discard_queued_ops(uid)?;
|
self.discard_queued_ops(uid)?;
|
||||||
@@ -5410,7 +5418,7 @@ impl Core {
|
|||||||
// Local-first or offline, or the network goes away under the call:
|
// Local-first or offline, or the network goes away under the call:
|
||||||
// queue the trash. The rename that follows queues as well, behind it.
|
// queue the trash. The rename that follows queues as well, behind it.
|
||||||
if !self.sends_inline() {
|
if !self.sends_inline() {
|
||||||
return self.queue_trash(uid, name);
|
return self.queue_trash(uid, name, open);
|
||||||
}
|
}
|
||||||
self.require_uid_writable(uid)?;
|
self.require_uid_writable(uid)?;
|
||||||
if let Err(e) = self
|
if let Err(e) = self
|
||||||
@@ -5418,7 +5426,7 @@ impl Core {
|
|||||||
.and_then(batch::into_unit)
|
.and_then(batch::into_unit)
|
||||||
{
|
{
|
||||||
if self.lost_link(&e, "trash a replaced node") {
|
if self.lost_link(&e, "trash a replaced node") {
|
||||||
return self.queue_trash(uid, name);
|
return self.queue_trash(uid, name, open);
|
||||||
}
|
}
|
||||||
error!(%uid, name, error = %e, "trashing the node a rename replaces failed");
|
error!(%uid, name, error = %e, "trashing the node a rename replaces failed");
|
||||||
self.log_activity(ActivityKind::Trash, name, e.to_string(), false);
|
self.log_activity(ActivityKind::Trash, name, e.to_string(), false);
|
||||||
@@ -9258,7 +9266,7 @@ mod tests {
|
|||||||
assert!(!replaced.contains("st.forget(uid)"));
|
assert!(!replaced.contains("st.forget(uid)"));
|
||||||
assert_before(
|
assert_before(
|
||||||
replaced,
|
replaced,
|
||||||
"if self.is_open_anywhere(uid)",
|
"let open = self.is_open_anywhere(uid)",
|
||||||
"self.discard_queued_ops(uid)",
|
"self.discard_queued_ops(uid)",
|
||||||
);
|
);
|
||||||
assert_before(replaced, "if open_now", "self.cache.evict(uid)");
|
assert_before(replaced, "if open_now", "self.cache.evict(uid)");
|
||||||
|
|||||||
@@ -130,6 +130,22 @@ impl Daemon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether nothing is queued and no upload is still landing, as `pdfs
|
||||||
|
/// status` counts it. An empty [`Self::pending`] is not enough before a
|
||||||
|
/// read that needs the cache: the op goes before the uploaded bytes are
|
||||||
|
/// cached, so a test that went offline at once raced the drain.
|
||||||
|
pub(crate) fn drained(&self) -> bool {
|
||||||
|
self.pending().is_ok_and(|items| items.is_empty())
|
||||||
|
&& matches!(
|
||||||
|
self.request(&Request::Status),
|
||||||
|
Ok(Response::Status {
|
||||||
|
pending_uploads: 0,
|
||||||
|
pending_changes: 0,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the mount believes Proton is reachable, as `pdfs status` shows
|
/// Whether the mount believes Proton is reachable, as `pdfs status` shows
|
||||||
/// it.
|
/// it.
|
||||||
pub(crate) fn online(&self) -> io::Result<bool> {
|
pub(crate) fn online(&self) -> io::Result<bool> {
|
||||||
@@ -565,7 +581,7 @@ mod tests {
|
|||||||
|
|
||||||
std::fs::write(&path, b"0123456789").unwrap();
|
std::fs::write(&path, b"0123456789").unwrap();
|
||||||
assert!(wait_until(Duration::from_secs(30), || {
|
assert!(wait_until(Duration::from_secs(30), || {
|
||||||
daemon.pending().is_ok_and(|items| items.is_empty())
|
daemon.drained()
|
||||||
&& drive.tree().get("f.txt") == Some(&Entry::File(Arc::new(b"0123456789".to_vec())))
|
&& drive.tree().get("f.txt") == Some(&Entry::File(Arc::new(b"0123456789".to_vec())))
|
||||||
}));
|
}));
|
||||||
daemon.client.set_online(false);
|
daemon.client.set_online(false);
|
||||||
@@ -692,6 +708,55 @@ mod tests {
|
|||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
|
||||||
|
fn a_new_file_deleted_while_open_as_its_create_lands_still_reads() {
|
||||||
|
// The create's op was retired and the tree still had the placeholder.
|
||||||
|
// The queue read empty, and the delete trashed the node by its real
|
||||||
|
// uid, which the tree did not have yet: it took the open file for
|
||||||
|
// closed and dropped the bytes its handle reads (B257).
|
||||||
|
use std::io::{Read, Seek, SeekFrom, Write};
|
||||||
|
let drive = FakeDrive::new();
|
||||||
|
let dir = scratch("delete-open-as-create-lands");
|
||||||
|
let daemon = Daemon::start(&dir, drive.client(1, Faults::lan())).unwrap();
|
||||||
|
let path = daemon.mountpoint.join("f.txt");
|
||||||
|
|
||||||
|
let landing = daemon.client.hold_next_landing_of_any();
|
||||||
|
std::fs::write(&path, b"0123456789").unwrap();
|
||||||
|
assert!(wait_until(Duration::from_secs(30), || landing.reached()));
|
||||||
|
assert!(!daemon.drained(), "the landing create is counted");
|
||||||
|
let mut file = std::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(&path)
|
||||||
|
.unwrap();
|
||||||
|
drop(landing);
|
||||||
|
assert!(wait_until(Duration::from_secs(30), || daemon.drained()));
|
||||||
|
|
||||||
|
daemon.client.set_online(false);
|
||||||
|
std::fs::remove_file(&path).unwrap();
|
||||||
|
file.seek(SeekFrom::End(0)).unwrap();
|
||||||
|
file.write_all(b"ab").unwrap();
|
||||||
|
let mut read = Vec::new();
|
||||||
|
file.seek(SeekFrom::Start(0)).unwrap();
|
||||||
|
file.read_to_end(&mut read).unwrap();
|
||||||
|
assert_eq!(read, b"0123456789ab");
|
||||||
|
drop(file);
|
||||||
|
|
||||||
|
daemon.client.set_online(true);
|
||||||
|
assert!(
|
||||||
|
wait_until(Duration::from_secs(30), || {
|
||||||
|
daemon.drained() && drive.tree().is_empty()
|
||||||
|
}),
|
||||||
|
"{:?} {:?}",
|
||||||
|
daemon.pending(),
|
||||||
|
drive.tree()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(matches!(daemon.stop(), Ok(MountOutcome::Shutdown)));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
|
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
|
||||||
fn a_restart_does_not_bring_back_the_write_of_a_file_deleted_while_open() {
|
fn a_restart_does_not_bring_back_the_write_of_a_file_deleted_while_open() {
|
||||||
@@ -706,7 +771,7 @@ mod tests {
|
|||||||
|
|
||||||
std::fs::write(&path, b"0123456789").unwrap();
|
std::fs::write(&path, b"0123456789").unwrap();
|
||||||
assert!(wait_until(Duration::from_secs(30), || {
|
assert!(wait_until(Duration::from_secs(30), || {
|
||||||
daemon.pending().is_ok_and(|items| items.is_empty())
|
daemon.drained()
|
||||||
&& drive.tree().get("f.txt") == Some(&Entry::File(Arc::new(b"0123456789".to_vec())))
|
&& drive.tree().get("f.txt") == Some(&Entry::File(Arc::new(b"0123456789".to_vec())))
|
||||||
}));
|
}));
|
||||||
daemon.client.set_online(false);
|
daemon.client.set_online(false);
|
||||||
|
|||||||
+105
-6
@@ -109,17 +109,17 @@ a live account since.
|
|||||||
|
|
||||||
| Entry | Title |
|
| Entry | Title |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [B265](#b265--address-keys-whose-token-is-for-an-inactive-account-key-never-open) | Address keys whose token is for an inactive account key never open |
|
| [B276](#b276--a-stop-can-end-with-a-panic-in-a-thread-the-stop-never-waited-for) | A stop can end with a panic in a thread the stop never waited for |
|
||||||
| [B274](#b274--opening-a-file-for-writing-fails-with-eio-while-its-queued-revision-lands) | Opening a file for writing fails with EIO while its queued revision lands |
|
| [B274](#b274--opening-a-file-for-writing-fails-with-eio-while-its-queued-revision-lands) | Opening a file for writing fails with EIO while its queued revision lands |
|
||||||
|
| [B272](#b272--after-a-folder-is-removed-and-added-again-its-file-events-go-to-the-old-id) | After a folder is removed and added again, its file events go to the old id |
|
||||||
| [B271](#b271--one-failed-listing-makes-a-large-first-sync-start-over-from-nothing) | One failed listing makes a large first sync start over from nothing |
|
| [B271](#b271--one-failed-listing-makes-a-large-first-sync-start-over-from-nothing) | One failed listing makes a large first sync start over from nothing |
|
||||||
| [B266](#b266--pin-unpin-search-and-node-loads-report-a-database-error-as-offline) | Pin, unpin, search and node loads report a database error as "offline" |
|
| [B266](#b266--pin-unpin-search-and-node-loads-report-a-database-error-as-offline) | Pin, unpin, search and node loads report a database error as "offline" |
|
||||||
|
| [B265](#b265--address-keys-whose-token-is-for-an-inactive-account-key-never-open) | Address keys whose token is for an inactive account key never open |
|
||||||
| [B264](#b264--a-mount-that-fails-after-starting-its-workers-leaves-them-running) | A mount that fails after starting its workers leaves them running |
|
| [B264](#b264--a-mount-that-fails-after-starting-its-workers-leaves-them-running) | A mount that fails after starting its workers leaves them running |
|
||||||
| [B261](#b261--a-folder-that-has-never-synced-reads-up-to-date) | A folder that has never synced reads "up to date" |
|
| [B261](#b261--a-folder-that-has-never-synced-reads-up-to-date) | A folder that has never synced reads "up to date" |
|
||||||
| [B272](#b272--after-a-folder-is-removed-and-added-again-its-file-events-go-to-the-old-id) | After a folder is removed and added again, its file events go to the old id |
|
|
||||||
| [B260](#b260--the-poll-pushes-a-new-folders-first-sync-behind-every-other-folder) | The poll pushes a new folder's first sync behind every other folder |
|
| [B260](#b260--the-poll-pushes-a-new-folders-first-sync-behind-every-other-folder) | The poll pushes a new folder's first sync behind every other folder |
|
||||||
| [B258](#b258--journalctl--p-warning-shows-nothing-whatever-the-daemon-logged) | `journalctl -p warning` shows nothing, whatever the daemon logged |
|
| [B258](#b258--journalctl--p-warning-shows-nothing-whatever-the-daemon-logged) | `journalctl -p warning` shows nothing, whatever the daemon logged |
|
||||||
| [B256](#b256--pdfs-copy-right-after-a-save-copies-the-old-version) | `pdfs copy` right after a save copies the old version |
|
| [B257](#b257--deleting-a-new-file-while-it-is-open-can-drop-the-bytes-its-handle-reads) | Deleting a new file while it is open can drop the bytes its handle reads |
|
||||||
| [B255](#b255--a-synced-folder-whose-files-are-all-gone-here-is-stuck-for-good) | A synced folder whose files are all gone here is stuck for good |
|
|
||||||
| [B254](#b254--an-edit-in-the-same-second-as-a-new-upload-never-arrives) | An edit in the same second as a new upload never arrives |
|
| [B254](#b254--an-edit-in-the-same-second-as-a-new-upload-never-arrives) | An edit in the same second as a new upload never arrives |
|
||||||
| [B253](#b253--the-app-shows-the-sign-in-page-when-the-keyring-is-locked) | The app shows the sign-in page when the keyring is locked |
|
| [B253](#b253--the-app-shows-the-sign-in-page-when-the-keyring-is-locked) | The app shows the sign-in page when the keyring is locked |
|
||||||
| [B252](#b252--computers-sharing-a-device-overwrite-each-others-profile) | Computers sharing a device overwrite each other's profile |
|
| [B252](#b252--computers-sharing-a-device-overwrite-each-others-profile) | Computers sharing a device overwrite each other's profile |
|
||||||
@@ -294,6 +294,8 @@ a live account since.
|
|||||||
|
|
||||||
| Entry | Title |
|
| Entry | Title |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| [B256](#b256--pdfs-copy-right-after-a-save-copies-the-old-version) | `pdfs copy` right after a save copies the old version |
|
||||||
|
| [B255](#b255--a-synced-folder-whose-files-are-all-gone-here-is-stuck-for-good) | A synced folder whose files are all gone here is stuck for good |
|
||||||
| [B194](#b194--on-a-fresh-install-the-tray-exits-and-the-takeout-page-cannot-remember-its-archives) | On a fresh install the tray exits and the Takeout page cannot remember its archives |
|
| [B194](#b194--on-a-fresh-install-the-tray-exits-and-the-takeout-page-cannot-remember-its-archives) | On a fresh install the tray exits and the Takeout page cannot remember its archives |
|
||||||
| [B192](#b192--a-new-file-closed-empty-reaches-drive-only-half-a-minute-later) | A new file closed empty reaches Drive only half a minute later |
|
| [B192](#b192--a-new-file-closed-empty-reaches-drive-only-half-a-minute-later) | A new file closed empty reaches Drive only half a minute later |
|
||||||
| [B181](#b181--the-app-shows-a-queued-edit-kept-as-a-conflict-copy-in-english) | The app shows a queued edit kept as a conflict copy in English |
|
| [B181](#b181--the-app-shows-a-queued-edit-kept-as-a-conflict-copy-in-english) | The app shows a queued edit kept as a conflict copy in English |
|
||||||
@@ -377,6 +379,65 @@ a live account since.
|
|||||||
|
|
||||||
Newest first.
|
Newest first.
|
||||||
|
|
||||||
|
### B276 — A stop can end with a panic in a thread the stop never waited for
|
||||||
|
|
||||||
|
**Status:** Fixed, not verified live
|
||||||
|
**Found:** 2026-10-31, by an account run of `scripts/fuse-acceptance.sh --journal-check
|
||||||
|
--durability` against the audit branches. Every case passed, and the run still failed. During
|
||||||
|
the restart in the durability case on My files, the journal had three
|
||||||
|
`ERROR fuser::reply: Failed to send FUSE reply: No such file or directory (os error 2)` lines,
|
||||||
|
and after them a panic that the check did not report:
|
||||||
|
|
||||||
|
```
|
||||||
|
thread '<unnamed>' (1536340) panicked at .../tokio-1.53.1/src/runtime/time/entry.rs:539:9:
|
||||||
|
A Tokio 1.x context was found, but it is being shutdown.
|
||||||
|
```
|
||||||
|
|
||||||
|
The same panic is in the journal of a 3.6.0 stop on 2026-10-28, twice. B91 first saw it in a
|
||||||
|
meta worker.
|
||||||
|
|
||||||
|
**Where:** `mount_once` and `release_runtime` in `crates/pdfs-cli/src/main.rs`;
|
||||||
|
`JournalWatch` and `journal_problems` in `scripts/fuse-acceptance.py`.
|
||||||
|
|
||||||
|
**Cause.** A stop joins the drain workers, the sync engine and the FUSE worker pool, and then
|
||||||
|
`mount_once` shut the tokio runtime down. Other threads also use the runtime and are never
|
||||||
|
joined: a size lookup for a listing, a profile backup, a control request, a database vacuum.
|
||||||
|
A request still in progress when the stop began can start one of them. The thread id here is
|
||||||
|
nine below the next daemon's pid, so this thread started during the stop. Once the runtime is
|
||||||
|
shut down, that thread's next timer panics. Every call to Drive has a timer, because each one is
|
||||||
|
bounded.
|
||||||
|
|
||||||
|
The three `fuser::reply` lines are part of a correct stop. The stop aborts each mount's FUSE
|
||||||
|
connection so the unmount cannot hang. A request that was still running replies after that,
|
||||||
|
and the kernel answers a reply on an aborted connection with `ENOENT`.
|
||||||
|
|
||||||
|
The journal check matched only the `ERROR` token. A panic goes to stderr with no tracing level,
|
||||||
|
so the check did not see it.
|
||||||
|
|
||||||
|
**Fix.**
|
||||||
|
|
||||||
|
- On a stop, `release_runtime` leaves the runtime running for the process to end with, instead
|
||||||
|
of shutting it down. The process exits right after, so a thread the stop did not wait for
|
||||||
|
can finish its call or be ended by the exit, but it cannot panic. A mount that is about to
|
||||||
|
be tried again still shuts its runtime down with the 2 s limit from B165, because a new
|
||||||
|
runtime replaces it.
|
||||||
|
- The journal check counts `panicked at` lines.
|
||||||
|
- The journal check ignores the `ENOENT` reply error between the connection abort and the end
|
||||||
|
of that stop ("daemon stopping", or "control socket listening" for a mount that comes back).
|
||||||
|
At any other time the error still counts.
|
||||||
|
|
||||||
|
Making the bounded call helpers refuse new calls once the stop flag is set was rejected. The
|
||||||
|
stop sets that flag before it waits up to 10 s for the drain workers, so the refusal would cut
|
||||||
|
off the second half of an upload the stop is waiting for.
|
||||||
|
|
||||||
|
**Test:** `a_thread_left_running_at_a_stop_can_still_use_the_runtime` and
|
||||||
|
`a_mount_tried_again_shuts_its_runtime_down` in `crates/pdfs-cli/src/main.rs`. With the old
|
||||||
|
shutdown, the first one fails with the same panic at `entry.rs:539`. The new journal
|
||||||
|
filter was run on the journal of the run that found this: the three reply errors are no longer
|
||||||
|
counted, and the panic is.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### B275 — On a slow link, a file's last write can land only in a conflict copy
|
### B275 — On a slow link, a file's last write can land only in a conflict copy
|
||||||
|
|
||||||
**Status:** Open
|
**Status:** Open
|
||||||
@@ -876,9 +937,46 @@ are off. A terminal that merely inherited `JOURNAL_STREAM` keeps the plain forma
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### B256 — `pdfs copy` right after a save copies the old version
|
### B257 — Deleting a new file while it is open can drop the bytes its handle reads
|
||||||
|
|
||||||
**Status:** Fixed, not verified live
|
**Status:** Fixed, not verified live
|
||||||
|
**Found:** 2026-10-27, the simulation
|
||||||
|
`a_file_deleted_offline_while_open_still_reads_and_takes_writes` failing once on CI with a cache
|
||||||
|
tag left behind, then on every run with a 50 ms pause added to `ContentCache::store_file`.
|
||||||
|
**Where:** `retire_create` in `crates/pdfs-fuse/src/drain.rs`; `queue_trash` and
|
||||||
|
`remove_replaced` in `crates/pdfs-fuse/src/lib.rs`; `trash_child` in
|
||||||
|
`crates/pdfs-fuse/src/filesystem.rs`; `ContentCache::reconcile` in `crates/pdfs-core/src/cache.rs`
|
||||||
|
|
||||||
|
**Repro:** Create a file in the mount, wait until `pdfs status` shows nothing queued, open it,
|
||||||
|
and delete it while the link is down. If the create was still landing, a partial write or a read
|
||||||
|
through the open handle fails with "Input/output error", and the cache can keep a tag with no
|
||||||
|
bytes until the next start.
|
||||||
|
|
||||||
|
**Cause.** A create that carries the file's bytes retires its op, then reads the node back,
|
||||||
|
caches the bytes and only then gives the tree the real uid. Nothing counted that window, so the
|
||||||
|
queue read empty while it ran, which B119 promises means the file reads without the network. A
|
||||||
|
delete in that window found the placeholder uid in the tree, and trashed the node by its real
|
||||||
|
uid. `queue_trash` asked the tree whether that uid was open, found no entry for it, and dropped
|
||||||
|
the cached bytes the open handle reads, which B186 had kept. When the cache store was running at
|
||||||
|
the same moment, its tag was written after the eviction, and nothing removed it.
|
||||||
|
|
||||||
|
**Fix.** The create's landing is counted like an upload's, from the op's retirement until the
|
||||||
|
tree has the real uid. The delete and the replace pass whether they found the file open, so
|
||||||
|
`queue_trash` no longer depends on the tree having the new uid. Opening the cache removes a tag
|
||||||
|
whose bytes are gone, which also covers a crash between an eviction's two removals.
|
||||||
|
|
||||||
|
**Test:** The simulation `a_new_file_deleted_while_open_as_its_create_lands_still_reads` holds a
|
||||||
|
create's landing and fails without the count. The simulation above passes again with the 50 ms
|
||||||
|
pause, since it now waits for `Daemon::drained`, which counts the landing. Unit test
|
||||||
|
`a_tag_without_its_blob_is_swept_at_open` in `crates/pdfs-core/src/cache.rs`. No test hits the
|
||||||
|
delete inside the cache store itself: the window is too short to hold from the simulation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B256 — `pdfs copy` right after a save copies the old version
|
||||||
|
|
||||||
|
**Status:** Fixed, checked on 2026-10-27 by the acceptance case "regression B256" on a live
|
||||||
|
account
|
||||||
**Found:** 2026-10-26, the simulation `a_pdfs_copy_copies_on_drive_and_waits_for_unsynced_changes`
|
**Found:** 2026-10-26, the simulation `a_pdfs_copy_copies_on_drive_and_waits_for_unsynced_changes`
|
||||||
failing on most runs. The feature was not released yet.
|
failing on most runs. The feature was not released yet.
|
||||||
**Where:** `release` in `crates/pdfs-fuse/src/filesystem.rs`; `copy_to` in
|
**Where:** `release` in `crates/pdfs-fuse/src/filesystem.rs`; `copy_to` in
|
||||||
@@ -902,7 +1000,8 @@ A move checks the same set.
|
|||||||
|
|
||||||
### B255 — A synced folder whose files are all gone here is stuck for good
|
### B255 — A synced folder whose files are all gone here is stuck for good
|
||||||
|
|
||||||
**Status:** Fixed, not verified live
|
**Status:** Fixed, checked on 2026-10-27 by the acceptance case "regression B255" on a live
|
||||||
|
account
|
||||||
**Found:** 2026-10-26, issue #46: a folder restored from another computer never downloaded.
|
**Found:** 2026-10-26, issue #46: a folder restored from another computer never downloaded.
|
||||||
**Where:** `push_pass` in `crates/pdfs-fuse/src/sync.rs`; `reset_sync_folder` in
|
**Where:** `push_pass` in `crates/pdfs-fuse/src/sync.rs`; `reset_sync_folder` in
|
||||||
`crates/pdfs-fuse/src/devices.rs`
|
`crates/pdfs-fuse/src/devices.rs`
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ scratch (user data in `staging/` and `recovery/` is never touched by this).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **A new file reads without the network as soon as `pdfs status` shows nothing queued**
|
||||||
|
(B257). A new file's upload counted as done before its bytes were cached, so a script that
|
||||||
|
waited for an empty queue and then went offline could not read it. A new file deleted while
|
||||||
|
open in that moment could also lose the bytes its open handle reads, which made a write or
|
||||||
|
read through that handle fail with "Input/output error". The cache also removes leftover
|
||||||
|
tags of evicted files when it opens.
|
||||||
|
|
||||||
## [3.6.0] — 2026-10-27
|
## [3.6.0] — 2026-10-27
|
||||||
|
|
||||||
Schema: **44**, unchanged.
|
Schema: **44**, unchanged.
|
||||||
|
|||||||
+1
-1
@@ -246,7 +246,7 @@ remaining cases are skipped. Cases that make many round trips by design get twic
|
|||||||
| `--report-json PATH` | Every result and recorded observation. Attach it to a bug report |
|
| `--report-json PATH` | Every result and recorded observation. Attach it to a bug report |
|
||||||
| `--report-junit PATH` | JUnit XML. `known` is reported as skipped |
|
| `--report-junit PATH` | JUnit XML. `known` is reported as skipped |
|
||||||
| `--budget SECONDS` | Flags cases that pass but take longer (`PDFS_ACCEPTANCE_BUDGET`) |
|
| `--budget SECONDS` | Flags cases that pass but take longer (`PDFS_ACCEPTANCE_BUDGET`) |
|
||||||
| `--journal-check` | Fails the run if `proton-drive.service` logged an error during it, or the warnings "queued write conflicts; keeping a conflict copy" and "a fuse worker has held the same job for a long time" |
|
| `--journal-check` | Fails the run if `proton-drive.service` logged an error or a panic during it, or the warnings "queued write conflicts; keeping a conflict copy" and "a fuse worker has held the same job for a long time". A `Failed to send FUSE reply: No such file or directory` during a stop does not count: it is a request answering after the stop aborted its connection |
|
||||||
| `--durability` | Restarts the service mid-suite and checks the bytes written before it. Proves the queue survives a restart. `PDFS_ACCEPTANCE_UNIT` names another unit |
|
| `--durability` | Restarts the service mid-suite and checks the bytes written before it. Proves the queue survives a restart. `PDFS_ACCEPTANCE_UNIT` names another unit |
|
||||||
| `--fail-fast` | Stops at the first failure |
|
| `--fail-fast` | Stops at the first failure |
|
||||||
| `--list` | Prints every case and its targets |
|
| `--list` | Prints every case and its targets |
|
||||||
|
|||||||
@@ -3561,8 +3561,36 @@ JOURNAL_WARNINGS = (
|
|||||||
"queued write conflicts; keeping a conflict copy",
|
"queued write conflicts; keeping a conflict copy",
|
||||||
"a fuse worker has held the same job for a long time",
|
"a fuse worker has held the same job for a long time",
|
||||||
"re-interned an authority missing from the local tree",
|
"re-interned an authority missing from the local tree",
|
||||||
|
# A Rust panic goes to stderr without a tracing level, so the ERROR match
|
||||||
|
# does not see it (`docs/BUGS.md` B276).
|
||||||
|
"panicked at",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# A stop aborts each mount's FUSE connection so the unmount cannot hang. A
|
||||||
|
# request still running then replies into a connection the kernel has
|
||||||
|
# closed, and the kernel refuses that reply with ENOENT. While a stop is in
|
||||||
|
# progress that error is expected (`docs/BUGS.md` B276); at any other time it
|
||||||
|
# still counts.
|
||||||
|
STOP_STARTED = "aborted FUSE connection to unblock unmount"
|
||||||
|
STOP_ENDED = ("daemon stopping", "control socket listening")
|
||||||
|
LATE_REPLY = "Failed to send FUSE reply: No such file or directory"
|
||||||
|
|
||||||
|
|
||||||
|
def journal_problems(lines: list[str]) -> list[str]:
|
||||||
|
"""The lines of `lines` that count as problems."""
|
||||||
|
problems = []
|
||||||
|
stopping = False
|
||||||
|
for line in lines:
|
||||||
|
if STOP_STARTED in line:
|
||||||
|
stopping = True
|
||||||
|
elif any(marker in line for marker in STOP_ENDED):
|
||||||
|
stopping = False
|
||||||
|
if stopping and LATE_REPLY in line:
|
||||||
|
continue
|
||||||
|
if ERROR_LEVEL.search(line) or any(warning in line for warning in JOURNAL_WARNINGS):
|
||||||
|
problems.append(line)
|
||||||
|
return problems
|
||||||
|
|
||||||
|
|
||||||
def read_journal(unit: str, since: float) -> list[str] | None:
|
def read_journal(unit: str, since: float) -> list[str] | None:
|
||||||
"""The unit's journal lines since `since`, or None without journalctl.
|
"""The unit's journal lines since `since`, or None without journalctl.
|
||||||
@@ -3621,11 +3649,7 @@ class JournalWatch:
|
|||||||
# Filtering on syslog priority (`-p err`) finds nothing: the daemon writes
|
# Filtering on syslog priority (`-p err`) finds nothing: the daemon writes
|
||||||
# tracing levels as text on stdout, which journald files at the unit's
|
# tracing levels as text on stdout, which journald files at the unit's
|
||||||
# default priority. Match the level token instead.
|
# default priority. Match the level token instead.
|
||||||
return [
|
return journal_problems(lines)
|
||||||
line
|
|
||||||
for line in lines
|
|
||||||
if ERROR_LEVEL.search(line) or any(warning in line for warning in JOURNAL_WARNINGS)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in new issue
Block a user