Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Development contracts:
| `.cdidx/` | Created with mode `0700`. |
| `codeindex.db` plus WAL/SHM sidecars | Mode `0600` is applied when the files exist. |
| `suggestions-*.json` suggestion stores | Written atomically with owner-only mode `0600` on POSIX. |
| Indexed workspace source reads | Source-file content and checksum reads use `FileShare.ReadWrite | FileShare.Delete`, long-path normalization, the configured max-file byte cap, and modified-time retry checks so indexing can inspect files that build tools keep open without allowing unbounded growth. |
| Atomic file writes | `AtomicFileWriter` writes to a sibling temp file, applies the requested POSIX mode before replacement, flushes file contents, renames over the target, and fsyncs the parent directory on Unix. Callers must use the `Sensitive` write profile for local state, caches, suggestions, checkpoints, and other private payloads; user-requested exports and reports use the default `Public` profile unless their content is explicitly private. If the parent directory flush fails after replacement, the command fails explicitly so callers know the file was replaced but directory durability was not confirmed. Windows skips directory fsync because the helper only promises it on supported Unix platforms. |
| Index locks, watch sub-run spools, staged hook scripts, lock metadata sidecars, and active workspace `active.json` | Created or written as owner-only files (`0600`) before contents are exposed, and read through small bounded buffers where applicable so stale or corrupted diagnostics cannot expose local paths more broadly or force unbounded allocation. |
| Checkpoint roots, snapshot directories, manifest files, copied DB/WAL/SHM snapshots, and restore staging/backup directories | Forced owner-only on POSIX. |
Expand Down Expand Up @@ -2334,6 +2335,7 @@ net9 CI lane に合わせる場合は `FRAMEWORK=net9.0 make test` を使いま
| `.cdidx/` | mode `0700` で作成。 |
| `codeindex.db` と WAL/SHM sidecar | ファイルが存在する場合は mode `0600` を適用。 |
| `suggestions-*.json` suggestion store | POSIX では owner-only の mode `0600` で atomic write します。 |
| インデックス対象ワークスペースソースの読み取り | ソースファイル本文とチェックサムの読み取りは `FileShare.ReadWrite | FileShare.Delete`、長いパスの正規化、設定された最大ファイルバイト数、更新時刻の再確認を使い、ビルドツールが開いたままのファイルも無制限な肥大化を許さず検査できるようにします。 |
| atomic file write | `AtomicFileWriter` は sibling temp file に書き込み、要求された POSIX mode を置換前に適用し、file content を flush してから target へ rename し、Unix では parent directory を fsync します。local state、cache、suggestion、checkpoint など private payload には `Sensitive` write profile を使い、user-requested export や report は内容が明示的に private でない限り既定の `Public` profile を使います。置換後に parent directory flush が失敗した場合、file は置換済みだが directory durability を確認できていないことが caller に分かるよう command は明示的に失敗します。Windows では、この helper の directory fsync 保証は supported Unix platform に限定されるため skip します。 |
| index lock、watch sub-run spool、staged hook script、lock metadata sidecar、active workspace の `active.json` | 内容が露出する前に owner-only file (`0600`) として作成または書き込み、該当するものは stale / corrupt diagnostic が local path を広く漏らしたり unbounded allocation を強制したりしないよう小さな bounded buffer で読みます。 |
| database checkpoint root、snapshot directory、manifest file、copy された DB/WAL/SHM snapshot、restore staging/backup directory | POSIX では owner-only に固定。 |
Expand Down
20 changes: 20 additions & 0 deletions changelog.d/unreleased/4078.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
category: fixed
issues:
- 4078
affected:
- src/CodeIndex/BoundedFile.cs
- src/CodeIndex/Indexer/Scanning/FileContentLoader.RawBytes.cs
- src/CodeIndex/Indexer/Scanning/FileContentLoader.Checksum.cs
- tests/CodeIndex.Tests/FileIndexerContentLoadingTests.cs
- tests/CodeIndex.Tests/FileIndexerTests.cs
- DEVELOPER_GUIDE.md
---

## English

