Skip to content
This repository was archived by the owner on Apr 28, 2020. It is now read-only.

Commit dca3e34

Browse files
Nathan Potternhooyr
Nathan Potter
authored andcommitted
Use host assigned port when starting code-server
1 parent 9d5f88f commit dca3e34

File tree

9 files changed

+339
-68
lines changed

9 files changed

+339
-68
lines changed

codeserver.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,32 @@ func loadCodeServer(ctx context.Context) (string, error) {
6767

6868
return cachePath, nil
6969
}
70+
71+
// codeServerPort gets the port of the running code-server binary.
72+
//
73+
// It will retry for 5 seconds if we fail to find the port in case
74+
// the code-server binary is still starting up.
75+
func codeServerPort(cntName string) (string, error) {
76+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
77+
defer cancel()
78+
79+
var (
80+
port string
81+
err error
82+
)
83+
84+
for ctx.Err() == nil {
85+
port, err = codeserver.Port(cntName)
86+
if err == nil {
87+
return port, nil
88+
}
89+
90+
if !xerrors.Is(err, codeserver.PortNotFoundError) {
91+
return "", err
92+
}
93+
94+
time.Sleep(time.Millisecond * 100)
95+
}
96+
97+
return "", xerrors.Errorf("failed while trying to find code-server port: %w", err)
98+
}

