Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings
This repository was archived by the owner on Aug 30, 2024. It is now read-only.
/coder-v1-cliPublic archive

Commit9244ee8

Browse files
authored
feat: Add Ping command to monitor workspace latency (#409)
* feat: Add Ping command to monitor workspace latency* Handle shut off with nice error* Organize funcs* Add docs* Organize imports* Move to subdommand of workspaces* Refactor to be smarter* Enable scheme filtering* Add count flag* Fix import order* Disable linting for nested if* Generate docs* Extract funcs* Update docs* Remove receiver
1 parent3536869 commit9244ee8

File tree

5 files changed

+269
-19
lines changed

5 files changed

+269
-19
lines changed

‎docs/coder_workspaces.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Perform operations on the Coder workspaces owned by the active user.
2626
*[coder workspaces edit](coder_workspaces_edit.md) - edit an existing workspace and initiate a rebuild.
2727
*[coder workspaces edit-from-config](coder_workspaces_edit-from-config.md) - change the template a workspace is tracking
2828
*[coder workspaces ls](coder_workspaces_ls.md) - list all workspaces owned by the active user
29+
*[coder workspaces ping](coder_workspaces_ping.md) - ping Coder workspaces by name
2930
*[coder workspaces policy-template](coder_workspaces_policy-template.md) - Set workspace policy template
3031
*[coder workspaces rebuild](coder_workspaces_rebuild.md) - rebuild a Coder workspace
3132
*[coder workspaces rm](coder_workspaces_rm.md) - remove Coder workspaces by name

‎docs/coder_workspaces_ping.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
##coder workspaces ping
2+
3+
ping Coder workspaces by name
4+
5+
###Synopsis
6+
7+
ping Coder workspaces by name
8+
9+
```
10+
coder workspaces ping <workspace_name> [flags]
11+
```
12+
13+
###Examples
14+
15+
```
16+
coder workspaces ping front-end-workspace
17+
```
18+
19+
###Options
20+
21+
```
22+
-c, --count int stop after <count> replies
23+
-h, --help help for ping
24+
-s, --scheme strings customize schemes to filter ice servers (default [stun,stuns,turn,turns])
25+
```
26+
27+
###Options inherited from parent commands
28+
29+
```
30+
-v, --verbose show verbose output
31+
```
32+
33+
###SEE ALSO
34+
35+
*[coder workspaces](coder_workspaces.md) - Interact with Coder workspaces
36+

‎internal/cmd/cmd.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,25 @@ func Make() *cobra.Command {
2222
}
2323

2424
app.AddCommand(
25+
agentCmd(),
26+
completionCmd(),
27+
configSSHCmd(),
28+
envCmd(),// DEPRECATED.
29+
genDocsCmd(app),
30+
imgsCmd(),
2531
loginCmd(),
2632
logoutCmd(),
33+
providersCmd(),
34+
resourceCmd(),
35+
satellitesCmd(),
2736
sshCmd(),
28-
usersCmd(),
29-
tagsCmd(),
30-
configSSHCmd(),
31-
envCmd(),// DEPRECATED.
32-
workspacesCmd(),
3337
syncCmd(),
34-
urlCmd(),
38+
tagsCmd(),
3539
tokensCmd(),
36-
resourceCmd(),
37-
completionCmd(),
38-
imgsCmd(),
39-
providersCmd(),
40-
genDocsCmd(app),
41-
agentCmd(),
4240
tunnelCmd(),
43-
satellitesCmd(),
41+
urlCmd(),
42+
usersCmd(),
43+
workspacesCmd(),
4444
)
4545
app.PersistentFlags().BoolVarP(&verbose,"verbose","v",false,"show verbose output")
4646
returnapp

‎internal/cmd/workspaces.go

Lines changed: 214 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,27 @@ import (
44
"bytes"
55
"context"
66
"encoding/json"
7+
"errors"
78
"fmt"
89
"io"
910
"io/ioutil"
11+
"os"
12+
"strings"
13+
"time"
14+
15+
"nhooyr.io/websocket"
1016

1117
"cdr.dev/coder-cli/coder-sdk"
1218
"cdr.dev/coder-cli/internal/coderutil"
1319
"cdr.dev/coder-cli/internal/x/xcobra"
1420
"cdr.dev/coder-cli/pkg/clog"
1521
"cdr.dev/coder-cli/pkg/tablewriter"
22+
"cdr.dev/coder-cli/wsnet"
1623

24+
"github.com/fatih/color"
1725
"github.com/manifoldco/promptui"
26+
"github.com/pion/ice/v2"
27+
"github.com/pion/webrtc/v3"
1828
"github.com/spf13/cobra"
1929
"golang.org/x/xerrors"
2030
)
@@ -38,16 +48,17 @@ func workspacesCmd() *cobra.Command {
3848
}
3949

4050
cmd.AddCommand(
51+
createWorkspaceCmd(),
52+
editWorkspaceCmd(),
4153
lsWorkspacesCommand(),
42-
stopWorkspacesCmd(),
54+
pingWorkspaceCommand(),
55+
rebuildWorkspaceCommand(),
4356
rmWorkspacesCmd(),
57+
setPolicyTemplate(),
58+
stopWorkspacesCmd(),
4459
watchBuildLogCommand(),
45-
rebuildWorkspaceCommand(),
46-
createWorkspaceCmd(),
47-
workspaceFromConfigCmd(true),
4860
workspaceFromConfigCmd(false),
49-
editWorkspaceCmd(),
50-
setPolicyTemplate(),
61+
workspaceFromConfigCmd(true),
5162
)
5263
returncmd
5364
}
@@ -120,6 +131,203 @@ func lsWorkspacesCommand() *cobra.Command {
120131
returncmd
121132
}
122133

