14 KiB
Run Preview npm Install Stall Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make run-preview restart/stop terminate all descendant npm processes and reuse one persistent npm download cache across generated frontends and preview sessions.
Architecture: Wrap Windows preview commands in a no-profile PowerShell process that records its PID, then return a ManagedPreviewProcess carrying the PID file alongside the delegated Java Process. The killer uses taskkill /T for managed processes before its existing Java and port-based fallbacks. FrontProjectRunPreviewServiceImpl places one npm cache directly under the configured preview workspace root and passes it to both frontends.
Tech Stack: Java 8, Spring Boot 2.5, JUnit 4, Mockito, Windows PowerShell, npm 6+
File Structure
- Create
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/ManagedPreviewProcess.java: delegatesProcessoperations and exposes the recorded Windows root PID. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/RunPreviewProcessRunner.java: launch Windows commands through the PID-recording wrapper. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/RunPreviewProcessKiller.java: terminate managed Windows process trees before existing fallbacks. - Create
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/RunPreviewProcessKillerTest.java: regression tests for tree termination and fallback behavior. - Modify
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/RunPreviewProcessRunnerTest.java: verify managed process PID capture with a real short-lived child process. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectRunPreviewServiceImpl.java: configure one persistent shared npm cache. - Modify
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectRunPreviewServiceImplTest.java: verify both frontends share that cache and prefer cached packages.
Task 1: Managed Windows Process Tree
Files:
-
Create:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/ManagedPreviewProcess.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/RunPreviewProcessRunner.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/RunPreviewProcessKiller.java -
Test:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/RunPreviewProcessKillerTest.java -
Test:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/RunPreviewProcessRunnerTest.java -
Step 1: Write the failing killer tests
Create RunPreviewProcessKillerTest with:
@Test
public void stopRequestsWholeTreeTerminationForManagedProcess() throws Exception
{
File pidFile = temporaryFolder.newFile("preview.pid");
Files.write(pidFile.toPath(), "4321".getBytes(StandardCharsets.UTF_8));
FakeProcess delegate = new FakeProcess();
ManagedPreviewProcess process = new ManagedPreviewProcess(delegate, pidFile);
RecordingProcessKiller killer = new RecordingProcessKiller();
killer.stop(process, null);
assertEquals(Long.valueOf(4321L), killer.killedPid);
assertTrue(delegate.destroyed);
assertTrue(!pidFile.exists());
}
@Test
public void stopFallsBackWhenManagedProcessHasNoPid() throws Exception
{
File missingPidFile = new File(temporaryFolder.getRoot(), "missing.pid");
FakeProcess delegate = new FakeProcess();
ManagedPreviewProcess process = new ManagedPreviewProcess(delegate, missingPidFile);
RecordingProcessKiller killer = new RecordingProcessKiller();
killer.stop(process, null);
assertEquals(null, killer.killedPid);
assertTrue(delegate.destroyed);
}
The recording killer overrides a production seam:
@Override
protected boolean killWindowsProcessTree(long pid)
{
killedPid = Long.valueOf(pid);
return true;
}
- Step 2: Run the killer tests and verify RED
Run:
mvn -pl ruoyi-generator -Dtest=RunPreviewProcessKillerTest test
Expected: compilation fails because ManagedPreviewProcess and killWindowsProcessTree(long) do not exist.
- Step 3: Add a failing runner integration test
Add a Windows-only test to RunPreviewProcessRunnerTest:
@Test
public void startCommandRecordsManagedWindowsProcessPid() throws Exception
{
if (!isWindows())
{
return;
}
File projectDirectory = temporaryFolder.newFolder("managed-process");
RunPreviewProcessRunner runner = new RunPreviewProcessRunner();
Process process = runner.startCommandForTesting(
"ping 127.0.0.1 -n 3 > nul", projectDirectory, new HashMap<String, String>());
try
{
assertTrue(process instanceof ManagedPreviewProcess);
assertTrue(((ManagedPreviewProcess) process).waitForRootPid(2000L) > 0L);
}
finally
{
process.destroyForcibly();
}
}
- Step 4: Run the runner test and verify RED
Run:
mvn -pl ruoyi-generator -Dtest=RunPreviewProcessRunnerTest test
Expected: compilation fails because startCommandForTesting and ManagedPreviewProcess do not exist.
- Step 5: Implement
ManagedPreviewProcess
Create a package-private final class extending Process. Delegate all standard stream, wait, exit, destroy, destroyForcibly, and isAlive methods to the original process. Add:
long waitForRootPid(long timeoutMillis)
{
long deadline = System.currentTimeMillis() + timeoutMillis;
do
{
Long pid = readRootPid();
if (pid != null)
{
return pid.longValue();
}
sleepQuietly(25L);
}
while (System.currentTimeMillis() < deadline && delegate.isAlive());
return -1L;
}
void deletePidFile()
{
if (pidFile.isFile())
{
pidFile.delete();
}
}
readRootPid() reads UTF-8 text, trims it, accepts only a positive long, and returns null for a missing or invalid file.
- Step 6: Wrap Windows commands in
RunPreviewProcessRunner
Keep the existing shell path on non-Windows. On Windows:
private Process startWindowsManaged(String shellCommand, File workingDirectory,
Map<String, String> environment) throws IOException
{
File pidFile = new File(workingDirectory, ".easycode-process-" + UUID.randomUUID() + ".pid");
ProcessBuilder builder = new ProcessBuilder(
windowsPowerShellExecutable(),
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
WINDOWS_PROCESS_WRAPPER);
builder.directory(workingDirectory);
builder.redirectErrorStream(true);
builder.environment().putAll(environment);
builder.environment().put("EASYCODE_PREVIEW_COMMAND", shellCommand);
builder.environment().put("EASYCODE_PREVIEW_PID_FILE", pidFile.getAbsolutePath());
return new ManagedPreviewProcess(builder.start(), pidFile);
}
Use this wrapper script:
[System.IO.File]::WriteAllText(
$env:EASYCODE_PREVIEW_PID_FILE,
[string]$PID,
[System.Text.Encoding]::UTF8
)
& $env:ComSpec /d /s /c $env:EASYCODE_PREVIEW_COMMAND
exit $LASTEXITCODE
Expose only a package-private startCommandForTesting(...) that calls the same private start(...) path as production.
- Step 7: Terminate the managed process tree
At the start of RunPreviewProcessKiller.stopProcess:
ManagedPreviewProcess managed = process instanceof ManagedPreviewProcess
? (ManagedPreviewProcess) process
: null;
if (managed != null)
{
long pid = managed.waitForRootPid(GRACEFUL_WAIT_MILLIS);
if (pid > 0L)
{
killWindowsProcessTree(pid);
waitForExit(process, GRACEFUL_WAIT_MILLIS);
}
}
Then retain the existing destroy() and destroyForcibly() fallback. In finally, call managed.deletePidFile().
Implement the overridable OS seam:
protected boolean killWindowsProcessTree(long pid)
{
return executeAndWait(Arrays.asList(
"taskkill", "/PID", String.valueOf(pid), "/T", "/F"));
}
Change the current quiet command helper to return true only when the command exits with status 0; callers that do not care may ignore the result.
- Step 8: Run focused tests and verify GREEN
Run:
mvn -pl ruoyi-generator -Dtest=RunPreviewProcessKillerTest,RunPreviewProcessRunnerTest test
Expected: both test classes pass with zero failures.
- Step 9: Commit the process-tree fix
git add ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/ManagedPreviewProcess.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/RunPreviewProcessRunner.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/RunPreviewProcessKiller.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/RunPreviewProcessKillerTest.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/RunPreviewProcessRunnerTest.java
git commit -m "Fix run preview process tree cleanup"
Task 2: Persistent Shared npm Cache
Files:
-
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectRunPreviewServiceImpl.java -
Modify:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectRunPreviewServiceImplTest.java -
Step 1: Change service assertions to the desired cache behavior
In startCreatesWorkspaceInitializesDatabaseAndStartsProcesses, assert:
assertEquals("true", frontendEnvCaptor.getValue().get("NPM_CONFIG_PREFER_OFFLINE"));
assertEquals("false", frontendEnvCaptor.getValue().get("NPM_CONFIG_OFFLINE"));
assertEquals(".npm-cache", new File(frontendCache).getName());
assertEquals(new File(status.getWorkspacePath()).getParentFile().getCanonicalFile(),
new File(frontendCache).getParentFile().getCanonicalFile());
In startSupportsCombinedDownloadPackageLayout, replace the separate-cache assertion with:
assertEquals(frontendCache, adminFrontendCache);
assertEquals(".npm-cache", new File(frontendCache).getName());
- Step 2: Run the service test and verify RED
Run:
mvn -pl ruoyi-generator -Dtest=FrontProjectRunPreviewServiceImplTest test
Expected: failures show separate frontend / admin-frontend cache paths and NPM_CONFIG_PREFER_OFFLINE=false.
- Step 3: Configure one cache under the preview root
After creating the workspace, resolve:
File sharedNpmCache = new File(new File(workspaceRoot).getCanonicalFile(), ".npm-cache");
Pass sharedNpmCache to putNpmPreviewEnvironment for both frontend environments.
Inside putNpmPreviewEnvironment, retain online fallback:
environment.put("NPM_CONFIG_OFFLINE", "false");
environment.put("npm_config_offline", "false");
environment.put("NPM_CONFIG_PREFER_OFFLINE", "true");
environment.put("npm_config_prefer_offline", "true");
environment.put("NPM_CONFIG_PREFER_ONLINE", "false");
environment.put("npm_config_prefer_online", "false");
- Step 4: Run the service test and verify GREEN
Run:
mvn -pl ruoyi-generator -Dtest=FrontProjectRunPreviewServiceImplTest test
Expected: all tests in the class pass.
- Step 5: Commit the shared-cache fix
git add ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectRunPreviewServiceImpl.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectRunPreviewServiceImplTest.java
git commit -m "Reuse npm cache for run previews"
Task 3: Regression Verification
Files:
-
Verify: all files changed in Tasks 1 and 2
-
Step 1: Run all focused run-preview tests
Run:
mvn -pl ruoyi-generator -Dtest=RunPreviewProcessRunnerTest,RunPreviewProcessKillerTest,FrontProjectRunPreviewServiceImplTest test
Expected: zero failures and zero errors.
- Step 2: Run the generator module test suite
Run:
mvn -pl ruoyi-generator test
Expected: BUILD SUCCESS.
- Step 3: Inspect the scoped diff
Run:
git diff HEAD~2 -- ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/ManagedPreviewProcess.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/RunPreviewProcessRunner.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/RunPreviewProcessKiller.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectRunPreviewServiceImpl.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/RunPreviewProcessRunnerTest.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/RunPreviewProcessKillerTest.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectRunPreviewServiceImplTest.java
Expected: only process-tree management, shared-cache configuration, and their tests are present.
- Step 4: Perform a live process-tree smoke test
Start a managed Windows command through the runner test helper, verify the PID file appears, invoke RunPreviewProcessKiller.stop, and verify the child command is no longer alive. This behavior is covered by the focused integration test and must pass on the current Windows host.
- Step 5: Report the operational cleanup requirement
The code fix cannot retroactively attach PID metadata to npm processes started before deployment. Identify any pre-fix preview npm/node processes still running and stop only those process IDs after confirming they belong to directories under preview-workspaces.