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,8 +88,8 @@ 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 {
@@ -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+
260266func (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.
337335func (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 )
0 commit comments