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>
238 lines
7.6 KiB
PHP
238 lines
7.6 KiB
PHP
<?php
|
|
|
|
namespace App\RSpade\Core\Bootstrap;
|
|
|
|
use Exception;
|
|
use RuntimeException;
|
|
use App\RSpade\Core\Locks\RsxLocks;
|
|
|
|
/**
|
|
* RsxBootstrap - Early initialization for the RSpade framework
|
|
*
|
|
* This class handles critical early initialization tasks that must occur
|
|
* before the manifest or other systems are loaded. Most importantly, it
|
|
* acquires the global application read lock that ensures we can coordinate
|
|
* with other processes for exclusive operations like manifest rebuilding.
|
|
*/
|
|
class RsxBootstrap
|
|
{
|
|
/**
|
|
* The global application lock token
|
|
* @var string|null
|
|
*/
|
|
private static ?string $application_lock_token = null;
|
|
|
|
/**
|
|
* Whether bootstrap has been initialized
|
|
* @var bool
|
|
*/
|
|
private static bool $initialized = false;
|
|
|
|
/**
|
|
* Initialize the RSpade framework
|
|
*
|
|
* This MUST be called as early as possible in the application lifecycle,
|
|
* ideally in the bootstrap/app.php file right after Laravel boots.
|
|
*
|
|
* Acquires a global READ lock for the application that can be upgraded
|
|
* to WRITE when exclusive operations like manifest rebuilding are needed.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function initialize(): void
|
|
{
|
|
if (self::$initialized) {
|
|
return;
|
|
}
|
|
|
|
// Acquire global application read lock
|
|
// This ensures we can coordinate with other processes
|
|
self::__acquire_application_lock();
|
|
|
|
// Register shutdown handler to release lock
|
|
register_shutdown_function([self::class, 'shutdown']);
|
|
|
|
self::$initialized = true;
|
|
}
|
|
|
|
/**
|
|
* Acquire the global application lock
|
|
*
|
|
* All PHP processes acquire a READ lock by default.
|
|
* Artisan commands and always_write_lock mode use WRITE lock.
|
|
* This can be upgraded to WRITE for exclusive operations.
|
|
*
|
|
* @return void
|
|
*/
|
|
private static function __acquire_application_lock(): void
|
|
{
|
|
$always_write = config('rsx.locking.always_write_lock', false);
|
|
|
|
// Detect artisan commands by checking if running from CLI and the script name contains 'artisan'
|
|
$is_artisan = php_sapi_name() === 'cli' &&
|
|
isset($_SERVER['argv'][0]) &&
|
|
strpos($_SERVER['argv'][0], 'artisan') !== false;
|
|
|
|
// Determine lock type
|
|
// Artisan commands always get write lock
|
|
// Or if always_write_lock is enabled
|
|
$lock_type = ($is_artisan || $always_write) ? RsxLocks::WRITE_LOCK : RsxLocks::READ_LOCK;
|
|
|
|
// Issue warning if always_write_lock is enabled in production CLI mode
|
|
if ($always_write && php_sapi_name() === 'cli' && app()->environment('production')) {
|
|
fwrite(STDERR, "\033[33mWARNING: RSX_ALWAYS_WRITE_LOCK is enabled in production. " .
|
|
'ALL requests are serialized with exclusive write lock. ' .
|
|
"This severely impacts performance and should only be used for critical operations.\033[0m\n");
|
|
}
|
|
|
|
console_debug('CONCURRENCY', "Acquiring global application '" . ($lock_type == RsxLocks::WRITE_LOCK ? 'WRITE' : 'READ') . "'lock");
|
|
|
|
try {
|
|
self::$application_lock_token = RsxLocks::get_lock(
|
|
RsxLocks::SERVER_LOCK,
|
|
RsxLocks::LOCK_APPLICATION,
|
|
$lock_type,
|
|
config('rsx.locking.timeout', 30)
|
|
);
|
|
} catch (Exception $e) {
|
|
// If we can't acquire the lock, the application cannot proceed
|
|
shouldnt_happen(
|
|
"Failed to acquire application {$lock_type} lock: " . $e->getMessage() . "\n" .
|
|
'This likely means another process has an exclusive lock.'
|
|
);
|
|
}
|
|
|
|
console_debug('CONCURRENCY', 'Global application lock acquired');
|
|
|
|
// Check if log rotation is needed (development mode only)
|
|
// TODO: cant log rotate before getting build key
|
|
// We logrotate somewhere else, dont we?
|
|
// if (env('APP_ENV') !== 'production') {
|
|
// $current_hash = Manifest::get_build_key();
|
|
// $version_file = storage_path('rsx-tmp/log_version');
|
|
// $should_rotate = false;
|
|
|
|
// // Ensure directory exists
|
|
// $dir = dirname($version_file);
|
|
// if (!is_dir($dir)) {
|
|
// @mkdir($dir, 0755, true);
|
|
// }
|
|
|
|
// // Check if version file exists and compare hash
|
|
// if (file_exists($version_file)) {
|
|
// $stored_hash = trim(file_get_contents($version_file));
|
|
// if ($stored_hash !== $current_hash) {
|
|
// $should_rotate = true;
|
|
// }
|
|
// } else {
|
|
// // File doesn't exist, first run
|
|
// $should_rotate = true;
|
|
// }
|
|
|
|
// // Rotate logs if needed
|
|
// if ($should_rotate) {
|
|
// Debugger::logrotate();
|
|
|
|
// // Update version file with new hash
|
|
// file_put_contents($version_file, $current_hash);
|
|
// }
|
|
// }
|
|
}
|
|
|
|
/**
|
|
* Upgrade the application lock to exclusive write mode
|
|
*
|
|
* Used when performing operations that require exclusive access,
|
|
* such as manifest rebuilding or bundle compilation.
|
|
*
|
|
* @return string New lock token after upgrade
|
|
* @throws RuntimeException if upgrade fails
|
|
*/
|
|
public static function upgrade_to_write_lock(): string
|
|
{
|
|
if (!self::$application_lock_token) {
|
|
shouldnt_happen('Cannot upgrade lock - no application lock held');
|
|
}
|
|
|
|
try {
|
|
$new_token = RsxLocks::upgrade_lock(
|
|
self::$application_lock_token,
|
|
config('rsx.locking.timeout', 30)
|
|
);
|
|
|
|
self::$application_lock_token = $new_token;
|
|
|
|
return $new_token;
|
|
} catch (Exception $e) {
|
|
throw new RuntimeException(
|
|
'Failed to upgrade application lock to write mode: ' . $e->getMessage()
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the current application lock token
|
|
*
|
|
* @return string|null
|
|
*/
|
|
public static function get_application_lock_token(): ?string
|
|
{
|
|
return self::$application_lock_token;
|
|
}
|
|
|
|
/**
|
|
* Check if we hold the application lock
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function has_application_lock(): bool
|
|
{
|
|
return self::$application_lock_token !== null;
|
|
}
|
|
|
|
/**
|
|
* Shutdown handler - releases locks
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function shutdown(): void
|
|
{
|
|
if (self::$application_lock_token) {
|
|
try {
|
|
RsxLocks::release_lock(self::$application_lock_token);
|
|
} catch (Exception $e) {
|
|
// Ignore errors during shutdown
|
|
} finally {
|
|
self::$application_lock_token = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Force release of application lock (emergency use only)
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function force_release(): void
|
|
{
|
|
if (self::$application_lock_token) {
|
|
RsxLocks::release_lock(self::$application_lock_token);
|
|
self::$application_lock_token = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Temporarily release application lock for testing
|
|
* Used by rsx:debug to prevent lock contention with Playwright
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function temporarily_release_lock(): void
|
|
{
|
|
if (self::$application_lock_token) {
|
|
RsxLocks::release_lock(self::$application_lock_token);
|
|
self::$application_lock_token = null;
|
|
}
|
|
}
|
|
}
|