- **Indexer source reads now use the shared bounded file-open policy (#4078)** - source content and checksum reads now allow concurrent build-tool writers while preserving max-file byte caps and modified-time retry checks.

## 日本語

- **インデックス対象ソースの読み取りが共有の境界付きファイルオープン方針を使うようになりました (#4078)** - ソース本文とチェックサムの読み取りは、最大ファイルバイト数の上限と更新時刻の再確認を保ったまま、ビルドツールによる同時書き込みを許容するようになりました。
3 changes: 3 additions & 0 deletions src/CodeIndex/BoundedFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ internal static FileStream OpenReadForLengthCheckedText(string path)
internal static FileStream OpenReadForPrefixProbe(string path)
=> OpenRead(path, FileShare.ReadWrite | FileShare.Delete, SmallReadBufferSize);

internal static FileStream OpenReadForIndexContent(string path)
=> OpenRead(path, FileShare.ReadWrite | FileShare.Delete, DefaultReadBufferSize);

internal static FileStream OpenReadForTail(string path)
=> OpenRead(path, FileShare.ReadWrite | FileShare.Delete, SmallReadBufferSize);

Expand Down
8 changes: 1 addition & 7 deletions src/CodeIndex/Indexer/Scanning/FileContentLoader.Checksum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,7 @@ internal static bool TryComputeChecksum(
throw new ArgumentOutOfRangeException(nameof(maxBytes), maxBytes, "Maximum byte count must be non-negative.");

checksum = string.Empty;
using var stream = new FileStream(
filePath,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: StreamBufferSize,
options: FileOptions.SequentialScan);
using var stream = BoundedFile.OpenReadForIndexContent(filePath);
return TryComputeChecksum(stream, maxBytes, out checksum, cancellationToken);
}

Expand Down
16 changes: 2 additions & 14 deletions src/CodeIndex/Indexer/Scanning/FileContentLoader.RawBytes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,7 @@ internal sealed partial class FileContentLoader
for (var attempt = 0; ; attempt++)
{
var modifiedBeforeRead = File.GetLastWriteTimeUtc(ioPath);
using (var stream = new FileStream(
ioPath,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: StreamBufferSize,
options: FileOptions.SequentialScan))
using (var stream = BoundedFile.OpenReadForIndexContent(absolutePath))
{
var initialLength = stream.Length;
if (initialLength > maxFileSizeBytes)
Expand Down Expand Up @@ -73,13 +67,7 @@ internal bool RawByteChunksMayMatch(
{
var modifiedBeforeRead = File.GetLastWriteTimeUtc(ioPath);
bool matched;
using (var stream = new FileStream(
ioPath,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: StreamBufferSize,
options: FileOptions.SequentialScan))
using (var stream = BoundedFile.OpenReadForIndexContent(absolutePath))
{
var initialLength = stream.Length;
if (initialLength > maxFileSizeBytes)
Expand Down
29 changes: 29 additions & 0 deletions tests/CodeIndex.Tests/FileIndexerContentLoadingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,35 @@ public void FileContentLoader_Load_LfOnlyUtf8CanReuseRawChecksum()
Assert.Equal(expected, loaded.Checksum);
}

[Fact]
public void FileContentLoader_Load_AllowsConcurrentWriterShare_Issue4078()
{
var tempDir = TestProjectHelper.CreateTempProject("codeindex_loader_share");
try
{
var path = Path.Combine(tempDir, "sample.cs");
var bytes = Encoding.UTF8.GetBytes("class Sample {}\n");
File.WriteAllBytes(path, bytes);

using var writer = new FileStream(
path,
FileMode.Open,
FileAccess.Write,
FileShare.ReadWrite | FileShare.Delete);
var loader = new FileContentLoader(FileIndexer.DefaultMaxFileSizeBytes);

var loaded = loader.Load(path, "sample.cs", "sample.cs", CancellationToken.None);

Assert.Equal("class Sample {}\n", loaded.Content);
Assert.Equal(bytes.Length, loaded.SizeBytes);
Assert.Equal(FileIndexer.ComputeChecksum(bytes), loaded.Checksum);
}
finally
{
TestProjectHelper.DeleteDirectory(tempDir);
}
}

[Fact]
public void FileContentLoader_Load_CarriesConflictMarkerLine()
{
Expand Down
28 changes: 28 additions & 0 deletions tests/CodeIndex.Tests/FileIndexerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6353,4 +6353,32 @@ public void TryComputeChecksum_CancellationDuringRead_ThrowsOperationCanceled_Is
FileContentLoader.TryComputeChecksum(stream, long.MaxValue, out _, cancellation.Token));
}

[Fact]
public void TryComputeChecksum_FilePathAllowsConcurrentWriterShare_Issue4078()
{
var tempDir = TestProjectHelper.CreateTempProject("codeindex_checksum_share");
try
{
var path = Path.Combine(tempDir, "sample.cs");
var bytes = Encoding.UTF8.GetBytes("class Sample {}\n");
File.WriteAllBytes(path, bytes);

using var writer = new FileStream(
path,
FileMode.Open,
FileAccess.Write,
FileShare.ReadWrite | FileShare.Delete);

Assert.True(FileContentLoader.TryComputeChecksum(
path,
FileIndexer.DefaultMaxFileSizeBytes,
out var checksum));
Assert.Equal(FileIndexer.ComputeChecksum(bytes), checksum);
}
finally
{
TestProjectHelper.DeleteDirectory(tempDir);
}
}

}
Loading