Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.
1 change: 1 addition & 0 deletions docs/coder_workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Perform operations on the Coder workspaces owned by the active user.
* [coder workspaces edit](coder_workspaces_edit.md) - edit an existing workspace and initiate a rebuild.
* [coder workspaces edit-from-config](coder_workspaces_edit-from-config.md) - change the template a workspace is tracking
* [coder workspaces ls](coder_workspaces_ls.md) - list all workspaces owned by the active user
* [coder workspaces ping](coder_workspaces_ping.md) - ping Coder workspaces by name
* [coder workspaces policy-template](coder_workspaces_policy-template.md) - Set workspace policy template
* [coder workspaces rebuild](coder_workspaces_rebuild.md) - rebuild a Coder workspace
* [coder workspaces rm](coder_workspaces_rm.md) - remove Coder workspaces by name
Expand Down
36 changes: 36 additions & 0 deletions docs/coder_workspaces_ping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## coder workspaces ping

ping Coder workspaces by name

### Synopsis

ping Coder workspaces by name

```
coder workspaces ping <workspace_name> [flags]
```

### Examples

```
coder workspaces ping front-end-workspace
```

### Options

```
-c, --count int stop after <count> replies
-h, --help help for ping
-s, --scheme strings customize schemes to filter ice servers (default [stun,stuns,turn,turns])
```

### Options inherited from parent commands

```
-v, --verbose show verbose output
```

### SEE ALSO

* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces

26 changes: 13 additions & 13 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,25 @@ func Make() *cobra.Command {
}

app.AddCommand(
agentCmd(),
completionCmd(),
configSSHCmd(),
envCmd(), // DEPRECATED.
genDocsCmd(app),
imgsCmd(),
loginCmd(),
logoutCmd(),
providersCmd(),
resourceCmd(),
satellitesCmd(),
sshCmd(),
usersCmd(),
tagsCmd(),
configSSHCmd(),
envCmd(), // DEPRECATED.
workspacesCmd(),
syncCmd(),
urlCmd(),
tagsCmd(),
tokensCmd(),
resourceCmd(),
completionCmd(),
imgsCmd(),
providersCmd(),
genDocsCmd(app),
agentCmd(),
tunnelCmd(),
satellitesCmd(),
urlCmd(),
usersCmd(),
workspacesCmd(),
)
app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output")
return app
Expand Down
220 changes: 214 additions & 6 deletions internal/cmd/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,27 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"time"

"nhooyr.io/websocket"

"cdr.dev/coder-cli/coder-sdk"
"cdr.dev/coder-cli/internal/coderutil"
"cdr.dev/coder-cli/internal/x/xcobra"
"cdr.dev/coder-cli/pkg/clog"
"cdr.dev/coder-cli/pkg/tablewriter"
"cdr.dev/coder-cli/wsnet"

"github.com/fatih/color"
"github.com/manifoldco/promptui"
"github.com/pion/ice/v2"
"github.com/pion/webrtc/v3"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
)
Expand All @@ -38,16 +48,17 @@ func workspacesCmd() *cobra.Command {
}

cmd.AddCommand(
createWorkspaceCmd(),
editWorkspaceCmd(),
lsWorkspacesCommand(),
stopWorkspacesCmd(),
pingWorkspaceCommand(),
rebuildWorkspaceCommand(),
rmWorkspacesCmd(),
setPolicyTemplate(),
stopWorkspacesCmd(),
watchBuildLogCommand(),
rebuildWorkspaceCommand(),
createWorkspaceCmd(),
workspaceFromConfigCmd(true),
workspaceFromConfigCmd(false),
editWorkspaceCmd(),
setPolicyTemplate(),
workspaceFromConfigCmd(true),
)
return cmd
}
Expand Down Expand Up @@ -120,6 +131,203 @@ func lsWorkspacesCommand() *cobra.Command {
return cmd
}

func pingWorkspaceCommand() *cobra.Command {
var (
schemes []string
count int
)

cmd := &cobra.Command{
Use: "ping <workspace_name>",
Short: "ping Coder workspaces by name",
Long: "ping Coder workspaces by name",
Example: `coder workspaces ping front-end-workspace`,
Args: xcobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := newClient(ctx, true)
if err != nil {
return err
}
workspace, err := findWorkspace(ctx, client, args[0], coder.Me)
if err != nil {
return err
}

iceSchemes := map[ice.SchemeType]interface{}{}
for _, rawScheme := range schemes {
scheme := ice.NewSchemeType(rawScheme)
if scheme == ice.Unknown {
return fmt.Errorf("scheme type %q not recognized", rawScheme)
}
iceSchemes[scheme] = nil
}

pinger := &wsPinger{
client: client,
workspace: workspace,
iceSchemes: iceSchemes,
}

seq := 0
ticker := time.NewTicker(time.Second)
for {
select {
case <-ticker.C:
err := pinger.ping(ctx)
if err != nil {
return err
}
seq++
if count > 0 && seq >= count {
os.Exit(0)
}
case <-ctx.Done():
return nil
}
}
},
}

