Skip to content

EXTERNAL_SERVICE on Windows: local process units (services/*.json) - #174

Closed
Trombo38 wants to merge 1 commit into
nathaninline:mainfrom
Trombo38:external-service-windows
Closed

Trombo38 wants to merge 1 commit into
nathaninline:mainfrom
Trombo38:external-service-windows

Conversation

@Trombo38

Copy link
Copy Markdown
Contributor

EXTERNAL_SERVICE on Windows (local process units)

What

EXTERNAL_SERVICE on an External API preset now works on Windows, not only Linux.
Today the four platform hooks (unitAction, unitActiveState, unitPIDs,
waitGPUsReleased) are stubs on Windows that return
"EXTERNAL_SERVICE n'est pris en charge que sous Linux" — so a preset that names a
service fails to switch on Windows instead of starting its engine.

This PR implements them on Windows with the same contract the Linux systemd units
already provide:

  • switch to the preset → start the named service
  • switch away → stop it, then wait for the GPUs to be released
    (nvidia-smi --query-compute-apps=pid, ported from sys_service_linux.go)
    before the next engine loads — so one engine owns the GPUs at a time.

On Windows the "unit" is a local process defined by a JSON file in
$AJEAN_HOME\services\ajean-<name>.json (the new services/ folder, created on
first use — it does not exist in a current install):

{
  "command": "I:\\Strata\\.venv\\Scripts\\python.exe",
  "args": ["I:\\Strata\\serve\\server.py", "--engine", "strata",
           "--config", "I:\\Strata\\strata-swift-iq2_xs.json", "--port", "8080"],
  "cwd": "I:\\Strata",
  "env": { "PYTHONUTF8": "1" }
}

command/cwd may be relative (resolved from the file's folder); env is merged
over the inherited environment. The process is launched detached (same flags as
ajean serve), tracked by a PID file, logged to $AJEAN_HOME\<unit>.log, stopped
with taskkill /T /F — the exact primitives the local engine supervisor already
uses, no admin rights, no external tools.

The preset itself is unchanged and portable across platforms:
EXTERNAL_SERVICE=ajean-strata-iq2xs (same ^ajean-[a-z0-9][a-z0-9-]{0,40}$ regex).

Why the GPU-mutex is safe

SwitchToPreset (backend_presets.go) already orders the calls correctly and this
PR does not change that path — it only makes the Windows hooks real:

  • → external preset: serviceAction("restart") first (which on Windows +
    external config stops the local llama-server and waits for the port to free),
    then syncExternalService starts the external service.
  • → local preset: syncExternalService stops the external unit,
    waitGPUsReleased waits for its PIDs to leave the GPU, then llama.cpp restarts.

So the two engines never coexist — the Windows equivalent of the Linux
Conflicts=ajean-engine.service, obtained by call order.

Files

  • internal/ajean/run.go — servicesDir() = $AJEAN_HOME\services
  • internal/ajean/sys_service_windows.go — remove the 4 "Linux only" stubs
  • internal/ajean/sys_service_windows_extservice.go (new) — the 4 hooks
  • internal/ajean/sys_service_windows_extservice_test.go (new) — def parsing,
    state tracking, GPU-mutex order (via syncExternalService)
  • internal/ajean/backend_extservice.go — doc comment updated (no behavior change)
  • README.md / README.fr.md — "Third-party engine" section now Linux & Windows

Testing (CI gates)

  • gofmt -l . (Go 1.25) — clean.
  • go vet ./... — clean.
  • staticcheck@latest — 0 warnings (project is zero-warning).
  • Cross-compile CGO_ENABLED=0 — 4/4: linux/amd64, linux/arm64, windows/amd64, windows/arm64.
  • go test ./... (with AJEAN_HOME isolated) — all pass, except TestMoeMigrate
    which fails on this Windows host due to a symlink privilege it does not have
    ("A required privilege is not held by the client"); it passes on the CI ubuntu
    runner and is unrelated to this change (the PR touches no MoE or symlink code).
  • All EXTERNAL_SERVICE / external tests pass: TestSyncExternalService,
    TestSyncExternalServiceRejectsForeignUnits, TestExternalServiceReady,
    TestExternalPresetEditKeepsServiceAndMachine, plus the new
    TestExtServiceDefParsing, TestExtUnitStateTracking,
    TestWindowsExternalServiceMutex.
  • Manual end-to-end (run, then removed): a dummy engine defined in
    services/ajean-e2e.json was started via syncExternalService, reached
    externalServiceReady (unit active + /health 200), and was stopped cleanly
    (port no longer listening).

@Trombo38
Trombo38 force-pushed the external-service-windows branch from e78f465 to c798ba1 Compare October 25, 2026 18:00
@Trombo38 Trombo38 closed this Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant