Skip to content

Commit a3fd899

Browse files
committed
fix: centralize signal handling, remove dead code, fix ListSessions lock
- Centralize host-process signal forwarding into a single Manager-level goroutine instead of one per terminal session. Prevents signal delivery contention with multiple terminals open. - Use RLock instead of Lock in ListSessions (read-only operation). - Remove unused cleanPTYOutput function and listenOnOut function.
1 parent 2c26ff6 commit a3fd899

3 files changed

Lines changed: 46 additions & 60 deletions

File tree

backend/pkg/plugin/exec/controller.go

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -117,23 +117,8 @@ func (c *controller) safeSend(ch chan exec.StreamInput, input exec.StreamInput)
117117
return nil
118118
}
119119

120-
func listenOnOut(
121-
cancel chan struct{},
122-
source chan exec.StreamOutput,
123-
target chan exec.StreamOutput,
124-
) {
125-
for {
126-
select {
127-
case <-cancel:
128-
return
129-
case output := <-source:
130-
target <- output
131-
}
132-
}
133-
}
134-
135120
func (c *controller) runLocalMux() {
136-
manager, inMux, outMux, resizeMux := terminal.NewManager(c.logger)
121+
manager, inMux, outMux, resizeMux := terminal.NewManager(c.ctx, c.logger)
137122
c.terminalManager = manager
138123
for {
139124
select {

backend/pkg/terminal/manager.go

Lines changed: 43 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"os"
1010
"os/exec"
1111
"os/signal"
12-
"regexp"
1312
"sync"
1413
"syscall"
1514
"time"
@@ -33,9 +32,11 @@ const (
3332

3433
// Manager manages terminal sessions, allowing creation, attachment, and more.
3534
type Manager struct {
35+
ctx context.Context
3636
log logging.Logger
3737
sessions map[string]*sdkexec.Session
3838
ptys map[string]*os.File
39+
cmds map[string]*exec.Cmd
3940
cancels map[string]context.CancelFunc
4041
buffers map[string]*sdkexec.OutputBuffer
4142

@@ -49,22 +50,29 @@ type Manager struct {
4950
// latency sensitive with the local manager, we're going to directly return
5051
// the channels for in and out that the exec controller will use.
5152
func NewManager(
53+
ctx context.Context,
5254
log logging.Logger,
5355
) (*Manager, chan sdkexec.StreamInput, chan sdkexec.StreamOutput, chan sdkexec.StreamResize) {
5456
inMux := make(chan sdkexec.StreamInput)
5557
outMux := make(chan sdkexec.StreamOutput)
5658
resizeMux := make(chan sdkexec.StreamResize)
5759

58-
return &Manager{
60+
mgr := &Manager{
61+
ctx: ctx,
5962
log: log.Named("TerminalManager"),
6063
sessions: make(map[string]*sdkexec.Session),
6164
ptys: make(map[string]*os.File),
65+
cmds: make(map[string]*exec.Cmd),
6266
cancels: make(map[string]context.CancelFunc),
6367
buffers: make(map[string]*sdkexec.OutputBuffer),
6468
inMux: inMux,
6569
outMux: outMux,
6670
resizeMux: resizeMux,
67-
}, inMux, outMux, resizeMux
71+
}
72+
73+
go mgr.forwardSignals()
74+
75+
return mgr, inMux, outMux, resizeMux
6876
}
6977

7078
// GetSession returns a session by its ID.
@@ -80,8 +88,8 @@ func (m *Manager) GetSession(sessionID string) (*sdkexec.Session, error) {
8088

8189
// ListSessions returns a list of details for all active sessions.
8290
func (m *Manager) ListSessions(_ *types.PluginContext) []*sdkexec.Session {
83-
m.mux.Lock()
84-
defer m.mux.Unlock()
91+
m.mux.RLock()
92+
defer m.mux.RUnlock()
8593
sessions := make([]*sdkexec.Session, 0, len(m.sessions))
8694

8795
for _, session := range m.sessions {
@@ -176,6 +184,7 @@ func (m *Manager) StartSession(
176184
m.mux.Lock()
177185
m.sessions[opts.ID] = session
178186
m.ptys[opts.ID] = ptyFile
187+
m.cmds[opts.ID] = cmd
179188
m.cancels[opts.ID] = cancel
180189
m.buffers[opts.ID] = sdkexec.NewDefaultOutputBuffer()
181190
m.mux.Unlock()
@@ -187,7 +196,7 @@ func (m *Manager) StartSession(
187196

188197
// Start handling terminal output in a separate goroutine.
189198
go m.handleOutStream(ctx, opts.ID, ptyFile)
190-
go m.handleSignals(ctx, opts.ID, cmd)
199+
go m.handleSessionClose(ctx, opts.ID)
191200
go m.handleWaitForCompletion(ctx, opts.ID, cmd)
192201

193202
return session, nil
@@ -216,47 +225,44 @@ func (m *Manager) handleWaitForCompletion(_ context.Context, sessionID string, c
216225
m.terminateSession(sessionID)
217226
}
218227

219-
func (m *Manager) handleSignals(ctx context.Context, sessionID string, cmd *exec.Cmd) {
228+
// forwardSignals listens for host-process signals once and forwards them to
229+
// all active terminal child processes. Runs for the lifetime of the Manager.
230+
func (m *Manager) forwardSignals() {
220231
ch := make(chan os.Signal, 1)
221-
signal.Notify(ch, syscall.SIGTERM)
222-
signal.Notify(ch, syscall.SIGINT)
223-
signal.Notify(ch, syscall.SIGQUIT)
224-
225-
defer func() { signal.Stop(ch); close(ch) }()
232+
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT)
233+
defer signal.Stop(ch)
226234

227235
for {
228236
select {
229237
case sig := <-ch:
230-
switch sig {
231-
case syscall.SIGTERM:
232-
m.log.Debugw(ctx, "SIGTERM received")
233-
cmd.Process.Signal(syscall.SIGTERM)
234-
case syscall.SIGINT:
235-
m.log.Debugw(ctx, "SIGINT received")
236-
cmd.Process.Signal(syscall.SIGINT)
237-
case syscall.SIGQUIT:
238-
m.log.Debugw(ctx, "SIGQUIT received")
239-
cmd.Process.Signal(syscall.SIGQUIT)
240-
}
241-
case <-ctx.Done():
242-
m.log.Debugw(ctx,
243-
"context cancelled, stopping signal handling",
244-
"session", sessionID,
245-
)
246-
247-
// signal to ide we're done
248-
m.outMux <- sdkexec.StreamOutput{
249-
SessionID: sessionID,
250-
Target: sdkexec.StreamTargetStdOut,
251-
Data: []byte("Session terminated"),
252-
Signal: sdkexec.StreamSignalClose,
238+
m.mux.RLock()
239+
for sid, cmd := range m.cmds {
240+
if cmd.Process != nil {
241+
if err := cmd.Process.Signal(sig); err != nil {
242+
m.log.Debugw(context.Background(), "failed to forward signal",
243+
"signal", sig, "session", sid, "error", err)
244+
}
245+
}
253246
}
254-
247+
m.mux.RUnlock()
248+
case <-m.ctx.Done():
255249
return
256250
}
257251
}
258252
}
259253

254+
// handleSessionClose waits for a session's context to be cancelled and emits
255+
// a CLOSE signal to the frontend.
256+
func (m *Manager) handleSessionClose(ctx context.Context, sessionID string) {
257+
<-ctx.Done()
258+
m.outMux <- sdkexec.StreamOutput{
259+
SessionID: sessionID,
260+
Target: sdkexec.StreamTargetStdOut,
261+
Data: []byte("Session terminated"),
262+
Signal: sdkexec.StreamSignalClose,
263+
}
264+
}
265+
260266
func (m *Manager) handleOutStream(
261267
_ context.Context,
262268
sessionID string,
@@ -325,14 +331,6 @@ func (m *Manager) writeToSession(sessionID string, bytes []byte) error {
325331
return nil
326332
}
327333

328-
// cleanPTYOutput removes the `%` symbol and its associated escape sequences.
329-
func cleanPTYOutput(output string) string {
330-
// Define a regex pattern to match the escape sequence for `%`
331-
pattern := `\x1b\[1m\x1b\[7m%\x1b\[27m\x1b\[1m\x1b\[0m`
332-
re := regexp.MustCompile(pattern)
333-
return re.ReplaceAllString(output, "")
334-
}
335-
336334
// WriteSession writes data to the session's input.
337335
func (m *Manager) WriteSession(sessionID string, input []byte) error {
338336
return m.writeToSession(sessionID, input)
@@ -421,6 +419,7 @@ func (m *Manager) terminateSessionLocked(sessionID string) {
421419
}
422420
delete(m.sessions, sessionID)
423421
delete(m.ptys, sessionID)
422+
delete(m.cmds, sessionID)
424423
delete(m.cancels, sessionID)
425424
delete(m.buffers, sessionID)
426425
m.log.Debugw(context.Background(), "session terminated", "session", sessionID)

backend/pkg/terminal/manager_windows.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package terminal
44

55
import (
6+
"context"
67
"fmt"
78
"os"
89
"sync"
@@ -28,6 +29,7 @@ type Manager struct {
2829
}
2930

3031
func NewManager(
32+
_ context.Context,
3133
log logging.Logger,
3234
) (*Manager, chan sdkexec.StreamInput, chan sdkexec.StreamOutput, chan sdkexec.StreamResize) {
3335
inMux := make(chan sdkexec.StreamInput)

0 commit comments

Comments
 (0)