Logging changes. Use --debug-file for a replaced log. Use --append to append to the log file if it exists. Also fixed a bug where cthulhu could try to restart without replacing the old running version.

This commit is contained in:
Storm Dragon committed 2026-10-21 15:11:19 -04:00
1 parent 4643f8e47f
commit 50e32f24df
9 files changed
+125 -13

No files matched your search

+10 -2
View File
@@ -96,11 +96,19 @@ in the same desktop session:
"$HOME/.local/bin/cthulhu" --replace "$HOME/.local/bin/cthulhu" --replace
``` ```
The replacement command finds the running reader through its session D-Bus The replacement command uses the same session-bus discovery as the reader's
service. A terminal using a different session bus can start another reader D-Bus service, including when `DBUS_SESSION_BUS_ADDRESS` is unset. It waits
for the identified reader process to exit before starting the replacement.
A terminal using a different session bus can start another reader
instead of replacing the intended one. Preserve the original reader's desktop instead of replacing the intended one. Preserve the original reader's desktop
session environment when restarting it from automation. session environment when restarting it from automation.
Check replacement and session-bus discovery without touching the desktop reader:
```sh
python3 tests/native_test_session.py env CTHULHU_NATIVE_REPLACE=1 CTHULHU_NATIVE_DBUS_TEST=1 go test ./cmd/cthulhu ./internal/sessionbus -run 'TestReplace|TestNativeDesktopSessionBusDiscovery' -count=1 -v
```
Wine accessibility also needs the separate standalone `wine-access` Wine accessibility also needs the separate standalone `wine-access`
installation described in the [README](README.md#wine-access). Building the Go installation described in the [README](README.md#wine-access). Building the Go
reader does not build or refresh that helper. After restoring an overwritten reader does not build or refresh that helper. After restoring an overwritten
+5
View File
@@ -187,6 +187,11 @@ update immediately. Moving focus, navigating, changing tabs or windows, and
locking the session discard pending announcements. Live regions retain their locking the session discard pending announcements. Live regions retain their
existing announcement behavior. existing announcement behavior.
Debug logging with `--debug-file PATH` replaces any existing log at that path.
Add `--append` to preserve earlier sessions, for example
`cthulhu --debug-file /tmp/cthulhu.log --append`.
`--append` requires `--debug-file`. A log can still grow during a long session.
For browser bug reports, start Cthulhu with `--debug-file /tmp/cthulhu-browser.log` For browser bug reports, start Cthulhu with `--debug-file /tmp/cthulhu-browser.log`
and reproduce the affected navigation. The startup record includes the release and reproduce the affected navigation. The startup record includes the release
version, embedded Git revision and modified-build status, Go and AT-SPI versions, version, embedded Git revision and modified-build status, Go and AT-SPI versions,
+6 -2
View File
@@ -78,7 +78,8 @@ func run(ctx context.Context, arguments []string, stdout, stderr io.Writer) (res
clipboardWatch := options.Bool("clipboard-watch", false, "run the internal clipboard change helper (private pipe protocol)") clipboardWatch := options.Bool("clipboard-watch", false, "run the internal clipboard change helper (private pipe protocol)")
ocrCapture := options.Bool("ocr-capture", false, "run the internal OCR capture helper (private pipe protocol)") ocrCapture := options.Bool("ocr-capture", false, "run the internal OCR capture helper (private pipe protocol)")
ocrConfirm := options.Bool("ocr-confirm", false, "run the internal OCR click confirmation helper") ocrConfirm := options.Bool("ocr-confirm", false, "run the internal OCR click confirmation helper")
debugFile := options.String("debug-file", "", "enable debug logging and append logs only to this file") appendLog := options.Bool("append", false, "preserve existing debug log contents (requires --debug-file)")
debugFile := options.String("debug-file", "", "enable debug logging only to this file, replacing existing contents")
speechSocket := options.String( speechSocket := options.String(
"speech-socket", "speech-socket",
"", "",
@@ -123,6 +124,9 @@ func run(ctx context.Context, arguments []string, stdout, stderr io.Writer) (res
if options.NArg() != 0 { if options.NArg() != 0 {
return fmt.Errorf("unexpected argument %q", options.Arg(0)) return fmt.Errorf("unexpected argument %q", options.Arg(0))
} }
if *appendLog && *debugFile == "" {
return errors.New("--append requires --debug-file")
}
if *showVersion || *shortVersion { if *showVersion || *shortVersion {
fmt.Fprintln(stdout, version.Details( fmt.Fprintln(stdout, version.Details(
atspi.Version(), atspi.Version(),
@@ -174,7 +178,7 @@ func run(ctx context.Context, arguments []string, stdout, stderr io.Writer) (res
} }
logger := slog.New(slog.DiscardHandler) logger := slog.New(slog.DiscardHandler)
if *debugFile != "" { if *debugFile != "" {
fileWriter, err := logging.OpenFile(*debugFile, nil) fileWriter, err := logging.OpenFile(*debugFile, nil, *appendLog)
if err != nil { if err != nil {
return fmt.Errorf("open debug log: %w", err) return fmt.Errorf("open debug log: %w", err)
} }
+29 -1
View File
@@ -146,7 +146,7 @@ func TestDebugFileCapturesStartupFailureAndAppends(t *testing.T) {
for range 2 { for range 2 {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
err := run(context.Background(), []string{ err := run(context.Background(), []string{
"--debug-file", logPath, "--debug-file", logPath, "--append",
"--config", configPath, "--config", configPath,
"--speech-socket", filepath.Join(directory, "absent.sock"), "--speech-socket", filepath.Join(directory, "absent.sock"),
}, &stdout, &stderr) }, &stdout, &stderr)
@@ -244,3 +244,31 @@ func TestStandaloneDebugFlagRejected(t *testing.T) {
t.Fatalf("standalone --debug error = %v", err) t.Fatalf("standalone --debug error = %v", err)
} }
} }
func TestAppendRequiresDebugFile(t *testing.T) {
var output bytes.Buffer
err := run(context.Background(), []string{"--append"}, &output, &output)
if err == nil || err.Error() != "--append requires --debug-file" {
t.Fatalf("error = %v", err)
}
}
func TestDebugFileStartsFresh(t *testing.T) {
logPath := filepath.Join(t.TempDir(), "debug.log")
configPath := filepath.Join(t.TempDir(), "absent.toml")
for range 2 {
var output bytes.Buffer
if err := run(context.Background(), []string{
"--check-config", "--config", configPath, "--debug-file", logPath,
}, &output, &output); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(logPath)
if err != nil {
t.Fatal(err)
}
if strings.Count(string(data), `"message":"starting Cthulhu"`) != 1 {
t.Fatalf("expected only the current session: %s", data)
}
}
}
+4 -1
View File
@@ -15,6 +15,7 @@ import (
"time" "time"
"git.stormux.org/storm/cthulhu/internal/remote" "git.stormux.org/storm/cthulhu/internal/remote"
"git.stormux.org/storm/cthulhu/internal/sessionbus"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -23,7 +24,9 @@ const replaceWait = 8 * time.Second
// replaceRunning identifies the active Go reader through its private session // replaceRunning identifies the active Go reader through its private session
// bus name. It never signals a process merely because its name matches. // bus name. It never signals a process merely because its name matches.
func replaceRunning(ctx context.Context) error { func replaceRunning(ctx context.Context) error {
connection, err := dbus.ConnectSessionBus() // Match the reader's service discovery without rewriting the environment.
// Other discovery paths can select another bus when the address is unset.
connection, err := sessionbus.Connect(nil, "replacement")
if err != nil { if err != nil {
return fmt.Errorf("connect to session bus: %w", err) return fmt.Errorf("connect to session bus: %w", err)
} }
+6 -2
View File
@@ -16,6 +16,7 @@ import (
"time" "time"
"git.stormux.org/storm/cthulhu/internal/remote" "git.stormux.org/storm/cthulhu/internal/remote"
"git.stormux.org/storm/cthulhu/internal/sessionbus"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -23,7 +24,7 @@ func TestReplaceHelper(t *testing.T) {
if os.Getenv("CTHULHU_REPLACE_HELPER") != "1" { if os.Getenv("CTHULHU_REPLACE_HELPER") != "1" {
return return
} }
connection, err := dbus.ConnectSessionBus() connection, err := sessionbus.Connect(nil, "replacement-test-reader")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -32,10 +33,10 @@ func TestReplaceHelper(t *testing.T) {
if err != nil || reply != dbus.RequestNameReplyPrimaryOwner { if err != nil || reply != dbus.RequestNameReplyPrimaryOwner {
t.Fatalf("own reader bus name: %v, %v", reply, err) t.Fatalf("own reader bus name: %v, %v", reply, err)
} }
fmt.Println("READY")
signals := make(chan os.Signal, 1) signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGTERM) signal.Notify(signals, syscall.SIGTERM)
defer signal.Stop(signals) defer signal.Stop(signals)
fmt.Println("READY")
<-signals <-signals
} }
@@ -86,6 +87,9 @@ func TestReplaceRunningNative(t *testing.T) {
if err := replaceRunning(ctx); err != nil { if err := replaceRunning(ctx); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if state, _, err := processIdentity(command.Process.Pid); !os.IsNotExist(err) && (err != nil || state != "Z") {
t.Fatalf("replacement returned before reader exit: state=%q, error=%v", state, err)
}
if err := command.Wait(); err != nil { if err := command.Wait(); err != nil {
t.Fatalf("running reader did not exit cleanly: %v", err) t.Fatalf("running reader did not exit cleanly: %v", err)
} }
@@ -4,7 +4,7 @@
pkgbase=cthulhu-git pkgbase=cthulhu-git
pkgname=cthulhu-git pkgname=cthulhu-git
_pkgname=cthulhu _pkgname=cthulhu
pkgver=2026.10.15.r26.g4be35fc pkgver=2026.10.20.r61.g4643f8e
pkgrel=1 pkgrel=1
pkgdesc="Desktop-agnostic screen reader with speech, Braille, and Tentacle support" pkgdesc="Desktop-agnostic screen reader with speech, Braille, and Tentacle support"
url="https://git.stormux.org/storm/cthulhu" url="https://git.stormux.org/storm/cthulhu"
+8 -2
View File
@@ -359,11 +359,17 @@ type fileWriter struct {
closeOnce sync.Once closeOnce sync.Once
} }
func OpenFile(path string, fallback io.Writer) (DiagnosticsWriter, error) { func OpenFile(path string, fallback io.Writer, appendLog bool) (DiagnosticsWriter, error) {
if path == "" || path == "." || path == ".." { if path == "" || path == "." || path == ".." {
return nil, errors.New("invalid diagnostics file path") return nil, errors.New("invalid diagnostics file path")
} }
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) // Keep writes at the end even after truncation: an outgoing reader may
// still append its shutdown records while --replace is in progress.
flags := os.O_WRONLY | os.O_CREATE | os.O_APPEND
if !appendLog {
flags |= os.O_TRUNC
}
file, err := os.OpenFile(path, flags, 0o600)
if err != nil { if err != nil {
return nil, fmt.Errorf("open diagnostics file: %w", err) return nil, fmt.Errorf("open diagnostics file: %w", err)
} }
+56 -2
View File
@@ -294,7 +294,7 @@ func TestAsyncRepeatedStopAfterSuccess(t *testing.T) {
func TestOpenFileIsAppendOnlyOwnerRestrictedAndCancellable(t *testing.T) { func TestOpenFileIsAppendOnlyOwnerRestrictedAndCancellable(t *testing.T) {
path := filepath.Join(t.TempDir(), "diagnostics.log") path := filepath.Join(t.TempDir(), "diagnostics.log")
writer, err := OpenFile(path, nil) writer, err := OpenFile(path, nil, true)
if err != nil { if err != nil {
t.Fatalf("OpenFile: %v", err) t.Fatalf("OpenFile: %v", err)
} }
@@ -312,7 +312,7 @@ func TestOpenFileIsAppendOnlyOwnerRestrictedAndCancellable(t *testing.T) {
if info.Mode().Perm() != 0o600 { if info.Mode().Perm() != 0o600 {
t.Fatalf("file mode = %o, want 0600", info.Mode().Perm()) t.Fatalf("file mode = %o, want 0600", info.Mode().Perm())
} }
writer, err = OpenFile(path, nil) writer, err = OpenFile(path, nil, true)
if err != nil { if err != nil {
t.Fatalf("reopen: %v", err) t.Fatalf("reopen: %v", err)
} }
@@ -332,3 +332,57 @@ func TestOpenFileIsAppendOnlyOwnerRestrictedAndCancellable(t *testing.T) {
} }
var _ io.Writer = (*controlledWriter)(nil) var _ io.Writer = (*controlledWriter)(nil)
func TestOpenFileReplacesPreviousContents(t *testing.T) {
path := filepath.Join(t.TempDir(), "diagnostics.log")
if err := os.WriteFile(path, []byte("previous session with longer contents\n"), 0o600); err != nil {
t.Fatal(err)
}
writer, err := OpenFile(path, nil, false)
if err != nil {
t.Fatal(err)
}
if _, err := writer.Write([]byte("new session\n")); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(data) != "new session\n" {
t.Fatalf("file contents = %q", data)
}
}
func TestFreshLogPreservesWritesFromOutgoingReader(t *testing.T) {
path := filepath.Join(t.TempDir(), "diagnostics.log")
old, err := OpenFile(path, nil, true)
if err != nil {
t.Fatal(err)
}
defer old.Close()
if _, err := old.Write([]byte("old session\n")); err != nil {
t.Fatal(err)
}
next, err := OpenFile(path, nil, false)
if err != nil {
t.Fatal(err)
}
defer next.Close()
if _, err := old.Write([]byte("outgoing shutdown\n")); err != nil {
t.Fatal(err)
}
if _, err := next.Write([]byte("new session\n")); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(data) != "outgoing shutdown\nnew session\n" {
t.Fatalf("overlapping reader writes = %q", data)
}
}