detached 8062ab3

fix(daemon): retire terminal remote views

commit
8062ab38839a92f433873d2ddd2a6a84f3afbc93
author
brice · · 2026-08-07 01:19 UTC
parent
afa0cb5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +52 -1
M internal/usecase/daemon/remote_link.go +2 -1
@@ -694,7 +694,8 @@ func (d *Daemon) handleRemoteLinkFrame(link *remoteLink, frame ports.Frame) erro
694694 if err != nil {
695695 return fmt.Errorf("remote view: decode detached: %w", err)
696696 }
697- return errors.New("remote view: remote session detached")
697+ d.retireTerminalRemoteView(link, ports.ReasonSessionKilled)
698+ return nil
698699 case ports.MsgError:
699700 remoteErr, err := ports.UnmarshalErrorMsg(frame.Payload)
700701 if err != nil {
@@ -694,7 +694,8 @@ func (d *Daemon) handleRemoteLinkFrame(link *remoteLink, frame ports.Frame) erro
694 if err != nil {694 if err != nil {
695 return fmt.Errorf("remote view: decode detached: %w", err)695 return fmt.Errorf("remote view: decode detached: %w", err)
696 }696 }
697- return errors.New("remote view: remote session detached")697+ d.retireTerminalRemoteView(link, ports.ReasonSessionKilled)
698+ return nil
698 case ports.MsgError:699 case ports.MsgError:
699 remoteErr, err := ports.UnmarshalErrorMsg(frame.Payload)700 remoteErr, err := ports.UnmarshalErrorMsg(frame.Payload)
700 if err != nil {701 if err != nil {
A internal/usecase/daemon/remote_terminal.go +50 -0
@@ -0,0 +1,50 @@
1+package daemon
2+
3+// retireTerminalRemoteView invalidates one exact remote-link generation after
4+// the remote daemon reports that its session detached. It removes the local
5+// view before interrupting transports, so stale receive callbacks, warm timers,
6+// and resume credentials cannot resurrect it.
7+func (d *Daemon) retireTerminalRemoteView(link *remoteLink, reason uint8) {
8+ if d == nil || link == nil || link.view == nil {
9+ return
10+ }
11+ view := link.view
12+ d.mu.Lock()
13+ if d.closing || !d.attachmentOwnerRegisteredByDaemonLocked(view) {
14+ d.mu.Unlock()
15+ return
16+ }
17+ view.mu.Lock()
18+ if view.closed || view.link != link || view.linkGeneration != link.generation {
19+ view.mu.Unlock()
20+ d.mu.Unlock()
21+ return
22+ }
23+ view.closed = true
24+ attachments := make([]*attachedClient, 0, len(view.attachments))
25+ for attachment := range view.attachments {
26+ attachments = append(attachments, attachment)
27+ }
28+ clear(view.attachments)
29+ warm := view.warm
30+ view.warm = nil
31+ view.warmGeneration++
32+ retirements := d.purgeParkedForRemoteViewLocked(view)
33+ d.purgeParkingForRemoteViewLocked(view)
34+ view.link = nil
35+ view.linkGeneration++
36+ link.active = false
37+ _ = d.unregisterRemoteViewLocked(view)
38+ view.mu.Unlock()
39+ d.mu.Unlock()
40+
41+ warm.stop()
42+ d.finishParkedAttachmentRetirements(retirements)
43+ for _, attachment := range attachments {
44+ d.retireShutdownRemoteAttachment(view, attachment, reason)
45+ }
46+ link.cancel()
47+ if link.transport != nil {
48+ _ = link.transport.Close()
49+ }
50+}
@@ -0,0 +1,50 @@
1+package daemon
2+
3+// retireTerminalRemoteView invalidates one exact remote-link generation after
4+// the remote daemon reports that its session detached. It removes the local
5+// view before interrupting transports, so stale receive callbacks, warm timers,
6+// and resume credentials cannot resurrect it.
7+func (d *Daemon) retireTerminalRemoteView(link *remoteLink, reason uint8) {
8+ if d == nil || link == nil || link.view == nil {
9+ return
10+ }
11+ view := link.view
12+ d.mu.Lock()
13+ if d.closing || !d.attachmentOwnerRegisteredByDaemonLocked(view) {
14+ d.mu.Unlock()
15+ return
16+ }
17+ view.mu.Lock()
18+ if view.closed || view.link != link || view.linkGeneration != link.generation {
19+ view.mu.Unlock()
20+ d.mu.Unlock()
21+ return
22+ }
23+ view.closed = true
24+ attachments := make([]*attachedClient, 0, len(view.attachments))
25+ for attachment := range view.attachments {
26+ attachments = append(attachments, attachment)
27+ }
28+ clear(view.attachments)
29+ warm := view.warm
30+ view.warm = nil
31+ view.warmGeneration++
32+ retirements := d.purgeParkedForRemoteViewLocked(view)
33+ d.purgeParkingForRemoteViewLocked(view)
34+ view.link = nil
35+ view.linkGeneration++
36+ link.active = false
37+ _ = d.unregisterRemoteViewLocked(view)
38+ view.mu.Unlock()
39+ d.mu.Unlock()
40+
41+ warm.stop()
42+ d.finishParkedAttachmentRetirements(retirements)
43+ for _, attachment := range attachments {
44+ d.retireShutdownRemoteAttachment(view, attachment, reason)
45+ }
46+ link.cancel()
47+ if link.transport != nil {
48+ _ = link.transport.Close()
49+ }
50+}

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 2

Tags 13

↑↓ moveenter openesc close