147 lines
4.9 KiB
Go
147 lines
4.9 KiB
Go
package sdkserver
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const clientUnstuckRequestFile = "scripts/client_unstuck_request.lua"
|
|
|
|
type clientUnstuckStatus struct {
|
|
RequestID string `json:"request_id"`
|
|
State string `json:"state"`
|
|
Error string `json:"error,omitempty"`
|
|
QueuedAt int64 `json:"queued_at"`
|
|
CompletedAt int64 `json:"completed_at,omitempty"`
|
|
}
|
|
|
|
var clientUnstuckState struct {
|
|
sync.Mutex
|
|
status clientUnstuckStatus
|
|
}
|
|
|
|
func queueClientUnstuck() (clientUnstuckStatus, error) {
|
|
clientUnstuckState.Lock()
|
|
defer clientUnstuckState.Unlock()
|
|
var nonce [16]byte
|
|
if _, err := rand.Read(nonce[:]); err != nil {
|
|
return clientUnstuckStatus{}, err
|
|
}
|
|
id := hex.EncodeToString(nonce[:])
|
|
// Only a generated hexadecimal nonce is interpolated. No user-supplied
|
|
// command, path, endpoint or Lua source can enter this heartbeat script.
|
|
script := fmt.Sprintf(clientUnstuckLua, id)
|
|
if err := os.MkdirAll(filepath.Dir(clientUnstuckRequestFile), 0700); err != nil {
|
|
return clientUnstuckStatus{}, err
|
|
}
|
|
file, err := os.CreateTemp(filepath.Dir(clientUnstuckRequestFile), ".client-unstuck-*.tmp")
|
|
if err != nil {
|
|
return clientUnstuckStatus{}, err
|
|
}
|
|
defer os.Remove(file.Name())
|
|
if _, err = file.WriteString(script); err != nil {
|
|
file.Close()
|
|
return clientUnstuckStatus{}, err
|
|
}
|
|
if err = file.Close(); err != nil {
|
|
return clientUnstuckStatus{}, err
|
|
}
|
|
if err = os.Rename(file.Name(), clientUnstuckRequestFile); err != nil {
|
|
return clientUnstuckStatus{}, err
|
|
}
|
|
status := clientUnstuckStatus{RequestID: id, State: "queued", QueuedAt: time.Now().UnixMilli()}
|
|
clientUnstuckState.status = status
|
|
Log("[UNSTUCK] UI recovery queued request=" + id)
|
|
return status, nil
|
|
}
|
|
|
|
func apiClientUnstuck(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
clientUnstuckState.Lock()
|
|
status := clientUnstuckState.status
|
|
clientUnstuckState.Unlock()
|
|
writeJSON(w, status)
|
|
case http.MethodPost:
|
|
status, err := queueClientUnstuck()
|
|
if err != nil {
|
|
http.Error(w, "cannot queue UI recovery", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusAccepted)
|
|
writeJSON(w, status)
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func apiClientUnstuckResult(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
var result struct {
|
|
RequestID string `json:"request_id"`
|
|
Success bool `json:"success"`
|
|
Error string `json:"error"`
|
|
}
|
|
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8192))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&result); err != nil {
|
|
http.Error(w, "invalid recovery result", http.StatusBadRequest)
|
|
return
|
|
}
|
|
clientUnstuckState.Lock()
|
|
defer clientUnstuckState.Unlock()
|
|
if result.RequestID == "" || result.RequestID != clientUnstuckState.status.RequestID {
|
|
http.Error(w, "recovery request is no longer current", http.StatusConflict)
|
|
return
|
|
}
|
|
clientUnstuckState.status.State = "failed"
|
|
if result.Success {
|
|
clientUnstuckState.status.State = "completed"
|
|
}
|
|
clientUnstuckState.status.Error = result.Error
|
|
clientUnstuckState.status.CompletedAt = time.Now().UnixMilli()
|
|
Log("[UNSTUCK] UI recovery " + clientUnstuckState.status.State + " request=" + result.RequestID)
|
|
writeJSON(w, clientUnstuckState.status)
|
|
}
|
|
|
|
// Named UIManager API observed on the current Client. Clear only network UI
|
|
// waits: never close a Battle, change a scene, fabricate a successful gameplay
|
|
// response, erase a run, or reset the Client's loading/phase flags.
|
|
const clientUnstuckLua = `do
|
|
local id = %q
|
|
if _G.__GAC_UNSTUCK_HTTP and _G.__GAC_UNSTUCK_HTTP.isDone then
|
|
pcall(function() _G.__GAC_UNSTUCK_HTTP:Dispose() end); _G.__GAC_UNSTUCK_HTTP=nil
|
|
end
|
|
if _G.__GAC_UNSTUCK_LAST ~= id then
|
|
_G.__GAC_UNSTUCK_LAST=id
|
|
local ok,err=pcall(function()
|
|
local ui=CS.RPG.Client.GlobalVars.s_UIManager
|
|
if not ui then error('Client UIManager is unavailable') end
|
|
ui:ClearWaitNetPacketFullScreenBlock()
|
|
end)
|
|
CS.UnityEngine.Debug.Log('[GAC_UNSTUCK] request='..id..' success='..tostring(ok)..' '..tostring(err or ''))
|
|
pcall(function()
|
|
local url=_G.__GAC_SRTOOL_LIVE_URL
|
|
if type(url)~='string' or url=='' then return end
|
|
local body='{"request_id":"'..id..'","success":'..tostring(ok)..',"error":'..CS.Newtonsoft.Json.JsonConvert.SerializeObject(tostring(err or ''))..'}'
|
|
local req=CS.UnityEngine.Networking.UnityWebRequest(url..'/api/client/unstuck/result','POST')
|
|
req.uploadHandler=CS.UnityEngine.Networking.UploadHandlerRaw(CS.System.Text.Encoding.UTF8:GetBytes(body))
|
|
req.downloadHandler=CS.UnityEngine.Networking.DownloadHandlerBuffer()
|
|
req:SetRequestHeader('Content-Type','application/json');req.timeout=10
|
|
req:SendWebRequest();_G.__GAC_UNSTUCK_HTTP=req
|
|
end)
|
|
end
|
|
end
|
|
`
|