Skip to content

Commit 708cff7

Browse files
Copilotkovidgoyal
andauthored
Convert RemoveChildren from recursive to iterative stack-based
Agent-Logs-Url: https://github.com/kovidgoyal/kitty/sessions/bd5ca209-e811-4c49-bec7-5dd3a40d8c57 Co-authored-by: kovidgoyal <1308621+kovidgoyal@users.noreply.github.com>
1 parent c655df1 commit 708cff7

1 file changed

Lines changed: 68 additions & 37 deletions

File tree

tools/utils/file_at_fd.go

Lines changed: 68 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -230,60 +230,91 @@ func LinkAt(oldparent *os.File, oldname string, newparent *os.File, newname stri
230230
return
231231
}
232232

233-
// RemoveChildren recursively removes all files and subdirectories
234-
// within the directory pointed to by dirFile. Removes all it can but returns
235-
// the first error, if any.
233+
// RemoveChildren removes all files and subdirectories within the directory
234+
// pointed to by dirFile using an explicit stack instead of recursion. Removes
235+
// all it can but returns the first error, if any.
236236
func RemoveChildren(dirFile *os.File) error {
237237
var firstErr error
238238

239-
// Rewind directory pointer to ensure we start from the beginning
240-
if _, err := dirFile.Seek(0, io.SeekStart); err != nil {
241-
return err
239+
// Each stack frame is one of two kinds:
240+
// expand: dir != nil – read dir's children, unlink files, push subdirs
241+
// rmdir: dir == nil – rmdir name from parent (a dup'd fd), then close parent
242+
type frame struct {
243+
dir *os.File // non-nil: expand this directory
244+
name string // rmdir: child name to remove from parent
245+
parent *os.File // rmdir: dup'd parent fd (closed after use)
246+
close bool // expand: close dir when done (false only for the root)
242247
}
243248

244-
for {
245-
// Read names in small chunks to handle very large directories
246-
entries, err := dirFile.ReadDir(64)
247-
if err != nil {
248-
if errors.Is(err, io.EOF) {
249-
break
249+
stack := []frame{{dir: dirFile, close: false}}
250+
251+
for len(stack) > 0 {
252+
f := stack[len(stack)-1]
253+
stack = stack[:len(stack)-1]
254+
255+
if f.dir == nil {
256+
// rmdir sentinel: the subdirectory is now empty, remove it.
257+
if err := RemoveDirAt(f.parent, f.name); err != nil && firstErr == nil {
258+
firstErr = err
250259
}
260+
f.parent.Close()
261+
continue
262+
}
263+
264+
// Rewind so we always start from the beginning of the directory.
265+
if _, err := f.dir.Seek(0, io.SeekStart); err != nil {
251266
if firstErr == nil {
252-
firstErr = &os.PathError{Op: "readdirnames", Path: dirFile.Name(), Err: err}
267+
firstErr = err
253268
}
254-
break
269+
if f.close {
270+
f.dir.Close()
271+
}
272+
continue
255273
}
256274

257-
for _, entry := range entries {
258-
name := entry.Name()
259-
if entry.IsDir() {
260-
// Open subdirectory relative to parent FD
261-
var childFile *os.File
262-
if childFile, err = OpenDirAt(dirFile, name); err != nil {
263-
if firstErr == nil {
264-
firstErr = err
275+
for {
276+
// Read entries in small chunks to handle very large directories.
277+
entries, err := f.dir.ReadDir(64)
278+
for _, entry := range entries {
279+
name := entry.Name()
280+
if entry.IsDir() {
281+
childFile, openErr := OpenDirAt(f.dir, name)
282+
if openErr != nil {
283+
if firstErr == nil {
284+
firstErr = openErr
285+
}
286+
continue
265287
}
266-
continue
267-
}
268-
err = RemoveChildren(childFile)
269-
childFile.Close()
270-
if err == nil {
271-
// Remove the empty subdirectory
272-
if err = RemoveDirAt(dirFile, name); err != nil && firstErr == nil {
273-
firstErr = err
288+
parentDup, dupErr := DupFile(f.dir)
289+
if dupErr != nil {
290+
if firstErr == nil {
291+
firstErr = dupErr
292+
}
293+
childFile.Close()
294+
continue
295+
}
296+
// Push rmdir sentinel first; LIFO ensures the expand frame
297+
// below is processed before this rmdir sentinel.
298+
stack = append(stack, frame{name: name, parent: parentDup}, frame{dir: childFile, close: true})
299+
} else {
300+
if unlinkErr := UnlinkAt(f.dir, name); unlinkErr != nil && firstErr == nil {
301+
firstErr = unlinkErr
274302
}
275-
} else if firstErr == nil {
276-
firstErr = err
277303
}
278-
childFile.Close()
279-
} else {
280-
// Remove file/symlink
281-
if err = UnlinkAt(dirFile, name); err != nil && firstErr == nil {
282-
firstErr = err
304+
}
305+
if err != nil {
306+
if !errors.Is(err, io.EOF) && firstErr == nil {
307+
firstErr = &os.PathError{Op: "readdir", Path: f.dir.Name(), Err: err}
283308
}
309+
break
284310
}
285311
}
312+
313+
if f.close {
314+
f.dir.Close()
315+
}
286316
}
317+
287318
_, _ = dirFile.Seek(0, io.SeekStart)
288319
return firstErr
289320
}

0 commit comments

Comments
 (0)