fix(crop): rotatie op Windows landt weer, en een mislukte schrijfbeurt wist niets meer #1776

Merged
brenno merged 2 commits from fix/rotatie-windows-handle into main 2026-08-24 14:51:29 +00:00
Owner

De reden, eindelijk

De diagnose uit #1774 leverde bij de eerstvolgende spiegel-run precies wat ontbrak:

PathAccessException: Cannot delete file, path = 'C:\...\ocideck_crop…\foto.png'
(OS Error: The process cannot access the file because it is being used by
another process, errno = 32)
de geroteerde bytes zijn niet weggeschreven

De voorvertoning van het bijsnijdvenster houdt het bestand dat ze net las nog even vast. Op Windows weigert rename over een bestaand bestand, waarna de terugval het doel wil verwijderen — en verwijderen mag niet zolang iemand het openhoudt. Die iemand waren wij. Dit is een echte gebruikersbug: wie op Windows een afbeelding draait die in beeld staat, raakt zijn rotatie kwijt zonder melding.

Drie dingen

  1. _releasePreview() geeft de stream en de plek in de beeldcache op vóór het schrijven. Synchroon: voor een FileImage is de sleutel een SynchronousFuture, dus de dialoog hoeft niet op IO te wachten om te kunnen sluiten — dat was het uitgangspunt van het oorspronkelijke ontwerp en dat blijft zo.
  2. writeBytesAtomicSyncRetrying: vier herkansingen van 40 ms. Blokkerend en niet async, omdat de houder een ánder proces is (op een Windows-bouwmachine klassiek de virusscanner) en een await het venster op schijf-IO zou laten wachten.
  3. Het gat dat de toets erbij blootlegde. Verwijder-dan-hernoem haalt eerst het doel weg; faalt de tweede hernoeming, dan gooide de opruiming in de catch óók het tijdelijke bestand weg. Een schrijfbeurt die daar strandde liet niets over — geen nieuw bestand en geen origineel. Het commentaar beloofde het tegendeel ("de geschreven .tmp overleeft"), en dat klopte alleen bij een crash, niet bij een uitzondering. De kopie blijft nu staan zodra het doel verwijderd is, en de herkansingslus ruimt hem op zodra een poging alsnog slaagt — anders bleef er een foto.png.3.tmp naast het beeld van de gebruiker liggen. Dezelfde reparatie in de asynchrone variant, want dat is de route waarlangs decks worden opgeslagen.

Toetsing

  • make check groen (exit 0).
  • Beide nieuwe gevallen staan in test/atomic_file_test.dart met een geïnjecteerde hernoeming, dus ze worden rood zónder Windows: de tijdelijke vergrendeling die wordt uitgezeten (drie hernoemingen, één pauze) en de blijvende die gooit en de herstelkopie achterlaat.
  • Wat deze PR niet bewijst: dat de twee rotatietoetsen op de echte Windows-runner groen worden. Dat toetst de volgende spiegel-run; de diagnose uit #1774 blijft staan, dus als het opnieuw misgaat noemt hij weer de reden in plaats van alleen de pixels.

Bewaker

Overgeslagen, expliciet: geen formaat, opslag, afhankelijkheid of publieke belofte. Wel raakt punt 3 gegevensbehoud in de schrijfroute van decks, en dat gaat de goede kant op: van "kan alles kwijtraken" naar "laat een herstelbestand achter".

