From d3bc3354c9705dfc6dc5e9b9adb5eb64d41e4c49 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 30 Aug 2026 23:01:25 +0900 Subject: feat(plugin): serve ProcessExecutor as a proxy stub Composer reaches this class two ways: the object graph hands one out through Composer::getLoop()->getProcessExecutor(), and plugins write `new ProcessExecutor($io)` freely. Both bind to a Rust-side entity, so the timeout the run shares -- seeded from process-timeout and rewritten while the run is in flight -- has one value instead of one per world, and the executor can still be passed to the classes that take one (`new Filesystem($process)`). Three things the stub generator was missing came with it: - By-ref parameters. The call carries their positions and the answer carries what each holds afterwards; a position the answer omits was never assigned to, which is what PHP does with an untouched by-ref parameter. ProcessExecutor::execute is the only one on a proxied class. - Argument arity, reproduced where the real body reads func_num_args(). execute($cmd) forwards the child's output and execute($cmd, $out) captures it, and nothing but the argument count separates the two. - Static methods that cannot run in the worker. One that reads a static property the Rust side owns, or that reaches a guarded class, forwards through __shirabeCallStatic instead of being materialized. That also fixes Filesystem::isLocalPath and getPlatformPath, whose materialized bodies called the guarded Composer\Util\Platform. The async surface stays an explicit error. executeAsync resolves its promise with a Symfony Process, whose proc_open() resource and pipes belong to whichever process called start(), so a Rust-side spawn has none to hand back; running the real start() in the worker needs a promise representation that crosses the boundary unresolved. The fixture project drives the whole synchronous surface from plugin code and compares the trace against upstream Composer byte for byte. Co-Authored-By: Claude Opus 5 (1M context) --- .../php/stubs/Composer/Util/Filesystem.php | 32 ++-- .../php/stubs/Composer/Util/ProcessExecutor.php | 176 +++++++++++++++++++++ 2 files changed, 188 insertions(+), 20 deletions(-) create mode 100644 crates/shirabe-php-rpc/php/stubs/Composer/Util/ProcessExecutor.php (limited to 'crates/shirabe-php-rpc/php/stubs/Composer') diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php b/crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php index e1532889..c66bcf56 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php @@ -83,26 +83,6 @@ class Filesystem implements \ShirabeRustStub return $path; } - public static function isLocalPath(string $path) - { - // on windows, \\foo indicates network paths so we exclude those from local paths, however it is unsafe - // on linux as file:////foo (which would be a network path \\foo on windows) will resolve to /foo which could be a local path - if (Platform::isWindows()) { - return Preg::isMatch('{^(file://(?!//)|/(?!/)|/?[a-z]:[\\\\/]|\.\.[\\\\/]|[a-z0-9_.-]+[\\\\/])}i', $path); - } - - return Preg::isMatch('{^(file://|/|/?[a-z]:[\\\\/]|\.\.[\\\\/]|[a-z0-9_.-]+[\\\\/])}i', $path); - } - - public static function getPlatformPath(string $path) - { - if (Platform::isWindows()) { - $path = Preg::replace('{^(?:file:///([a-z]):?/)}i', 'file://$1:/', $path); - } - - return Preg::replace('{^file://}i', '', $path); - } - public static function isReadable(string $path) { if (is_readable($path)) { @@ -121,6 +101,18 @@ class Filesystem implements \ShirabeRustStub return false; } + // Forwarded rather than materialized: it references Composer\Util\Platform, which a guard shadows in the worker. + public static function isLocalPath(string $path) + { + return \ShirabeRpcRuntime::callRust(0, '__shirabeCallStatic', [self::class, 'isLocalPath', [$path]]); + } + + // Forwarded rather than materialized: it references Composer\Util\Platform, which a guard shadows in the worker. + public static function getPlatformPath(string $path) + { + return \ShirabeRpcRuntime::callRust(0, '__shirabeCallStatic', [self::class, 'getPlatformPath', [$path]]); + } + public function remove(string $file) { return \ShirabeRpcRuntime::callRust($this->__rhandle, 'remove', [$file]); diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Util/ProcessExecutor.php b/crates/shirabe-php-rpc/php/stubs/Composer/Util/ProcessExecutor.php new file mode 100644 index 00000000..dfa1f0d2 --- /dev/null +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Util/ProcessExecutor.php @@ -0,0 +1,176 @@ +__rhandle = $rhandle; + $this->__epoch = $epoch; + } + + public function __destruct() + { + \ShirabeRustObjectRegistry::release($this->__rhandle); + } + + public function __shirabeRustHandleDescriptor(): array + { + return [ + '__rhandle' => $this->__rhandle, + '__class' => static::class, + '__epoch' => $this->__epoch, + ]; + } + + public function __clone() + { + // PHP has already shallow-copied this stub, so both copies would point at one + // entity and release it twice. The Rust side clones the entity instead, applying + // whatever __clone semantics the real class defines, and this copy rebinds to the + // fresh handle. Entities without clone semantics answer with an explicit error. + [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust($this->__rhandle, '__shirabeClone', []); + \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); + } + + public function __get($name) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]); + } + + public function __set($name, $value): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); + } + + public function __isset($name): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__isset', [$name]); + } + + public function __unset($name): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__unset', [$name]); + } + + public function __construct(?IOInterface $io = null) + { + [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$io]]); + \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); + } + + private const STATUS_QUEUED = 1; + private const STATUS_STARTED = 2; + private const STATUS_COMPLETED = 3; + private const STATUS_FAILED = 4; + private const STATUS_ABORTED = 5; + private const BUILTIN_CMD_COMMANDS = [ + 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', + 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', + 'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause', + 'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set', + 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', + ]; + private const GIT_CMDS_NEED_GIT_DIR = [ + ['show'], + ['log'], + ['branch'], + ['remote', 'set-url'], + ]; + + // Forwarded rather than materialized: it reads the static property $timeout, whose value the Rust side owns. + public static function getTimeout(): int + { + return \ShirabeRpcRuntime::callRust(0, '__shirabeCallStatic', [self::class, 'getTimeout', []]); + } + + // Forwarded rather than materialized: it reads the static property $timeout, whose value the Rust side owns. + public static function setTimeout(int $timeout): void + { + \ShirabeRpcRuntime::callRust(0, '__shirabeCallStatic', [self::class, 'setTimeout', [$timeout]]); + } + + // Forwarded rather than materialized: it references Composer\Util\Platform, which a guard shadows in the worker. + public static function escape($argument): string + { + return \ShirabeRpcRuntime::callRust(0, '__shirabeCallStatic', [self::class, 'escape', [$argument]]); + } + + public function execute($command, &$output = null, ?string $cwd = null): int + { + $__args = [$command, $output, $cwd]; + array_splice($__args, func_num_args()); + $__out = []; + $__result = \ShirabeRpcRuntime::callRust($this->__rhandle, 'execute', $__args, [1], $__out); + if (array_key_exists(1, $__out)) { + $output = $__out[1]; + } + return $__result; + } + + public function executeTty($command, ?string $cwd = null): int + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'executeTty', [$command, $cwd]); + } + + public function executeAsync($command, ?string $cwd = null): PromiseInterface + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'executeAsync', [$command, $cwd]); + } + + public function setMaxJobs(int $maxJobs): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setMaxJobs', [$maxJobs]); + } + + public function resetMaxJobs(): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'resetMaxJobs', []); + } + + public function wait($index = null): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'wait', [$index]); + } + + public function enableAsync(): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'enableAsync', []); + } + + public function countActiveJobs($index = null): int + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'countActiveJobs', [$index]); + } + + public function splitLines(?string $output): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'splitLines', [$output]); + } + + public function getErrorOutput(): string + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getErrorOutput', []); + } + + public function requiresGitDirEnv($command): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'requiresGitDirEnv', [$command]); + } +} -- cgit v1.3.1-4-g156e