@@ -4,17 +4,27 @@ import (
4
4
"bytes"
5
5
"context"
6
6
"encoding/json"
7
+ "errors"
7
8
"fmt"
8
9
"io"
9
10
"io/ioutil"
11
+ "os"
12
+ "strings"
13
+ "time"
14
+
15
+ "nhooyr.io/websocket"
10
16
11
17
"cdr.dev/coder-cli/coder-sdk"
12
18
"cdr.dev/coder-cli/internal/coderutil"
13
19
"cdr.dev/coder-cli/internal/x/xcobra"
14
20
"cdr.dev/coder-cli/pkg/clog"
15
21
"cdr.dev/coder-cli/pkg/tablewriter"
22
+ "cdr.dev/coder-cli/wsnet"
16
23
24
+ "github.com/fatih/color"
17
25
"github.com/manifoldco/promptui"
26
+ "github.com/pion/ice/v2"
27
+ "github.com/pion/webrtc/v3"
18
28
"github.com/spf13/cobra"
19
29
"golang.org/x/xerrors"
20
30
)
@@ -38,16 +48,17 @@ func workspacesCmd() *cobra.Command {
38
48
}
39
49
40
50
cmd .AddCommand (
51
+ createWorkspaceCmd (),
52
+ editWorkspaceCmd (),
41
53
lsWorkspacesCommand (),
42
- stopWorkspacesCmd (),
54
+ pingWorkspaceCommand (),
55
+ rebuildWorkspaceCommand (),
43
56
rmWorkspacesCmd (),
57
+ setPolicyTemplate (),
58
+ stopWorkspacesCmd (),
44
59
watchBuildLogCommand (),
45
- rebuildWorkspaceCommand (),
46
- createWorkspaceCmd (),
47
- workspaceFromConfigCmd (true ),
48
60
workspaceFromConfigCmd (false ),
49
- editWorkspaceCmd (),
50
- setPolicyTemplate (),
61
+ workspaceFromConfigCmd (true ),
51
62
)
52
63
return cmd
53
64
}
@@ -120,6 +131,203 @@ func lsWorkspacesCommand() *cobra.Command {
120
131
return cmd
121
132
}
122
133
134
+ func pingWorkspaceCommand () * cobra.Command {
135
+ var (
136
+ schemes []string
137
+ count int
138
+ )
139
+
140
+ cmd := & cobra.Command {
141
+ Use : "ping <workspace_name>" ,
142
+ Short : "ping Coder workspaces by name" ,
143
+ Long : "ping Coder workspaces by name" ,
144
+ Example : `coder workspaces ping front-end-workspace` ,
145
+ Args : xcobra .ExactArgs (1 ),
146
+ RunE : func (cmd * cobra.Command , args []string ) error {
147
+ ctx := cmd .Context ()
148
+ client , err := newClient (ctx , true )
149
+ if err != nil {
150
+ return err
151
+ }
152
+ workspace , err := findWorkspace (ctx , client , args [0 ], coder .Me )
153
+ if err != nil {
154
+ return err
155
+ }
156
+
157
+ iceSchemes := map [ice.SchemeType ]interface {}{}
158
+ for _ , rawScheme := range schemes {
159
+ scheme := ice .NewSchemeType (rawScheme )
160
+ if scheme == ice .Unknown {
161
+ return fmt .Errorf ("scheme type %q not recognized" , rawScheme )
162
+ }
163
+ iceSchemes [scheme ] = nil
164
+ }
165
+
166
+ pinger := & wsPinger {
167
+ client : client ,
168
+ workspace : workspace ,
169
+ iceSchemes : iceSchemes ,
170
+ }
171
+
172
+ seq := 0
173
+ ticker := time .NewTicker (time .Second )
174
+ for {
175
+ select {
176
+ case <- ticker .C :
177
+ err := pinger .ping (ctx )
178
+ if err != nil {
179
+ return err
180
+ }
181
+ seq ++
182
+ if count > 0 && seq >= count {
183
+ os .Exit (0 )
184
+ }
185
+ case <- ctx .Done ():
186
+ return nil
187
+ }
188
+ }
189
+ },
190
+ }
191
+
192
+ cmd .Flags ().StringSliceVarP (& schemes , "scheme" , "s" , []string {"stun" , "stuns" , "turn" , "turns" }, "customize schemes to filter ice servers" )
193
+ cmd .Flags ().IntVarP (& count , "count" , "c" , 0 , "stop after <count> replies" )
194
+ return cmd
195
+ }
196
+
197
+ type wsPinger struct {
198
+ client coder.Client
199
+ workspace * coder.Workspace
200
+ dialer * wsnet.Dialer
201
+ iceSchemes map [ice.SchemeType ]interface {}
202
+ tunneled bool
203
+ }
204
+
205
+ func (* wsPinger ) logFail (msg string ) {
206
+ fmt .Printf ("%s: %s\n " , color .New (color .Bold , color .FgRed ).Sprint ("——" ), msg )
207
+ }
208
+
209
+ func (* wsPinger ) logSuccess (timeStr , msg string ) {
210
+ fmt .Printf ("%s: %s\n " , color .New (color .Bold , color .FgGreen ).Sprint (timeStr ), msg )
211
+ }
212
+
213
+ // Only return fatal errors
214
+ func (w * wsPinger ) ping (ctx context.Context ) error {
215
+ ctx , cancelFunc := context .WithTimeout (ctx , time .Second * 15 )
216
+ defer cancelFunc ()
217
+ url := w .client .BaseURL ()
218
+
219
+ // If the dialer is nil we create a new!
220
+ // nolint:nestif
221
+ if w .dialer == nil {
222
+ servers , err := w .client .ICEServers (ctx )
223
+ if err != nil {
224
+ w .logFail (fmt .Sprintf ("list ice servers: %s" , err .Error ()))
225
+ return nil
226
+ }
227
+ filteredServers := make ([]webrtc.ICEServer , 0 , len (servers ))
228
+ for _ , server := range servers {
229
+ good := true
230
+ for _ , rawURL := range server .URLs {
231
+ url , err := ice .ParseURL (rawURL )
232
+ if err != nil {
233
+ return fmt .Errorf ("parse url %q: %w" , rawURL , err )
234
+ }
235
+ if _ , ok := w .iceSchemes [url .Scheme ]; ! ok {
236
+ good = false
237
+ }
238
+ }
239
+ if good {
240
+ filteredServers = append (filteredServers , server )
241
+ }
242
+ }
243
+ if len (filteredServers ) == 0 {
244
+ schemes := make ([]string , 0 )
245
+ for scheme := range w .iceSchemes {
246
+ schemes = append (schemes , scheme .String ())
247
+ }
248
+ return fmt .Errorf ("no ice servers match the schemes provided: %s" , strings .Join (schemes , "," ))
249
+ }
250
+ workspace , err := w .client .WorkspaceByID (ctx , w .workspace .ID )
251
+ if err != nil {
252
+ return err
253
+ }
254
+ if workspace .LatestStat .ContainerStatus != coder .WorkspaceOn {
255
+ w .logFail (fmt .Sprintf ("workspace is unreachable (status=%s)" , workspace .LatestStat .ContainerStatus ))
256
+ return nil
257
+ }
258
+ connectStart := time .Now ()
259
+ w .dialer , err = wsnet .DialWebsocket (ctx , wsnet .ConnectEndpoint (& url , w .workspace .ID , w .client .Token ()), & wsnet.DialOptions {
260
+ ICEServers : filteredServers ,
261
+ TURNProxyAuthToken : w .client .Token (),
262
+ TURNRemoteProxyURL : & url ,
263
+ TURNLocalProxyURL : & url ,
264
+ }, & websocket.DialOptions {})
265
+ if err != nil {
266
+ w .logFail (fmt .Sprintf ("dial workspace: %s" , err .Error ()))
267
+ return nil
268
+ }
269
+ connectMS := float64 (time .Since (connectStart ).Microseconds ()) / 1000
270
+
271
+ candidates , err := w .dialer .Candidates ()
272
+ if err != nil {
273
+ return err
274
+ }
275
+ isRelaying := candidates .Local .Typ == webrtc .ICECandidateTypeRelay
276
+ w .tunneled = false
277
+ candidateURLs := []string {}
278
+
279
+ for _ , server := range filteredServers {
280
+ if server .Username == wsnet .TURNProxyICECandidate ().Username {
281
+ candidateURLs = append (candidateURLs , fmt .Sprintf ("turn:%s" , url .Host ))
282
+ if ! isRelaying {
283
+ continue
284
+ }
285
+ w .tunneled = true
286
+ continue
287
+ }
288
+
289
+ candidateURLs = append (candidateURLs , server .URLs ... )
290
+ }
291
+
292
+ connectionText := "direct via STUN"
293
+ if isRelaying {
294
+ connectionText = "proxied via TURN"
295
+ }
296
+ if w .tunneled {
297
+ connectionText = fmt .Sprintf ("proxied via %s" , url .Host )
298
+ }
299
+ w .logSuccess ("——" , fmt .Sprintf (
300
+ "connected in %.2fms (%s) candidates=%s" ,
301
+ connectMS ,
302
+ connectionText ,
303
+ strings .Join (candidateURLs , "," ),
304
+ ))
305
+ }
306
+
307
+ pingStart := time .Now ()
308
+ err := w .dialer .Ping (ctx )
309
+ if err != nil {
310
+ if errors .Is (err , io .EOF ) {
311
+ w .dialer = nil
312
+ w .logFail ("connection timed out" )
313
+ return nil
314
+ }
315
+ if errors .Is (err , webrtc .ErrConnectionClosed ) {
316
+ w .dialer = nil
317
+ w .logFail ("webrtc connection is closed" )
318
+ return nil
319
+ }
320
+ return fmt .Errorf ("ping workspace: %w" , err )
321
+ }
322
+ pingMS := float64 (time .Since (pingStart ).Microseconds ()) / 1000
323
+ connectionText := "you ↔ workspace"
324
+ if w .tunneled {
325
+ connectionText = fmt .Sprintf ("you ↔ %s ↔ workspace" , url .Host )
326
+ }
327
+ w .logSuccess (fmt .Sprintf ("%.2fms" , pingMS ), connectionText )
328
+ return nil
329
+ }
330
+
123
331
func stopWorkspacesCmd () * cobra.Command {
124
332
var user string
125
333
cmd := & cobra.Command {
0 commit comments