studio: open linked-folder sources in binary mode on windows - #8621
studio: open linked-folder sources in binary mode on windows#8621mahiatlinux wants to merge 2 commits into
Conversation
folder_sync._snapshot opened each linked source with os.O_RDONLY and no O_BINARY, so the Windows CRT opened it in text mode. Reads then collapse CRLF to LF and stop at the first Ctrl-Z, while _copy_exact requires exactly st_size bytes. The short read raised "Linked source changed while it was copied", every file landed in the failure list, and the folder reported "N file(s) could not be indexed". That hit every format: CRLF text and markdown, and any Flate-compressed PDF or docx, where a 0x1A byte appears within the first few KB. A 225 KB 20-page PDF read back as 2 KB. Bulk upload was unaffected because it never goes through _snapshot. Python documents this: "on Windows adding O_BINARY is needed to open files in binary mode". backend/auth/storage.py already carries the same guard. Fixes unslothai#8617
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
Reviewed this one closely because the change looks obviously right, and I do not think it does anything. The descriptor is already in binary mode before a single byte is read
with os.fdopen(fd, "rb", closefd = False) as src, open(target, "xb") as dst:
_copy_exact(src, dst, before.st_size)
#if defined(MS_WINDOWS) || defined(__CYGWIN__)
/* don't translate newlines (\r\n <=> \n) */
_setmode(self->fd, O_BINARY);
#endifBetween the The general facts in the description are correct: What that means for the reportIf a user is seeing linked-folder files marked unindexable on Windows, this change will not fix it, and the cause is somewhere else. Worth chasing before this lands, otherwise the issue gets closed against a no-op. The test asserts the flag, not the behaviour
Small note on the monkeypatch: Things I checked that are fine
TestsOn the head, Python 3.13, Linux: (21 passed / 59 skipped on the first run; the skips were What I could not checkI have no Windows host. The experiment that would settle it is copying a CRLF plus Ctrl-Z file through the pre-PR Happy to be wrong here if the repro says otherwise, but as it stands the comment on line 1004 describes a defect this code path does not have. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4e692fff25
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| compared = 4 if metadata["inode"] not in (None, 0) else 2 | ||
| if actual[:compared] != expected[:compared]: |
There was a problem hiding this comment.
Preserve file identity for Windows reconciliation
When Windows os.scandir supplies st_dev/st_ino as 0, this fallback allows the initial snapshot but the same zero identity is persisted in linked_folder_files. Every later scan therefore compares (0, 0) with (0, 0), so replacing a document with different same-length content while preserving its mtime is never added to work and the index remains stale indefinitely—contradicting the existing test_same_size_same_mtime_inode_replacement_is_reconciled invariant. Obtain a usable identity during _scan (for example through a handle/stat path that supplies the Windows file ID) rather than discarding identity checks here.
Useful? React with 👍 / 👎.
os.scandir leaves st_dev and st_ino at 0 on Windows, so the identity _scan records never matches the one os.fstat fills in during _snapshot and every file is rejected as changed during reconciliation. _scan already guards for a zero inode, so apply the same guard here and compare the post-copy stat against the pre-copy stat instead of the scan metadata. Also correct the O_BINARY comment: os.fdopen forces the descriptor binary before any read, so the flag guards a future raw os.read rather than fixing a live short-copy, and swap the flag-spy test for behaviour tests.
4e692ff to
8e351b7
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8e351b7e50
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # os.fdopen already forces this descriptor binary on Windows; O_BINARY only guards a raw os.read. | ||
| flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_BINARY", 0) |
There was a problem hiding this comment.
Fix binary mode at the actual read boundary
On Windows this flag cannot fix the reported short reads: as the new comment notes, os.fdopen(fd, "rb") already switches the CRT descriptor to binary mode before _copy_exact performs any read, so the pre-change implementation reads the same bytes as this one. The added byte-for-byte test likewise exercises that existing fdopen path without asserting the os.open flags and therefore would pass against the parent commit. The linked-folder failure will remain if it is reproducible, so its actual cause/read path needs to be identified and covered by a regression that fails before the fix.
Useful? React with 👍 / 👎.
|
I took this one over and pushed a commit to the branch. Short version: the original diagnosis was wrong, but the bug is real, I found the actual cause, and it is worse than reported. Before the PROn Windows, linking a folder for RAG succeeded and then every file in it came back as "could not be indexed". Bulk upload of the same files worked. What the PR assumedThat That is a no-op here, on every interpreter Studio supports. with os.fdopen(fd, "rb", closefd = False) as src, open(target, "xb") as dst:
#if defined(MS_WINDOWS) || defined(__CYGWIN__)
/* don't translate newlines (\r\n <=> \n) */
_setmode(self->fd, O_BINARY);
#endifConfirmed at v3.10.14 L475, v3.11.9 L473, v3.12.7 L478, v3.13.1 L489, v3.14.0 L505. Every control transfer between the fd/path fork and that line is a The general facts in the description are right, and worth keeping: CPython never sets the CRT The actual cause
Four things make this fit #8617 exactly: After the commit I pushed
Does it break anythingNo POSIX behaviour changes. Simulation
Before the fix, 7a raised "Linked source changed during reconciliation", which is the user-visible failure. Cases 1, 2 and 3 pass identically with the 7b and 7c are the uncomfortable part: with zeros on both sides the old check passed vacuously, and 7c swaps a different file with matching size and mtime underneath and the check still passes. So on Windows that identity check has never carried information. Before this it converted that into rejecting everything; now it degrades honestly to size plus mtime plus the fstat-to-fstat mid-copy check. Tests: 84 passed in Found but deliberately not fixed here
What I could not verifyI have no Windows host. The chain is docs plus CPython source across five tags plus a maintainer's own repro in the tracker plus a Linux simulation of the metadata shape. Strong, but not the same as running it. @aardvarkpaul, if you still have the install: the Studio backend log lines matching |
Fixes #8617.
Cause
folder_sync._snapshotopened each linked source withos.O_RDONLYand noO_BINARY, so on Windows the CRT opened it in text mode. Reads then collapse CRLF to LF and stop at the first Ctrl-Z (0x1A), but_copy_exactrequires exactlyst_sizebytes. The short read raisedLinked source changed while it was copied, every file landed in the failure list, and the folder surfacedN file(s) could not be indexed- the exact message in the issue.Python documents the requirement: "on Windows adding
O_BINARYis needed to open files in binary mode".backend/auth/storage.py:84already carries the same guard for the same reason.Bulk upload was unaffected because it writes the stored file through the upload route and never calls
_snapshot. Both paths converge oningestion.start_ingestionafterwards, so parsing was never the problem.Scope
Every supported format was affected, not just text:
Any real PDF compresses its streams, so a 0x1A byte appears within the first few KB. A trivial uncompressed PDF happens to survive, which is why a minimal smoke test would miss this.
Fix
Add
O_BINARYto the snapshot open flags, guarded withgetattrso POSIX is unchanged.Verification
Verified end to end through the real
reconcile_folderpath with a folder of PDF, docx, html, md and txt sources. Before the fix the job endsfailed, 0 added, 5 failed, with5 file(s) could not be indexed; after it the job completes, all five map to documents and every search term retrieves. Verification ran on Linux with the Windows CRT text-mode read behaviour reproduced at the file-descriptor layer, since the defect is unreachable natively on POSIX.The added regression test asserts the flag is passed and that the snapshot is byte-identical to the source, and it fails without the fix.
tests/test_rag_linked_folders.pyis 80/80, and the RAG store and ingestion suites pass alongside it.Existing folders left in the
errorstate need one re-sync to clear.