commit e247b0cba247a06b478e752df7ca44a0c07b681e
parent 60efb2ca4d52b4907e976ebe1c948315dcf570bd
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Sun, 20 Sep 2026 15:54:24 -0300
main: run a reader from one place
Driving a reader was written twice, once for the batch path and once for the live
one, which left a file about drawing frames holding an MFT read loop. The two
agreed, but had already drifted on a point worth not drifting on: the batch path
named the root after folding rather than inside the projection, which is what once
let a watcher catch the table's ".".
fill.odin owns it now. Whether the tree is folded as it fills or once at the end
is the only difference left between a watched scan and an unwatched one, and
live.odin imports scan and nothing else.
Diffstat:
| A | fill.odin | | | 161 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | live.odin | | | 159 | ++++++++----------------------------------------------------------------------- |
| M | main.odin | | | 69 | +++++++++++++++++++-------------------------------------------------- |
3 files changed, 195 insertions(+), 194 deletions(-)
diff --git a/fill.odin b/fill.odin
@@ -0,0 +1,161 @@
+package main
+
+import "core:fmt"
+import "core:sync"
+import "core:thread"
+
+import "ntfs"
+import "scan"
+import "walk"
+
+/*
+Runs a reader into the tree.
+
+The one place that knows how each reader is driven. A watched scan and an unwatched
+one reach the same tree by the same route, rather than by two routes that happen to
+agree; the only difference between them is whether the tree is folded as it fills or
+once at the end.
+
+`scanning` is clear once nothing is filling the tree any more. A caller that draws
+while it runs watches that; one that does not simply waits for `fill_run` to return.
+*/
+Fill :: struct {
+ // What to read, and how.
+ target: scan.Target,
+ engine: scan.Engine,
+ opts: ntfs.Read_Options,
+ wcfg: walk.Config,
+ // Whether anything is reading the tree while it fills. A reader that has to fold
+ // pays for doing so repeatedly, which is worth it only if someone is looking.
+ watched: bool,
+ tree: ^scan.Tree,
+ // The MFT reader's own table, which carries what only NTFS knows. It outlives the
+ // tree, whose names are borrowed from it.
+ table: ntfs.Mft,
+ // What it left behind.
+ mft_err: ntfs.Error,
+ walk_err: walk.Error,
+ tree_err: scan.Error,
+ reading: b32, // the table is still being read
+ scanning: b32, // the tree is still being filled
+}
+
+fill_destroy :: proc(f: ^Fill) {
+ ntfs.mft_destroy(&f.table)
+}
+
+// Run the reader to completion. The one place a reader is chosen.
+fill_run :: proc(f: ^Fill) {
+ defer sync.atomic_store(&f.scanning, false)
+
+ switch f.engine {
+ case .Mft:
+ fill_mft(f)
+ case .Walk:
+ fill_walk(f)
+ case .None:
+ }
+}
+
+// Say what went wrong, if anything did. The MFT's failures get their own words
+// because they are the ones a reader of them can act on.
+fill_report :: proc(f: ^Fill) -> bool {
+ if f.mft_err != nil {
+ #partial switch f.mft_err {
+ case .Access_Denied:
+ fmt.eprintln(
+ "error: access denied. Reading a raw volume needs an administrator prompt.",
+ )
+ case .Not_Ntfs:
+ fmt.eprintln("error: not an NTFS volume")
+ case .Open_Failed:
+ fmt.eprintfln("error: could not open %s", f.target.volume)
+ case:
+ fmt.eprintfln("error: %v", f.mft_err)
+ }
+ return true
+ }
+ if f.walk_err != nil {
+ fmt.eprintfln(
+ "error: walking %s: %v",
+ scan.location(f.target, context.temp_allocator),
+ f.walk_err,
+ )
+ return true
+ }
+ if f.tree_err != nil {
+ fmt.eprintfln("error: %v", f.tree_err)
+ return true
+ }
+ return false
+}
+
+/*
+Read the table, folding it into the tree.
+
+read_mft blocks until the whole table is read, so a watched scan gives it a thread and
+folds beside it. Either way a record settled part way through misses whatever
+extension records added to it afterwards, so every record is folded again at the end
+and the tree restated, which is how a watcher learns its totals have to start over.
+
+The root is named inside the projection rather than afterwards: the table calls it
+".", and a watcher has been known to catch the dot.
+*/
+@(private = "file")
+fill_mft :: proc(f: ^Fill) {
+ p: ntfs.Projection
+ p.root_name = scan.top(f.target)
+ defer ntfs.projection_destroy(&p)
+
+ if f.watched {
+ sync.atomic_store(&f.reading, true)
+ reader := thread.create_and_start_with_poly_data(f, fill_mft_read)
+ if reader == nil {
+ f.mft_err = .Open_Failed
+ return
+ }
+ defer thread.destroy(reader)
+
+ for {
+ // Read first, so a table that finishes mid-fold still gets one more pass.
+ reading := bool(sync.atomic_load(&f.reading))
+ if ntfs.mft_ready(&f.table) {
+ if err := ntfs.to_tree(&f.table, f.tree, &p); err != nil {
+ f.tree_err = err
+ break
+ }
+ }
+ if !reading {
+ break
+ }
+ }
+ thread.join(reader)
+ } else {
+ f.mft_err = ntfs.read_mft(f.target.volume, &f.table, f.opts)
+ }
+ if f.mft_err != nil || f.tree_err != nil {
+ return
+ }
+
+ ntfs.projection_reset(&p)
+ if err := ntfs.to_tree(&f.table, f.tree, &p); err != nil {
+ f.tree_err = err
+ return
+ }
+ scan.restate(f.tree)
+}
+
+@(private = "file")
+fill_mft_read :: proc(f: ^Fill) {
+ f.mft_err = ntfs.read_mft(f.target.volume, &f.table, f.opts)
+ sync.atomic_store(&f.reading, false)
+}
+
+// Walk directories straight into the tree. Nothing is restated: a walk reaches a child
+// through its parent, so a node is final when it is written, watched or not.
+@(private = "file")
+fill_walk :: proc(f: ^Fill) {
+ root := scan.location(f.target, context.allocator)
+ defer delete(root)
+ f.walk_err = walk.scan(root, f.tree, f.wcfg)
+}
diff --git a/live.odin b/live.odin
@@ -5,9 +5,7 @@ import "core:sync"
import "core:thread"
import "core:time"
-import "ntfs"
import "scan"
-import "walk"
/*
Stand in for a UI.
@@ -17,89 +15,30 @@ everything derived from it, and this loop drawing. The point of the arrangement
that a frame costs the same whatever the volume holds, because it reads a published
snapshot of twenty rows rather than the tree.
-Which reader fills it is decided in one switch below and nowhere else. The builder is
-told nothing about it: a reader settles nodes, and restates if it has to correct
-itself, and that is the whole of the conversation.
+Which reader fills it is not decided here, and nothing here knows which one did. A
+reader settles nodes, and restates if it has to correct itself; that is the whole of
+the conversation.
Nothing here waits on the builder. If a build pass is mid-write the loop draws the
answer from the last one.
*/
FRAME :: 16 * time.Millisecond
-// A reader filling the tree, and what it left behind. Only the fields its own engine
-// uses are set.
-@(private = "file")
-Fill :: struct {
- tree: ^scan.Tree,
- table: ^ntfs.Mft,
- volume: string,
- // Where a walk starts, or what an MFT calls its root. One reader runs, and it
- // reads the one that means something to it.
- root: string,
- opts: ntfs.Read_Options,
- wcfg: walk.Config,
- mft_err: ntfs.Error,
- walk_err: walk.Error,
- tree_err: scan.Error,
- reading: b32, // the table is still being read
- scanning: b32, // the tree is still being filled
-}
-
-run_live :: proc(
- target: scan.Target,
- choice: scan.Choice,
- opts: ntfs.Read_Options,
- wcfg: walk.Config,
-) -> int {
- m: ntfs.Mft
- defer ntfs.mft_destroy(&m)
-
- t: scan.Tree
- if err := scan.tree_init(&t, 8); err != nil {
- fmt.eprintfln("error: %v", err)
- return 1
- }
- defer scan.tree_destroy(&t)
-
+run_live :: proc(f: ^Fill, t: ^scan.Tree) -> int {
out: scan.Publisher
started := time.tick_now()
- f := Fill {
- tree = &t,
- table = &m,
- volume = target.volume,
- opts = opts,
- wcfg = wcfg,
- scanning = true,
- }
+ f.watched = true
+ sync.atomic_store(&f.scanning, true)
+
b := Builder {
- tree = &t,
+ tree = t,
out = &out,
scanning = &f.scanning,
started = started,
}
- // Freed at function scope. A defer inside the switch below would run as the case
- // ended, with the reader still holding the string.
- owned_root: string
- defer if owned_root != "" {
- delete(owned_root)
- }
-
- // The one place a reader is chosen for a live scan, as in the batch path.
- filler: ^thread.Thread
- switch choice.engine {
- case .Mft:
- f.root = scan.top(target)
- filler = thread.create_and_start_with_poly_data(&f, mft_fill)
- case .Walk:
- owned_root = scan.location(target, context.allocator)
- f.root = owned_root
- filler = thread.create_and_start_with_poly_data(&f, walk_fill)
- case .None:
- fmt.eprintln("error: nothing here can be scanned")
- return 1
- }
+ filler := thread.create_and_start_with_poly_data(f, fill_thread)
if filler == nil {
fmt.eprintln("error: could not start the reader")
return 1
@@ -138,7 +77,7 @@ run_live :: proc(
last.nodes,
last.pending,
human(last.rows[0].bytes),
- scan.path(&t, last.rows[0].node, context.temp_allocator),
+ scan.path(t, last.rows[0].node, context.temp_allocator),
time.duration_milliseconds(time.tick_since(frame_start)),
last.complete ? " (final)" : "",
)
@@ -158,16 +97,7 @@ run_live :: proc(
thread.join(filler)
thread.join(builder)
- if f.mft_err != nil {
- fmt.eprintfln("error: %v", f.mft_err)
- return 1
- }
- if f.walk_err != nil {
- fmt.eprintfln("error: %v", f.walk_err)
- return 1
- }
- if f.tree_err != nil {
- fmt.eprintfln("error: %v", f.tree_err)
+ if fill_report(f) {
return 1
}
@@ -180,74 +110,15 @@ run_live :: proc(
missed,
)
- print_largest(&t, b.roll.totals[:], .files)
- print_largest(&t, b.roll.totals[:], .directories)
+ print_largest(t, b.roll.totals[:], .files)
+ print_largest(t, b.roll.totals[:], .directories)
return 0
}
-/*
-Read the table on its own threads, folding it into the tree as it fills.
-
-read_mft blocks until the whole table is read, so it gets a thread and this one folds
-beside it. A record settled part way through misses whatever extension records added
-to it afterwards, so once the reading is over every record is folded again and the
-tree restated, which is how the builder learns its totals have to start over.
-*/
-@(private = "file")
-mft_fill :: proc(f: ^Fill) {
- defer sync.atomic_store(&f.scanning, false)
-
- sync.atomic_store(&f.reading, true)
- reader := thread.create_and_start_with_poly_data(f, mft_read)
- if reader == nil {
- f.mft_err = .Open_Failed
- return
- }
- defer thread.destroy(reader)
-
- p: ntfs.Projection
- p.root_name = f.root
- defer ntfs.projection_destroy(&p)
-
- for {
- // Read first, so a table that finishes mid-fold still gets one more pass.
- reading := bool(sync.atomic_load(&f.reading))
- if ntfs.mft_ready(f.table) {
- if err := ntfs.to_tree(f.table, f.tree, &p); err != nil {
- f.tree_err = err
- break
- }
- }
- if !reading {
- break
- }
- }
- thread.join(reader)
- if f.mft_err != nil || f.tree_err != nil {
- return
- }
-
- ntfs.projection_reset(&p)
- if err := ntfs.to_tree(f.table, f.tree, &p); err != nil {
- f.tree_err = err
- return
- }
- scan.restate(f.tree)
-}
-
-@(private = "file")
-mft_read :: proc(f: ^Fill) {
- f.mft_err = ntfs.read_mft(f.volume, f.table, f.opts)
- sync.atomic_store(&f.reading, false)
-}
-
-// Walk directories straight into the tree. Nothing is restated: a walk reaches a child
-// through its parent, so a node is final when it is written.
@(private = "file")
-walk_fill :: proc(f: ^Fill) {
- defer sync.atomic_store(&f.scanning, false)
- f.walk_err = walk.scan(f.root, f.tree, f.wcfg)
+fill_thread :: proc(f: ^Fill) {
+ fill_run(f)
}
@(private = "file")
diff --git a/main.odin b/main.odin
@@ -116,10 +116,6 @@ run :: proc() -> int {
return 1
}
- if opt.live {
- return run_live(target, engine, opts, wcfg)
- }
-
t: scan.Tree
if err := scan.tree_init(&t, 8); err != nil {
fmt.eprintfln("error: %v", err)
@@ -127,56 +123,29 @@ run :: proc() -> int {
}
defer scan.tree_destroy(&t)
- start := time.tick_now()
- // The one place a reader is chosen. Each fills the same tree; the MFT reader also
- // keeps its own table, which carries what only NTFS knows.
- m: ntfs.Mft
- have_mft := false
- // Registered before the read, not after the switch: projecting the table can fail,
- // and a defer below that point never covers the return it takes.
- defer if have_mft {
- ntfs.mft_destroy(&m)
+ // The table outlives the tree, whose names are borrowed from it.
+ f := Fill {
+ target = target,
+ engine = engine.engine,
+ opts = opts,
+ wcfg = wcfg,
+ tree = &t,
}
- switch engine.engine {
- case .Mft:
- err := ntfs.read_mft(target.volume, &m, opts)
- if err != nil {
- #partial switch err {
- case .Access_Denied:
- fmt.eprintln(
- "error: access denied. Reading a raw volume needs an administrator prompt.",
- )
- case .Not_Ntfs:
- fmt.eprintln("error: not an NTFS volume")
- case .Open_Failed:
- fmt.eprintfln("error: could not open %s", target.volume)
- case:
- fmt.eprintfln("error: %v", err)
- }
- return 1
- }
- have_mft = true
- projection: ntfs.Projection
- defer ntfs.projection_destroy(&projection)
- if err := ntfs.to_tree(&m, &t, &projection); err != nil {
- fmt.eprintfln("error: %v", err)
- return 1
- }
- // The table calls its root ".", so paths would lose the volume it came from.
- rw := scan.writer(&t, 0)
- scan.node(&t, t.root).name = scan.intern(&rw, scan.top(target))
- case .Walk:
- root := scan.location(target, context.temp_allocator)
- if err := walk.scan(root, &t, wcfg); err != nil {
- fmt.eprintfln("error: walking %s: %v", root, err)
- return 1
- }
- case .None:
+ defer fill_destroy(&f)
+
+ if opt.live {
+ return run_live(&f, &t)
+ }
+
+ start := time.tick_now()
+ fill_run(&f)
+ if fill_report(&f) {
+ return 1
}
elapsed := time.tick_since(start)
- if have_mft {
- print_native(&m, elapsed)
+ if engine.engine == .Mft {
+ print_native(&f.table, elapsed)
}
fmt.println()
fmt.printfln(