Files
Ant-Browser/backend/internal/launchcode/runtime_session_api.go
T

187 lines
5.7 KiB
Go

package launchcode
import (
"encoding/json"
"io"
"net/http"
"time"
"ant-chrome/backend/internal/browser"
)
const (
defaultRuntimeSessionTimeout = 45 * time.Second
minRuntimeSessionTimeout = 1 * time.Second
maxRuntimeSessionTimeout = 2 * time.Minute
)
type RuntimeSessionRequest struct {
RuntimeRequest
LaunchRequestParams
TimeoutMs int `json:"timeoutMs"`
}
func decodeRuntimeSessionRequest(r *http.Request) (RuntimeSessionRequest, int, string) {
if r.Method != http.MethodPost {
return RuntimeSessionRequest{}, http.StatusMethodNotAllowed, "method not allowed"
}
var req RuntimeSessionRequest
dec := json.NewDecoder(io.LimitReader(r.Body, 1<<20))
dec.DisallowUnknownFields()
if err := dec.Decode(&req); err != nil {
return RuntimeSessionRequest{}, http.StatusBadRequest, "invalid request body"
}
return req, http.StatusOK, ""
}
func normalizeRuntimeSessionTimeout(timeoutMs int) time.Duration {
if timeoutMs <= 0 {
return defaultRuntimeSessionTimeout
}
timeout := time.Duration(timeoutMs) * time.Millisecond
if timeout < minRuntimeSessionTimeout {
return minRuntimeSessionTimeout
}
if timeout > maxRuntimeSessionTimeout {
return maxRuntimeSessionTimeout
}
return timeout
}
func (s *LaunchServer) handleRuntimeSession(w http.ResponseWriter, r *http.Request) {
startAt := time.Now()
clientIP := remoteIP(r.RemoteAddr)
selector := LaunchSelector{}
req, status, errMsg := decodeRuntimeSessionRequest(r)
if errMsg != "" {
writeJSON(w, status, map[string]interface{}{
"ok": false,
"error": errMsg,
})
s.appendLaunchLog(r.Method, r.URL.Path, clientIP, "", selector, LaunchRequestParams{}, false, status, errMsg, "", "", startAt)
return
}
selector = mergeRuntimeSelector(req.RuntimeRequest)
if selector.IsEmpty() {
errMsg = "selector is required"
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
"ok": false,
"error": errMsg,
})
s.appendLaunchLog(r.Method, r.URL.Path, clientIP, "", selector, req.LaunchRequestParams, false, http.StatusBadRequest, errMsg, "", "", startAt)
return
}
if err := validateRuntimeSelector(selector); err != nil {
errMsg = err.Error()
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
"ok": false,
"error": errMsg,
})
s.appendLaunchLog(r.Method, r.URL.Path, clientIP, selector.Code, selector, req.LaunchRequestParams, false, http.StatusBadRequest, errMsg, "", "", startAt)
return
}
req.LaunchRequestParams = normalizeLaunchRequestParams(req.LaunchRequestParams)
profile, launchCode, status, errMsg := s.launchBySelector(selector, req.LaunchRequestParams)
if errMsg != "" {
writeJSON(w, status, map[string]interface{}{
"ok": false,
"error": errMsg,
})
s.appendLaunchLog(r.Method, r.URL.Path, clientIP, launchCode, selector, req.LaunchRequestParams, false, status, errMsg, "", "", startAt)
return
}
waitTimeout := normalizeRuntimeSessionTimeout(req.TimeoutMs)
profile, ready, err := s.prepareRuntimeSession(profile, waitTimeout)
if err != nil {
writeJSON(w, mapProfileWriteErrorStatus(err), map[string]interface{}{
"ok": false,
"error": err.Error(),
})
s.appendLaunchLog(r.Method, r.URL.Path, clientIP, launchCode, selector, req.LaunchRequestParams, false, mapProfileWriteErrorStatus(err), err.Error(), "", "", startAt)
return
}
if profile == nil {
errMsg = "runtime session is not available"
writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{
"ok": false,
"error": errMsg,
})
s.appendLaunchLog(r.Method, r.URL.Path, clientIP, launchCode, selector, req.LaunchRequestParams, false, http.StatusServiceUnavailable, errMsg, "", "", startAt)
return
}
if launchCode != "" && profile.LaunchCode == "" {
profile.LaunchCode = launchCode
}
responseStatus := http.StatusAccepted
if ready {
responseStatus = http.StatusOK
}
payload := s.runtimeSessionPayload(profile, waitTimeout, ready)
writeJSON(w, responseStatus, payload)
s.appendLaunchLog(r.Method, r.URL.Path, clientIP, launchCode, selector, req.LaunchRequestParams, ready, responseStatus, "", profile.ProfileId, profile.ProfileName, startAt)
}
func (s *LaunchServer) prepareRuntimeSession(profile *browser.Profile, timeout time.Duration) (*browser.Profile, bool, error) {
normalized := s.normalizeRuntimeProfile(profile)
if normalized == nil {
return nil, false, nil
}
if normalized.DebugReady {
s.SetActiveProfile(normalized)
return normalized, true, nil
}
if timeout <= 0 {
return normalized, false, nil
}
if waiter, ok := s.starter.(BrowserDebugWaiter); ok {
waited, ready, err := waiter.WaitInstanceDebugReady(normalized.ProfileId, normalized.DebugPort, timeout)
if err != nil {
return nil, false, err
}
if waited != nil {
normalized = s.normalizeRuntimeProfile(waited)
}
if normalized != nil && ready && normalized.DebugReady {
s.SetActiveProfile(normalized)
return normalized, true, nil
}
return normalized, normalized != nil && normalized.DebugReady, nil
}
deadline := time.Now().Add(timeout)
for {
snapshot, _, errMsg := s.statusProfile(normalized.ProfileId)
if errMsg != "" {
return nil, false, runtimeRequestError(errMsg)
}
if snapshot != nil {
normalized = snapshot
}
if normalized != nil && normalized.DebugReady {
s.SetActiveProfile(normalized)
return normalized, true, nil
}
if time.Now().After(deadline) {
return normalized, false, nil
}
time.Sleep(250 * time.Millisecond)
}
}
func (s *LaunchServer) runtimeSessionPayload(profile *browser.Profile, timeout time.Duration, ready bool) map[string]interface{} {
payload := s.profileRuntimePayload(profile)
payload["ready"] = ready
payload["timeoutMs"] = timeout.Milliseconds()
payload["waitTimedOut"] = !ready
payload["retryable"] = !ready
return payload
}