mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix: flag a Files panel folder that did not answer the probe (release review)
A subfolder whose bounded probe answered `unknown` came back as `children: []` with no `incomplete` and `truncated` false, so a dead mount, or a healthy one slower than the 1.5 s probe on first touch, read as an empty folder and a search missed it in silence. `unknown` is never `absent` (#516). Such a folder now goes on the walk's `unread` list, which flags the node and sets `truncated` in both walks. A folder the filesystem says is gone drops out quietly as before. The row said "file limit reached", which would be false here, so it now reads "Not fully scanned" for either reason. Reproduced on a real disk with a parent at mode 0444, where the child's stat answers EACCES: the API, the search and the panel all report it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f2f4952c34
commit
37dfb73b1e
9 files changed
+55
-26
No files matched your search
@@ -1144,7 +1144,7 @@
|
||||
Worktree: '工作树',
|
||||
'Session folder': '会话文件夹',
|
||||
'Back to worktree': '返回工作树',
|
||||
'Not fully scanned (file limit reached)': '未完全扫描(已达到文件数量上限)',
|
||||
'Not fully scanned': '未完全扫描',
|
||||
'Any status': '任意状态',
|
||||
'Any time': '任意时间',
|
||||
'Last hour': '最近一小时',
|
||||
|
||||
@@ -3795,10 +3795,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
}
|
||||
|
||||
// The scan's file limit stopped short of this folder, so what is listed above is not all of
|
||||
// it. Not a .file-tree-item: the click wiring below would try to open it as a file.
|
||||
// The scan stopped at a limit or the folder did not answer, so what is listed above is not all
|
||||
// of it. Not a .file-tree-item: the click wiring below would try to open it as a file.
|
||||
if (isDir && isExpanded && node.incomplete === true) {
|
||||
html.push(`<div class="file-tree-incomplete" data-depth="${depth + 1}">${escapeHtml('Not fully scanned (file limit reached)')}</div>`);
|
||||
html.push(`<div class="file-tree-incomplete" data-depth="${depth + 1}">${escapeHtml('Not fully scanned')}</div>`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -800,12 +800,15 @@ interface WalkCursor {
|
||||
index: number;
|
||||
/** The read kept fewer entries than the folder lists, so running out of `entries` does not finish it. */
|
||||
more?: boolean;
|
||||
/** Its probe answered `unknown`: it may hold anything, so it must not pass for an empty folder. */
|
||||
unanswered?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* First turn of a folder: reads it and sets `cursor.entries` to the part the listing could use,
|
||||
* which is the entries it lists, sorted, up to what is left of the budget. Leaves `entries` unset
|
||||
* for a folder it could not read. Answers the number of entries read, for the walk's read ceiling.
|
||||
* for a folder it could not read, and sets `unanswered` when that was a probe that established
|
||||
* nothing. Answers the number of entries read, for the walk's read ceiling.
|
||||
*/
|
||||
async function readWalkFolder(
|
||||
cursor: WalkCursor,
|
||||
@@ -814,8 +817,13 @@ async function readWalkFolder(
|
||||
): Promise<number> {
|
||||
// The walk's root is the session's own folder. Below it any folder can be a mount that stopped
|
||||
// answering, and a bulk scan keeps the stall cap (#516): an unbounded readdir there would hold a
|
||||
// threadpool worker for good. Skipped like an unreadable one: it is not the file limit's doing.
|
||||
if (cursor.depth > 1 && (await probePathKind(cursor.dirPath)) !== 'directory') return 0;
|
||||
// threadpool worker for good. `unknown` is never `absent`, so only a folder the filesystem says
|
||||
// is gone drops out quietly: one that did not answer goes on the walk's unfinished list.
|
||||
if (cursor.depth > 1) {
|
||||
const kind = await probePathKind(cursor.dirPath);
|
||||
cursor.unanswered = kind === 'unknown';
|
||||
if (kind !== 'directory') return 0;
|
||||
}
|
||||
|
||||
let read: Dirent[];
|
||||
try {
|
||||
@@ -1734,7 +1742,7 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
size?: number;
|
||||
extension?: string;
|
||||
children?: FileTreeNode[];
|
||||
/** Set only on a folder the walk's limits stopped it short of, so `children` is not all of it. */
|
||||
/** Set only on a folder the walk stopped short of or could not reach, so `children` is not all of it. */
|
||||
incomplete?: boolean;
|
||||
}
|
||||
|
||||
@@ -1757,7 +1765,7 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
const maxMatches = 1000;
|
||||
|
||||
const queue: WalkCursor[] = [{ dirPath: walkRoot, depth: 1, index: 0 }];
|
||||
// Folders the read ceiling kept shut. What was already read still gets its turns.
|
||||
// Folders the read ceiling kept shut or that did not answer. What was already read still gets its turns.
|
||||
const unread: WalkCursor[] = [];
|
||||
while (queue.length > 0 && totalFiles + totalDirectories < maxFiles && matches.length < maxMatches) {
|
||||
// Per-directory round-robin (breadth-first): each queued directory gets an equal share of what
|
||||
@@ -1786,6 +1794,7 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
continue;
|
||||
}
|
||||
entriesRead += await readWalkFolder(cursor, maxFiles - totalFiles - totalDirectories, isListed);
|
||||
if (cursor.unanswered) unread.push(cursor);
|
||||
if (!cursor.entries) continue;
|
||||
}
|
||||
|
||||
@@ -1876,6 +1885,7 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
continue;
|
||||
}
|
||||
entriesRead += await readWalkFolder(cursor, maxFiles - totalFiles - totalDirectories, isListed);
|
||||
if (cursor.unanswered) unread.push(cursor);
|
||||
if (!cursor.entries) continue;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user