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
```
The replacement command finds the running reader through its session D-Bus
service. A terminal using a different session bus can start another reader
The replacement command uses the same session-bus discovery as the reader's
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
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`
installation described in the [README](README.md#wine-access). Building the Go
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
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`
and reproduce the affected navigation. The startup record includes the release
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)")
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")
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(
"speech-socket",
"",
@@ -123,6 +124,9 @@ func run(ctx context.Context, arguments []string, stdout, stderr io.Writer) (res
if options.NArg() != 0 {
return fmt.Errorf("unexpected argument %q", options.Arg(0))
}
if *appendLog && *debugFile == "" {
return errors.New("--append requires --debug-file")
}
if *showVersion || *shortVersion {
fmt.Fprintln(stdout, version.Details(
atspi.Version(),
@@ -174,7 +178,7 @@ func run(ctx context.Context, arguments []string, stdout, stderr io.Writer) (res
}
logger := slog.New(slog.DiscardHandler)
if *debugFile != "" {
fileWriter, err := logging.OpenFile(*debugFile, nil)
fileWriter, err := logging.OpenFile(*debugFile, nil, *appendLog)
if err != nil {
return fmt.Errorf("open debug log: %w", err)
}
+29 -1
View File
@@ -146,7 +146,7 @@ func TestDebugFileCapturesStartupFailureAndAppends(t *testing.T) {
for range 2 {
var stdout, stderr bytes.Buffer
err := run(context.Background(), []string{
"--debug-file", logPath,
"--debug-file", logPath, "--append",
"--config", configPath,
"--speech-socket", filepath.Join(directory, "absent.sock"),
}, &stdout, &stderr)
@@ -244,3 +244,31 @@ func TestStandaloneDebugFlagRejected(t *testing.T) {
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"
"git.stormux.org/storm/cthulhu/internal/remote"
"git.stormux.org/storm/cthulhu/internal/sessionbus"
"github.com/godbus/dbus/v5"
)
@@ -23,7 +24,9 @@ const replaceWait = 8 * time.Second
// replaceRunning identifies the active Go reader through its private session
// bus name. It never signals a process merely because its name matches.
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 {
return fmt.Errorf("connect to session bus: %w", err)
}
+6 -2
View File
@@ -16,6 +16,7 @@ import (
"time"
"git.stormux.org/storm/cthulhu/internal/remote"
"git.stormux.org/storm/cthulhu/internal/sessionbus"
"github.com/godbus/dbus/v5"
)
@@ -23,7 +24,7 @@ func TestReplaceHelper(t *testing.T) {
if os.Getenv("CTHULHU_REPLACE_HELPER") != "1" {
return
}
connection, err := dbus.ConnectSessionBus()
connection, err := sessionbus.Connect(nil, "replacement-test-reader")
if err != nil {
t.Fatal(err)
}
@@ -32,10 +33,10 @@ func TestReplaceHelper(t *testing.T) {
if err != nil || reply != dbus.RequestNameReplyPrimaryOwner {
t.Fatalf("own reader bus name: %v, %v", reply, err)
}
fmt.Println("READY")
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGTERM)
defer signal.Stop(signals)
fmt.Println("READY")
<-signals
}
@@ -86,6 +87,9 @@ func TestReplaceRunningNative(t *testing.T) {
if err := replaceRunning(ctx); err != nil {
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 {
t.Fatalf("running reader did not exit cleanly: %v", err)
}
@@ -4,7 +4,7 @@
pkgbase=cthulhu-git
pkgname=cthulhu-git
_pkgname=cthulhu
pkgver=2026.10.15.r26.g4be35fc
pkgver=2026.10.20.r61.g4643f8e
pkgrel=1
pkgdesc="Desktop-agnostic screen reader with speech, Braille, and Tentacle support"
url="https://git.stormux.org/storm/cthulhu"
+8 -2
View File
@@ -359,11 +359,17 @@ type fileWriter struct {
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 == ".." {
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 {
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) {
path := filepath.Join(t.TempDir(), "diagnostics.log")
writer, err := OpenFile(path, nil)
writer, err := OpenFile(path, nil, true)
if err != nil {
t.Fatalf("OpenFile: %v", err)
}
@@ -312,7 +312,7 @@ func TestOpenFileIsAppendOnlyOwnerRestrictedAndCancellable(t *testing.T) {
if info.Mode().Perm() != 0o600 {
t.Fatalf("file mode = %o, want 0600", info.Mode().Perm())
}
writer, err = OpenFile(path, nil)
writer, err = OpenFile(path, nil, true)
if err != nil {
t.Fatalf("reopen: %v", err)
}
@@ -332,3 +332,57 @@ func TestOpenFileIsAppendOnlyOwnerRestrictedAndCancellable(t *testing.T) {
}
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)
}
}