diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index c204a49f1..954670582 100644 --- a/app/Livewire/Project/Application/Deployment/Show.php +++ b/app/Livewire/Project/Application/Deployment/Show.php @@ -121,6 +121,25 @@ public function copyLogs(): string return sanitizeLogsForExport($logs); } + public function downloadAllLogs(): string + { + $logs = decode_remote_command_output($this->application_deployment_queue, includeAll: true) + ->map(function ($line) { + $prefix = ''; + if ($line['hidden']) { + $prefix = '[DEBUG] '; + } + if (isset($line['command']) && $line['command']) { + $prefix .= '[CMD]: '; + } + + return $line['timestamp'].' '.$prefix.trim($line['line']); + }) + ->join("\n"); + + return sanitizeLogsForExport($logs); + } + public function render() { return view('livewire.project.application.deployment.show'); diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index 5f8046efd..22605e1bb 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -23,6 +23,8 @@ class GetLogs extends Component { public const MAX_LOG_LINES = 50000; + public const MAX_DOWNLOAD_SIZE_BYTES = 50 * 1024 * 1024; // 50MB + public string $outputs = ''; public string $errors = ''; @@ -164,10 +166,12 @@ public function getLogs($refresh = false) } // Collect new logs into temporary variable first to prevent flickering // (avoids clearing output before new data is ready) - $newOutputs = ''; - Process::run($sshCommand, function (string $type, string $output) use (&$newOutputs) { - $newOutputs .= removeAnsiColors($output); + // Use array accumulation + implode for O(n) instead of O(n²) string concatenation + $logChunks = []; + Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand, function (string $type, string $output) use (&$logChunks) { + $logChunks[] = removeAnsiColors($output); }); + $newOutputs = implode('', $logChunks); if ($this->showTimeStamps) { $newOutputs = str($newOutputs)->split('/\n/')->sort(function ($a, $b) { @@ -215,11 +219,40 @@ public function downloadAllLogs(): string $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command); - $allLogs = ''; - Process::run($sshCommand, function (string $type, string $output) use (&$allLogs) { - $allLogs .= removeAnsiColors($output); + // Use array accumulation + implode for O(n) instead of O(n²) string concatenation + // Enforce 50MB size limit to prevent memory exhaustion from large logs + $logChunks = []; + $accumulatedBytes = 0; + $truncated = false; + + Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand, function (string $type, string $output) use (&$logChunks, &$accumulatedBytes, &$truncated) { + if ($truncated) { + return; + } + + $output = removeAnsiColors($output); + $outputBytes = strlen($output); + + if ($accumulatedBytes + $outputBytes > self::MAX_DOWNLOAD_SIZE_BYTES) { + $remaining = self::MAX_DOWNLOAD_SIZE_BYTES - $accumulatedBytes; + if ($remaining > 0) { + $logChunks[] = substr($output, 0, $remaining); + } + $truncated = true; + + return; + } + + $logChunks[] = $output; + $accumulatedBytes += $outputBytes; }); + $allLogs = implode('', $logChunks); + + if ($truncated) { + $allLogs .= "\n\n[... Output truncated at 50MB limit ...]"; + } + if ($this->showTimeStamps) { $allLogs = str($allLogs)->split('/\n/')->sort(function ($a, $b) { $a = explode(' ', $a); diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index fc70aa7da..217c82929 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -175,7 +175,7 @@ function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) throw new \RuntimeException($errorMessage, $exitCode); } -function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection +function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null, bool $includeAll = false): Collection { if (is_null($application_deployment_queue)) { return collect([]); @@ -216,7 +216,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d $seenCommands = collect(); $formatted = collect($decoded); - if (! $is_debug_enabled) { + if (! $is_debug_enabled && ! $includeAll) { $formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false); } diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index 2b6afe75a..28872f4bc 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -173,7 +173,7 @@ class="flex flex-col w-full bg-white dark:text-white dark:bg-coolgray-100 dark:border-coolgray-300" :class="fullscreen ? 'h-full' : 'border border-dotted rounded-sm'">