Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 78 additions & 2 deletions core/src/Console/SiteUpdateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -708,14 +708,35 @@ protected function composerBinaryCommand(): string
}

foreach ($this->composerBinaryCandidates() as $candidate) {
if (is_file($candidate) && is_executable($candidate)) {
if ($this->isExecutableFile($candidate)) {
return escapeshellarg($candidate);
}
}

return 'composer';
}

/**
* Check whether a path is something the shell can run.
*
* On Windows is_executable() answers false even for a genuine
* composer.bat — it does not consult PATHEXT the way the shell does — so
* every candidate would be rejected no matter which paths were offered.
* There the file existing is the only signal available.
*
* @since 3.5.8
* @param string $path Absolute path to test.
* @return bool
*/
protected function isExecutableFile(string $path): bool
{
if (!is_file($path)) {
return false;
}

return windows_os() ? true : is_executable($path);
}

/**
* Build fallback Composer executable candidates.
*
Expand All @@ -733,9 +754,55 @@ protected function composerBinaryCandidates(): array
$candidates[] = $home . '/.composer/composer';
}

// Appended rather than switched on the platform. Every candidate is
// filtered by isExecutableFile() anyway, so an entry that cannot exist
// here costs one is_file() call, while a platform branch would be a
// new way to guess wrong — under WSL, or wherever the environment does
// not match what PHP_OS_FAMILY suggests.
$candidates = array_merge($candidates, $this->windowsComposerBinaryCandidates());

return array_values(array_unique($candidates));
}

/**
* Build fallback Composer executable candidates for Windows layouts.
*
* The POSIX list finds nothing here: there is no /usr/local/bin, and a
* per-user install puts a shim in %APPDATA%\Composer rather than in a
* ~/.composer/composer file. Only shell-runnable shims are listed —
* composer.phar is deliberately absent, because it needs `php` in front of
* it and this list feeds a command that is executed directly.
*
* @since 3.5.8
* @return array<int, string>
*/
protected function windowsComposerBinaryCandidates(): array
{
$candidates = [];

// Where the Composer-Setup installer puts a machine-wide install.
$programData = trim((string) getenv('ProgramData'));
if ($programData !== '') {
$base = rtrim(str_replace('\\', '/', $programData), '/') . '/ComposerSetup/bin/composer';
$candidates[] = $base . '.bat';
$candidates[] = $base . '.exe';
}

// A per-user install.
$appData = trim((string) getenv('APPDATA'));
if ($appData !== '') {
$base = rtrim(str_replace('\\', '/', $appData), '/') . '/Composer/composer';
$candidates[] = $base . '.bat';
$candidates[] = $base . '.exe';
}

foreach ($this->homeDirectories() as $home) {
$candidates[] = $home . '/AppData/Roaming/Composer/composer.bat';
}

return array_values(array_unique(array_filter($candidates)));
}

