|
1 | 1 | package check
|
2 | 2 |
|
3 | 3 | import (
|
4 |
| - "bufio" |
5 |
| - "errors" |
6 | 4 | "fmt"
|
7 | 5 | "io"
|
8 | 6 | "log"
|
9 | 7 | "net"
|
| 8 | + "os/exec" |
10 | 9 | "strings"
|
11 | 10 | "time"
|
12 | 11 | )
|
@@ -34,41 +33,29 @@ func (c *TCPPortClientCheck) Check() (bool, error) {
|
34 | 33 | if err != nil {
|
35 | 34 | return false, fmt.Errorf("Port %d on host %q is unreachable. Error was: %v", c.PortNumber, c.IPAddress, err)
|
36 | 35 | }
|
37 |
| - |
38 |
| - testMsg := "ECHO\n" |
39 |
| - fmt.Fprint(conn, testMsg) |
40 |
| - resp, err := bufio.NewReader(conn).ReadString('\n') |
41 |
| - if err == io.EOF { |
42 |
| - return false, nil // The server sent an empty response |
43 |
| - } |
44 |
| - if err != nil { |
45 |
| - return false, fmt.Errorf("error reading from TCP socket: %v", err) |
46 |
| - } |
47 |
| - if resp != testMsg { |
48 |
| - return false, nil |
49 |
| - } |
| 36 | + conn.Close() |
50 | 37 | return true, nil
|
51 | 38 | }
|
52 | 39 |
|
53 |
| -// TCPPortServerCheck ensures that the given port is free, and stands up a TCP server that can be used to |
54 |
| -// check TCP connectivity to the host using TCPPortClientCheck |
| 40 | +// TCPPortServerCheck ensures that the given port is free, or bound to the right |
| 41 | +// process. In the case that it is free, it stands up a TCP server that can be |
| 42 | +// used to check TCP connectivity to the host using TCPPortClientCheck |
55 | 43 | type TCPPortServerCheck struct {
|
56 | 44 | PortNumber int
|
| 45 | + ProcName string |
57 | 46 | started bool
|
58 | 47 | closeListener func() error
|
59 | 48 | listenerClosed chan interface{}
|
60 | 49 | }
|
61 | 50 |
|
62 |
| -// Check returns true if the port is available for the server. Otherwise returns false |
63 |
| -// and an error message |
| 51 | +// Check returns true if the port is free, or taken by the expected process. |
64 | 52 | func (c *TCPPortServerCheck) Check() (bool, error) {
|
65 | 53 | ln, err := net.Listen("tcp", fmt.Sprintf(":%d", c.PortNumber))
|
66 | 54 | if err != nil && strings.Contains(err.Error(), "address already in use") {
|
67 |
| - return false, nil |
| 55 | + return portTakenByProc(c.PortNumber, c.ProcName) |
68 | 56 | }
|
69 | 57 | if err != nil {
|
70 |
| - // TODO: We could check if the port is being used here.. |
71 |
| - return false, fmt.Errorf("error listening on port %d", c.PortNumber) |
| 58 | + return false, fmt.Errorf("error listening on port %d: %v", c.PortNumber, err) |
72 | 59 | }
|
73 | 60 | c.closeListener = ln.Close
|
74 | 61 | // Setup go routine for accepting connections
|
@@ -98,11 +85,40 @@ func (c *TCPPortServerCheck) Check() (bool, error) {
|
98 | 85 | return true, nil
|
99 | 86 | }
|
100 | 87 |
|
101 |
| -// Close the TCP server |
| 88 | +// Close the TCP server if it was started. Otherwise this is a noop. |
102 | 89 | func (c *TCPPortServerCheck) Close() error {
|
103 | 90 | if c.started {
|
104 | 91 | close(c.listenerClosed)
|
105 | 92 | return c.closeListener()
|
106 | 93 | }
|
107 |
| - return errors.New("called close on a TCPPortServerCheck that is not started") |
| 94 | + return nil |
| 95 | +} |
| 96 | + |
| 97 | +// Returns true if the port is taken by a process with the given name. |
| 98 | +func portTakenByProc(port int, procName string) (bool, error) { |
| 99 | + // Use lsof to find the process that is bound to the tcp port in listen |
| 100 | + // mode. |
| 101 | + // ~# lsof -i TCP:2379 -s TCP:LISTEN -Pn +c 0 |
| 102 | + // COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME |
| 103 | + // docker-proxy 7294 root 4u IPv6 43407 0t0 TCP *:2379 (LISTEN) |
| 104 | + portArg := fmt.Sprintf("TCP:%d", port) |
| 105 | + cmd := exec.Command("lsof", "-i", portArg, "-s", "TCP:LISTEN", "-Pn", "+c", "0") |
| 106 | + out, err := cmd.Output() |
| 107 | + if err != nil { |
| 108 | + return false, fmt.Errorf("error running lsof: %v", err) |
| 109 | + } |
| 110 | + lines := strings.Split(string(out), "\n") |
| 111 | + if len(lines) < 2 { |
| 112 | + return false, fmt.Errorf("expected lsof to return at least 2 lines, but returned %d", len(lines)) |
| 113 | + } |
| 114 | + // There are cases where lsof will return multiple lines for the same port. For example: |
| 115 | + // ~# lsof -i TCP:$port -s TCP:LISTEN -Pn +c 0 |
| 116 | + // COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME |
| 117 | + // nginx 18611 root 10u IPv4 82841 0t0 TCP *:80 (LISTEN) |
| 118 | + // nginx 18628 nobody 10u IPv4 82841 0t0 TCP *:80 (LISTEN) |
| 119 | + // nginx 18629 nobody 10u IPv4 82841 0t0 TCP *:80 (LISTEN) |
| 120 | + // |
| 121 | + // Use the first line after the header for verifying the proc name. |
| 122 | + lsofFields := strings.Fields(lines[1]) |
| 123 | + return lsofFields[0] == procName, nil |
108 | 124 | }
|
0 commit comments