cmd.Flags().StringSliceVarP(&schemes, "scheme", "s", []string{"stun", "stuns", "turn", "turns"}, "customize schemes to filter ice servers")
cmd.Flags().IntVarP(&count, "count", "c", 0, "stop after <count> replies")
return cmd
}

type wsPinger struct {
client coder.Client
workspace *coder.Workspace
dialer *wsnet.Dialer
iceSchemes map[ice.SchemeType]interface{}
tunneled bool
}

func (*wsPinger) logFail(msg string) {
fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgRed).Sprint("——"), msg)
}

func (*wsPinger) logSuccess(timeStr, msg string) {
fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgGreen).Sprint(timeStr), msg)
}

// Only return fatal errors
func (w *wsPinger) ping(ctx context.Context) error {
ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15)
defer cancelFunc()
url := w.client.BaseURL()

// If the dialer is nil we create a new!
// nolint:nestif
if w.dialer == nil {
servers, err := w.client.ICEServers(ctx)
if err != nil {
w.logFail(fmt.Sprintf("list ice servers: %s", err.Error()))
return nil
}
filteredServers := make([]webrtc.ICEServer, 0, len(servers))
for _, server := range servers {
good := true
for _, rawURL := range server.URLs {
url, err := ice.ParseURL(rawURL)
if err != nil {
return fmt.Errorf("parse url %q: %w", rawURL, err)
}
if _, ok := w.iceSchemes[url.Scheme]; !ok {
good = false
}
}
if good {
filteredServers = append(filteredServers, server)
}
}
if len(filteredServers) == 0 {
schemes := make([]string, 0)
for scheme := range w.iceSchemes {
schemes = append(schemes, scheme.String())
}
return fmt.Errorf("no ice servers match the schemes provided: %s", strings.Join(schemes, ","))
}
workspace, err := w.client.WorkspaceByID(ctx, w.workspace.ID)
if err != nil {
return err
}
if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn {
w.logFail(fmt.Sprintf("workspace is unreachable (status=%s)", workspace.LatestStat.ContainerStatus))
return nil
}
connectStart := time.Now()
w.dialer, err = wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(&url, w.workspace.ID, w.client.Token()), &wsnet.DialOptions{
ICEServers: filteredServers,
TURNProxyAuthToken: w.client.Token(),
TURNRemoteProxyURL: &url,
TURNLocalProxyURL: &url,
}, &websocket.DialOptions{})
if err != nil {
w.logFail(fmt.Sprintf("dial workspace: %s", err.Error()))
return nil
}
connectMS := float64(time.Since(connectStart).Microseconds()) / 1000

candidates, err := w.dialer.Candidates()
if err != nil {
return err
}
isRelaying := candidates.Local.Typ == webrtc.ICECandidateTypeRelay
w.tunneled = false
candidateURLs := []string{}

for _, server := range filteredServers {
if server.Username == wsnet.TURNProxyICECandidate().Username {
candidateURLs = append(candidateURLs, fmt.Sprintf("turn:%s", url.Host))
if !isRelaying {
continue
}
w.tunneled = true
continue
}

candidateURLs = append(candidateURLs, server.URLs...)
}

connectionText := "direct via STUN"
if isRelaying {
connectionText = "proxied via TURN"
}
if w.tunneled {
connectionText = fmt.Sprintf("proxied via %s", url.Host)
}
w.logSuccess("——", fmt.Sprintf(
"connected in %.2fms (%s) candidates=%s",
connectMS,
connectionText,
strings.Join(candidateURLs, ","),
))
}

pingStart := time.Now()
err := w.dialer.Ping(ctx)
if err != nil {
if errors.Is(err, io.EOF) {
w.dialer = nil
w.logFail("connection timed out")
return nil
}
if errors.Is(err, webrtc.ErrConnectionClosed) {
w.dialer = nil
w.logFail("webrtc connection is closed")
return nil
}
return fmt.Errorf("ping workspace: %w", err)
}
pingMS := float64(time.Since(pingStart).Microseconds()) / 1000
connectionText := "you ↔ workspace"
if w.tunneled {
connectionText = fmt.Sprintf("you ↔ %s ↔ workspace", url.Host)
}
w.logSuccess(fmt.Sprintf("%.2fms", pingMS), connectionText)
return nil
}

func stopWorkspacesCmd() *cobra.Command {
var user string
cmd := &cobra.Command{
Expand Down
5 changes: 5 additions & 0 deletions wsnet/dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,11 @@ func (d *Dialer) activeConnections() int {
return int(stats.DataChannelsRequested-stats.DataChannelsClosed) - 1
}

// Candidates returns the candidate pair that was chosen for the connection.
func (d *Dialer) Candidates() (*webrtc.ICECandidatePair, error) {
return d.rtc.SCTP().Transport().ICETransport().GetSelectedCandidatePair()
}

// Close closes the RTC connection.
// All data channels dialed will be closed.
func (d *Dialer) Close() error {
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy