mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
135 lines
3.0 KiB
Go
135 lines
3.0 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// logUploadGate is shared by both sockets. Reserve before opening or collecting
|
|
// anything, and cool down after completion, including failures. Logging reloads
|
|
// must never reset this state.
|
|
type logUploadGate struct {
|
|
mu sync.Mutex
|
|
busy bool
|
|
next time.Time
|
|
}
|
|
|
|
func (g *logUploadGate) reserve() bool {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
if g.busy || time.Now().Before(g.next) {
|
|
return false
|
|
}
|
|
g.busy = true
|
|
return true
|
|
}
|
|
|
|
func (g *logUploadGate) release() {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
g.busy = false
|
|
g.next = time.Now().Add(logWriterSentInterval)
|
|
}
|
|
|
|
const delegatedSendTimeout = 5 * time.Minute
|
|
|
|
type logSendPeerKey struct{}
|
|
|
|
// logSendServer has no administrative mux. Its dependencies are immutable
|
|
// snapshots of daemon state, never request arguments or reloaded config pointers.
|
|
type logSendServer struct {
|
|
gate *logUploadGate
|
|
collect func(context.Context) (io.ReadCloser, error)
|
|
upload func(context.Context, io.ReadCloser) error
|
|
audit func(int64, int)
|
|
server *http.Server
|
|
cancel context.CancelFunc
|
|
mu sync.Mutex
|
|
stopped bool
|
|
active sync.WaitGroup
|
|
}
|
|
|
|
func (s *logSendServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
uid, known := r.Context().Value(logSendPeerKey{}).(uint32)
|
|
caller := int64(-1)
|
|
if known {
|
|
caller = int64(uid)
|
|
}
|
|
code := http.StatusForbidden
|
|
accepted := false
|
|
admitted := false
|
|
defer func() {
|
|
if admitted {
|
|
defer s.active.Done()
|
|
}
|
|
if accepted && s.audit != nil {
|
|
s.audit(caller, code)
|
|
}
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
w.Header().Set("Connection", "close")
|
|
w.WriteHeader(code) // status only: no logs, credentials, paths or API errors
|
|
}()
|
|
if !known {
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost || r.RequestURI != sendLogsPath || r.URL.RawQuery != "" || r.ContentLength != 0 || len(r.TransferEncoding) != 0 || r.Header.Get("Expect") != "" {
|
|
code = http.StatusBadRequest
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
if s.stopped {
|
|
s.mu.Unlock()
|
|
code = http.StatusServiceUnavailable
|
|
return
|
|
}
|
|
s.active.Add(1)
|
|
admitted = true
|
|
s.mu.Unlock()
|
|
if !s.gate.reserve() {
|
|
code = http.StatusServiceUnavailable
|
|
return
|
|
}
|
|
accepted = true
|
|
defer s.gate.release()
|
|
ctx, cancel := context.WithTimeout(r.Context(), delegatedSendTimeout)
|
|
defer cancel()
|
|
body, err := s.collect(ctx)
|
|
if err != nil {
|
|
code = http.StatusPreconditionFailed
|
|
return
|
|
}
|
|
defer body.Close()
|
|
if err := s.upload(ctx, body); err != nil {
|
|
code = http.StatusBadGateway
|
|
return
|
|
}
|
|
code = http.StatusOK
|
|
}
|
|
|
|
func (s *logSendServer) stop() {
|
|
s.mu.Lock()
|
|
s.stopped = true
|
|
s.mu.Unlock()
|
|
if s.cancel != nil {
|
|
s.cancel()
|
|
}
|
|
if s.server != nil {
|
|
_ = s.server.Close()
|
|
}
|
|
s.active.Wait()
|
|
}
|
|
|
|
// The lifecycle lock also covers startup racing with shutdown. Stop fences new
|
|
// requests, cancels active uploads, and joins them before log writers close.
|
|
func (p *prog) stopLogSendServer() {
|
|
p.logSendMu.Lock()
|
|
defer p.logSendMu.Unlock()
|
|
p.logSendStopped = true
|
|
if p.logSend != nil {
|
|
p.logSend.stop()
|
|
}
|
|
}
|