# Architecture and contracts `Config → Handoffs → Jobs → WorkerSupervisor → Coordinator → Platform` is the control path. `Gate` is independent of Platform and never calls Docker/libvirt. A single RAM registry `flock` makes multi-group reservation acquisition atomic; the planner sorts group IDs, and reservations remain job records throughout every external operation. Disjoint jobs can run concurrently; groups or touched workloads that overlap cannot. Supervisors are detached with `setsid` and claim a queued job using their boot/PID/start-tick identity, preventing duplicate child launches. Each supervisor waits in `proc_close` for one worker, reviews that job after exit, then terminates. Workers claim queued jobs atomically and persist command intent before starting a subprocess. A child that exits before claiming its job produces a visible startup failure. Recovery never steals a live worker or unlocks an uncertain command. A job proceeds through validation, reserved-group shutdown, release confirmation, target action and confirmation. Reservations already exist while it is queued. All members are inspected before any stop. Immediately before starting, conflicts are inspected again. Paused, suspended, restarting, releasing and unknown states are not stopped. An active VM must produce a newer `release/end` event and be shut off. The hook writes that event after libvirt's resource cleanup. Docker must report not running/restarting with PID zero and exited/created state. Container shutdown uses `docker stop --timeout -- `, matching Unraid’s native Stop operation. Docker uses the configured StopSignal and escalates to SIGKILL after the timeout. The command runs inside the background worker with a client deadline 30 seconds beyond the shutdown timeout; a lost response retains the in-flight reservation. After a successful response, the coordinator still verifies that the container is stopped. VM shutdown uses the installed Unraid VM manager’s PHP libvirt binding, with a domain handle resolved by UUID. A bounded CLI helper submits the request; the coordinator still waits for confirmed shutoff and a new release event. VM shutdown retains its separate opt-in force policy. Legacy `forceContainer` fields are discarded during configuration normalization; existing groups automatically adopt native Stop. VM Restart calls `virsh reboot`; Docker Restart is stop/wait/start. Grouped actions retain internal jobs, reservations and VM start authorizations even when all conflicts are stopped. The coordinator marks a job as a handoff only when it attempts to stop another member. Restarting the requested container alone is not a handoff. Ordinary successful actions produce no handoff notifications, transition log or Recent handoffs entry. Failed or quarantined ordinary actions appear under Troubleshooting → Action errors; native error dialogs report failures without a handoff toast. Existing jobs without the classification flag remain visible as historical handoffs. Configuration identifies VMs by UUID, containers by name. Native Docker button IDs are resolved against a fresh inventory. The adapter pins the full container ID for the job and rejects mid-job recreation. All process calls use argv arrays, fixed executables and bounded output/time. There is no caller-supplied command or executable. A lost mutating command response is uncertain; client termination does not prove daemon cancellation. ## Files and lifecycle `/etc/libvirt/hooks/qemu.d/99-deadlock-guard` is independent of the primary QEMU hook. It reads the UUID from stdin XML with network/entity loading disabled, consumes a permission once under a nonblocking per-VM lock, checks the live worker's boot/PID/start-tick identity, and records events locally. It never takes the registry lock and never calls or waits for a process that calls libvirt. Unsupported migration/restore/attach paths are rejected for grouped VMs. Reconnect events are left alone to avoid killing existing VMs on daemon restart. Installation records the current libvirtd process identity. Hook activation is considered pending until a later daemon instance, or the first instance after an install while it was stopped. The supported user activation procedure is a planned Unraid reboot after first installation and whenever an upgrade reports pending activation. The installer and Settings direct users to reboot instead of toggling the VM service, which can leave the libvirt image attached to a loop device. Neither the host nor the VM/Docker daemons are restarted automatically. The final plugin installation step restarts Unraid API after successful module setup. Startup, disks-mounted and array-started entrypoints restore/check integration and review pending jobs. VM-start checks integration and pending jobs; Docker-start only reviews pending jobs. There is no maintenance cron. Before a protected request is admitted, the batch is validated, dead workers are reviewed, an accepted duplicate is returned immediately, and new requests check required integration and member state. Docker-only requests recover maintenance barriers without touching the VM hook. The coordinator repeats state validation before stopping conflicts and checks readiness before each mutation. A hook replaced while the daemon is already running needs activation again. Array stopping/stopping-services and unmounting entrypoints drain admission and prevents subsequent actions. Maintenance admission checks active jobs atomically before creating its barrier. The barrier records the updater process identity and a hash of the complete installed payload: automatic recovery requires that exact updater to be gone and all payload bytes unchanged. Array events preserve that barrier and separately track whether the array is stopping, so neither array startup nor package completion clears the other safety block. Checks cannot enable a removed plugin and do not rewrite flash installation markers. A persistent hook has a local-only fallback when the current boot lacks a supported payload: it rejects configured grouped VM starts and permits unrelated VMs. Runtime files are root-only. Uninstall retains flash configuration, removes only the plugin's files and hook, and disables any stale hook inside a currently unmounted libvirt image through the persistent install marker. Lifecycle checks and the two manual operations are independent of history. Settings reads history once with its initial snapshot, then polls it every three seconds while visible. Polling is read-only. If both supervisor and worker are terminated, the job remains reserved until a subsequent lifecycle, handoff or manual check establishes whether recovery is safe. Interrupted starts with persisted command intent stay quarantined until host reboot. Progress responses include `progress.workloads`, an array of workload names and types still awaiting release. The current workload remains listed until shutdown and release are confirmed; other conflicts already observed stopped are omitted. This display data does not change stop order or reservation behavior. Notifications call Unraid's global `window.toast` API with a plugin-specific ID, leaving theme, position and rendering to Unraid. Loading messages update in place, success messages expire after five seconds, and errors persist until dismissed. A blocked console popup offers a native notification action. If the asynchronously mounted toaster is unavailable, final results use Unraid's existing SweetAlert dialog with HTML disabled. No notification library or custom popup styles are packaged. History rows are keyed by job ID and only changed text is replaced, preserving expanded/collapsed details and selection across unchanged refreshes. ## Opt-in diagnostics Debug preference is a separate versioned `debug.json` on flash so it can be changed during an active job or when group configuration fails to load. Troubleshooting loads it independently. All writes/downloads use the same authenticated, CSRF-protected endpoint as group configuration; there is no public log URL or caller-supplied file path. The download combines four fixed files and installed version metadata. Browser console traces are separate and omit response/request bodies. PHP and API traces use separate root-only RAM files, each capped at 512 KiB with one rotated copy. Each record contains a UTC timestamp, component, PID, event and selected diagnostic fields. PHP uses a separate nonblocking log lock, including on the hook path; it never holds up authorization or invokes libvirt. API tracing stays inside the current process without spawning a logger. Both read the current opt-in preference and swallow logging errors. The worker traces state transitions instead of every repeated poll. Raw exceptions, process input/output, authentication context, arbitrary objects and configuration payloads are excluded. Existing native errors remain available through their normal UI/log channels. ## Internal endpoint `POST /plugins/deadlock-guard/include/api.php` accepts JSON. Every operation requires Unraid's authenticated root session, `X-CSRF-Token` for the native prepend handler, and the same token in JSON `csrf`. The duplicate token is intentional: Unraid removes the validated header before invoking PHP. Sessions close before reading platform state. Responses are uncached JSON; failures return an HTTP error and an escaped JSON error string. Settings uses DOM text nodes for all user/workload strings. | `op` | Payload / response | | --- | --- | | `snapshot` | configuration, revision, inventory, health, `jobs` (handoffs) and `actionErrors` | | `inventory` | workloads and per-service errors | | `config` | `{config, revision}`; returns saved config and new revision | | `route` | `{native:{type,id,action}}` or `{type,bulk:true,action}`, plus `key`; returns unmanaged passthrough or a job | | `job` | `{requests:[{workload:{type,id},action}], key}`; returns `job.id` | | `status` | `{id}`; handoff flag, phase, timestamps, affected states, history, error | | `history` | `jobs` (handoffs) and `actionErrors` (failed/quarantined ordinary actions) | | `debug` | optional Boolean `enabled`; reads or updates the separate debug preference | | `debug-log` | bounded plain-text log bundle in `log`; fixed server files only | | `console` | VM workload; validated VNC/SPICE ports, no start | | `integration` | checks/restores VM hook and returns health; does not inspect or launch jobs | | `pending` | reviews abandoned jobs, retries queued workers unless draining, and returns history; does not change integration | Actions are `start`, `restart`, `resume`, and VM-only `wake`. Keys are 8–128 alphanumeric/underscore/hyphen characters. A reused key with different content is rejected. An identical active request joins the existing job. Idempotency persists within the RAM history retention window. ## Verified upstream contracts The browser fixture models public function signatures, not a copy of Unraid's UI. Reference branch `7.3`, inspected at commit `a68f44a5bee72c9a0d897329adb1a2536c7244b0`: - [Docker dispatch](https://github.com/unraid/webgui/blob/7.3/emhttp/plugins/dynamix.docker.manager/javascript/docker.js): `eventControl`, `startAll`, `resumeAll`. - [VM dispatch](https://github.com/unraid/webgui/blob/7.3/emhttp/plugins/dynamix.vm.manager/javascript/vmmanager.js): dispatch, console and Remote Viewer variants, bulk `startAll`; Dashboard reuses the dispatch functions. - [Page injection](https://github.com/unraid/webgui/blob/7.3/emhttp/plugins/dynamix/include/DefaultPageLayout.php): `Menu="Buttons"` pages; no stock-file edits. - [Native toaster mount](https://github.com/unraid/webgui/blob/7.3/emhttp/plugins/dynamix/include/DefaultPageLayout/ToastSetup.php) and [global toast API](https://github.com/unraid/api/blob/main/unraid-ui/src/components/common/toast/Toaster.vue): use the existing vue-sonner instance. The test-only toast double verifies API calls and lifetimes, not Unraid's actual appearance; visual validation remains a host check. - [Sessions](https://github.com/unraid/webgui/blob/7.3/emhttp/auth-request.php) and [CSRF prepend](https://github.com/unraid/webgui/blob/7.3/emhttp/plugins/dynamix/include/local_prepend.php). - [Libvirt hook lifecycle and recursion restriction](https://www.libvirt.org/hooks.html), [Docker stop escalation](https://docs.docker.com/reference/cli/docker/container/stop/). Unraid updates may change these contracts. The package and runtime limit this beta to 7.3.x. Browser fixtures cannot establish real-host compatibility. ## API adapter and per-group sources Version 1 configuration adds Boolean `webui` and `api` fields per group. Both default true; false explicitly bypasses that group's handoff rules. At least one must be true. Plans and bulk conflicts include only enabled groups selected for the trusted request source. The browser endpoint always supplies `webui`; the CLI-only, root-only API bridge supplies `api`. Neither takes the source from client JSON. Jobs record it and include it in duplicate identity, while shared groups/member touches still serialize conflicting callers across sources. The bundled `unraid-api-plugin-deadlock-guard` Nest module requires Unraid API 4.36.0 or newer. Build metadata does not affect eligibility; required providers, mutation arguments/results, and service methods are checked before wrappers are installed. New version numbers alone do not certify compatibility. It wraps verified GraphQL schema fields to carry request context through the original resolver, then intercepts the corresponding Docker/VM service call after native authentication and authorization. AsyncLocalStorage keeps concurrent callers isolated. A global interceptor would miss these nested fields: this API enables field guards, not interceptors. Native AuthZService evaluates UPDATE_ANY permission for DOCKER and VMS; the bridge checks all affected types again against the final plan under the registry lock. No credentials are passed to PHP or stored by the plugin. All selected start-like fields in the operation are resolved with GraphQL's argument coercion, aliases, fragments and directives before submission. A conflicting batch fails before any handoff stop; the locked admission path repeats this check against current configuration. A fixed PHP executable and script receive bounded JSON over stdin with argv-array process execution. Polling reports the accepted worker's result. Success returns the API's native result shape without issuing a second start. A client disconnect or API process exit does not cancel an accepted background job; coordinator crash quarantine rules still apply. Unchecked Docker sources call the original service. Grouped VM starts still use an empty-conflict job to get a single-use gate authorization. Grouped API reboot always uses a guest reboot; native 4.37.4 `rebootVm` uses shutdown/create and would fail at the gate after shutdown. Native `resetVm` destroys/creates, so grouped API resets reject before destruction; ungrouped reset stays native. Ordinary stops do not enter the coordinator. The build embeds the dependency-free API module and a reproducible npm tarball. Installation copies only our module into the API’s `node_modules`, adds its root package metadata entry, registers it with native `unraid-api plugins install ... --bundled --no-restart`, and updates the native dependency archive. It does not run npm or resolve host dependencies. Removal uses `plugins remove ... --bypass-npm --no-restart`, deletes only our module and metadata entry, and archives the result. The existing installation marker disables loaded copies. Interrupted registration and archive operations retain a retry marker until successful. Setup errors appear as a concise status, with escaped details inside Troubleshooting. The final manifest step calls `lifecycle install --restart-api`, which runs `unraid-api restart` once after setup and archiving succeed. Package extraction prepares the module without restarting, so the two installation steps do not trigger duplicate restarts. Routine lifecycle checks, startup restoration and removal do not restart the API. A restart failure retains a visible error while WebUI protection remains available. Routine restoration preserves that error; `lifecycle api-install` retries setup and restart together and clears it only after success. No API source patch, proxy, new daemon or timer is installed. Code hash plus Linux process identity proves the currently loaded module matches the installed bridge; a stale loaded module rejects requests rather than falling back to a native start. Settings separates VM and API status. Native dependency archives contain node_modules only; they do not persist root package.json plugin discovery metadata. Lifecycle installation restores that metadata. If the API loaded first, a manual API restart is required after reboot or API replacement. Do not infer active API enforcement from saved checkboxes or an installed package. Actual boot ordering and the built API service remain host validation gates. Verified primary interfaces at [Unraid API ad268301](https://github.com/unraid/api/tree/ad268301ca78da1fa47fd3bb87e60fcedc458c5b): `plugin/plugin.service.ts`, `plugin/plugin-management.service.ts`, `cli/plugins/plugin.command.ts`, Docker/VM mutation resolvers and services, authentication/authorization guards, and `dependencies.sh`. Tests use matching Nest 11.1.26, GraphQL 16.11.0, and the existing API permission interface; a Nest bootstrap fixture exercises discovery/activation/shutdown with simulated host services. The required loader, CLI, authorization, Docker/VM service and mutation contracts match between official API tags v4.36.0 and v4.37.4. Docker Restart first appeared in v4.36.0. The shared version fixtures exercise PHP installation/health and the actual Nest bootstrap, including 4.37.4+ad268301.