Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2ffea3d

Browse files
committed
fix(cli/ssh): Avoid connection hang when workspace is stopped
Two issues are addressed here: 1. We were not detecting disconnects due to waiting for Stdin to close (disconnect would only propagate after entering input and failing to write to the connection). 2. In other scenarios, where the connection drop is not detected, we now also watch workspace status and drop the connection when a workspace reaches the stopped state. Fixes: https://github.com/coder/jetbrains-coder/issues/199 Refs: #6180, #6175
1 parent f94ac55 commit 2ffea3d

File tree

1 file changed

+48
-9
lines changed

1 file changed

+48
-9
lines changed

cli/ssh.go

+48-9
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,62 @@ func (r *RootCmd) ssh() *clibase.Cmd {
100100
stopPolling := tryPollWorkspaceAutostop(ctx, client, workspace)
101101
defer stopPolling()
102102

103+
// Enure connection is closed if the context is canceled or
104+
// the workspace reaches the stopped state.
105+
//
106+
// Watching the stopped state is a work-around for cases
107+
// where the agent is not gracefully shut down and the
108+
// connection is left open. If, for instance, the networking
109+
// is stopped before the agent is shut down, the disconnect
110+
// will usually not propagate.
111+
//
112+
// See: https://github.com/coder/coder/issues/6180
113+
wsWatch, err := client.WatchWorkspace(ctx, workspace.ID)
114+
if err != nil {
115+
return err
116+
}
117+
watchAndClose := func(c io.Closer) {
118+
// Ensure session is ended on both context cancellation
119+
// and workspace stop.
120+
defer c.Close()
121+
122+
for {
123+
select {
124+
case <-ctx.Done():
125+
return
126+
case w, ok := <-wsWatch:
127+
if !ok {
128+
return
129+
}
130+
131+
// Note, we only react to the stopped state here because we
132+
// want to give the agent a chance to gracefully shut down
133+
// during "stopping".
134+
if w.LatestBuild.Status == codersdk.WorkspaceStatusStopped {
135+
_, _ = fmt.Fprintf(inv.Stderr, "Workspace %q has stopped. Closing connection.\r\n", workspace.Name)
136+
return
137+
}
138+
}
139+
}
140+
}
141+
103142
if stdio {
104143
rawSSH, err := conn.SSH(ctx)
105144
if err != nil {
106145
return err
107146
}
108147
defer rawSSH.Close()
148+
defer watchAndClose(rawSSH)
109149

110150
go func() {
111-
_, _ = io.Copy(inv.Stdout, rawSSH)
151+
// Ensure stdout copy closes inc ase stdin is closed
152+
// unexpectedly. Typically we wouldn't worry about
153+
// this since OpenSSH should kill the proxy command.
154+
defer rawSSH.Close()
155+
156+
_, _ = io.Copy(rawSSH, inv.Stdin)
112157
}()
113-
_, _ = io.Copy(rawSSH, inv.Stdin)
158+
_, _ = io.Copy(inv.Stdout, rawSSH)
114159
return nil
115160
}
116161

@@ -125,13 +170,7 @@ func (r *RootCmd) ssh() *clibase.Cmd {
125170
return err
126171
}
127172
defer sshSession.Close()
128-
129-
// Ensure context cancellation is propagated to the
130-
// SSH session, e.g. to cancel `Wait()` at the end.
131-
go func() {
132-
<-ctx.Done()
133-
_ = sshSession.Close()
134-
}()
173+
defer watchAndClose(sshSession)
135174

136175
if identityAgent == "" {
137176
identityAgent = os.Getenv("SSH_AUTH_SOCK")

0 commit comments

Comments
 (0)