One deployment, many independently managed Unidbg projects. The Python edition
provides FastAPI, Swagger UI at /docs, and isolated reusable Python/JPype worker
processes. The host itself never starts a JVM.
中文说明 · Architecture · Migration from 0.2 · Standalone Java edition
Requires Python 3.11+ and a compatible installed JVM (JDK 17 is tested).
pip install py_unidbg_server==1.0.0
unidbg-server start --core ./unidbg_core --projects ./projectsOpen http://127.0.0.1:8888/docs. Keep one host process per runtime directory; do not use multiple Uvicorn/Gunicorn workers against the same catalog. Increase project replicas instead. The CLI uses standard asyncio on Windows and Linux.
deployment/
unidbg_core/ shared core JAR inputs
projects/
demo/
project.jar business code, not a standalone HTTP service
unidbg-project.json
resources/ APK/SO/read-only rootfs seeds
.unidbg-runtime/ host-owned snapshots, state, writable instances
Deploy only trusted local project packages. The host is not a security sandbox. Its management API intentionally defaults to loopback and has no built-in user authentication; put an authenticated proxy in front before exposing it remotely.
Java projects keep their own normal main(String[] args) for debugging. Expose
any number of public static Object method(String payload) methods. HTTP data
is serialized as JSON once before entering that String contract; results are
returned as strings. No server SDK, Spring application, or per-project port is
required.
{
"version": "2026.09.1",
"execution": {
"replicas": 2,
"queue_capacity": 32,
"queue_timeout": 30,
"call_timeout": 60,
"idle_timeout": 300
},
"lifecycle": {
"class": "com.example.Project",
"load_method": "initialize",
"unload_method": "close",
"pass_project_dir": true
},
"apis": {
"sign": {
"class": "com.example.Project",
"method": "sign",
"description": "Sign an input using this project's emulator"
},
"decrypt": {
"class": "com.example.Project",
"method": "decrypt"
}
}
}Lifecycle is optional. load_method, when present, must be static
initialize(String projectDirectory) (the configured method name can differ).
Use it to warm the emulator before readiness. Cleanup is static
close(String projectDirectory) when pass_project_dir=true, otherwise
close(). Close emulators, native handles, threads and executors there.
Each replica owns one JVM and a private writable copy of the project resources. All APIs on that replica execute serially; replicas and projects run concurrently. Do not hard-code a writable shared rootfs/cache path across replicas. Stateful sessions spanning multiple calls are not routed with affinity in 1.0: use one replica and avoid rolling updates until the session ends, or externalize state.
curl -X POST http://127.0.0.1:8888/projects/demo/reload
curl -X POST http://127.0.0.1:8888/projects/demo/apis/sign -H "Content-Type: application/json" -d '{"data":{"value":1}}'
curl http://127.0.0.1:8888/list
curl -X DELETE "http://127.0.0.1:8888/projects/demo?timeout=30"| Endpoint | Meaning |
|---|---|
POST /call-java |
Legacy shape: project, class_name, optional method_name=start, data |
POST /projects/{project}/apis/{alias} |
Call a declared alias with {"data": ...} |
GET /projects/{project}/apis |
Cached API declarations and verification facts |
GET /list, GET /health |
Nonblocking generation, readiness, running/queued counts |
POST /projects/refresh |
Discover staged directories and refresh Swagger entries |
POST /projects/{project}/reload |
Snapshot, warm, validate, and switch to a new generation |
DELETE /projects/{project}?timeout=30 |
Persist disablement and drain accepted work |
GET /docs, GET /openapi.json |
Generic routes plus concrete paths for every project alias |
For new packages, stage the directory completely before refresh/reload. Do not modify the staging tree during snapshot creation; publish files through an operator-controlled atomic directory switch. Updating business code requires rebuilding the business JAR, not rebuilding/restarting this host.
Reload prepares every new replica before publishing its generation. Failed initialization or an invalid alias leaves the serving version unchanged. Already-accepted requests, including queued ones, stay pinned to the old version. Old resources close only after those leases drain. Allow spare process/memory budget for overlapping generations.
available: null means declared but not yet worker-verified. true means the
class and public/static/String signature passed worker initialization, not that
every input is business-valid. Readiness counts are separate. Reload or a first
call performs verification; /list does not create a JVM per deployed directory.
Queue overflow/wait timeout returns 429. Disabled projects return 409. Java execution timeout returns 504 and discards the worker; a subsequent request can create a replacement. Worker disconnection returns 502. Submitted calls are never automatically replayed because their effects may already have occurred. Cancelling a client wait does not return an executing emulator to the pool.
A DELETE drain timeout returns 409 but remains disabled and drains in the background. Explicit reload re-enables it. Idle eviction, unlike disablement, keeps the project enabled and lazily reloadable. Idle timeout 0 disables eviction.
Use process environment variables (a .env.example is provided as a reference;
the CLI does not implicitly load .env):
| Variable | Default |
|---|---|
UNIDBG_RUNTIME_DIR |
<base>/.unidbg-runtime |
UNIDBG_MAX_PROCESSES |
16, including warming/draining generations |
UNIDBG_STARTUP_TIMEOUT |
60 seconds per replica |
UNIDBG_SHUTDOWN_TIMEOUT |
60 seconds for owned calls/control tasks |
UNIDBG_MAX_MESSAGE_BYTES |
4 MiB per HTTP body/IPC frame |
UNIDBG_MAX_PROJECT_BYTES |
1 GiB per project copy |
Core/project/base/host/port environment settings remain available to ASGI factory
users through ServerSettings.from_environment(). CLI core/project/host/port
arguments take precedence. unidbg-server edit copies an editable ASGI factory.
Stopped Python worker instance directories are reclaimed after path/link checks. If a project created links, oversized output or locked files, cleanup retains the directory and logs a warning rather than traversing/retrying destructively. Immutable release snapshots are retained for diagnosis and restart; plan disk retention explicitly. Do not delete a selected or draining release. External files, subprocess trees created by business code, and external side effects are owned by the project and are not magically reclaimed by terminating one JVM.
Version 1 removes hidden global JVM state from the public API:
import asyncio
from py_unidbg_server import ServerSettings, create_manager
async def main():
settings = ServerSettings.build()
async with create_manager(settings) as manager:
result = await manager.call("demo", '{"value":1}', api="sign")
print(result)
await manager.unload("demo")
asyncio.run(main())Use the same manager on its owning event loop; do not create it per request. An OS file lock prevents concurrent hosts from mutating one runtime directory. The in-memory request queue is not a durable job queue: host failure can lose responses and clients must decide whether an unknown outcome is safe to retry.
pip install -e ".[dev]"
pytest -q
python -m build
python -m twine check dist/*Tests cover units, HTTP composition, and real compiled JARs/child JVMs: multiple APIs, parallel instances, hot replacement, queue bounds, cancellation, process failure, timeout, and durable disablement. They do not prove arbitrary third-party Unidbg native backends are thread-safe or that a particular APK/SO business call is correct. Validate your real artifacts on the target OS.
The java-host/ directory is a separate JDK 17/Maven implementation. It is
committed and tested on GitHub, but explicitly excluded from Python wheel/sdist
and is not published to PyPI or Maven Central by this repository.
BSD-3-Clause license.