Skip to content

Commit 1845299

Browse files
authored
fix: avoid memory leak and proper cleanups (#80)
1 parent bded86e commit 1845299

7 files changed

Lines changed: 54 additions & 19 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ This is an intentional design choice to keep the project lean, maintainable, and
6363
- Consider zIndex for hints on different layers? There might be multiple layers when we are considering `Menubar`, `Dock` and `Notification bar`
6464
- Better UI representation for action menu (maybe auto edge detection like tooltip in browser, that will place itself around the element based on the space available around it)
6565
- Find a way to auto deduplicate hints that are targeting the same point
66-
- Scroll areas are hardcoded from 1-9, do we ever need more than that and make it dynamic upon query? We can still use <Tab> to cycle next tho
6766
- Get rid of `PageHeight` in config and find a way to calculate available scroll for (c-d, c-u, gg, G) more reliably.
6867
- Homerow supports `continuous clicks`, is that something important?
6968
- Cant actually make scroll works in Electron apps, what is the best approach? There are tons of random `AXGroup` where some is scrollable and some is not...

cmd/neru/main.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,25 +1085,44 @@ func isLetter(b byte) bool {
10851085
func (a *App) Cleanup() {
10861086
a.logger.Info("Cleaning up")
10871087

1088+
// Exit any active mode first
10881089
a.exitMode()
10891090

1090-
// Unregister all hotkeys
1091-
a.hotkeyManager.UnregisterAll()
1092-
1093-
a.hintOverlay.Destroy()
1094-
1095-
// Stop IPC server
1091+
// Stop IPC server first to prevent new requests
10961092
if a.ipcServer != nil {
10971093
if err := a.ipcServer.Stop(); err != nil {
10981094
a.logger.Error("Failed to stop IPC server", zap.Error(err))
10991095
}
11001096
}
11011097

1098+
// Unregister all hotkeys
1099+
if a.hotkeyManager != nil {
1100+
a.hotkeyManager.UnregisterAll()
1101+
}
1102+
1103+
// Clean up UI elements
1104+
if a.hintOverlay != nil {
1105+
a.hintOverlay.Destroy()
1106+
}
1107+
1108+
// Clean up scroll controller if exists
1109+
if a.scrollController != nil {
1110+
a.scrollController.Cleanup()
1111+
}
1112+
11021113
// Cleanup event tap
11031114
if a.eventTap != nil {
11041115
a.eventTap.Destroy()
11051116
}
11061117

1118+
// Sync and close logger at the end
1119+
if err := logger.Sync(); err != nil {
1120+
// Ignore "inappropriate ioctl for device" error which occurs when syncing stdout/stderr
1121+
if !strings.Contains(err.Error(), "inappropriate ioctl for device") {
1122+
a.logger.Error("Failed to sync logger", zap.Error(err))
1123+
}
1124+
}
1125+
11071126
// Stop app watcher
11081127
a.appWatcher.Stop()
11091128

internal/accessibility/cache.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,6 @@ func (c *InfoCache) cleanup() {
9797

9898
// Stop stops the cleanup goroutine
9999
func (c *InfoCache) Stop() {
100-
c.mu.Lock()
101-
defer c.mu.Unlock()
102-
103100
if !c.stopped {
104101
close(c.stopCh)
105102
c.stopped = true
@@ -110,14 +107,12 @@ func (c *InfoCache) Stop() {
110107
func (c *InfoCache) Clear() {
111108
c.mu.Lock()
112109
defer c.mu.Unlock()
113-
114110
c.data = make(map[uintptr]*CachedInfo, 100)
115111
}
116112

117113
// Size returns the number of cached entries
118114
func (c *InfoCache) Size() int {
119115
c.mu.RLock()
120116
defer c.mu.RUnlock()
121-
122117
return len(c.data)
123118
}

internal/accessibility/element.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,15 @@ func (e *Element) Release() {
355355
}
356356
}
357357

358+
// ReleaseAll releases all elements in a slice
359+
func ReleaseAll(elements []*Element) {
360+
for _, elem := range elements {
361+
if elem != nil {
362+
elem.Release()
363+
}
364+
}
365+
}
366+
358367
// GetAllWindows returns all windows of the focused application
359368
func GetAllWindows() ([]*Element, error) {
360369
var count C.int

internal/eventtap/eventtap.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,16 @@ func (et *EventTap) Disable() {
9090
// Destroy destroys the event tap
9191
func (et *EventTap) Destroy() {
9292
if et.handle != nil {
93+
// Disable first to prevent any pending callbacks
94+
et.Disable()
95+
96+
// Destroy the tap
9397
C.destroyEventTap(et.handle)
9498
et.handle = nil
9599

100+
// Clear callback to prevent any lingering references
101+
et.callback = nil
102+
96103
// Clear global reference if this is the global event tap
97104
globalEventTapMu.Lock()
98105
if globalEventTap == et {

internal/ipc/ipc.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ func (s *Server) handleConnection(conn net.Conn) {
105105
}
106106
}()
107107

108+
// Set read deadline to prevent hanging connections
109+
if err := conn.SetDeadline(time.Now().Add(30 * time.Second)); err != nil {
110+
s.logger.Error("Failed to set connection deadline", zap.Error(err))
111+
return
112+
}
113+
108114
decoder := json.NewDecoder(conn)
109115
encoder := json.NewEncoder(conn)
110116

internal/logger/logger.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,20 +117,20 @@ func Close() error {
117117
logFileMu.Lock()
118118
defer logFileMu.Unlock()
119119

120-
// Sync logger first
121120
if globalLogger != nil {
122121
if err := globalLogger.Sync(); err != nil {
123-
// Ignore sync errors on stdout/stderr (common on macOS)
124-
// but log them for debugging
125-
if !strings.Contains(err.Error(), "sync /dev/stdout") &&
126-
!strings.Contains(err.Error(), "sync /dev/stderr") {
127-
fmt.Fprintf(os.Stderr, "Warning: failed to sync logger: %v\n", err)
122+
// Ignore common sync errors that occur during shutdown
123+
if !strings.Contains(err.Error(), "invalid argument") &&
124+
!strings.Contains(err.Error(), "inappropriate ioctl for device") {
125+
return fmt.Errorf("failed to sync logger: %w", err)
128126
}
129127
}
128+
globalLogger = nil
130129
}
131130

132-
// Close log file
133131
if logFile != nil {
132+
// Best effort sync, ignore errors on stdout/stderr
133+
_ = logFile.Sync()
134134
if err := logFile.Close(); err != nil {
135135
return fmt.Errorf("failed to close log file: %w", err)
136136
}

0 commit comments

Comments
 (0)