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.
3534type 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.
5152func 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,15 +88,15 @@ func (m *Manager) GetSession(sessionID string) (*sdkexec.Session, error) {
8088
8189// ListSessions returns a list of details for all active sessions.
8290func (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 {
8896 sessions = append (sessions , session )
8997 }
9098
91- m .log .Debugw (context .Background (), "listed sessions" , "sessions " , sessions )
99+ m .log .Debugw (context .Background (), "listed sessions" , "count " , len ( sessions ) )
92100 return sessions
93101}
94102
@@ -98,10 +106,10 @@ func (m *Manager) StartSession(
98106 opts sdkexec.SessionOptions ,
99107) (* sdkexec.Session , error ) {
100108 logger := m .log .With (logging .Any ("action" , "StartSession" ))
101- logger .Debugw (context .Background (), "starting session" , "options " , opts , "context " , pCtx )
109+ logger .Debugw (context .Background (), "starting session" , "command " , opts . Command , "tty " , opts . TTY )
102110
103- // Set up the command to run in a new pseudo-terminal .
104- ctx , cancel := context .WithCancel (context . Background () )
111+ // Derive from manager context so shutdown cascades to all sessions .
112+ ctx , cancel := context .WithCancel (m . ctx )
105113
106114 // determine the default shell from the commands passed in, since we may want to add flags
107115 shell := DefaultLocalShell
@@ -174,9 +182,9 @@ func (m *Manager) StartSession(
174182 }
175183
176184 m .mux .Lock ()
177- logger .Debugw (ctx , "past lock" )
178185 m .sessions [opts .ID ] = session
179186 m .ptys [opts .ID ] = ptyFile
187+ m .cmds [opts .ID ] = cmd
180188 m .cancels [opts .ID ] = cancel
181189 m .buffers [opts .ID ] = sdkexec .NewDefaultOutputBuffer ()
182190 m .mux .Unlock ()
@@ -188,7 +196,7 @@ func (m *Manager) StartSession(
188196
189197 // Start handling terminal output in a separate goroutine.
190198 go m .handleOutStream (ctx , opts .ID , ptyFile )
191- go m .handleSignals (ctx , opts .ID , cmd )
199+ go m .handleSessionClose (ctx , opts .ID )
192200 go m .handleWaitForCompletion (ctx , opts .ID , cmd )
193201
194202 return session , nil
@@ -217,47 +225,44 @@ func (m *Manager) handleWaitForCompletion(_ context.Context, sessionID string, c
217225 m .terminateSession (sessionID )
218226}
219227
220- 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 () {
221231 ch := make (chan os.Signal , 1 )
222- signal .Notify (ch , syscall .SIGTERM )
223- signal .Notify (ch , syscall .SIGINT )
224- signal .Notify (ch , syscall .SIGQUIT )
225-
226- defer func () { signal .Stop (ch ); close (ch ) }()
232+ signal .Notify (ch , syscall .SIGTERM , syscall .SIGINT , syscall .SIGQUIT )
233+ defer signal .Stop (ch )
227234
228235 for {
229236 select {
230237 case sig := <- ch :
231- switch sig {
232- case syscall .SIGTERM :
233- m .log .Debugw (ctx , "SIGTERM received" )
234- cmd .Process .Signal (syscall .SIGTERM )
235- case syscall .SIGINT :
236- m .log .Debugw (ctx , "SIGINT received" )
237- cmd .Process .Signal (syscall .SIGINT )
238- case syscall .SIGQUIT :
239- m .log .Debugw (ctx , "SIGQUIT received" )
240- cmd .Process .Signal (syscall .SIGQUIT )
241- }
242- case <- ctx .Done ():
243- m .log .Debugw (ctx ,
244- "context cancelled, stopping signal handling" ,
245- "session" , sessionID ,
246- )
247-
248- // signal to ide we're done
249- m .outMux <- sdkexec.StreamOutput {
250- SessionID : sessionID ,
251- Target : sdkexec .StreamTargetStdOut ,
252- Data : []byte ("Session terminated" ),
253- 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+ }
254246 }
255-
247+ m .mux .RUnlock ()
248+ case <- m .ctx .Done ():
256249 return
257250 }
258251 }
259252}
260253
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+
261266func (m * Manager ) handleOutStream (
262267 _ context.Context ,
263268 sessionID string ,
@@ -326,14 +331,6 @@ func (m *Manager) writeToSession(sessionID string, bytes []byte) error {
326331 return nil
327332}
328333
329- // cleanPTYOutput removes the `%` symbol and its associated escape sequences.
330- func cleanPTYOutput (output string ) string {
331- // Define a regex pattern to match the escape sequence for `%`
332- pattern := `\x1b\[1m\x1b\[7m%\x1b\[27m\x1b\[1m\x1b\[0m`
333- re := regexp .MustCompile (pattern )
334- return re .ReplaceAllString (output , "" )
335- }
336-
337334// WriteSession writes data to the session's input.
338335func (m * Manager ) WriteSession (sessionID string , input []byte ) error {
339336 return m .writeToSession (sessionID , input )
@@ -356,7 +353,7 @@ func (m *Manager) AttachSession(sessionID string) (*sdkexec.Session, []byte, err
356353 if buffer != nil {
357354 data = buffer .GetAll ()
358355 }
359- m .log .Debugw (context .Background (), fmt . Sprintf ( "session buffer data: %q" , data ))
356+ m .log .Debugw (context .Background (), "session buffer loaded" , "session" , sessionID , "bufferSize" , len ( data ))
360357
361358 // pointer, no need to reassign
362359 session .Attached = true
@@ -422,6 +419,7 @@ func (m *Manager) terminateSessionLocked(sessionID string) {
422419 }
423420 delete (m .sessions , sessionID )
424421 delete (m .ptys , sessionID )
422+ delete (m .cmds , sessionID )
425423 delete (m .cancels , sessionID )
426424 delete (m .buffers , sessionID )
427425 m .log .Debugw (context .Background (), "session terminated" , "session" , sessionID )
0 commit comments