Cosmic-files version:
Installed: 1.5.0~1786455995~24.04~5bdfffa
Apt candidate inspected: 1.6.0~1787090492~24.04~3c9a78a
The installed package was used for the behavioral tests. The relevant negative
cache logic remains present in the inspected 1.6.0 candidate source.
Issue/Bug description:
When an external thumbnail command fails, COSMIC Files writes a versioned
failure marker. For an unchanged source URI, modification time, and size, that
marker prevents later thumbnail attempts in the same COSMIC Files version.
This can leave a valid video showing the generic icon after a transient
thumbnailer failure, even when the exact thumbnail command now succeeds.
Reopening the folder does not recover it. Moving only the matching failure
marker aside and reopening the folder causes COSMIC Files to generate and show
the thumbnail normally.
The freedesktop Thumbnail Managing Standard intentionally defines
application-and-version-specific failure markers to avoid repeatedly processing
known unsupported, broken, or prohibitively expensive files. This report does
not propose removing that mechanism. The issue is that COSMIC Files gives any
unsuccessful external process attempt the same persistent treatment, including
a temporary failure that succeeds on a later identical invocation.
No private media, filenames, tag values, or local identifiers are included
with this report. Failure-marker PNGs should not be attached because their
metadata contains the source URI.
Steps to reproduce:
This is a recovery/retry reproduction for an existing failure marker. The
original transient process failure was not captured, so this report does not
claim a deterministic trigger for that first failure.
-
Find a readable video that shows a generic icon and has a current-version
failure marker but no successful thumbnail cache entry.
-
Confirm that the same thumbnailer entry point and registered size now exit
successfully. Direct output to /dev/null to avoid retaining a private
probe frame:
FILE=/path/to/affected-video.mp4
cosmic-player --thumbnail /dev/null --size 512 "$FILE"
echo "$?"
-
Reopen the directory in COSMIC Files. The generic icon remains.
-
Calculate and inspect the matching negative-cache entry:
uri=$(gio info "$FILE" | sed -n 's/^uri: //p' | head -n1)
key=$(printf '%s' "$uri" | md5sum | cut -d' ' -f1)
marker="$HOME/.cache/thumbnails/fail/cosmic-files-1.5.0/$key.png"
test -f "$marker" && echo 'failure marker present'
-
Back up only that marker outside the active thumbnail cache, then reopen the
directory.
-
COSMIC Files now generates a valid thumbnail and displays it in place of the
generic icon.
Expected behavior:
A transient external-thumbnailer failure should be retried after a bounded
interval, on an explicit refresh, or in a later COSMIC Files session. A valid
unchanged file should not remain suppressed for the lifetime of that version's
failure marker.
Persistent negative caching can remain useful for deterministic unsupported or
invalid files, provided it does not make temporary process or environment
failures effectively permanent.
Other notes:
Redacted local evidence
- The inspected folder contained both successful video thumbnails and generic
video icons.
- A filename-free sweep exercised the exact registered thumbnail command
against all 74 current-version failure markers in the folder, including one
source that also had a successful cache entry. Thumbnail output was directed
to /dev/null, so no private frames were retained.
- 67 of 74 commands exited
0. Only seven remained repeatable failures: four
invalid-framerate cases, two structurally empty or near-zero-duration inputs
reported generically as missing-plugin failures, and one 1 ms extreme-rate
input that timed out.
- For one affected H.264/AAC MP4, the exact
--size 512 command exited 0.
After only its 1x1 failure marker was moved to a backup and the folder was
reloaded, COSMIC Files produced a valid 512-pixel PNG and the thumbnail became
visible.
- The source media was not modified. The marker remains preserved locally for
rollback and inspection.
Media-metadata classification
A filename-free metadata matrix compared cache state with container, codec,
profile, pixel format, resolution, stream counts, duration, start time,
framerate fields, permissions, and metadata tag keys (never values).
Common successful and failed entries substantially overlapped: H.264/AAC,
HEVC/AAC, VP9/Opus, MP4 and WebM containers, common resolutions, ordinary
one-video/one-audio layouts, file modes, and common tag-key sets all appeared
on both sides. No ordinary metadata field separated the 67 successful retries
from existing failure markers.
The four repeatable invalid-framerate cases were different. GStreamer reported
Frame rate: 0/1 for each, while FFmpeg reported a nonzero average rate and
nonzero packet durations. The video library used by COSMIC Player rejects a
zero framerate after reading the negotiated appsink caps. COSMIC Player then
maps that underlying open error to the generic text missing required plugin
in thumbnail mode:
This confirms that some media can fail deterministically because of negotiated
stream properties. It does not account for the much larger class of unchanged
files whose identical command now succeeds. Those stale markers require a
separate retry/revalidation path even if deterministic media failures continue
to be cached.
Privacy-safe controls
Locally generated H.264/AAC MP4 and VP9/Opus WebM fixtures both generated
thumbnails successfully. Five rounds of four concurrent commands against those
fixtures also passed 20 of 20 attempts. The installed source limits thumbnail
generation to four workers. Concurrency at that limit or codec choice alone was
therefore not sufficient to reproduce the original transient failures, and
this report does not assign that initial cause:
|
// Thumbnail generation semaphore - limits parallel thumbnail workers |
|
// Uses 4 workers for balanced throughput and memory usage |
|
pub static THUMB_SEMAPHORE: LazyLock<tokio::sync::Semaphore> = |
|
LazyLock::new(|| tokio::sync::Semaphore::const_new(num_cpus::get().min(4))); |
Source-level path
The installed source:
- Returns
CachedThumbnail::Failed whenever a valid failure marker exists:
|
pub fn get_cached_thumbnail(&self) -> CachedThumbnail { |
|
// If the file is already a thumbnail, just use it so we don't generate |
|
// cached thumbnails of thumbnails. |
|
if let (Some(cache_base_dir), Ok(metadata)) = ( |
|
THUMBNAIL_CACHE_BASE_DIR.as_ref(), |
|
std::fs::metadata(&self.file_path), |
|
) && metadata.is_file() |
|
&& self.file_path.starts_with(cache_base_dir) |
|
{ |
|
return CachedThumbnail::Valid((self.file_path.clone(), None)); |
|
} |
|
|
|
// Use cached thumbnail if it is valid. |
|
if self.is_thumbnail_valid(&self.thumbnail_path) { |
|
return CachedThumbnail::Valid(( |
|
self.thumbnail_path.clone(), |
|
Some(self.thumbnail_size), |
|
)); |
|
} |
|
|
|
// Check if there is a fail marker from an earlier failure. |
|
if self.is_thumbnail_valid(&self.thumbnail_fail_marker_path) { |
|
return CachedThumbnail::Failed; |
|
} |
|
|
|
CachedThumbnail::RequiresUpdate(self.thumbnail_size) |
|
} |
- Validates both successful thumbnails and failure markers using source URI,
modification time, and size, without an age or retry condition:
|
fn is_thumbnail_valid(&self, thumbnail_path: &Path) -> bool { |
|
let thumbnail_file = match File::open(thumbnail_path) { |
|
Ok(file) => file, |
|
Err(_) => return false, |
|
}; |
|
let decoder = png::Decoder::new(BufReader::new(thumbnail_file)); |
|
let reader = match decoder.read_info() { |
|
Ok(reader) => reader, |
|
Err(err) => { |
|
log::warn!( |
|
"failed to decode {} as PNG: {}", |
|
thumbnail_path.display(), |
|
err |
|
); |
|
return false; |
|
} |
|
}; |
|
|
|
let texts = &reader.info().uncompressed_latin1_text; |
|
|
|
// Thumb::URI is required and must match. |
|
let thumb_uri = texts |
|
.iter() |
|
.find(|&text| text.keyword == "Thumb::URI") |
|
.map(|t| &t.text); |
|
if let Some(thumb_uri) = thumb_uri { |
|
if *thumb_uri != self.file_uri { |
|
return false; |
|
} |
|
} else { |
|
return false; |
|
} |
|
|
|
let metadata = match std::fs::metadata(&self.file_path) { |
|
Ok(m) => m, |
|
Err(err) => { |
|
log::warn!( |
|
"failed to get metatdata of {}: {}", |
|
self.file_path.display(), |
|
err |
|
); |
|
return false; |
|
} |
|
}; |
|
|
|
// Thumb::MTime is required and must match. |
|
let thumb_mtime = texts |
|
.iter() |
|
.find(|&text| text.keyword == "Thumb::MTime") |
|
.map(|t| &t.text); |
|
if let Some(thumb_mtime) = thumb_mtime { |
|
let modified = match metadata.modified() { |
|
Ok(m) => m, |
|
Err(err) => { |
|
log::warn!( |
|
"failed to get modified from metatdata of {}, {}", |
|
self.file_path.display(), |
|
err |
|
); |
|
return false; |
|
} |
|
}; |
|
let mtime = modified |
|
.duration_since(UNIX_EPOCH) |
|
.unwrap_or_default() |
|
.as_secs() |
|
.to_string(); |
|
if *thumb_mtime != mtime { |
|
return false; |
|
} |
|
} else { |
|
return false; |
|
} |
|
|
|
// Thumb::Size isn't required, but it should be verified if present. |
|
let thumb_size = texts |
|
.iter() |
|
.find(|&text| text.keyword == "Thumb::Size") |
|
.map(|t| &t.text); |
|
if let Some(thumb_size) = thumb_size { |
|
let size = metadata.len(); |
|
if *thumb_size != size.to_string() { |
|
return false; |
|
} |
|
} |
|
|
|
true |
- Creates a failure marker whenever thumbnail generation returns no image for
a MIME type with a registered thumbnailer:
|
// If we weren't able to create a thumbnail, but we should have |
|
// been able to, create a fail marker so that it isn't tried the |
|
// next time. |
|
if let Ok(cacher) = thumbnail_cacher |
|
&& tried_supported_file |
|
&& let Err(err) = cacher.create_fail_marker() |
|
{ |
|
log::warn!( |
|
"failed to create thumbnail fail marker for {}: {}", |
|
path.display(), |
|
err |
|
); |
|
} |
|
|
|
Self::NotImage |
- Treats any nonzero external-thumbnailer status as a failed generation:
|
let Some(mut command) = thumbnailer.command(path, file.path(), thumbnail_size) else { |
|
continue; |
|
}; |
|
match command.status() { |
|
Ok(status) => { |
|
if status.success() { |
|
match image::ImageReader::open(file.path()) |
|
.and_then(ImageReader::with_guessed_format) |
|
{ |
|
Ok(reader) => match reader.decode().map(DynamicImage::into_rgba8) { |
|
Ok(image) => { |
|
return Some(( |
|
Self::Image( |
|
widget::image::Handle::from_rgba( |
|
image.width(), |
|
image.height(), |
|
image.into_raw(), |
|
), |
|
None, |
|
), |
|
file, |
|
)); |
|
} |
|
Err(err) => { |
|
log::warn!("failed to decode {}: {}", path.display(), err); |
|
} |
|
}, |
|
Err(err) => { |
|
log::warn!("failed to read {}: {}", path.display(), err); |
|
} |
|
} |
|
} else { |
|
log::warn!( |
|
"failed to run {:?} for {}: {}", |
|
thumbnailer, |
|
path.display(), |
|
status |
|
); |
|
} |
|
} |
|
Err(err) => { |
|
log::warn!( |
|
"failed to run {thumbnailer:?} for {}: {}", |
|
path.display(), |
|
err |
|
); |
|
} |
|
} |
|
} |
|
|
|
None |
The failure-marker mechanism comes from the freedesktop
Thumbnail Managing Standard,
which describes preserving failures to avoid repeatedly retrying files that are
unknown, broken, or too expensive to thumbnail. It does not define how an
application should distinguish a durable content failure from a temporary
external-process or environment failure.
The inspected 1.6.0 candidate retains the same behavior:
|
// Use cached thumbnail if it is valid. |
|
if self.is_thumbnail_valid(&self.thumbnail_path) { |
|
return CachedThumbnail::Valid(( |
|
self.thumbnail_path.clone(), |
|
Some(self.thumbnail_size), |
|
)); |
|
} |
|
|
|
// Check if there is a fail marker from an earlier failure. |
|
if self.is_thumbnail_valid(&self.thumbnail_fail_marker_path) { |
|
return CachedThumbnail::Failed; |
|
} |
|
|
|
CachedThumbnail::RequiresUpdate(self.thumbnail_size) |
|
} |
|
// If we weren't able to create a thumbnail, but we should have |
|
// been able to, create a fail marker so that it isn't tried the |
|
// next time. |
|
if let Ok(cacher) = thumbnail_cacher |
|
&& tried_supported_file |
|
&& let Err(err) = cacher.create_fail_marker() |
|
{ |
|
log::warn!( |
|
"failed to create thumbnail fail marker for {}: {}", |
|
path.display(), |
|
err |
|
); |
|
} |
|
|
|
Self::NotImage |
The same failure-marker path is also present in upstream master at
28546795b0f4ff65d0c96d98a503b1dfec0d8e8e (2026-08-20), inspected but not
behaviorally tested:
|
if self.is_thumbnail_valid(&self.thumbnail_path) { |
|
return CachedThumbnail::Valid(( |
|
self.thumbnail_path.clone(), |
|
Some(self.thumbnail_size), |
|
)); |
|
} |
|
|
|
// Check if there is a fail marker from an earlier failure. |
|
if self.is_thumbnail_valid(&self.thumbnail_fail_marker_path) { |
|
return CachedThumbnail::Failed; |
|
// been able to, create a fail marker so that it isn't tried the |
|
// next time. |
|
if let Ok(cacher) = thumbnail_cacher |
|
&& tried_supported_file |
|
&& let Err(err) = cacher.create_fail_marker() |
|
{ |
|
log::warn!( |
|
"failed to create thumbnail fail marker for {}: {}", |
|
path.display(), |
Possible fix directions
- Keep the first external-process failure session-local, and persist a failure
marker only after a bounded repeated failure.
- Give failure markers a bounded retry age while retaining URI/mtime/size
validation.
- Let an explicit refresh retry negative-cache entries.
- Distinguish deterministic decode/unsupported-media failures from temporary
process, resource, or environment failures before writing a persistent
marker.
Related reports such as
#501 and
#1950 concern thumbnail
resource usage and process concurrency. The installed four-worker limit and
the 20-of-20 bounded concurrency control distinguish this report from those
issues. Neither report covers successful retries being suppressed by the
negative cache.
Cosmic-files version:
The installed package was used for the behavioral tests. The relevant negative
cache logic remains present in the inspected
1.6.0candidate source.Issue/Bug description:
When an external thumbnail command fails, COSMIC Files writes a versioned
failure marker. For an unchanged source URI, modification time, and size, that
marker prevents later thumbnail attempts in the same COSMIC Files version.
This can leave a valid video showing the generic icon after a transient
thumbnailer failure, even when the exact thumbnail command now succeeds.
Reopening the folder does not recover it. Moving only the matching failure
marker aside and reopening the folder causes COSMIC Files to generate and show
the thumbnail normally.
The freedesktop Thumbnail Managing Standard intentionally defines
application-and-version-specific failure markers to avoid repeatedly processing
known unsupported, broken, or prohibitively expensive files. This report does
not propose removing that mechanism. The issue is that COSMIC Files gives any
unsuccessful external process attempt the same persistent treatment, including
a temporary failure that succeeds on a later identical invocation.
No private media, filenames, tag values, or local identifiers are included
with this report. Failure-marker PNGs should not be attached because their
metadata contains the source URI.
Steps to reproduce:
This is a recovery/retry reproduction for an existing failure marker. The
original transient process failure was not captured, so this report does not
claim a deterministic trigger for that first failure.
Find a readable video that shows a generic icon and has a current-version
failure marker but no successful thumbnail cache entry.
Confirm that the same thumbnailer entry point and registered size now exit
successfully. Direct output to
/dev/nullto avoid retaining a privateprobe frame:
Reopen the directory in COSMIC Files. The generic icon remains.
Calculate and inspect the matching negative-cache entry:
Back up only that marker outside the active thumbnail cache, then reopen the
directory.
COSMIC Files now generates a valid thumbnail and displays it in place of the
generic icon.
Expected behavior:
A transient external-thumbnailer failure should be retried after a bounded
interval, on an explicit refresh, or in a later COSMIC Files session. A valid
unchanged file should not remain suppressed for the lifetime of that version's
failure marker.
Persistent negative caching can remain useful for deterministic unsupported or
invalid files, provided it does not make temporary process or environment
failures effectively permanent.
Other notes:
Redacted local evidence
video icons.
against all 74 current-version failure markers in the folder, including one
source that also had a successful cache entry. Thumbnail output was directed
to
/dev/null, so no private frames were retained.0. Only seven remained repeatable failures: fourinvalid-framerate cases, two structurally empty or near-zero-duration inputs
reported generically as missing-plugin failures, and one 1 ms extreme-rate
input that timed out.
--size 512command exited0.After only its 1x1 failure marker was moved to a backup and the folder was
reloaded, COSMIC Files produced a valid 512-pixel PNG and the thumbnail became
visible.
rollback and inspection.
Media-metadata classification
A filename-free metadata matrix compared cache state with container, codec,
profile, pixel format, resolution, stream counts, duration, start time,
framerate fields, permissions, and metadata tag keys (never values).
Common successful and failed entries substantially overlapped: H.264/AAC,
HEVC/AAC, VP9/Opus, MP4 and WebM containers, common resolutions, ordinary
one-video/one-audio layouts, file modes, and common tag-key sets all appeared
on both sides. No ordinary metadata field separated the 67 successful retries
from existing failure markers.
The four repeatable invalid-framerate cases were different. GStreamer reported
Frame rate: 0/1for each, while FFmpeg reported a nonzero average rate andnonzero packet durations. The video library used by COSMIC Player rejects a
zero framerate after reading the negotiated appsink caps. COSMIC Player then
maps that underlying open error to the generic text
missing required pluginin thumbnail mode:
This confirms that some media can fail deterministically because of negotiated
stream properties. It does not account for the much larger class of unchanged
files whose identical command now succeeds. Those stale markers require a
separate retry/revalidation path even if deterministic media failures continue
to be cached.
Privacy-safe controls
Locally generated H.264/AAC MP4 and VP9/Opus WebM fixtures both generated
thumbnails successfully. Five rounds of four concurrent commands against those
fixtures also passed 20 of 20 attempts. The installed source limits thumbnail
generation to four workers. Concurrency at that limit or codec choice alone was
therefore not sufficient to reproduce the original transient failures, and
this report does not assign that initial cause:
cosmic-files/src/tab.rs
Lines 85 to 88 in 5bdfffa
Source-level path
The installed source:
CachedThumbnail::Failedwhenever a valid failure marker exists:cosmic-files/src/thumbnail_cacher.rs
Lines 62 to 88 in 5bdfffa
modification time, and size, without an age or retry condition:
cosmic-files/src/thumbnail_cacher.rs
Lines 202 to 288 in 5bdfffa
a MIME type with a registered thumbnailer:
cosmic-files/src/tab.rs
Lines 2189 to 2203 in 5bdfffa
cosmic-files/src/tab.rs
Lines 2243 to 2293 in 5bdfffa
The failure-marker mechanism comes from the freedesktop
Thumbnail Managing Standard,
which describes preserving failures to avoid repeatedly retrying files that are
unknown, broken, or too expensive to thumbnail. It does not define how an
application should distinguish a durable content failure from a temporary
external-process or environment failure.
The inspected
1.6.0candidate retains the same behavior:cosmic-files/src/thumbnail_cacher.rs
Lines 74 to 88 in 3c9a78a
cosmic-files/src/tab.rs
Lines 2189 to 2203 in 3c9a78a
The same failure-marker path is also present in upstream
masterat28546795b0f4ff65d0c96d98a503b1dfec0d8e8e(2026-08-20), inspected but notbehaviorally tested:
cosmic-files/src/thumbnail_cacher.rs
Lines 75 to 84 in 2854679
cosmic-files/src/tab.rs
Lines 2192 to 2200 in 2854679
Possible fix directions
marker only after a bounded repeated failure.
validation.
process, resource, or environment failures before writing a persistent
marker.
Related reports such as
#501 and
#1950 concern thumbnail
resource usage and process concurrency. The installed four-worker limit and
the 20-of-20 bounded concurrency control distinguish this report from those
issues. Neither report covers successful retries being suppressed by the
negative cache.