internal/codeserver/proc.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package codeserver
2+
3+
import (
4+
"bytes"
5+
"strconv"
6+
"strings"
7+
8+
"go.coder.com/sail/internal/dockutil"
9+
"golang.org/x/xerrors"
10+
)
11+
12+
var (
13+
// PortNotFoundError is returned whenever the port isn't found.
14+
// This can happen if code-server hasn't started it's listener yet
15+
// or if the code-server process failed for any reason.
16+
PortNotFoundError = xerrors.New("failed to find port")
17+
)
18+
19+
// PID returns the pid of code-server running inside of the container.
20+
func PID(containerName string) (int, error) {
21+
out, err := dockutil.FmtExec(containerName, "pgrep -P 1 code-server").CombinedOutput()
22+
if err != nil {
23+
return 0, xerrors.Errorf("%s: %w", out, err)
24+
}
25+
26+
return strconv.Atoi(strings.TrimSpace(string(out)))
27+
}
28+
29+
// Port returns the port that code-server is listening on.
30+
// To get the port value, it first finds all of the socket
31+
// inodes the process is using, then reads the entries in
32+
// `/proc/net/tcp`. It maps the socket inodes to the inodes
33+
// listed in `/proc/net/tcp` and returns the port that has a
34+
// remote address of `0` since this is the listener.
35+
func Port(containerName string) (string, error) {
36+
inodes, err := codeServerSocketInodes(containerName)
37+
if err != nil {
38+
return "", err
39+
}
40+
41+
stats, err := netTCPStats(containerName)
42+
if err != nil {
43+
return "", err
44+
}
45+
46+
m := make(map[string]*netStat, len(stats))
47+
for _, stat := range stats {
48+
m[stat.inode] = stat
49+
}
50+
51+
for _, inode := range inodes {
52+
stat, ok := m[inode]
53+
if !ok {
54+
continue
55+
}
56+
57+
if stat.remotePort == "0" {
58+
return stat.localPort, nil
59+
}
60+
}
61+
62+
return "", PortNotFoundError
63+
}
64+
65+
// codeServerSocketInodes returns all of the socket inodes in use by the code-server process.
66+
//
67+
// This function reads the code-server processes' `/proc/<pid>/fd` directory to see all of
68+
// the open file descriptors the process has open. We grep for any file descriptors that
69+
// are links to sockets and we awk to just parse out the inode field.
70+
//
71+
// See: http://man7.org/linux/man-pages/man5/proc.5.html for more information about `/proc/<pid>/fd`.
72+
func codeServerSocketInodes(containerName string) ([]string, error) {
73+
pid, err := PID(containerName)
74+
if err != nil {
75+
return nil, xerrors.Errorf("failed to find code-server pid: %w", err)
76+
}
77+
78+
// This command parses the output of `find` to access the inode field.
79+
// For example, this line from `find`:
80+
// `65595472 0 lrwx------ 1 root root 64 Apr 23 11:30 /proc/1/fd/8 -> socket:[50784188]`
81+
//
82+
// would turn into:
83+
// `50784188`
84+
out, err := dockutil.FmtExec(
85+
containerName,
86+
// find all fd that are links | grep for sockets | get the socket:[inode] | split on `:`, remove the `[]` brackets, and output the inode.
87+
`find /proc/%d/fd -type l -ls | grep socket | awk '{ print $13 }' | awk -F ":" '{ gsub("\\[|\\]", "", $2); print $2 }'`,
88+
pid,
89+
).CombinedOutput()
90+
if err != nil {
91+
return nil, xerrors.Errorf("%s: %w", out, err)
92+
}
93+
94+
return strings.Split(string(bytes.TrimSpace(out)), "\n"), nil
95+
}
96+
97+
type netStat struct {
98+
remotePort string
99+
localPort string
100+
inode string
101+
}
102+
103+
// netTCPStats returns the entries in /proc/net/tcp inside of the container.
104+
func netTCPStats(containerName string) ([]*netStat, error) {
105+
// This command reads the entries in `/proc/net/tcp`, removes the header line with `NR > 1`, and gets
106+
// the local_address, rem_address, and inode fields. See: https://www.kernel.org/doc/Documentation/networking/proc_net_tcp.txt
107+
//
108+
// An example of the first two lines in `/proc/net/tcp` before doing any awk transformations:
109+
// `sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode`
110+
// `0: 0100007F:BEB3 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 58828878 1 0000000000000000 100 0 0 10 0`
111+
//
112+
// After the awk transformation, it would turn into:
113+
// `0100007F:BEB3 00000000:0000 58828878`
114+
out, err := dockutil.FmtExec(containerName, `cat /proc/net/tcp | awk 'NR > 1 {print $2, $3, $10 }'`).CombinedOutput()
115+
if err != nil {
116+
return nil, xerrors.Errorf("%s: %w", out, err)
117+
}
118+
119+
return parseNetTCPStats(out)
120+
}
121+
122+
// parseNetTCPStats parses the fields from the netTCPStats output.
123+
func parseNetTCPStats(out []byte) ([]*netStat, error) {
124+
lines := bytes.Split(bytes.TrimSpace(out), []byte("\n"))
125+
126+
var (
127+
err error
128+
stats = make([]*netStat, 0, len(lines))
129+
)
130+
for _, line := range lines {
131+
fields := strings.Fields(string(bytes.TrimSpace(line)))
132+
if len(fields) != 3 {
133+
return nil, xerrors.Errorf("line formatted incorrectly: %s", line)
134+
}
135+
136+
var stat netStat
137+
stat.localPort, err = parseHexPort(fields[0])
138+
if err != nil {
139+
return nil, err
140+
}
141+
142+
stat.remotePort, err = parseHexPort(fields[1])
143+
if err != nil {
144+
return nil, err
145+
}
146+
147+
stat.inode = fields[2]
148+
149+
stats = append(stats, &stat)
150+
}
151+
152+
return stats, nil
153+
}
154+
155+
// parseHexPort parses the port field from the ip:port hex combination.
156+
// This takes in a local_address or rem_address field from `/proc/net/tcp`
157+
// and parses the hex port into a base 10 port string.
158+
func parseHexPort(ipPortHex string) (string, error) {
159+
fields := strings.Split(ipPortHex, ":")
160+
if len(fields) != 2 {
161+
return "", xerrors.Errorf("failed to parse port: %s", ipPortHex)
162+
}
163+
164+
portHex := fields[1]
165+
166+
port, err := strconv.ParseUint(portHex, 16, 16)
167+
if err != nil {
168+
return "", err
169+
}
170+
171+
return strconv.FormatUint(port, 10), nil
172+
}

