detached ba97260

fix(startup): sanitize presentation evidence

commit
ba97260808f6dce0316f532dcd548414bb470ed9
author
brice · · 2026-07-14 08:09 UTC
committer
brice · · 2026-07-14 09:35 UTC
parent
3bc6123
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +84 -16
M docs/first-presentation.md +4 -2
@@ -32,5 +32,7 @@ The script requires the current display and the CEF 147 runtime at
3232 `~/.local/share/cef-147-runtime` (override with `DUMBER_CEF_DIR`). It performs
3333 exactly five bounded launches with fresh XDG homes and CEF root cache, fixes the
3434 DMABUF/Vulkan environment, and writes `metadata.json`, `run-01.json` through
35-`run-05.json`, and `baseline.json` below `phase1/first-presentation`. A missing
36-or incomplete timeline, non-DMABUF backend, or invalid run fails collection.
35+`run-05.json`, and `baseline.json` below `phase1/first-presentation`. Published
36+artifacts contain only logical labels, version/hash identifiers, and timing
37+fields; raw logs and temporary XDG homes are removed. A missing or incomplete
38+timeline, non-DMABUF backend, or invalid run fails collection.
@@ -32,5 +32,7 @@ The script requires the current display and the CEF 147 runtime at
32 `~/.local/share/cef-147-runtime` (override with `DUMBER_CEF_DIR`). It performs32 `~/.local/share/cef-147-runtime` (override with `DUMBER_CEF_DIR`). It performs
33 exactly five bounded launches with fresh XDG homes and CEF root cache, fixes the33 exactly five bounded launches with fresh XDG homes and CEF root cache, fixes the
34 DMABUF/Vulkan environment, and writes `metadata.json`, `run-01.json` through34 DMABUF/Vulkan environment, and writes `metadata.json`, `run-01.json` through
35-`run-05.json`, and `baseline.json` below `phase1/first-presentation`. A missing35+`run-05.json`, and `baseline.json` below `phase1/first-presentation`. Published
36-or incomplete timeline, non-DMABUF backend, or invalid run fails collection.36+artifacts contain only logical labels, version/hash identifiers, and timing
37+fields; raw logs and temporary XDG homes are removed. A missing or incomplete
38+timeline, non-DMABUF backend, or invalid run fails collection.
A internal/logging/first_presentation_collector_test.go +62 -0
@@ -0,0 +1,62 @@
1+package logging
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+
10+ "github.com/stretchr/testify/require"
11+)
12+
13+func TestFirstPresentationCollectorSanitizesMachineLocalValues(t *testing.T) {
14+ repoRoot, err := filepath.Abs(filepath.Join("..", ".."))
15+ require.NoError(t, err)
16+ temp := t.TempDir()
17+ runtime := filepath.Join(temp, "cef-147-runtime")
18+ require.NoError(t, os.Mkdir(runtime, 0o755))
19+
20+ binary := filepath.Join(temp, "dumber")
21+ require.NoError(t, os.WriteFile(binary, []byte(`#!/bin/sh
22+cat <<'EOF'
23+{"message":"startup_trace: milestone","milestone":"process_entry","t_ms":0,"delta_ms":0,"time":"2026-07-14T09:00:00+02:00","machine_path":"/home/alice/private"}
24+{"message":"startup_trace: milestone","milestone":"config_complete","t_ms":1,"delta_ms":1}
25+{"message":"startup_trace: milestone","milestone":"cef_library_load_begin","t_ms":2,"delta_ms":1}
26+{"message":"startup_trace: milestone","milestone":"cef_initialized","t_ms":3,"delta_ms":1}
27+{"message":"startup_trace: milestone","milestone":"browser_create_requested","t_ms":4,"delta_ms":1}
28+{"message":"startup_trace: milestone","milestone":"first_accelerated_paint_received","t_ms":5,"delta_ms":1}
29+{"message":"startup_trace: milestone","milestone":"first_dmabuf_texture_swap","t_ms":6,"delta_ms":1}
30+{"message":"startup_trace: milestone","milestone":"first_gtk_presentation","t_ms":7,"delta_ms":1}
31+{"message":"startup_trace: first presentation","backend":"gdk-dmabuf","incomplete_reason":"","total_ms":7,"host":"alice"}
32+EOF
33+exit 139
34+`), 0o755))
35+
36+ output := filepath.Join(temp, "artifacts")
37+ cmd := exec.Command(filepath.Join(repoRoot, "scripts", "collect_first_presentation.sh"))
38+ cmd.Dir = repoRoot
39+ cmd.Env = append(os.Environ(),
40+ "DISPLAY=:test",
41+ "DUMBER_CEF_DIR="+runtime,
42+ "DUMBER_FIRST_PRESENTATION_BIN="+binary,
43+ "DUMBER_FIRST_PRESENTATION_OUTPUT="+output,
44+ "DUMBER_FIRST_PRESENTATION_TIMEOUT_SECONDS=1",
45+ )
46+ result, err := cmd.CombinedOutput()
47+ require.NoErrorf(t, err, "collector failed: %s", result)
48+
49+ var artifacts strings.Builder
50+ for _, name := range append([]string{"metadata.json", "baseline.json"}, runArtifactNames()...) {
51+ contents, err := os.ReadFile(filepath.Join(output, name))
52+ require.NoError(t, err)
53+ artifacts.Write(contents)
54+ }
55+ for _, forbidden := range []string{"/home/", temp, "alice", "machine_path", `"time"`} {
56+ require.NotContainsf(t, artifacts.String(), forbidden, "committed artifact leaked %q", forbidden)
57+ }
58+}
59+
60+func runArtifactNames() []string {
61+ return []string{"run-01.json", "run-02.json", "run-03.json", "run-04.json", "run-05.json"}
62+}
@@ -0,0 +1,62 @@
1+package logging
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+
10+ "github.com/stretchr/testify/require"
11+)
12+
13+func TestFirstPresentationCollectorSanitizesMachineLocalValues(t *testing.T) {
14+ repoRoot, err := filepath.Abs(filepath.Join("..", ".."))
15+ require.NoError(t, err)
16+ temp := t.TempDir()
17+ runtime := filepath.Join(temp, "cef-147-runtime")
18+ require.NoError(t, os.Mkdir(runtime, 0o755))
19+
20+ binary := filepath.Join(temp, "dumber")
21+ require.NoError(t, os.WriteFile(binary, []byte(`#!/bin/sh
22+cat <<'EOF'
23+{"message":"startup_trace: milestone","milestone":"process_entry","t_ms":0,"delta_ms":0,"time":"2026-07-14T09:00:00+02:00","machine_path":"/home/alice/private"}
24+{"message":"startup_trace: milestone","milestone":"config_complete","t_ms":1,"delta_ms":1}
25+{"message":"startup_trace: milestone","milestone":"cef_library_load_begin","t_ms":2,"delta_ms":1}
26+{"message":"startup_trace: milestone","milestone":"cef_initialized","t_ms":3,"delta_ms":1}
27+{"message":"startup_trace: milestone","milestone":"browser_create_requested","t_ms":4,"delta_ms":1}
28+{"message":"startup_trace: milestone","milestone":"first_accelerated_paint_received","t_ms":5,"delta_ms":1}
29+{"message":"startup_trace: milestone","milestone":"first_dmabuf_texture_swap","t_ms":6,"delta_ms":1}
30+{"message":"startup_trace: milestone","milestone":"first_gtk_presentation","t_ms":7,"delta_ms":1}
31+{"message":"startup_trace: first presentation","backend":"gdk-dmabuf","incomplete_reason":"","total_ms":7,"host":"alice"}
32+EOF
33+exit 139
34+`), 0o755))
35+
36+ output := filepath.Join(temp, "artifacts")
37+ cmd := exec.Command(filepath.Join(repoRoot, "scripts", "collect_first_presentation.sh"))
38+ cmd.Dir = repoRoot
39+ cmd.Env = append(os.Environ(),
40+ "DISPLAY=:test",
41+ "DUMBER_CEF_DIR="+runtime,
42+ "DUMBER_FIRST_PRESENTATION_BIN="+binary,
43+ "DUMBER_FIRST_PRESENTATION_OUTPUT="+output,
44+ "DUMBER_FIRST_PRESENTATION_TIMEOUT_SECONDS=1",
45+ )
46+ result, err := cmd.CombinedOutput()
47+ require.NoErrorf(t, err, "collector failed: %s", result)
48+
49+ var artifacts strings.Builder
50+ for _, name := range append([]string{"metadata.json", "baseline.json"}, runArtifactNames()...) {
51+ contents, err := os.ReadFile(filepath.Join(output, name))
52+ require.NoError(t, err)
53+ artifacts.Write(contents)
54+ }
55+ for _, forbidden := range []string{"/home/", temp, "alice", "machine_path", `"time"`} {
56+ require.NotContainsf(t, artifacts.String(), forbidden, "committed artifact leaked %q", forbidden)
57+ }
58+}
59+
60+func runArtifactNames() []string {
61+ return []string{"run-01.json", "run-02.json", "run-03.json", "run-04.json", "run-05.json"}
62+}
M scripts/collect_first_presentation.sh +18 -14
@@ -16,23 +16,28 @@ readonly output="${DUMBER_FIRST_PRESENTATION_OUTPUT:-$PWD/phase1/first-presentat
1616
1717 rm -rf "$output"
1818 mkdir -p "$output"
19-python3 - "$output/metadata.json" "$runtime" "$binary" "$timeout_seconds" <<'PY'
20-import json, os, subprocess, sys
21-path, runtime, binary, timeout = sys.argv[1:]
19+# Raw logs and temporary XDG homes may contain machine-local paths. Keep them
20+# outside the committed artifact directory and always remove them.
21+work_root="$(mktemp -d "${TMPDIR:-/tmp}/dumber-first-presentation.XXXXXX")"
22+trap 'rm -rf "$work_root"' EXIT
23+python3 - "$output/metadata.json" "$binary" "$timeout_seconds" <<'PY'
24+import hashlib, json, os, subprocess, sys
25+path, binary, timeout = sys.argv[1:]
26+with open(binary, "rb") as candidate:
27+ binary_sha256 = hashlib.file_digest(candidate, "sha256").hexdigest()
2228 json.dump({
2329 "runs": 5,
24- "runtime": runtime,
25- "binary": binary,
30+ "runtime": {"label": "cef", "version": os.environ.get("DUMBER_CEF_RUNTIME_VERSION", "147")},
31+ "binary": {"label": "dumber", "sha256": binary_sha256},
2632 "timeout_seconds": int(timeout),
2733 "git_revision": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
28- "environment": {key: os.environ.get(key, "") for key in ("WAYLAND_DISPLAY", "DISPLAY", "XDG_CURRENT_DESKTOP")},
2934 "fixed_environment": {"DUMBER_RENDER_STACK": "vulkan-dmabuf", "PUREGO_CEF2GTK_BACKEND": "gdk-dmabuf", "GSK_RENDERER": "vulkan"}
3035 }, open(path, "w"), indent=2, sort_keys=True)
3136 PY
3237
3338 for number in $(seq 1 "$runs"); do
3439 run="$(printf 'run-%02d' "$number")"
35- root="$output/$run"
40+ root="$work_root/$run"
3641 mkdir -p "$root"/{config,data,state,cache}
3742 mkdir -p "$root/config/dumber"
3843 cat >"$root/config/dumber/config.toml" <<EOF
@@ -57,11 +62,8 @@ EOF
5762 timeout --signal=TERM --kill-after=5s "${timeout_seconds}s" "$binary" browse about:blank >"$root/process.log" 2>&1
5863 exit_code=$?
5964 set -e
60- # timeout is expected for a GUI kept alive after the first presentation.
61- if [[ "$exit_code" != 0 && "$exit_code" != 124 ]]; then
62- echo "$run: launch exited unexpectedly ($exit_code); see $root/process.log" >&2
63- exit 1
64- fi
65+ # A completed, valid first-presentation summary is the observation success
66+ # signal. The GUI may later be stopped by timeout or terminate independently.
6567 python3 - "$root/process.log" "$output/$run.json" "$run" <<'PY'
6668 import json, sys
6769 log, destination, run = sys.argv[1:]
@@ -70,8 +72,10 @@ records, summary = [], None
7072 for line in open(log, errors="replace"):
7173 try: event = json.loads(line)
7274 except json.JSONDecodeError: continue
73- if event.get("message") == "startup_trace: milestone": records.append(event)
74- if event.get("message") == "startup_trace: first presentation": summary = event
75+ if event.get("message") == "startup_trace: milestone":
76+ records.append({key: event.get(key) for key in ("milestone", "t_ms", "delta_ms")})
77+ if event.get("message") == "startup_trace: first presentation":
78+ summary = {key: event.get(key) for key in ("backend", "incomplete_reason", "total_ms")}
7579 names = [event.get("milestone") for event in records]
7680 times = [event.get("t_ms") for event in records]
7781 valid = names == order and all(isinstance(t, int) for t in times) and times == sorted(times)
@@ -16,23 +16,28 @@ readonly output="${DUMBER_FIRST_PRESENTATION_OUTPUT:-$PWD/phase1/first-presentat
16 16
17 rm -rf "$output"17 rm -rf "$output"
18 mkdir -p "$output"18 mkdir -p "$output"
19-python3 - "$output/metadata.json" "$runtime" "$binary" "$timeout_seconds" <<'PY'19+# Raw logs and temporary XDG homes may contain machine-local paths. Keep them
20-import json, os, subprocess, sys20+# outside the committed artifact directory and always remove them.
21-path, runtime, binary, timeout = sys.argv[1:]21+work_root="$(mktemp -d "${TMPDIR:-/tmp}/dumber-first-presentation.XXXXXX")"
22+trap 'rm -rf "$work_root"' EXIT
23+python3 - "$output/metadata.json" "$binary" "$timeout_seconds" <<'PY'
24+import hashlib, json, os, subprocess, sys
25+path, binary, timeout = sys.argv[1:]
26+with open(binary, "rb") as candidate:
27+ binary_sha256 = hashlib.file_digest(candidate, "sha256").hexdigest()
22 json.dump({28 json.dump({
23 "runs": 5,29 "runs": 5,
24- "runtime": runtime,30+ "runtime": {"label": "cef", "version": os.environ.get("DUMBER_CEF_RUNTIME_VERSION", "147")},
25- "binary": binary,31+ "binary": {"label": "dumber", "sha256": binary_sha256},
26 "timeout_seconds": int(timeout),32 "timeout_seconds": int(timeout),
27 "git_revision": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),33 "git_revision": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
28- "environment": {key: os.environ.get(key, "") for key in ("WAYLAND_DISPLAY", "DISPLAY", "XDG_CURRENT_DESKTOP")},
29 "fixed_environment": {"DUMBER_RENDER_STACK": "vulkan-dmabuf", "PUREGO_CEF2GTK_BACKEND": "gdk-dmabuf", "GSK_RENDERER": "vulkan"}34 "fixed_environment": {"DUMBER_RENDER_STACK": "vulkan-dmabuf", "PUREGO_CEF2GTK_BACKEND": "gdk-dmabuf", "GSK_RENDERER": "vulkan"}
30 }, open(path, "w"), indent=2, sort_keys=True)35 }, open(path, "w"), indent=2, sort_keys=True)
31 PY36 PY
32 37
33 for number in $(seq 1 "$runs"); do38 for number in $(seq 1 "$runs"); do
34 run="$(printf 'run-%02d' "$number")"39 run="$(printf 'run-%02d' "$number")"
35- root="$output/$run"40+ root="$work_root/$run"
36 mkdir -p "$root"/{config,data,state,cache}41 mkdir -p "$root"/{config,data,state,cache}
37 mkdir -p "$root/config/dumber"42 mkdir -p "$root/config/dumber"
38 cat >"$root/config/dumber/config.toml" <<EOF43 cat >"$root/config/dumber/config.toml" <<EOF
@@ -57,11 +62,8 @@ EOF
57 timeout --signal=TERM --kill-after=5s "${timeout_seconds}s" "$binary" browse about:blank >"$root/process.log" 2>&162 timeout --signal=TERM --kill-after=5s "${timeout_seconds}s" "$binary" browse about:blank >"$root/process.log" 2>&1
58 exit_code=$?63 exit_code=$?
59 set -e64 set -e
60- # timeout is expected for a GUI kept alive after the first presentation.65+ # A completed, valid first-presentation summary is the observation success
61- if [[ "$exit_code" != 0 && "$exit_code" != 124 ]]; then66+ # signal. The GUI may later be stopped by timeout or terminate independently.
62- echo "$run: launch exited unexpectedly ($exit_code); see $root/process.log" >&2
63- exit 1
64- fi
65 python3 - "$root/process.log" "$output/$run.json" "$run" <<'PY'67 python3 - "$root/process.log" "$output/$run.json" "$run" <<'PY'
66 import json, sys68 import json, sys
67 log, destination, run = sys.argv[1:]69 log, destination, run = sys.argv[1:]
@@ -70,8 +72,10 @@ records, summary = [], None
70 for line in open(log, errors="replace"):72 for line in open(log, errors="replace"):
71 try: event = json.loads(line)73 try: event = json.loads(line)
72 except json.JSONDecodeError: continue74 except json.JSONDecodeError: continue
73- if event.get("message") == "startup_trace: milestone": records.append(event)75+ if event.get("message") == "startup_trace: milestone":
74- if event.get("message") == "startup_trace: first presentation": summary = event76+ records.append({key: event.get(key) for key in ("milestone", "t_ms", "delta_ms")})
77+ if event.get("message") == "startup_trace: first presentation":
78+ summary = {key: event.get(key) for key in ("backend", "incomplete_reason", "total_ms")}
75 names = [event.get("milestone") for event in records]79 names = [event.get("milestone") for event in records]
76 times = [event.get("t_ms") for event in records]80 times = [event.get("t_ms") for event in records]
77 valid = names == order and all(isinstance(t, int) for t in times) and times == sorted(times)81 valid = names == order and all(isinstance(t, int) for t in times) and times == sorted(times)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close