walk.odin (5904B)
1 /* 2 Package walk enumerates directories through core:os. 3 4 It is the reader that works anywhere: any filesystem, any platform, no privilege. It 5 is also much slower than reading a filesystem's own metadata, because it pays a 6 syscall per directory and learns nothing the OS does not hand it. That makes it the 7 fallback rather than the default, and the yardstick a specialised reader is measured 8 against. 9 10 A directory listing is already the shape of the tree, so it writes straight into it. 11 Reading one directory is the only part that differs by platform, and it is the part 12 worth specialising: `read_dir` is portable through core:os everywhere but Linux, 13 which asks the kernel directly. 14 */ 15 package walk 16 17 import "core:mem" 18 import "core:strings" 19 20 import "jm:flow" 21 import "../scan" 22 23 Config :: struct { 24 workers: int, // 0 selects DEFAULT_WORKERS 25 follow: bool, // descend through symlinks and junctions; loops are not detected 26 } 27 28 // Directory reads are latency bound and hold no core while they wait, so more 29 // workers than cores is right here in a way it was not for the MFT reader. 30 DEFAULT_WORKERS :: 8 31 32 Error :: enum { 33 None, 34 Out_Of_Memory, 35 Cancelled, 36 } 37 38 /* 39 Read everything under `root` into the tree. 40 41 Every directory below it is opened, each entry it holds becomes a node, and each 42 directory among them is read in turn. A worker takes the next directory the moment 43 one is known rather than waiting for a level to finish, so a tree that is wide near 44 the root and one that is deep in a single branch both keep them busy. 45 46 Returns once the tree holds everything under `root`; Cancelled if the scan was called 47 off part way, and Out_Of_Memory if the tree ran out of room, in which case what is 48 there is short rather than wrong. 49 */ 50 read :: proc(root: string, t: ^scan.Tree, cfg := Config{}) -> Error { 51 allocator := context.allocator 52 53 w := scan.writer(t, 0) 54 first, claim_err := scan.claim(&w, 1) 55 if claim_err != nil { 56 return .Out_Of_Memory 57 } 58 n := scan.node(t, first) 59 n.parent = first // a root holds itself 60 n.name = scan.intern(&w, root) 61 n.flags = {.Used, .Settled, .Directory} 62 t.root = first 63 64 workers := cfg.workers 65 if workers <= 0 { 66 workers = DEFAULT_WORKERS 67 } 68 // One writer per worker, and a writer needs an arena of its own, so the tree 69 // caps how wide this can run. 70 workers = min(workers, len(t.arenas)) 71 72 states := make([]Worker, workers, allocator) 73 defer delete(states, allocator) 74 for i in 0 ..< workers { 75 states[i] = Worker { 76 writer = scan.writer(t, i), 77 found = make([dynamic]Dir, allocator), 78 follow = cfg.follow, 79 allocator = allocator, 80 } 81 } 82 defer for &s in states { 83 delete(s.found) 84 } 85 86 seed := []Dir{{index = first, path = strings.clone(root, allocator)}} 87 flow.manage(seed, states, walk_dir, hand_over, drop) 88 89 for &s in states { 90 if s.err != nil { 91 return s.err 92 } 93 } 94 if scan.cancelled(t) { 95 return .Cancelled 96 } 97 return .None 98 } 99 100 // Move what a worker found into the queue, and judge whatever it could not read. 101 // Serialised by `manage`, so this is the one place the set of directories still to 102 // walk is touched. 103 @(private) 104 hand_over :: proc(d: Dir, ok: bool, w: ^Worker, queue: ^[dynamic]Dir) -> bool { 105 for found in w.found { 106 append(queue, found) 107 } 108 clear(&w.found) 109 // A directory that would not open is worth nothing and is not worth retrying, 110 // but it is no reason to abandon the rest of the volume. Running out of room 111 // for the tree is, since everything after it would be missing silently. 112 return w.err == nil && !scan.cancelled(w.writer.tree) 113 } 114 115 // A directory a stopped scan never reached. `walk_dir` frees what it is handed; this 116 // is the other half, since cancelling mid-volume otherwise strands a path per 117 // directory found but not yet walked. Runs on the thread that called `scan`, and 118 // paths are cloned from that context, so the two allocators are the same one. 119 @(private) 120 drop :: proc(d: Dir) { 121 delete(d.path, context.allocator) 122 } 123 124 // A directory waiting to be read, and the node already standing for it. 125 @(private) 126 Dir :: struct { 127 index: u32, 128 path: string, // owned; freed once walked 129 } 130 131 @(private) 132 Worker :: struct { 133 writer: scan.Writer, 134 found: [dynamic]Dir, 135 follow: bool, 136 err: Error, // why this worker gave up, which only the manager acts on 137 allocator: mem.Allocator, 138 } 139 140 @(private) 141 walk_dir :: proc(d: Dir, w: ^Worker) -> bool { 142 defer delete(d.path, w.allocator) 143 if scan.cancelled(w.writer.tree) { 144 return false 145 } 146 nodes, bytes, ok := read_dir(d, w) 147 scan.progress(w.writer.tree, nodes, bytes) 148 return ok 149 } 150 151 // One entry as a directory read describes it, before it becomes a node. `disk` is 152 // whatever the platform could learn about allocation; every reader fills it, since 153 // only the reader knows how good its own answer is. 154 @(private) 155 Entry :: struct { 156 name: string, 157 size: u64, 158 disk: u64, 159 directory: bool, 160 symlink: bool, 161 } 162 163 /* 164 Record one entry against the directory holding it. 165 166 `descend` marks an entry to walk in turn: a directory, or a symlink when the caller 167 asked for those to be followed. Shared so that what a node means cannot drift between 168 one platform's reader and another's. 169 */ 170 @(private) 171 record :: proc(w: ^Worker, parent: u32, e: Entry) -> (index: u32, descend: bool, ok: bool) { 172 slot, err := scan.claim(&w.writer, 1) 173 if err != nil { 174 w.err = .Out_Of_Memory 175 return 0, false, false 176 } 177 index = slot 178 n := scan.node(w.writer.tree, index) 179 n.parent = parent 180 n.name = scan.intern(&w.writer, e.name) 181 n.size = e.size 182 183 // A walker reaches a child through its parent, so the link is never in doubt. 184 flags := scan.Node_Flags{.Used, .Settled} 185 switch { 186 case e.directory: 187 flags |= {.Directory} 188 descend = true 189 case e.symlink: 190 flags |= {.Reparse} 191 descend = w.follow 192 case: 193 n.disk = e.disk 194 } 195 // Last, so a builder watching this tree never sees a node before its size. 196 scan.node_publish(n, flags) 197 return index, descend, true 198 }