internal/codeserver/proc_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package codeserver
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func Test_parsePort(t *testing.T) {
10+
t.Parallel()
11+
12+
var tests = []struct {
13+
ipPortHex string
14+
port string
15+
err bool
16+
}{
17+
{
18+
"0100007F:8870",
19+
"34928",
20+
false,
21+
},
22+
{
23+
"3A01A8C0:DD14",
24+
"56596",
25+
false,
26+
},
27+
{
28+
"3A01A8C0:ACF8",
29+
"44280",
30+
false,
31+
},
32+
{
33+
"abc123:456:801294j",
34+
"0",
35+
true,
36+
},
37+
}
38+
39+
for _, test := range tests {
40+
port, err := parsePort(test.ipPortHex)
41+
if test.err {
42+
require.Error(t, err)
43+
return
44+
}
45+
46+
require.NoError(t, err)
47+
48+
require.Equal(t, test.port, port)
49+
}
50+
}
51+
52+
func Test_parseNetTCPStats(t *testing.T) {
53+
t.Parallel()
54+
55+
const procNetTcp = `0100007F:300C 00000000:0000 54838306
56+
0100007F:BEB3 00000000:0000 58828878
57+
00000000:E115 00000000:0000 42213838
58+
017AA8C0:0035 00000000:0000 34316
59+
0101007F:0035 00000000:0000 24568
60+
0100007F:0277 00000000:0000 63674503
61+
0100007F:8C57 00000000:0000 58075873
62+
0100007F:A7F9 00000000:0000 44917881
63+
00000000:227C 00000000:0000 64398395
64+
00000000:AE7D 00000000:0000 64539978
65+
3A01A8C0:8CB4 4AC23AD8:01BB 56951042
66+
3A01A8C0:9F96 35E0BA23:01BB 64464448
67+
3A01A8C0:C72C 4301A8C0:1F49 63436166
68+
3A01A8C0:EA26 A106D9AC:01BB 64534357
69+
3A01A8C0:C9EA 7CFD1EC0:01BB 64363317
70+
3A01A8C0:A878 8E09D9AC:01BB 64511233
71+
3A01A8C0:C21A A97D1A64:01BB 49489905
72+
3A01A8C0:A648 8AC13AD8:01BB 48923906`
73+
74+
netStats, err := parseNetTCPStats([]byte(procNetTcp))
75+
require.NoError(t, err)
76+
77+
require.Len(t, netStats, 18)
78+
require.Equal(t, "57621", netStats[2].localPort)
79+
80+
require.Equal(t, "48923906", netStats[17].inode)
81+
82+
require.Equal(t, "0", netStats[0].remotePort)
83+
}

internal/dockutil/exec.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package dockutil
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
"strings"
7+
)
8+
9+
func Exec(cntName, cmd string, args ...string) *exec.Cmd {
10+
args = append([]string{"exec", "-i", cntName, cmd}, args...)
11+
return exec.Command("docker", args...)
12+
}
13+
14+
func ExecTTY(cntName, dir, cmd string, args ...string) *exec.Cmd {
15+
args = append([]string{"exec", "-w", dir, "-it", cntName, cmd}, args...)
16+
return exec.Command("docker", args...)
17+
}
18+
19+
func FmtExec(cntName, cmdFmt string, args ...interface{}) *exec.Cmd {
20+
return Exec(cntName, "bash", "-c", fmt.Sprintf(cmdFmt, args...))
21+
}
22+
23+
func DetachedExec(cntName, cmd string, args ...string) *exec.Cmd {
24+
args = append([]string{"exec", "-d", cntName, cmd}, args...)
25+
return exec.Command("docker", args...)
26+
}
27+
28+
func ExecEnv(cntName string, envs []string, cmd string, args ...string) *exec.Cmd {
29+
args = append([]string{"exec", "-e", strings.Join(envs, ","), "-i", cntName, cmd}, args...)
30+
return exec.Command("docker", args...)
31+
}

lscmd.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,18 @@ func listProjects() ([]projectInfo, error) {
5151
for _, cnt := range cnts {
5252
var info projectInfo
5353

54-
info.name = trimDockerName(cnt)
55-
if info.name == "" {
54+
dockerName := trimDockerName(cnt)
55+
if dockerName == "" {
5656
flog.Error("container %v doesn't have a name.", cnt.ID)
5757
continue
5858
}
59-
info.name = toSailName(info.name)
59+
info.name = toSailName(dockerName)
6060

61-
info.url = "http://127.0.0.1:" + cnt.Labels[portLabel]
61+
port, err := codeServerPort(dockerName)
62+
if err != nil {
63+
return nil, xerrors.Errorf("failed to find container %s port: %w", info.name, err)
64+
}
65+
info.url = "http://127.0.0.1:" + port
6266
info.hat = cnt.Labels[hatLabel]
6367

6468
infos = append(infos, info)

0 commit comments

Comments
 (0)