Extracted program running to its own class
This commit is contained in:
parent
3cc761d2d8
commit
cbf77ecdbf
5 changed files with 152 additions and 110 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
91
scripts/Services/ProgramRunner.php
Normal file
91
scripts/Services/ProgramRunner.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -6,7 +6,8 @@
|
|||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Cli\\Commands\\": "Commands/"
|
||||
"Cli\\Commands\\": "Commands/",
|
||||
"Cli\\Services\\": "Services/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue