Skip to content

Conversation

@adamziel
Copy link
Collaborator

@adamziel adamziel commented Dec 9, 2025

Motivation

This PR changes the Playground CLI so that every PHP subprocess spawned by another PHP instance (e.g. proc_open('php -v')) is started in a separate OS process.

When the sandboxed spawn handler detects a PHP subprocess request, it now creates an entirely new Node.js worker thread running. Any file locks acquired by the subprocess will properly conflict with the parent's locks.

Before this PR two PHP instances running in the same OS process could acquire overlapping OS-level locks (via fcntl() and LockFileEx()).

Implementation

The way I'm augmenting sandboxedSpawnHandlerFactory calls in this PR is somewhat convoluted. I'd like to revisit it once we finish the OS-locks-related refactoring of the CLI code.

The sandboxedSpawnHandlerFactory now accepts a callback that creates a new PHP worker. When a PHP subprocess is requested via proc_open(), the CLI:

  1. Spawns a new Node.js worker thread via spawnWorkerThread('v1')
  2. Boots a fresh PHP runtime in that worker using bootWorker()
  3. Connects the subprocess I/O streams to the parent
  4. Terminates the worker when the subprocess exits

The key change is in worker-thread-v1.ts where createPHPWorker() now spawns an entirely new OS process instead of reusing the existing PHP process manager's pool.

Follow-up work

  • Refactor PlaygroundCliBlueprintV2Worker
  • Refactor @php-wasm/cli
  • Limit PHPProcessManager to one PHP instance per worker even for request handling
  • Get rid of PHPProcessManager usage in CLI workers

@adamziel adamziel force-pushed the do-max-1-php-instance-per-cli-worker branch from af6e674 to ad31298 Compare December 9, 2025 18:04
@adamziel adamziel marked this pull request as ready for review December 9, 2025 18:40
adamziel added a commit that referenced this pull request Dec 9, 2025
When PHP calls proc_open() to spawn a subprocess, each subprocess now runs
in a separate Node.js worker thread. This ensures OS-level file locks via
fcntl() and LockFileEx() properly conflict between parent and child processes,
matching native PHP behavior.

This is the V2 counterpart to the V1 worker change in PR #3001.
Copy link
Member

@brandonpayton brandonpayton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes read well to me, and the tests are passing.

There is a random hello() worker method that may be leftover from dev, but it can be easily removed later. So I'll go ahead and merge this.

Comment on lines +17 to +20
getPHPInstance: () => Promise<{
php: PHP | Remote<PHPWorker>;
reap: () => void;
}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.

Comment on lines +212 to +215
async hello() {
return 'hello';
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello! Is this left over from a development test, and do we still want it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it's a testing leftover, good spot

}
)
);
const worker = spawnWorkerThread(workerType, { onExit });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice change -- moving the new Promise() logic into spawnWorkerThread().

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this enables reusing it :)

@brandonpayton brandonpayton merged commit 947ed76 into trunk Dec 9, 2025
32 checks passed
@brandonpayton brandonpayton deleted the do-max-1-php-instance-per-cli-worker branch December 9, 2025 21:57
adamziel added a commit that referenced this pull request Dec 10, 2025
When PHP calls proc_open() to spawn a subprocess, each subprocess now runs
in a separate Node.js worker thread. This ensures OS-level file locks via
fcntl() and LockFileEx() properly conflict between parent and child processes,
matching native PHP behavior.

This is the V2 counterpart to the V1 worker change in PR #3001.
adamziel added a commit that referenced this pull request Dec 10, 2025
…ndler in Blueprint V2 worker (#3004)

## Motivation

Makes the Blueprints V2 worker spawn a new OS process when PHP calls
`proc_open()`. This enables using OS-level `flock()`, `LockFileEx()`,
etc. Before this PR, `proc_open()` would create a new PHP instance
within the same Node.js worker, which meant all the OS-level file locks
acquired by that "PHP sub-process" would be attributed to the same
OS-level process as the "parent PHP process".

Follows up on the V1 worker PR #3001.

This PR also ships the changes from
#3006:

Introduces `SinglePHPInstanceManager`, a `PHPProcessManager` alternative
to use with `PHPRequestHandler` that uses a single PHP instance without
spawning another one or killing it. Note the internal runtime rotation
after n requests is still supported separately via
`php.enableRuntimeRotation()`. `PHPProcessManager` was designed to use
multiple PHP instances. However, in CLI, we can only use a single PHP
instance per process if we want to support exclusive file locking via
`flock()` and `LockFileEx()`.

## Implementation

### Spawn

The `bootRequestHandler` method now passes a custom PHP-spawning factory
function to `sandboxedSpawnHandlerFactory`.

### Request handler

Introduce a `PHPInstanceManager` interface that abstracts PHP instance
lifecycle management:

```typescript
interface PHPInstanceManager extends AsyncDisposable {
    getPrimaryPhp(): Promise<PHP>;
    acquirePHPInstance(options?: { considerPrimary?: boolean }): Promise<AcquiredPHP>;
}
```

Two implementations:
- **PHPProcessManager**: Multiple instances with semaphore-based
concurrency control (web)
- **SinglePHPInstanceManager**: Single instance, no concurrency overhead
(CLI)


PHPRequestHandler now accepts either configuration:

```typescript
// Option 1: Provide an instance manager directly
const requestHandler = new PHPRequestHandler({
    instanceManager: new SinglePHPInstanceManager({ phpFactory: ... }),
    documentRoot: '/wordpress',
});

// Option 2: Provide a factory (creates PHPProcessManager internally, unchanged from before)
const requestHandler = new PHPRequestHandler({
    phpFactory: async () => loadWebRuntime('8.0'),
    maxPhpInstances: 5,
});
```

## Testing

CI
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants