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.
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 -->|BlockingQueuedConnection| Pane
Pane --> Auth
Pane --> Rate
Pane --> WL
Pane --> Met
Pane --> RV
RV --> RP
RP -->|Main thread| DAZ
RP --> ARM
ARM -->|BlockingQueuedConnection| Pane
Auth --> SR
JB -.-> Pane
SS -.-> Services
SS -.-> Exec
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
Rules that must never be broken:
DzScript,QScriptEngine, and all DAZ API calls on the main thread only.- HTTP handlers (running on
std::threads) must emit signals withQt::BlockingQueuedConnectionto cross into the main thread. DzScriptobjects must be created and destroyed on the main thread.killRender()must be invoked via a signal on the main thread, not from the HTTP thread.
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
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.
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]
- Generates 128-bit tokens via
SecureRandom(CryptoAPI on Windows,/dev/urandomon Unix/macOS) - Stores token to
~/.daz3d/dazscriptserver_token.txtwithchmod 600 - Validates
X-API-TokenandAuthorization: Bearerheaders - Thread-safe: token loaded once at startup, read-only thereafter
- Per-IP sliding-window counter (default: 60 req / 60 s)
QMutex-protected map; stale entries cleaned up every 100 requests
- Exact-match list, comma-separated in settings
QMutex-protected; applied after concurrent limit, before rate limit
- 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
- Thread-safe queue (
QMutex) for incoming async work items - Map of
request_id → AsyncRequestfor status and result retrieval - TTL cleanup timer (Qt timer, fires on main thread)
- Long-poll support: waiting
GET /requests/:id/result?wait=truecalls block inQWaitConditionuntil the result is available or the timeout fires
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" }
| 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.
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:
SecureRandomuses/dev/urandom;chmod 600via POSIXchmod(2)
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.
The DAZ Studio SDK explicitly requires all scene-graph and script-engine
operations on the main thread. BlockingQueuedConnection gives us the main-
thread guarantee while letting the HTTP thread block until the result is ready
(for synchronous /execute) or until the request is accepted into the queue
(for async endpoints).
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.