Maven single-module demo (target Java 17) teaching ProcessBuilder/Process. No lint, no exec-maven-plugin. Runtime dep: logback-classic (slf4j); test dep: junit-jupiter (JUnit 5, via surefire). Code comments and tutorial notes (process.md, doc.md) are in Chinese.
mvn test (JUnit 5)ProcessMechanicsTest drives the same fixtures end-to-end (compiles and runs child processes) and deliberately covers the stream-drain + timeout/destroy traps. Same user.dir rule applies — run from the repo root or the fixture .start() throws error=2. Tests recompile fixtures into src/main/resources/code/** (gitignored *.class). Keep tests fast: never block a full 10s sleep — use a short timeout then destroy().
Classes are plain public static void main entry points in src/main/java/space/anyi/process/. There is no run target, so run manually after compiling:
mvn -q compile
java -cp "target/classes:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout | tail -1)" space.anyi.process.<ClassName>
All examples declare static Logger log, so slf4j jars must be on the classpath or you get NoClassDefFoundError: org/slf4j/LoggerFactory (when manually invoking java -cp target/classes, not an env problem).
src/main/resources/code/: hello/ (prints greeting), io/ (echoes stdin, writes stderr), executeStatus/ (sleeps 10s). Each demo's workDir points at exactly one fixture dir.javac/java at runtime to compile and run fixture .java files; shared helpers live in space.anyi.process.Jdk (javac()/java() return absolute paths from java.home, compileFixture() runs javac). Spawned processes' PATH often lacks the JDK bin dir (error=2) — never go back to bare javac/java.workDir relative to user.dir → run them from the repo root, or ProcessBuilder.start() throws IOException: error=2. Keep workDir in sync with the fixture tree when moving files.space.anyi.process.Main in package root must NOT exist — Jdk.compileFixture uses -encoding UTF-8 + Main and fixtures are in the default package.src/main/resources/code/, never an ad-hoc script or system command. Keep the fixture tree in sync with each example's workDir.static method, invoked from main() — do not fold several points into one linear main.git add + git commit); commit in small increments, one point per commit.new BufferedReader(new InputStreamReader(p.getInputStream())).lines().forEach(...) blocks the calling thread until the child exits (stdout EOF only closes on exit). Draining stdout before waitFor(timeout) pins the main thread for the full child runtime (e.g. 10s) and silently defeats the timeout semantics. Drain child output on a background thread when using a timed waitFor.Process.waitFor(timeout, unit) returns a boolean, not an exit code. After a timeout it returns false with the child still alive; calling process.exitValue() there throws IllegalThreadStateException — destroy() + blocking waitFor() before reading exitValue().