github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/admin/http_over_uds_server.go (about) 1 package admin 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net" 8 "net/http" 9 "os" 10 "syscall" 11 "time" 12 ) 13 14 var ( 15 // ErrSocketStillResponding refers to when 16 // a) an instance of the server is still running normally; or 17 // b) server was not closed properly 18 ErrSocketStillResponding = errors.New("a server is still running and responding to socket") 19 // ErrInvalidSocketPathname refers to when the socket filepath is obviously invalid (eg empty string) 20 ErrInvalidSocketPathname = errors.New("the socket filepath is invalid") 21 // ErrListenerBind refers to generic errors 22 ErrListenerBind = errors.New("could not listen on socket") 23 24 // Anything works here 25 SocketHTTPAddress = "http://pyroscope" 26 HealthAddress = SocketHTTPAddress + "/health" 27 ) 28 29 type UdsHTTPServer struct { 30 server *http.Server 31 listener net.Listener 32 socketAddr string 33 } 34 35 type HTTPClient interface { 36 Get(url string) (resp *http.Response, err error) 37 } 38 39 // NewUdsHTTPServer creates a http server that responds over UDS (unix domain socket) 40 func NewUdsHTTPServer(socketAddr string, httpClient HTTPClient) (*UdsHTTPServer, error) { 41 if err := validateSocketAddress(socketAddr); err != nil { 42 return nil, err 43 } 44 45 listener, err := createListener(socketAddr, httpClient) 46 if err != nil { 47 return nil, err 48 } 49 50 return &UdsHTTPServer{ 51 listener: listener, 52 socketAddr: socketAddr, 53 }, nil 54 } 55 56 type myHandler struct { 57 originalHandler http.Handler 58 } 59 60 func (m myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 61 // enrich with an additional endpoint 62 // that we will use to probe when starting a new instance 63 if r.URL.Path == "/health" { 64 writeMessage(w, 200, "it works!") 65 return 66 } 67 68 m.originalHandler.ServeHTTP(w, r) 69 } 70 71 func (u *UdsHTTPServer) Start(handler http.Handler) error { 72 h := myHandler{handler} 73 u.server = &http.Server{Handler: h} 74 err := u.server.Serve(u.listener) 75 76 // ListenAndServe always returns a non-nil error. After Shutdown or Close, 77 // the returned error is ErrServerClosed. 78 if errors.Is(err, http.ErrServerClosed) { 79 return nil 80 } 81 82 return err 83 } 84 85 // createListener creates a listener on socketAddr UDS 86 // it tries to bind to socketAddr 87 // if it fails, it also tries to consume that socket 88 // if it's able to, it fails with ErrSocketStillResponding 89 // if it not able to, then it assumes it's a dangling socket and takes over it 90 // 91 // keep in mind there's a slight chance for a race condition there 92 // where a socket is verified to be not responding 93 // but the moment it's taken over, it starts to respond (probably because it was taken over by a different instance) 94 func createListener(socketAddr string, httpClient HTTPClient) (net.Listener, error) { 95 takeOver := func(socketAddr string) (net.Listener, error) { 96 err := os.Remove(socketAddr) 97 if err != nil { 98 return nil, err 99 } 100 101 return net.Listen("unix", socketAddr) 102 } 103 104 // we listen on a unix domain socket 105 // which will be created by the bind syscall 106 // https://man7.org/linux/man-pages/man2/bind.2.html 107 listener, err := net.Listen("unix", socketAddr) 108 109 if err != nil { 110 if isErrorAddressAlreadyInUse(err) { 111 // that socket is already being used 112 // let's check if the server is also responding 113 resp, err := httpClient.Get(HealthAddress) 114 115 // the httpclient failed 116 // let's take over 117 // TODO identify what kind of error happened 118 if err != nil { 119 return takeOver(socketAddr) 120 } 121 122 // httpclient responded 123 // let's check the status code 124 if resp.StatusCode == http.StatusOK { 125 return nil, ErrSocketStillResponding 126 } 127 128 // httpclient responded, but with a non 200 status code 129 // let's be optimistic and try to take over 130 return takeOver(socketAddr) 131 } 132 133 return nil, fmt.Errorf("could not bind to socket due to unrecoverable error: %w", err) 134 } 135 136 // no errors happened 137 return listener, err 138 } 139 140 func (u *UdsHTTPServer) Stop() error { 141 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 142 defer cancel() 143 144 // there's no need to remove the socket 145 // since go does it for us 146 // https://github.com/golang/go/blob/47db3bb443774c0b0df2cab188aa3d76b361dca2/src/net/unixsock_posix.go#L187 147 return u.server.Shutdown(ctx) 148 } 149 150 // https://stackoverflow.com/a/65865898 151 func isErrorAddressAlreadyInUse(err error) bool { 152 var eOsSyscall *os.SyscallError 153 if !errors.As(err, &eOsSyscall) { 154 return false 155 } 156 var errErrno syscall.Errno // doesn't need a "*" (ptr) because it's already a ptr (uintptr) 157 if !errors.As(eOsSyscall, &errErrno) { 158 return false 159 } 160 if errErrno == syscall.EADDRINUSE { 161 return true 162 } 163 164 return false 165 } 166 167 func validateSocketAddress(socketAddr string) error { 168 if socketAddr == "" { 169 return ErrInvalidSocketPathname 170 } 171 172 // TODO 173 // check for the filepath size? 174 // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/sys_un.h.html#tag_13_67_04 175 176 return nil 177 }