Files
rspade_system/app/RSpade/CodeQuality/Support/Js_CodeQuality_Rpc.php
root 84ca3dfe42 Fix code quality violations and rename select input components
Move small tasks from wishlist to todo, update npm packages
Replace #[Auth] attributes with manual auth checks and code quality rule
Remove on_jqhtml_ready lifecycle method from framework
Complete ACL system with 100-based role indexing and /dev/acl tester
WIP: ACL system implementation with debug instrumentation
Convert rsx:check JS linting to RPC socket server
Clean up docs and fix $id→$sid in man pages, remove SSR/FPC feature
Reorganize wishlists: priority order, mark sublayouts complete, add email
Update model_fetch docs: mark MVP complete, fix enum docs, reorganize
Comprehensive documentation overhaul: clarity, compression, and critical rules
Convert Contacts/Projects CRUD to Model.fetch() and add fetch_or_null()
Add JS ORM relationship lazy-loading and fetch array handling
Add JS ORM relationship fetching and CRUD documentation
Fix ORM hydration and add IDE resolution for Base_* model stubs
Rename Json_Tree_Component to JS_Tree_Debug_Component and move to framework
Enhance JS ORM infrastructure and add Json_Tree class name badges

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:39:43 +00:00

353 lines
11 KiB
PHP
Executable File

<?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);
}
}
}