You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CLAUDE.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@
4
4
5
5
Spawn is a Java 25 framework for programmatically launching and controlling processes, JVMs, and Docker containers. It provides a unified abstraction (`Platform` / `Application` / `Process`) over different execution environments. The core pattern: define a `Specification`, call `platform.launch(spec)`, get back an `Application` with `CompletableFuture`-based lifecycle hooks.
6
6
7
-
**Stack**: Java 25, Maven, Jackson, `build.base.*` and `build.codemodel.injection`
7
+
**Stack**: Java 25, Maven, `build.base.*` (incl. `build.base.json`) and `build.codemodel.injection`
8
8
9
9
**Structure**: 8 Maven modules in a monorepo, each mapping to a JPMS module:
10
10
-`spawn-option` → shared option types
@@ -31,5 +31,5 @@ Tests requiring Docker are gated by `@EnabledIf("isDockerAvailable")`. The `spaw
31
31
32
32
- All option types are immutable with static `of(...)` factories and `@Default` annotated defaults
33
33
-`Customizer` inner classes on `Application` interfaces are auto-discovered and applied at launch
|`AbstractTemplatedPlatform.java`|`ServiceLoader<LauncherRegistration>` discovery at construction; expression resolution; customizer auto-discovery; `min()` launcher selection by most-specific Application subtype |
156
+
|`AbstractTemplatedLauncher.java`| Full launch orchestration: facet assembly, argument conversion, diagnostics tabulation; DI via `bind(class).to(instance)`, `bindSet`, `addResolver`|
157
+
|`AbstractApplication.java`| Wires process I/O to console via Pipes; registers customizer callbacks; `@Inject Iterable<Lifecycle<?>> lifecycles = List.of()` (defaults to empty — safe without DI) |
**ServiceLoader SPI:**`uses LauncherRegistration` — modules contribute launcher registrations with `provides LauncherRegistration with ...` in their `module-info.java`
165
167
166
168
**Option type pattern** (applies everywhere):
167
169
- Private constructor + `static of(...)` factory
@@ -251,7 +253,7 @@ Server (listens on spawn:// URI)
251
253
|`LocalMachine.java`| Singleton `Machine`; extends `AbstractTemplatedPlatform`; PID from `RuntimeMXBean.getName()`|
252
254
|`LocalProcess.java`| Wraps `java.lang.Process`; virtual thread watcher; `suspend/resume` via `kill -STOP/-CONT`|
@@ -273,7 +275,7 @@ Server (listens on spawn:// URI)
273
275
|`JDKHomeBasedPatternDetector.java`| Native JPMS service impl (registered via `module-info.java``provides…with`); reads `java.home.properties` OS-specific globs; caches result in `AtomicReference`|
274
276
|`LocalJDKLauncher.java`| Builds `java` command line including SpawnAgent injection; discovers or creates `spawn-agent.jar`|
**Docker option types** — all implement `DockerOption.configure(ObjectNode, ObjectMapper)`:
319
+
**Docker option types** — `DockerOption` is a **sealed interface** (`permits Bind, Command, ExposedPort, ExtraHost, Link, PublishAllPorts, PublishPort`). Options carry data only; serialization into JSON is handled entirely by the command classes in `spawn-docker-jdk` via pattern-matching switch, keeping JSON libraries out of the public API:
@@ -334,7 +336,7 @@ Server (listens on spawn:// URI)
334
336
335
337
### `spawn-docker-jdk`
336
338
337
-
**Purpose:** JDK-native concrete implementation of `spawn-docker` interfaces. Uses `java.net.http.HttpClient` for TCP and `java.nio.channels.SocketChannel` with `UnixDomainSocketAddress` for Unix domain sockets. No third-party HTTP dependencies.
339
+
**Purpose:** JDK-native concrete implementation of `spawn-docker` interfaces. Uses `java.net.http.HttpClient` for TCP and `java.nio.channels.SocketChannel` with `UnixDomainSocketAddress` for Unix domain sockets. JSON handled by `build.base.json` (base-json). No third-party dependencies.
338
340
**Entry point:** Four `Session.Factory` implementations discovered via `ServiceLoader`, in priority order:
339
341
1.`UnixDomainSocketBasedSession.Factory` — Unix socket (`/var/run/docker.sock` or Docker Desktop socket)
-`GetSystemEvents` and `DockerContainer` have debug `System.out.println` calls in production code
@@ -377,7 +379,11 @@ Server (listens on spawn:// URI)
377
379
- Braces required on all blocks
378
380
379
381
### Launcher Registry Pattern
380
-
Platform classes have a `META-INF/<ConcreteClassName>` properties file on the classpath. Each line: `ApplicationClass=LauncherClass`. Multiple modules can contribute to the same registry file. `AbstractTemplatedPlatform` discovers launchers by reading all matching resources and selecting the most-specific `Application` supertype match (NOT `findFirst()` — avoids classpath-order bugs).
382
+
Launcher registration uses JPMS `ServiceLoader`. Each module that provides a launcher declares a `LauncherRegistration` implementation (typically a `record`) and registers it in `module-info.java`:
383
+
```
384
+
provides build.spawn.application.LauncherRegistration with com.example.MyLauncherRegistration;
385
+
```
386
+
`AbstractTemplatedPlatform` loads all `LauncherRegistration` providers at construction time and selects the most-specific `Application` subtype match using `min()` with class-hierarchy ordering — NOT `findFirst()`, which would produce silent order-dependent failures if multiple modules are on the classpath.
381
387
382
388
### Nested Class Conventions
383
389
-`Application.Implementation` (or `JDKApplication.Implementation`) — public static concrete class found by `Application.getImplementationClass()` via reflection
@@ -393,7 +399,7 @@ Tests requiring Docker are gated by `@EnabledIf("isDockerAvailable")` — they w
393
399
The returned `Application` from `launch()` is a JDK `Proxy` (`Faceted` implementation), NOT a direct instance of `Application.Implementation`. `instanceof Application.Implementation` will always return `false`. Use interface types or `Faceted.as(Class)`.
394
400
395
401
### `Iterable<Lifecycle<?>> lifecycles` in `AbstractApplication`
396
-
This is `@Inject`-annotated. If an `AbstractApplication` subclass is created outside the DI framework, `lifecycles`will be `null` and`onStart()`will throw `NullPointerException`.
402
+
This field is `@Inject`-annotated but also initialized to `List.of()` as a default. Subclasses created outside the DI frameworkwill have an empty lifecycle list rather than null —`onStart()`is safe.
397
403
398
404
### Default Environment Variables
399
405
`@Default` on `EnvironmentVariables.none()` means child processes inherit NO environment variables by default. Add `EnvironmentVariables.inherited()` explicitly to pass through the parent JVM's environment.
@@ -431,11 +437,14 @@ Child JDK processes are killed when the parent JVM exits by default. Use `Orphan
0 commit comments