/**
* Resolve possible home directories without relying on shell "~" expansion.
*
Expand Down Expand Up @@ -775,7 +842,16 @@ protected function shellCommandExists(string $command): bool
$output = [];
$exitCode = 1;

exec('command -v ' . escapeshellarg($command) . ' >/dev/null 2>&1', $output, $exitCode);
// `command -v` is a POSIX shell builtin and /dev/null is a POSIX
// device; cmd.exe has neither, so on Windows this probe reported "not
// found" for every command — including ones plainly on PATH — and the
// resolver fell through to candidate paths that do not exist there
// either. `where` is the native equivalent and answers 0 when found.
$probe = windows_os()
? 'where ' . escapeshellarg($command) . ' >NUL 2>NUL'
: 'command -v ' . escapeshellarg($command) . ' >/dev/null 2>&1';

exec($probe, $output, $exitCode);

return (int) $exitCode === 0;
}
Expand Down
123 changes: 37 additions & 86 deletions core/src/Console/SystemTasks/TaskWorkerCommand.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<?php namespace EvolutionCMS\Console\SystemTasks;

use EvolutionCMS\Services\SystemTasks\ConsoleInstallFlowService;
use EvolutionCMS\Services\SystemTasks\ConsoleUninstallFlowService;
use EvolutionCMS\Services\SystemTasks\SiteUpdateFlowService;
use EvolutionCMS\Services\SystemTasks\SystemTaskService;
use EvolutionCMS\Services\SystemTasks\SystemTaskRegistry;
use EvolutionCMS\Services\SystemTasks\WorkerHealthService;
use Illuminate\Console\Command;
use Illuminate\Console\Scheduling\Schedule;
Expand Down Expand Up @@ -35,90 +33,43 @@ public function handle()
$workerHealth->markPick($host, $pid);

try {
switch ((string) $task->type) {
case 'console_install':
$flow = new ConsoleInstallFlowService();
$result = $flow->execute($task, function ($step, $progress, $message, $level = 'info', array $context = []) use (&$task, $taskService) {
$task = $taskService->updateTaskProgress(
$task,
'running',
(int) $progress,
(string) $step,
(string) $message,
(string) $level,
$context
);
});

$taskService->markTaskSucceeded(
$task,
isset($result['message']) ? (string) $result['message'] : 'System task completed successfully.',
isset($result['result']) && is_array($result['result']) ? $result['result'] : []
);
$workerHealth->markSuccess($host, $pid);
$this->info('[system:task-worker] console install task completed');
return self::SUCCESS;

case 'console_uninstall':
$flow = new ConsoleUninstallFlowService();
$result = $flow->execute($task, function ($step, $progress, $message, $level = 'info', array $context = []) use (&$task, $taskService) {
$task = $taskService->updateTaskProgress(
$task,
'running',
(int) $progress,
(string) $step,
(string) $message,
(string) $level,
$context
);
});

$taskService->markTaskSucceeded(
$task,
isset($result['message']) ? (string) $result['message'] : 'System task completed successfully.',
isset($result['result']) && is_array($result['result']) ? $result['result'] : []
);
$workerHealth->markSuccess($host, $pid);
$this->info('[system:task-worker] console uninstall task completed');
return self::SUCCESS;

case 'site_update':
$flow = new SiteUpdateFlowService();
$result = $flow->execute($task, function ($step, $progress, $message, $level = 'info', array $context = []) use (&$task, $taskService) {
$task = $taskService->updateTaskProgress(
$task,
'running',
(int) $progress,
(string) $step,
(string) $message,
(string) $level,
$context
);
});

$taskService->markTaskSucceeded(
$task,
isset($result['message']) ? (string) $result['message'] : 'Site update completed successfully.',
isset($result['result']) && is_array($result['result']) ? $result['result'] : []
);
$workerHealth->markSuccess($host, $pid);
$this->info('[system:task-worker] site update task completed');
return self::SUCCESS;

default:
$taskService->markTaskFailed(
$task,
'TASK_TYPE_NOT_ALLOWED',
'Unsupported system task type for this worker.'
);
$workerHealth->markFailure('TASK_TYPE_NOT_ALLOWED', $host, $pid);
Log::warning('[system:task-worker] unsupported task type', [
'task_id' => (int) $task->id,
'type' => (string) $task->type,
]);
$this->warn('[system:task-worker] unsupported task type');
return self::SUCCESS;
$type = (string) $task->type;
if (!SystemTaskRegistry::has($type)) {
$taskService->markTaskFailed(
$task,
'TASK_TYPE_NOT_ALLOWED',
'Unsupported system task type for this worker.'
);
$workerHealth->markFailure('TASK_TYPE_NOT_ALLOWED', $host, $pid);
Log::warning('[system:task-worker] unsupported task type', [
'task_id' => (int) $task->id,
'type' => $type,
]);
$this->warn('[system:task-worker] unsupported task type');
return self::SUCCESS;
}

$handler = SystemTaskRegistry::handler($type);
$result = $handler->execute($task, function ($step, $progress, $message, $level = 'info', array $context = []) use (&$task, $taskService) {
$task = $taskService->updateTaskProgress(
$task,
'running',
(int) $progress,
(string) $step,
(string) $message,
(string) $level,
$context
);
});

$taskService->markTaskSucceeded(
$task,
isset($result['message']) ? (string) $result['message'] : SystemTaskRegistry::label($type) . ' completed successfully.',
isset($result['result']) && is_array($result['result']) ? $result['result'] : []
);
$workerHealth->markSuccess($host, $pid);
$this->info('[system:task-worker] ' . $type . ' task completed');
return self::SUCCESS;
} catch (\Throwable $exception) {
$errorCode = 'TASK_EXECUTION_FAILED';
$taskService->markTaskFailed($task, $errorCode, $exception->getMessage(), [
Expand Down
Loading