Metadata-Version: 2.4
Name: py_unidbg_server
Version: 1.0.0
Summary: FastAPI project host with isolated JVM workers, bounded concurrency, and rolling project updates
Author: aFunnyStrange
License-Expression: BSD-3-Clause
Project-URL: Homepage, https://github.com/aFunnyStrange/py_unidbg_server
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: jpype1>=1.5
Requires-Dist: fastapi>=0.100
Requires-Dist: pydantic>=2
Requires-Dist: uvicorn
Provides-Extra: dev
Requires-Dist: httpx; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# Unidbg Project Host — 1.0

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.

[中文说明](readme-chinese.md) · [Architecture](docs/architecture.md) ·
[Migration from 0.2](docs/migration-1.0.md) · [Standalone Java edition](java-host/README.md)

## Install and start

Requires Python 3.11+ and a compatible installed JVM (JDK 17 is tested).

```bash
pip install py_unidbg_server==1.0.0
unidbg-server start --core ./unidbg_core --projects ./projects
```

Open 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.

```text
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.

## Project contract

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.

```json
{
  "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.

## Calls, docs and lifecycle

```bash
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.

## Settings

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.

## Python API

Version 1 removes hidden global JVM state from the public API:

```python
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.

## Verification and distribution

```bash
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.
