Uploads werden weiterhin nach WebP umgewandelt, jetzt in vier statt drei Breiten (480/960/1600/2400) und ohne Hochskalieren. Hintergrundbilder waehlen ihre Variante je Geraet, weil CSS kein srcset kennt. Dabei kam ein Fehler heraus, den erst der Browsertest zeigte: der Dokumentpruefer erlaubte drei sources je Bild, also war mit der vierten Variante jedes Dokument mit einem Bild ungueltig - ohne sichtbare Meldung. Die Grenze haengt jetzt an MediaLibrary::WIDTHS. Geteiltes Hosting hat meistens keinen Composer. scripts/build-release.php baut deshalb auf einem Build-Rechner ein Archiv mit fertigem vendor/ ohne Entwicklungspakete, samt SHA-256. Der Updater bricht ab, wenn ein Update composer.lock aendert und Composer auf dem Server nicht aufrufbar ist, statt mit einem unpassenden vendor/ weiterzulaufen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
4.6 KiB
PHP
123 lines
4.6 KiB
PHP
<?php
|
|
|
|
namespace App\Ops;
|
|
|
|
/**
|
|
* Runs an external command, or says plainly that it cannot.
|
|
*
|
|
* `proc_open` is disabled on a lot of shared hosting, and that is not an edge case to discover
|
|
* halfway through an update - it is the first thing the updater has to ask. The command is passed
|
|
* as an array, so there is no shell and nothing to quote: an argument can contain a space, a
|
|
* quote or a semicolon without becoming a second command.
|
|
*/
|
|
final class CommandRunner
|
|
{
|
|
/** @var array<string, list<string>|null> */
|
|
private array $resolved = [];
|
|
|
|
public function available(): bool
|
|
{
|
|
if (!function_exists('proc_open')) {
|
|
return false;
|
|
}
|
|
$disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
|
|
|
|
return !in_array('proc_open', $disabled, true);
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $command
|
|
*
|
|
* @return array{ok: bool, exit: int, output: string}
|
|
*/
|
|
public function run(array $command, string $cwd, int $timeout = 300): array
|
|
{
|
|
if (!$this->available()) {
|
|
return ['ok' => false, 'exit' => -1, 'output' => 'proc_open ist auf diesem Server abgeschaltet.'];
|
|
}
|
|
$pipes = [];
|
|
$process = @proc_open(
|
|
$command,
|
|
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
|
|
$pipes,
|
|
$cwd,
|
|
// A minimal, predictable environment: no inherited APP_ENV that would make a console
|
|
// command run against the wrong environment.
|
|
['PATH' => getenv('PATH') ?: '', 'SystemRoot' => getenv('SystemRoot') ?: '', 'TEMP' => sys_get_temp_dir()],
|
|
);
|
|
if (!is_resource($process)) {
|
|
return ['ok' => false, 'exit' => -1, 'output' => sprintf('%s konnte nicht gestartet werden.', $command[0])];
|
|
}
|
|
foreach ($pipes as $pipe) {
|
|
stream_set_blocking($pipe, false);
|
|
}
|
|
$output = '';
|
|
$deadline = microtime(true) + $timeout;
|
|
while (true) {
|
|
$status = proc_get_status($process);
|
|
foreach ($pipes as $pipe) {
|
|
$output .= stream_get_contents($pipe) ?: '';
|
|
}
|
|
if (!$status['running']) {
|
|
break;
|
|
}
|
|
if (microtime(true) > $deadline) {
|
|
proc_terminate($process);
|
|
foreach ($pipes as $pipe) {
|
|
fclose($pipe);
|
|
}
|
|
proc_close($process);
|
|
|
|
return ['ok' => false, 'exit' => -1, 'output' => trim($output)."\n".sprintf('Abgebrochen nach %d Sekunden.', $timeout)];
|
|
}
|
|
usleep(50000);
|
|
}
|
|
foreach ($pipes as $pipe) {
|
|
$output .= stream_get_contents($pipe) ?: '';
|
|
fclose($pipe);
|
|
}
|
|
proc_close($process);
|
|
$exit = $status['exitcode'];
|
|
|
|
return ['ok' => 0 === $exit, 'exit' => $exit, 'output' => trim($output)];
|
|
}
|
|
|
|
/**
|
|
* Whether an executable can be found at all, so a missing one is reported, not guessed at.
|
|
*
|
|
* On Windows several of these are batch files, and proc_open without a shell cannot start a
|
|
* .bat - which reported "composer is not available" on a machine that has it. The variants are
|
|
* tried in order, and the one that answers is remembered for the next call.
|
|
*/
|
|
public function has(string $executable, string $cwd): bool
|
|
{
|
|
return null !== $this->resolve($executable, $cwd);
|
|
}
|
|
|
|
/** The spelling of a command that actually starts here, or null. */
|
|
public function resolve(string $executable, string $cwd): ?array
|
|
{
|
|
if (array_key_exists($executable, $this->resolved)) {
|
|
return $this->resolved[$executable];
|
|
}
|
|
$candidates = [[$executable]];
|
|
if ('\\' === DIRECTORY_SEPARATOR) {
|
|
// On Windows composer is a .bat, and CreateProcess cannot start a batch file on its
|
|
// own - it needs the command interpreter. The argument list stays fixed, nothing from
|
|
// a request ever reaches it.
|
|
$candidates[] = [getenv('COMSPEC') ?: 'cmd.exe', '/c', $executable];
|
|
}
|
|
// A phar in the project is the way on hosting where nothing is installed globally.
|
|
if (is_file($cwd.'/'.$executable.'.phar')) {
|
|
$candidates[] = [PHP_BINARY, $cwd.'/'.$executable.'.phar'];
|
|
}
|
|
foreach ($candidates as $candidate) {
|
|
if ($this->run([...$candidate, '--version'], $cwd, 30)['ok']) {
|
|
return $this->resolved[$executable] = $candidate;
|
|
}
|
|
}
|
|
|
|
return $this->resolved[$executable] = null;
|
|
}
|
|
}
|