Maven single-module demo (target Java 17) teaching ProcessBuilder/Process. No tests, no lint, no exec-maven-plugin; the only dependency is logback-classic (slf4j). Code comments and tutorial notes (process.md) are in Chinese.
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().