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:
1 parent
4643f8e47f
commit
50e32f24df
9 files changed
+125
-13
No files matched your search
+10
-2
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user