Skip to content

Latest commit

 

History

History
262 lines (206 loc) · 8.04 KB

File metadata and controls

262 lines (206 loc) · 8.04 KB

DazScript Server — Architecture

Overview

DazScript Server is a DAZ Studio pane plugin (.dll / .dylib) that embeds an HTTP server inside the DAZ Studio process. External clients send DazScript code over HTTP; the plugin executes it on DAZ Studio's main Qt thread and returns a JSON response.


Component Map

graph TD
    Client["External Client<br/>(Python / JS / PS1)"]

    subgraph Plugin["DazScript Server Plugin"]
        SLT["ServerListenThread<br/>(httplib, std::thread)"]
        Pane["DzScriptServerPane<br/>(Qt main thread)"]

        subgraph Services["Services"]
            Auth["AuthenticationService"]
            Rate["RateLimiterService"]
            WL["IPWhitelistService"]
            Met["MetricsCollector"]
        end

        subgraph Exec["Execution"]
            RV["RequestValidator"]
            RP["RequestProcessor"]
            ARM["AsyncRequestManager"]
        end

        subgraph Infra["Infrastructure"]
            SR["SecureRandom"]
            JB["JsonBuilder"]
            SS["ServerSettings / ServerConfig"]
        end
    end

    DAZ["DAZ Studio<br/>(DzScript engine, scene graph)"]

    Client -->|HTTP POST /execute| SLT
    SLT -->|sync + DS4 async submit:<br/>BlockingQueuedConnection| Pane
    SLT -->|DS6 async: validate + enqueue| ARM
    Pane --> Auth
    Pane --> Rate
    Pane --> WL
    Pane --> Met
    Pane --> RV
    RV --> RP
    RP -->|Main thread| DAZ
    RP --> ARM
    ARM -->|QueuedConnection| Pane
    Auth --> SR
    JB -.-> Pane
    SS -.-> Services
    SS -.-> Exec
Loading

Threading Model

sequenceDiagram
    participant C as Client
    participant H as HTTP thread (std::thread)
    participant M as Main Qt thread
    participant D as DzScript engine

    C->>H: POST /execute (JSON body)
    Note over H: Parse body, validate auth header
    H->>M: emit signal (BlockingQueuedConnection)
    Note over M: handleExecuteRequest()
    M->>M: Check concurrent limit
    M->>M: Check IP whitelist
    M->>M: Check rate limit
    M->>M: Validate body & token
    M->>D: DzScript::execute()
    D-->>M: result / error
    M-->>H: response struct
    H-->>C: HTTP 200 JSON
Loading

Rules that must never be broken:

  1. DzScript, QScriptEngine, and all DAZ API calls on the main thread only.
  2. Synchronous execution crosses from HTTP workers with Qt::BlockingQueuedConnection. On Studio 6, async script submission uses only local, reentrant Qt Core values and mutex-protected services so it can enqueue while the main thread is busy. Studio 4 retains the blocking crossing for submission because its JSON parser uses QScriptEngine.
  3. DzScript objects must be created and destroyed on the main thread.
  4. killRender() must be invoked via a signal on the main thread, not from the HTTP thread.
  5. HTTP workers must not read GUI-owned mutable settings; async handlers receive immutable snapshots captured before the server starts.

Async Request Lifecycle

stateDiagram-v2
    [*] --> queued : POST /execute/async
    queued --> running : dispatcher picks up request
    queued --> cancelled : DELETE /requests/:id
    running --> completed : script returns
    running --> failed : script throws
    running --> cancelled : cancel flag + killRender()
    completed --> [*] : TTL 1h cleanup
    failed --> [*] : TTL 1h cleanup
    cancelled --> [*] : TTL 1h cleanup
Loading

AsyncRequestManager owns the queue and the map of in-flight requests. A cleanup timer fires every 5 minutes and purges entries older than 1 hour.


Request Processing Pipeline

