Extracted program running to its own class

This commit is contained in:
Dan Brown 2023-03-04 15:06:38 +00:00
parent 3cc761d2d8
commit cbf77ecdbf
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
5 changed files with 152 additions and 110 deletions

View 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);

View 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);

View file

@ -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;
}
}

View file

@ -6,7 +6,8 @@
},
"autoload": {
"psr-4": {
"Cli\\Commands\\": "Commands/"
"Cli\\Commands\\": "Commands/",
"Cli\\Services\\": "Services/"
}
},
"config": {

View 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);