]> BookStack Code Mirror - system-cli/commitdiff
Extracted program running to its own class
authorDan Brown <redacted>
Sat, 4 Mar 2023 15:06:38 +0000 (15:06 +0000)
committerDan Brown <redacted>
Thu, 9 Mar 2023 15:28:13 +0000 (15:28 +0000)
scripts/Commands/BackupCommand.php
scripts/Commands/InitCommand.php
scripts/Services/ProgramRunner.php [new file with mode: 0644]
scripts/composer.json
scripts/run

index a607f342a0c55e2e80c6ffecc66bd92176c7b2d2..d41b36d4d5d82702671961f8c0f8d58e1e47b20e 100644 (file)
@@ -2,12 +2,11 @@
 
 namespace Cli\Commands;
 
+use Cli\Services\ProgramRunner;
 use Minicli\Command\CommandCall;
 use RecursiveDirectoryIterator;
 use SplFileInfo;
 use Symfony\Component\Process\Exception\ProcessTimedOutException;
-use Symfony\Component\Process\ExecutableFinder;
-use Symfony\Component\Process\Process;
 use ZipArchive;
 
 final class BackupCommand
@@ -36,6 +35,7 @@ final class BackupCommand
 
         // Create a new ZIP file
         $zipTempFile = tempnam(sys_get_temp_dir(), 'bsbackup');
+        $dumpTempFile = '';
         $zip = new ZipArchive();
         $zip->open($zipTempFile, ZipArchive::CREATE);
 
@@ -48,8 +48,6 @@ final class BackupCommand
             $dumpTempFile = $this->createDatabaseDump();
             echo "Adding database dump to backup archive...\n";
             $zip->addFile($dumpTempFile, 'db.sql');
-            // Delete our temporary DB dump file
-            unlink($dumpTempFile);
         }
 
         if ($handleUploads) {
@@ -64,6 +62,11 @@ final class BackupCommand
 
         // Close off our zip and move it to the required location
         $zip->close();
+        // Delete our temporary DB dump file if exists. Must be done after zip close.
+        if ($dumpTempFile) {
+            unlink($dumpTempFile);
+        }
+        // Move the zip into the target location
         rename($zipTempFile, $zipOutFile);
 
         // Announce end
@@ -161,48 +164,39 @@ final class BackupCommand
             }
         }
 
-        // Create a mysqldump for the BookStack database
-        $executableFinder = new ExecutableFinder();
-        $mysqldumpPath = $executableFinder->find('mysqldump', '/usr/bin/mysqldump');
-
-        if (!is_file($mysqldumpPath)) {
-            throw new CommandError('Could not locate "mysqldump" program');
-        }
-
-        $process = new Process([
-            $mysqldumpPath,
-            '-h', $dbOptions['host'],
-            '-u', $dbOptions['username'],
-            '-p' . $dbOptions['password'],
-            '--single-transaction',
-            '--no-tablespaces',
-            $dbOptions['database'],
-        ]);
-        $process->setTimeout(240);
-        $process->setIdleTimeout(5);
-        $process->start();
-
         $errors = "";
         $hasOutput = false;
-        $dumpTempFile = tempnam(sys_get_temp_dir(), 'bsbackup');
+        $dumpTempFile = tempnam(sys_get_temp_dir(), 'bsdbdump');
         $dumpTempFileResource = fopen($dumpTempFile, 'w');