134+
funcpingWorkspaceCommand()*cobra.Command {
135+
var (
136+
schemes []string
137+
countint
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+
iferr!=nil {
150+
returnerr
151+
}
152+
workspace,err:=findWorkspace(ctx,client,args[0],coder.Me)
153+
iferr!=nil {
154+
returnerr
155+
}
156+
157+
iceSchemes:=map[ice.SchemeType]interface{}{}
158+
for_,rawScheme:=rangeschemes {
159+
scheme:=ice.NewSchemeType(rawScheme)
160+
ifscheme==ice.Unknown {
161+
returnfmt.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+
iferr!=nil {
179+
returnerr
180+
}
181+
seq++
182+
ifcount>0&&seq>=count {
183+
os.Exit(0)
184+
}
185+
case<-ctx.Done():
186+
returnnil
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+
returncmd
195+
}
196+
197+
typewsPingerstruct {
198+
client coder.Client
199+
workspace*coder.Workspace
200+
dialer*wsnet.Dialer
201+
iceSchemesmap[ice.SchemeType]interface{}
202+
tunneledbool
203+
}
204+
205+
func (*wsPinger)logFail(msgstring) {
206+
fmt.Printf("%s: %s\n",color.New(color.Bold,color.FgRed).Sprint("——"),msg)
207+
}
208+
209+
func (*wsPinger)logSuccess(timeStr,msgstring) {
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+
defercancelFunc()
217+
url:=w.client.BaseURL()
218+
219+
// If the dialer is nil we create a new!
220+
// nolint:nestif
221+
ifw.dialer==nil {
222+
servers,err:=w.client.ICEServers(ctx)
223+
iferr!=nil {
224+
w.logFail(fmt.Sprintf("list ice servers: %s",err.Error()))
225+
returnnil
226+
}
227+
filteredServers:=make([]webrtc.ICEServer,0,len(servers))
228+
for_,server:=rangeservers {
229+
good:=true
230+
for_,rawURL:=rangeserver.URLs {
231+
url,err:=ice.ParseURL(rawURL)
232+
iferr!=nil {
233+
returnfmt.Errorf("parse url %q: %w",rawURL,err)
234+
}
235+
if_,ok:=w.iceSchemes[url.Scheme];!ok {
236+
good=false
237+
}
238+
}
239+
ifgood {
240+
filteredServers=append(filteredServers,server)
241+
}
242+
}
243+
iflen(filteredServers)==0 {
244+
schemes:=make([]string,0)
245+
forscheme:=rangew.iceSchemes {
246+
schemes=append(schemes,scheme.String())
247+
}
248+
returnfmt.Errorf("no ice servers match the schemes provided: %s",strings.Join(schemes,","))
249+
}
250+
workspace,err:=w.client.WorkspaceByID(ctx,w.workspace.ID)
251+
iferr!=nil {
252+
returnerr
253+
}
254+
ifworkspace.LatestStat.ContainerStatus!=coder.WorkspaceOn {
255+
w.logFail(fmt.Sprintf("workspace is unreachable (status=%s)",workspace.LatestStat.ContainerStatus))
256+
returnnil
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+
iferr!=nil {
266+
w.logFail(fmt.Sprintf("dial workspace: %s",err.Error()))
267+
returnnil
268+
}
269+
connectMS:=float64(time.Since(connectStart).Microseconds())/1000
270+
271+
candidates,err:=w.dialer.Candidates()
272+
iferr!=nil {
273+
returnerr
274+
}
275+
isRelaying:=candidates.Local.Typ==webrtc.ICECandidateTypeRelay
276+
w.tunneled=false
277+
candidateURLs:= []string{}
278+
279+
for_,server:=rangefilteredServers {
280+
ifserver.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+
ifisRelaying {
294+
connectionText="proxied via TURN"
295+
}
296+
ifw.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+
iferr!=nil {
310+
iferrors.Is(err,io.EOF) {
311+
w.dialer=nil
312+
w.logFail("connection timed out")
313+
returnnil
314+
}
315+
iferrors.Is(err,webrtc.ErrConnectionClosed) {
316+
w.dialer=nil
317+
w.logFail("webrtc connection is closed")
318+
returnnil
319+
}
320+
returnfmt.Errorf("ping workspace: %w",err)
321+
}
322+
pingMS:=float64(time.Since(pingStart).Microseconds())/1000
323+
connectionText:="you ↔ workspace"
324+
ifw.tunneled {
325+
connectionText=fmt.Sprintf("you ↔ %s ↔ workspace",url.Host)
326+
}
327+
w.logSuccess(fmt.Sprintf("%.2fms",pingMS),connectionText)
328+
returnnil
329+
}
330+
123331
funcstopWorkspacesCmd()*cobra.Command {
124332
varuserstring
125333
cmd:=&cobra.Command{

‎wsnet/dial.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,11 @@ func (d *Dialer) activeConnections() int {
301301
returnint(stats.DataChannelsRequested-stats.DataChannelsClosed)-1
302302
}
303303

304+
// Candidates returns the candidate pair that was chosen for the connection.
305+
func (d*Dialer)Candidates() (*webrtc.ICECandidatePair,error) {
306+
returnd.rtc.SCTP().Transport().ICETransport().GetSelectedCandidatePair()
307+
}
308+
304309
// Close closes the RTC connection.
305310
// All data channels dialed will be closed.
306311
func (d*Dialer)Close()error {

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp