From b444f10c8e582fb8d0744b08ce7f7852b3185c5d Mon Sep 17 00:00:00 2001 From: root Date: Tue, 23 Dec 2025 10:12:49 +0000 Subject: [PATCH] Add sudo support for migrate commands when not running as root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Migrate/Migrate_Begin_Command.php | 33 +++------- .../Migrate/Migrate_Commit_Command.php | 18 +----- .../Migrate/Migrate_Rollback_Command.php | 47 +++++---------- .../Migrate/PrivilegedCommandTrait.php | 60 +++++++++++++++++++ 4 files changed, 87 insertions(+), 71 deletions(-) create mode 100755 app/RSpade/Commands/Migrate/PrivilegedCommandTrait.php diff --git a/app/RSpade/Commands/Migrate/Migrate_Begin_Command.php b/app/RSpade/Commands/Migrate/Migrate_Begin_Command.php index 1dd5e0eba..481f65b7d 100755 --- a/app/RSpade/Commands/Migrate/Migrate_Begin_Command.php +++ b/app/RSpade/Commands/Migrate/Migrate_Begin_Command.php @@ -9,6 +9,7 @@ use Symfony\Component\Process\Exception\ProcessFailedException; class Migrate_Begin_Command extends Command { + use PrivilegedCommandTrait; protected $signature = 'migrate:begin'; protected $description = 'Begin a migration session by creating a MySQL snapshot'; @@ -68,7 +69,7 @@ class Migrate_Begin_Command extends Command try { // Step 1: Stop MySQL using supervisorctl $this->info('[1] Stopping MySQL server...'); - shell_exec('supervisorctl stop mysql 2>&1'); + $this->shell_exec_privileged('supervisorctl stop mysql 2>&1'); // Wait a moment for process to die sleep(3); @@ -76,16 +77,16 @@ class Migrate_Begin_Command extends Command // Step 2: Remove old backup if exists if (is_dir($this->backup_dir)) { $this->info('[2] Removing old backup...'); - $this->run_command(['rm', '-rf', $this->backup_dir]); + $this->run_privileged_command(['rm', '-rf', $this->backup_dir]); } - + // Step 3: Copy MySQL data directory $this->info('[3] Creating snapshot of MySQL data...'); - $this->run_command(['cp', '-r', $this->mysql_data_dir, $this->backup_dir]); + $this->run_privileged_command(['cp', '-r', $this->mysql_data_dir, $this->backup_dir]); // Step 4: Start MySQL again using supervisorctl $this->info('[4] Starting MySQL server...'); - shell_exec('supervisorctl start mysql 2>&1'); + $this->shell_exec_privileged('supervisorctl start mysql 2>&1'); // Step 5: Wait for MySQL to be ready $this->info('[5] Waiting for MySQL to be ready...'); @@ -146,31 +147,15 @@ class Migrate_Begin_Command extends Command } catch (\Exception $e) { $this->error('[ERROR] Failed to create snapshot: ' . $e->getMessage()); - + // Try to restart MySQL if it's stopped $this->info('Attempting to restart MySQL...'); - shell_exec('supervisorctl start mysql 2>&1'); - + $this->shell_exec_privileged('supervisorctl start mysql 2>&1'); + return 1; } } - /** - * Run a shell command and check for errors - */ - protected function run_command(array $command, bool $throw_on_error = true): string - { - $process = new Process($command); - $process->setTimeout(60); - $process->run(); - - if ($throw_on_error && !$process->isSuccessful()) { - throw new ProcessFailedException($process); - } - - return $process->getOutput(); - } - /** * Wait for MySQL to be ready for connections */ diff --git a/app/RSpade/Commands/Migrate/Migrate_Commit_Command.php b/app/RSpade/Commands/Migrate/Migrate_Commit_Command.php index 3d48e62f7..72b248e7c 100755 --- a/app/RSpade/Commands/Migrate/Migrate_Commit_Command.php +++ b/app/RSpade/Commands/Migrate/Migrate_Commit_Command.php @@ -10,6 +10,7 @@ use App\RSpade\Core\Database\MigrationPaths; class Migrate_Commit_Command extends Command { + use PrivilegedCommandTrait; protected $signature = 'migrate:commit'; protected $description = 'Commit the migration changes and end the migration session'; @@ -88,7 +89,7 @@ class Migrate_Commit_Command extends Command // Step 1: Remove backup directory if (is_dir($this->backup_dir)) { $this->info('[1] Removing backup snapshot...'); - $this->run_command(['rm', '-rf', $this->backup_dir]); + $this->run_privileged_command(['rm', '-rf', $this->backup_dir]); } // Step 2: Remove migration flag @@ -168,19 +169,4 @@ class Migrate_Commit_Command extends Command return $unran; } - /** - * Run a shell command - */ - protected function run_command(array $command): string - { - $process = new Process($command); - $process->setTimeout(60); - $process->run(); - - if (!$process->isSuccessful()) { - throw new \Exception($process->getErrorOutput()); - } - - return $process->getOutput(); - } } diff --git a/app/RSpade/Commands/Migrate/Migrate_Rollback_Command.php b/app/RSpade/Commands/Migrate/Migrate_Rollback_Command.php index 701e375eb..51e84d8fa 100755 --- a/app/RSpade/Commands/Migrate/Migrate_Rollback_Command.php +++ b/app/RSpade/Commands/Migrate/Migrate_Rollback_Command.php @@ -9,6 +9,7 @@ use Symfony\Component\Process\Exception\ProcessFailedException; class Migrate_Rollback_Command extends Command { + use PrivilegedCommandTrait; protected $signature = 'migrate:rollback'; protected $description = 'Rollback the database to the snapshot taken at migrate:begin'; @@ -42,29 +43,29 @@ class Migrate_Rollback_Command extends Command try { // Step 1: Stop MySQL using supervisorctl $this->info('[1] Stopping MySQL server...'); - shell_exec('supervisorctl stop mysql 2>&1'); - + $this->shell_exec_privileged('supervisorctl stop mysql 2>&1'); + // Wait a moment for process to die sleep(3); - + // Step 2: Clear current MySQL data $this->info('[2] Clearing current database data...'); // Use shell_exec to clear directory contents instead of removing directory - shell_exec("rm -rf {$this->mysql_data_dir}/* 2>/dev/null"); - shell_exec("rm -rf {$this->mysql_data_dir}/.* 2>/dev/null"); - + $this->shell_exec_privileged("rm -rf {$this->mysql_data_dir}/* 2>/dev/null"); + $this->shell_exec_privileged("rm -rf {$this->mysql_data_dir}/.* 2>/dev/null"); + // Step 3: Restore backup $this->info('[3] Restoring database snapshot...'); - shell_exec("cp -r {$this->backup_dir}/* {$this->mysql_data_dir}/"); - shell_exec("cp -r {$this->backup_dir}/.[^.]* {$this->mysql_data_dir}/ 2>/dev/null"); - + $this->shell_exec_privileged("cp -r {$this->backup_dir}/* {$this->mysql_data_dir}/"); + $this->shell_exec_privileged("cp -r {$this->backup_dir}/.[^.]* {$this->mysql_data_dir}/ 2>/dev/null"); + // Step 4: Fix permissions (MySQL needs to own the files) $this->info('[4] Setting correct permissions...'); - $this->run_command(['chown', '-R', 'mysql:mysql', $this->mysql_data_dir]); - + $this->run_privileged_command(['chown', '-R', 'mysql:mysql', $this->mysql_data_dir]); + // Step 5: Start MySQL using supervisorctl $this->info('[5] Starting MySQL server...'); - shell_exec('supervisorctl start mysql 2>&1'); + $this->shell_exec_privileged('supervisorctl start mysql 2>&1'); // Step 6: Wait for MySQL to be ready $this->info('[6] Waiting for MySQL to be ready...'); @@ -91,31 +92,15 @@ class Migrate_Rollback_Command extends Command } catch (\Exception $e) { $this->error('[ERROR] Rollback failed: ' . $e->getMessage()); $this->error('Manual intervention may be required!'); - + // Try to restart MySQL $this->info('Attempting to restart MySQL...'); - shell_exec('supervisorctl start mysql 2>&1'); - + $this->shell_exec_privileged('supervisorctl start mysql 2>&1'); + return 1; } } - /** - * Run a shell command and check for errors - */ - protected function run_command(array $command, bool $throw_on_error = true): string - { - $process = new Process($command); - $process->setTimeout(60); - $process->run(); - - if ($throw_on_error && !$process->isSuccessful()) { - throw new ProcessFailedException($process); - } - - return $process->getOutput(); - } - /** * Wait for MySQL to be ready for connections */ diff --git a/app/RSpade/Commands/Migrate/PrivilegedCommandTrait.php b/app/RSpade/Commands/Migrate/PrivilegedCommandTrait.php new file mode 100755 index 000000000..782c69809 --- /dev/null +++ b/app/RSpade/Commands/Migrate/PrivilegedCommandTrait.php @@ -0,0 +1,60 @@ +is_root() ? '' : 'sudo '; + } + + /** + * Run a shell command string with sudo if needed + */ + protected function shell_exec_privileged(string $command): ?string + { + $full_command = $this->sudo_prefix() . $command; + return shell_exec($full_command); + } + + /** + * Run a Process command array with sudo if needed + */ + protected function run_privileged_command(array $command, bool $throw_on_error = true): string + { + if (!$this->is_root()) { + array_unshift($command, 'sudo'); + } + + $process = new Process($command); + $process->setTimeout(60); + $process->run(); + + if ($throw_on_error && !$process->isSuccessful()) { + throw new ProcessFailedException($process); + } + + return $process->getOutput(); + } +}