flowchart TD
    A[HTTP handler receives request] --> B{Concurrent limit?}
    B -->|exceeded| R429a[HTTP 429]
    B -->|ok| C{IP whitelisted?}
    C -->|blocked| R403[HTTP 403]
    C -->|ok| D{Rate limited?}
    D -->|exceeded| R429b[HTTP 429]
    D -->|ok| E{Body size OK?}
    E -->|too large| R413[HTTP 413]
    E -->|ok| F{Token valid?}
    F -->|invalid| R401[HTTP 401]
    F -->|ok| G{Input valid?}
    G -->|invalid| R400[HTTP 400]
    G -->|ok| H[Wrap in IIFE, inject args]
    H --> I[Capture print output]
    I --> J[DzScript::execute]
    J --> K[Update metrics, write log]
    K --> L[HTTP 200 JSON response]
Loading

Service Responsibilities

AuthenticationService

  • Generates 128-bit tokens via SecureRandom (CryptoAPI on Windows, /dev/urandom on Unix/macOS)
  • Stores token to ~/.daz3d/dazscriptserver_token.txt with chmod 600
  • Validates X-API-Token and Authorization: Bearer headers
  • Thread-safe: token loaded once at startup, read-only thereafter

RateLimiterService

  • Per-IP sliding-window counter (default: 60 req / 60 s)
  • QMutex-protected map; stale entries cleaned up every 100 requests

IPWhitelistService

  • Exact-match list, comma-separated in settings
  • QMutex-protected; applied after concurrent limit, before rate limit

MetricsCollector

  • Monotonic counters for total, successful, and failed requests, plus auth failures
  • Persisted via QSettings across DAZ Studio restarts
  • Provides uptime (seconds since server start) and calculated success rate

AsyncRequestManager

  • Thread-safe queue (QMutex) for incoming async work items
  • Map of request_id → AsyncRequest for status and result retrieval
  • TTL cleanup timer (Qt timer, fires on main thread)
  • Long-poll support: waiting GET /requests/:id/result?wait=true calls block in QWaitCondition until the result is available or the timeout fires

Data Flow: Script Arguments

Client JSON body
  └─ "args": { "key": "value" }
       │
       ▼
  QScriptEngine::evaluate("var __args = JSON.parse('" + escaped_args + "');")
       │
       ▼
  Script wrapping:
    (function(){
      // user script
    }).call(null, __args)
       │
       ▼
  Inside DazScript: getArguments()[0]  →  { key: "value" }

Settings Storage

Platform Location
Windows HKEY_CURRENT_USER\Software\DAZ 3D\DazScriptServer
macOS ~/Library/Preferences/com.daz3d.DazScriptServer.plist
Linux ~/.config/DAZ 3D/DazScriptServer.conf

All settings are accessed through ServerSettings, which wraps QSettings and provides typed getters with range clamping from ServerConfig constants.


Build System

CMakeLists.txt          — top-level: sets plugin type, platform flags
src/CMakeLists.txt      — source list, DAZ SDK include/link, MSVC /MD flag
include/common_version.h — DZSRV_VERSION_STR, bumped per release

Platform-specific notes:

  • Windows MSVC: /MD /U_DEBUG — force multi-threaded DLL CRT, suppress DAZ SDK debug macros that conflict with Release builds
  • Windows link: ws2_32 (Winsock), advapi32 (CryptoAPI)
  • macOS/Linux: SecureRandom uses /dev/urandom; chmod 600 via POSIX chmod(2)

Key Design Decisions

Why cpp-httplib instead of a Qt HTTP server?

Qt 4.8 (the SDK's Qt) has QHttp but it is deprecated and removed in later Qt versions. cpp-httplib is header-only, zero-dependency, and well-maintained. It runs on a dedicated thread managed by ServerListenThread.

Why different crossings for synchronous and asynchronous execution?

The DAZ Studio SDK explicitly requires all scene-graph and script-engine operations on the main thread. Synchronous /execute therefore uses BlockingQueuedConnection and waits for the result. On Studio 6, async script endpoints validate request value data on the HTTP worker and submit directly to the mutex-protected AsyncRequestManager; blocking on the main thread here would prevent submission and queued cancellation while a prior script is running. Studio 4 keeps submission on the main thread because its JSON fallback uses QScriptEngine. In both builds the manager posts execution to the main thread, where DzScript is loaded and run.

Why session-only script registry?

Persistent script storage would require a file-system schema and migration logic. Session-only storage keeps the registry simple: clients re-register on HTTP 404 (which happens after a DAZ Studio restart). The tradeoff is that clients need a small registration step at startup.