Add 100+ automated unit tests from .expect file specifications Add session system test Add rsx:constants:regenerate command test Add rsx:logrotate command test Add rsx:clean command test Add rsx:manifest:stats command test Add model enum system test Add model mass assignment prevention test Add rsx:check command test Add migrate:status command test 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
353 lines
11 KiB
PHP
353 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\RSpade\CodeQuality\Support;
|
|
|
|
use Symfony\Component\Process\Process;
|
|
|
|
/**
|
|
* JavaScript Code Quality RPC Client
|
|
*
|
|
* Manages a persistent Node.js RPC server for JavaScript linting and
|
|
* this-usage analysis. This avoids spawning thousands of Node processes
|
|
* during code quality checks.
|
|
*
|
|
* RPC Methods:
|
|
* - lint: Check JavaScript syntax using Babel parser
|
|
* - analyze_this: Analyze 'this' usage patterns using Acorn
|
|
*/
|
|
class Js_CodeQuality_Rpc
|
|
{
|
|
/**
|
|
* Node.js RPC server script path
|
|
*/
|
|
protected const RPC_SERVER_SCRIPT = 'app/RSpade/CodeQuality/Support/resource/js-code-quality-server.js';
|
|
|
|
/**
|
|
* Unix socket path for RPC server
|
|
*/
|
|
protected const RPC_SOCKET = 'storage/rsx-tmp/js-code-quality-server.sock';
|
|
|
|
/**
|
|
* RPC server process
|
|
*/
|
|
protected static $rpc_server_process = null;
|
|
|
|
/**
|
|
* Request ID counter
|
|
*/
|
|
protected static $request_id = 0;
|
|
|
|
/**
|
|
* Lint a JavaScript file for syntax errors
|
|
*
|
|
* @param string $file_path Path to the JavaScript file
|
|
* @return array|null Error info array or null if no errors
|
|
*/
|
|
public static function lint(string $file_path): ?array
|
|
{
|
|
// Start RPC server on first use (lazy initialization)
|
|
if (static::$rpc_server_process === null) {
|
|
static::start_rpc_server();
|
|
}
|
|
|
|
return static::_lint_via_rpc($file_path);
|
|
}
|
|
|
|
/**
|
|
* Analyze a JavaScript file for 'this' usage violations
|
|
*
|
|
* @param string $file_path Path to the JavaScript file
|
|
* @return array Violations array (may be empty)
|
|
*/
|
|
public static function analyze_this(string $file_path): array
|
|
{
|
|
// Start RPC server on first use (lazy initialization)
|
|
if (static::$rpc_server_process === null) {
|
|
static::start_rpc_server();
|
|
}
|
|
|
|
return static::_analyze_this_via_rpc($file_path);
|
|
}
|
|
|
|
/**
|
|
* Lint via RPC server
|
|
*/
|
|
protected static function _lint_via_rpc(string $file_path): ?array
|
|
{
|
|
$base_path = function_exists('base_path') ? base_path() : '/var/www/html/system';
|
|
$socket_path = $base_path . '/' . self::RPC_SOCKET;
|
|
|
|
try {
|
|
$sock = @stream_socket_client("unix://{$socket_path}", $errno, $errstr, 0.5);
|
|
if (!$sock) {
|
|
throw new \RuntimeException("Failed to connect to RPC server: {$errstr}");
|
|
}
|
|
|
|
// Set blocking mode for reliable reads
|
|
stream_set_blocking($sock, true);
|
|
|
|
// Send lint request
|
|
$request = [
|
|
'id' => ++static::$request_id,
|
|
'method' => 'lint',
|
|
'files' => [$file_path]
|
|
];
|
|
|
|
fwrite($sock, json_encode($request) . "\n");
|
|
|
|
// Read response
|
|
$response = fgets($sock);
|
|
fclose($sock);
|
|
|
|
if (!$response) {
|
|
throw new \RuntimeException("No response from RPC server");
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
|
|
if (!$data || !is_array($data)) {
|
|
throw new \RuntimeException("Invalid JSON response from RPC server");
|
|
}
|
|
|
|
if (isset($data['error'])) {
|
|
throw new \RuntimeException("RPC error: " . $data['error']);
|
|
}
|
|
|
|
if (!isset($data['results'][$file_path])) {
|
|
throw new \RuntimeException("No result for file in RPC response");
|
|
}
|
|
|
|
$result = $data['results'][$file_path];
|
|
|
|
if ($result['status'] === 'success') {
|
|
// Return the error info if present, null if no errors
|
|
return $result['error'];
|
|
}
|
|
|
|
// Handle error response
|
|
if ($result['status'] === 'error' && isset($result['error'])) {
|
|
throw new \RuntimeException("Lint error: " . ($result['error']['message'] ?? 'Unknown error'));
|
|
}
|
|
|
|
return null;
|
|
|
|
} catch (\Exception $e) {
|
|
throw new \RuntimeException(
|
|
"JavaScript lint RPC error for {$file_path}: " . $e->getMessage()
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Analyze this-usage via RPC server
|
|
*/
|
|
protected static function _analyze_this_via_rpc(string $file_path): array
|
|
{
|
|
$base_path = function_exists('base_path') ? base_path() : '/var/www/html/system';
|
|
$socket_path = $base_path . '/' . self::RPC_SOCKET;
|
|
|
|
try {
|
|
$sock = @stream_socket_client("unix://{$socket_path}", $errno, $errstr, 0.5);
|
|
if (!$sock) {
|
|
throw new \RuntimeException("Failed to connect to RPC server: {$errstr}");
|
|
}
|
|
|
|
// Set blocking mode for reliable reads
|
|
stream_set_blocking($sock, true);
|
|
|
|
// Send analyze_this request
|
|
$request = [
|
|
'id' => ++static::$request_id,
|
|
'method' => 'analyze_this',
|
|
'files' => [$file_path]
|
|
];
|
|
|
|
fwrite($sock, json_encode($request) . "\n");
|
|
|
|
// Read response
|
|
$response = fgets($sock);
|
|
fclose($sock);
|
|
|
|
if (!$response) {
|
|
throw new \RuntimeException("No response from RPC server");
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
|
|
if (!$data || !is_array($data)) {
|
|
throw new \RuntimeException("Invalid JSON response from RPC server");
|
|
}
|
|
|
|
if (isset($data['error'])) {
|
|
throw new \RuntimeException("RPC error: " . $data['error']);
|
|
}
|
|
|
|
if (!isset($data['results'][$file_path])) {
|
|
throw new \RuntimeException("No result for file in RPC response");
|
|
}
|
|
|
|
$result = $data['results'][$file_path];
|
|
|
|
if ($result['status'] === 'success') {
|
|
return $result['violations'] ?? [];
|
|
}
|
|
|
|
// Handle error response - return empty violations, don't fail
|
|
return [];
|
|
|
|
} catch (\Exception $e) {
|
|
// Log error but don't fail the check
|
|
if (function_exists('console_debug')) {
|
|
console_debug('JS_CODE_QUALITY', "RPC error for {$file_path}: " . $e->getMessage());
|
|
}
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start the RPC server
|
|
*/
|
|
public static function start_rpc_server(): void
|
|
{
|
|
$base_path = function_exists('base_path') ? base_path() : '/var/www/html/system';
|
|
$socket_path = $base_path . '/' . self::RPC_SOCKET;
|
|
|
|
if (file_exists($socket_path)) {
|
|
// Server might be running, force stop it
|
|
if (function_exists('console_debug')) {
|
|
console_debug('JS_CODE_QUALITY', 'Found existing socket, forcing shutdown');
|
|
}
|
|
static::stop_rpc_server(force: true);
|
|
}
|
|
|
|
// Start new server
|
|
$server_script = $base_path . '/' . self::RPC_SERVER_SCRIPT;
|
|
|
|
if (function_exists('console_debug')) {
|
|
console_debug('JS_CODE_QUALITY', 'Starting RPC server: ' . $server_script);
|
|
}
|
|
|
|
$process = new Process([
|
|
'node',
|
|
$server_script,
|
|
'--socket=' . $socket_path
|
|
]);
|
|
|
|
$process->start();
|
|
static::$rpc_server_process = $process;
|
|
|
|
// Register shutdown handler
|
|
register_shutdown_function([self::class, 'stop_rpc_server']);
|
|
|
|
// Wait for server to be ready (ping/pong up to 10 seconds)
|
|
if (function_exists('console_debug')) {
|
|
console_debug('JS_CODE_QUALITY', 'Waiting for RPC server to be ready...');
|
|
}
|
|
|
|
$max_attempts = 200; // 10 seconds (50ms * 200)
|
|
$ready = false;
|
|
|
|
for ($i = 0; $i < $max_attempts; $i++) {
|
|
usleep(50000); // 50ms
|
|
|
|
if (file_exists($socket_path)) {
|
|
// Try to ping
|
|
if (static::ping_rpc_server()) {
|
|
$ready = true;
|
|
if (function_exists('console_debug')) {
|
|
console_debug('JS_CODE_QUALITY', 'RPC server ready after ' . ($i * 50) . 'ms');
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$ready) {
|
|
static::stop_rpc_server();
|
|
throw new \RuntimeException('Failed to start JS Code Quality RPC server - timeout after 10 seconds');
|
|
}
|
|
|
|
if (function_exists('console_debug')) {
|
|
console_debug('JS_CODE_QUALITY', 'RPC server started successfully');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ping the RPC server
|
|
*/
|
|
protected static function ping_rpc_server(): bool
|
|
{
|
|
$base_path = function_exists('base_path') ? base_path() : '/var/www/html/system';
|
|
$socket_path = $base_path . '/' . self::RPC_SOCKET;
|
|
|
|
try {
|
|
$sock = @stream_socket_client("unix://{$socket_path}", $errno, $errstr, 0.5);
|
|
if (!$sock) {
|
|
return false;
|
|
}
|
|
|
|
// Set blocking mode for reliable reads
|
|
stream_set_blocking($sock, true);
|
|
|
|
// Send ping
|
|
fwrite($sock, json_encode(['id' => ++static::$request_id, 'method' => 'ping']) . "\n");
|
|
|
|
// Read response
|
|
$response = fgets($sock);
|
|
fclose($sock);
|
|
|
|
if (!$response) {
|
|
return false;
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
return isset($data['result']) && $data['result'] === 'pong';
|
|
|
|
} catch (\Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop the RPC server
|
|
*/
|
|
public static function stop_rpc_server(bool $force = false): void
|
|
{
|
|
$base_path = function_exists('base_path') ? base_path() : '/var/www/html/system';
|
|
$socket_path = $base_path . '/' . self::RPC_SOCKET;
|
|
|
|
// Try graceful shutdown
|
|
if (file_exists($socket_path)) {
|
|
try {
|
|
$sock = @stream_socket_client("unix://{$socket_path}", $errno, $errstr, 0.5);
|
|
if ($sock) {
|
|
fwrite($sock, json_encode(['id' => 0, 'method' => 'shutdown']) . "\n");
|
|
fclose($sock);
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Ignore errors
|
|
}
|
|
}
|
|
|
|
// Only wait and force kill if $force = true
|
|
if ($force && static::$rpc_server_process && static::$rpc_server_process->isRunning()) {
|
|
if (function_exists('console_debug')) {
|
|
console_debug('JS_CODE_QUALITY', 'Force stopping RPC server');
|
|
}
|
|
|
|
// Wait for graceful shutdown
|
|
sleep(1);
|
|
|
|
// Force kill if still running
|
|
if (static::$rpc_server_process->isRunning()) {
|
|
static::$rpc_server_process->stop(3, SIGTERM);
|
|
}
|
|
}
|
|
|
|
// Clean up socket file
|
|
if (file_exists($socket_path)) {
|
|
@unlink($socket_path);
|
|
}
|
|
}
|
|
}
|