## De reden, eindelijk De diagnose uit #1774 leverde bij de eerstvolgende spiegel-run precies wat ontbrak: ``` PathAccessException: Cannot delete file, path = 'C:\...\ocideck_crop…\foto.png' (OS Error: The process cannot access the file because it is being used by another process, errno = 32) de geroteerde bytes zijn niet weggeschreven ``` De voorvertoning van het bijsnijdvenster houdt het bestand dat ze net las nog even vast. Op Windows weigert `rename` over een bestaand bestand, waarna de terugval het doel wil verwijderen — en verwijderen mag niet zolang iemand het openhoudt. Die iemand waren wij. Dit is een echte gebruikersbug: wie op Windows een afbeelding draait die in beeld staat, raakt zijn rotatie kwijt zonder melding. ## Drie dingen 1. **`_releasePreview()`** geeft de stream en de plek in de beeldcache op vóór het schrijven. Synchroon: voor een `FileImage` is de sleutel een `SynchronousFuture`, dus de dialoog hoeft niet op IO te wachten om te kunnen sluiten — dat was het uitgangspunt van het oorspronkelijke ontwerp en dat blijft zo. 2. **`writeBytesAtomicSyncRetrying`**: vier herkansingen van 40 ms. Blokkerend en niet async, omdat de houder een ánder proces is (op een Windows-bouwmachine klassiek de virusscanner) en een `await` het venster op schijf-IO zou laten wachten. 3. **Het gat dat de toets erbij blootlegde.** Verwijder-dan-hernoem haalt eerst het doel weg; faalt de tweede hernoeming, dan gooide de opruiming in de `catch` óók het tijdelijke bestand weg. Een schrijfbeurt die daar strandde liet **niets** over — geen nieuw bestand en geen origineel. Het commentaar beloofde het tegendeel ("de geschreven `.tmp` overleeft"), en dat klopte alleen bij een crash, niet bij een uitzondering. De kopie blijft nu staan zodra het doel verwijderd is, en de herkansingslus ruimt hem op zodra een poging alsnog slaagt — anders bleef er een `foto.png.3.tmp` naast het beeld van de gebruiker liggen. Dezelfde reparatie in de asynchrone variant, want dat is de route waarlangs **decks worden opgeslagen**. ## Toetsing - `make check` groen (exit 0). - Beide nieuwe gevallen staan in `test/atomic_file_test.dart` met een geïnjecteerde hernoeming, dus ze worden rood zónder Windows: de tijdelijke vergrendeling die wordt uitgezeten (drie hernoemingen, één pauze) en de blijvende die gooit en de herstelkopie achterlaat. - Wat deze PR **niet** bewijst: dat de twee rotatietoetsen op de echte Windows-runner groen worden. Dat toetst de volgende spiegel-run; de diagnose uit #1774 blijft staan, dus als het opnieuw misgaat noemt hij weer de reden in plaats van alleen de pixels. ## Bewaker Overgeslagen, expliciet: geen formaat, opslag, afhankelijkheid of publieke belofte. Wel raakt punt 3 gegevensbehoud in de schrijfroute van decks, en dat gaat de goede kant op: van "kan alles kwijtraken" naar "laat een herstelbestand achter".
De spiegel gaf eindelijk de reden, dankzij de diagnose uit #1774:

    PathAccessException: Cannot delete file, path = '…\foto.png'
    (OS Error: The process cannot access the file because it is being
    used by another process, errno = 32)

De voorvertoning van het bijsnijdvenster houdt het bestand dat ze net las nog
even vast. Op Windows weigert `rename` over een bestaand bestand, waarna de
terugval het doel wil verwijderen — en dat mag niet zolang iemand het openhoudt.
Die iemand waren wij.

Drie dingen:

- `_releasePreview()` geeft de stream en de cache-entry op vóór het schrijven.
  Synchroon: voor een FileImage is de sleutel een SynchronousFuture, dus de
  dialoog hoeft niet op IO te wachten om te kunnen sluiten.
- `writeBytesAtomicSyncRetrying` doet vier herkansingen van 40 ms. Blokkerend
  en niet async, omdat de houder een ander proces is (virusscanner) en het
  venster meteen dicht moet kunnen.
- **En het gat dat de toets erbij blootlegde:** verwijder-dan-hernoem haalt het
  doel weg, en de opruiming in de `catch` gooide daarna ook het tijdelijke
  bestand weg. Een schrijfbeurt die daar strandde liet niets over. De kopie
  blijft nu staan zodra het doel verwijderd is, en de herkansingslus ruimt hem
  op zodra een poging alsnog slaagt. Dezelfde reparatie in de asynchrone
  variant, want dat is de route waarlangs decks worden opgeslagen.

Beide gevallen staan als toets in test/atomic_file_test.dart, met een
geïnjecteerde hernoeming zodat ze zonder Windows rood kunnen worden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(changelog): de Windows-rotatie en het herstelbestand
All checks were successful
scans / scans (pull_request) Successful in 3m53s
static-gate / static-gate (pull_request) Successful in 8m49s
46ae613854
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
brenno merged commit e62a811221 into main 2026-08-24 14:51:29 +00:00
Sign in to join this conversation.
No description provided.