+
         try {
-            foreach ($process as $type => $data) {
-                if ($process::OUT === $type) {
+            (new ProgramRunner('mysqldump', '/usr/bin/mysqldump'))
+                ->withTimeout(240)
+                ->withIdleTimeout(15)
+                ->runWithoutOutputCallbacks([
+                    '-h', $dbOptions['host'],
+                    '-u', $dbOptions['username'],
+                    '-p' . $dbOptions['password'],
+                    '--single-transaction',
+                    '--no-tablespaces',
+                    $dbOptions['database'],
+                ], function ($data) use (&$dumpTempFileResource, &$hasOutput) {
                     fwrite($dumpTempFileResource, $data);
                     $hasOutput = true;
-                } else { // $process::ERR === $type
-                    $errors .= $data . "\n";
-                }
-            }
-        } catch (ProcessTimedOutException $timedOutException) {
+                }, function ($error) use (&$errors) {
+                    $errors .= $error . "\n";
+                });
+        } catch (\Exception $exception) {
             fclose($dumpTempFileResource);
             unlink($dumpTempFile);
-            if (!$hasOutput) {
-                throw new CommandError("mysqldump operation timed-out.\nNo data has been received so the connection to your database may have failed.");
-            } else {
-                throw new CommandError("mysqldump operation timed-out after data was received.");
+            if ($exception instanceof ProcessTimedOutException) {
+                if (!$hasOutput) {
+                    throw new CommandError("mysqldump operation timed-out.\nNo data has been received so the connection to your database may have failed.");
+                } else {
+                    throw new CommandError("mysqldump operation timed-out after data was received.");
+                }
             }
+            throw new CommandError($exception->getMessage());
         }
 
         fclose($dumpTempFileResource);
index 1c943a21c0928809db341f926795895a38b9a7bc..1041678ee9536ef18c68167c89f94cae11c4f04b 100644 (file)
@@ -2,9 +2,8 @@
 
 namespace Cli\Commands;
 
+use Cli\Services\ProgramRunner;
 use Minicli\Command\CommandCall;
-use Symfony\Component\Process\ExecutableFinder;
-use Symfony\Component\Process\Process;
 
 class InitCommand
 {
@@ -15,7 +14,6 @@ class InitCommand
     {
         $this->ensureRequiredExtensionInstalled(); // TODO - Ensure bookstack install deps are met?
 
-        // TODO - Dedupe the command stuff going on.
         // TODO - Check composer and git exists before running
         // TODO - Look at better way of handling env usage, on demand maybe where needed?
         //   Env loading in main `run` script if confilicting with certain bits here (app key generate, hence APP_KEY overload)
@@ -67,27 +65,14 @@ class InitCommand
 
     protected function generateAppKey(string $installDir): void
     {
-        // Find reference to php
-        $executableFinder = new ExecutableFinder();
-        $phpPath = $executableFinder->find('php', '/usr/bin/php');
-        if (!is_file($phpPath)) {
-            throw new CommandError('Could not locate "php" program.');
-        }
-
-        $process = new Process([
-            $phpPath,
-            $installDir . DIRECTORY_SEPARATOR . 'artisan',
-            'key:generate', '--force', '-n', '-q'
-        ], null, ['APP_KEY' => 'SomeRandomString']);
-        $process->setTimeout(240);
-        $process->setIdleTimeout(5);
-        $process->start();
-
-        $errors = '';
-        foreach ($process as $type => $data) {
-            // Errors are on stdout for artisan
-            $errors .= $data . "\n";
-        }
+        $errors = (new ProgramRunner('php', '/usr/bin/php'))
+            ->withTimeout(60)
+            ->withIdleTimeout(5)
+            ->withEnvironment(['APP_KEY' => 'SomeRandomString'])
+            ->runCapturingAllOutput([
+                $installDir . DIRECTORY_SEPARATOR . 'artisan',
+                'key:generate', '--force', '-n', '-q'
+            ]);
 
         if ($errors) {
             throw new CommandError("Failed 'php artisan key:generate' with errors:\n" . $errors);
@@ -100,28 +85,14 @@ class InitCommand
      */
     protected function installComposerDependencies(string $installDir): void
     {
-        // Find reference to composer
-        $executableFinder = new ExecutableFinder();
-        $composerPath = $executableFinder->find('composer', '/usr/local/bin/composer');
-        if (!is_file($composerPath)) {
-            throw new CommandError('Could not locate "composer" program.');
-        }
-
-        $process = new Process([
-            $composerPath, 'install',
-            '--no-dev', '-n', '-q', '--no-progress',
-            '-d', $installDir
-        ]);
-        $process->setTimeout(240);
-        $process->setIdleTimeout(15);
-        $process->start();
-
-        $errors = '';
-        foreach ($process as $type => $data) {
-            if ($process::ERR === $type) {
-                $errors .= $data . "\n";
-            }
-        }
+        $errors = (new ProgramRunner('composer', '/usr/local/bin/composer'))
+            ->withTimeout(300)
+            ->withIdleTimeout(15)
+            ->runCapturingStdErr([
+                'install',
+                '--no-dev', '-n', '-q', '--no-progress',
+                '-d', $installDir
+            ]);
 
         if ($errors) {
             throw new CommandError("Failed composer install with errors:\n" . $errors);
@@ -134,30 +105,16 @@ class InitCommand
      */
     protected function cloneBookStackViaGit(string $installDir): void
     {
-        // Find reference to git
-        $executableFinder = new ExecutableFinder();
-        $gitPath = $executableFinder->find('git', '/usr/bin/bit');
-        if (!is_file($gitPath)) {
-            throw new CommandError('Could not locate "git" program.');
-        }
-
-        $process = new Process([
-            $gitPath, 'clone', '-q',
-            '--branch', 'release',
-            '--single-branch',
-            'https://github.com/BookStackApp/BookStack.git',
-            $installDir
-        ]);
-        $process->setTimeout(240);
-        $process->setIdleTimeout(15);
-        $process->start();
-
-        $errors = '';
-        foreach ($process as $type => $data) {
-            if ($process::ERR === $type) {
-                $errors .= $data . "\n";
-            }
-        }
+        $errors = (new ProgramRunner('git', '/usr/bin/git'))
+            ->withTimeout(240)
+            ->withIdleTimeout(15)
+            ->runCapturingStdErr([
+                'clone', '-q',
+                '--branch', 'release',
+                '--single-branch',
+                'https://github.com/BookStackApp/BookStack.git',
+                $installDir
+            ]);
 
         if ($errors) {
             throw new CommandError("Failed git clone with errors:\n" . $errors);
diff --git a/scripts/Services/ProgramRunner.php b/scripts/Services/ProgramRunner.php
new file mode 100644 (file)
index 0000000..6f94f1e
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+
+namespace Cli\Services;
+
+use Symfony\Component\Process\ExecutableFinder;
+use Symfony\Component\Process\Process;
+
+class ProgramRunner
+{
+    protected int $timeout = 240;
+    protected int $idleTimeout = 15;
+    protected array $environment = [];
+
+    public function __construct(
+        protected string $program,
+        protected string $defaultPath
+    ) {
+    }
+
+    public function withTimeout(int $timeoutSeconds): static
+    {
+        $this->timeout = $timeoutSeconds;
+        return $this;
+    }
+
+    public function withIdleTimeout(int $idleTimeoutSeconds): static
+    {
+        $this->idleTimeout = $idleTimeoutSeconds;
+        return $this;
+    }
+
+    public function withEnvironment(array $environment): static
+    {
+        $this->environment = $environment;
+        return $this;
+    }
+
+    public function runCapturingAllOutput(array $args): string
+    {
+        $output = '';
+        $callable = function ($data) use (&$output) {
+            $output .= $data . "\n";
+        };
+
+        $this->runWithoutOutputCallbacks($args, $callable, $callable);
+        return $output;
+    }
+
+    public function runCapturingStdErr(array $args): string
+    {
+        $err = '';
+        $this->runWithoutOutputCallbacks($args, fn() => '', function ($data) use (&$err) {
+            $err .= $data . "\n";
+        });
+        return $err;
+    }
+
+    public function runWithoutOutputCallbacks(array $args, callable $stdOutCallback, callable $stdErrCallback): void
+    {
+        $process = $this->startProcess($args);
+        foreach ($process as $type => $data) {
+            if ($type === $process::ERR) {
+                $stdErrCallback($data);
+            } else {
+                $stdOutCallback($data);
+            }
+        }
+    }
+
+    protected function startProcess(array $args): Process
+    {
+        $programPath = $this->resolveProgramPath();
+        $process = new Process([$programPath, ...$args], null, $this->environment);
+        $process->setTimeout($this->timeout);
+        $process->setIdleTimeout($this->idleTimeout);
+        $process->start();
+        return $process;
+    }
+
+    protected function resolveProgramPath(): string
+    {
+        $executableFinder = new ExecutableFinder();
+        $path = $executableFinder->find($this->program, $this->defaultPath);
+
+        if (is_null($path) || !is_file($path)) {
+            throw new \Exception("Could not locate \"{$this->program}\" program.");
+        }
+
+        return $path;
+    }
+}
index 5bc28b41171f0913976e109516619337b86537f6..b79df7763561b5f52a495440a8a9867ba66d77cc 100644 (file)
@@ -6,7 +6,8 @@
     },
     "autoload": {
         "psr-4": {
-            "Cli\\Commands\\": "Commands/"
+            "Cli\\Commands\\": "Commands/",
+            "Cli\\Services\\": "Services/"
         }
     },
     "config": {
index 6f1d20567d6a557fe674313a50c775a104e423a8..fb7e23ce646d5246ad9914bd3a0759e2289782dd 100644 (file)
@@ -8,7 +8,6 @@ if (php_sapi_name() !== 'cli') {
 require __DIR__ . '/vendor/autoload.php';
 
 use Cli\Commands\BackupCommand;
-use Cli\Commands\CommandError;
 use Cli\Commands\InitCommand;
 use Minicli\App;
 
@@ -33,7 +32,7 @@ $app->registerCommand('init', [new InitCommand(), 'handle']);
 
 try {
     $app->runCommand($argv);
-} catch (CommandError $error) {
+} catch (Exception $error) {
     fwrite(STDERR, "An error occurred when attempting to run a command:\n");
     fwrite(STDERR, $error->getMessage() . "\n");
     exit(1);