destination->server; if ($server->isNonRoot()) { $commands = parseCommandsByLineForSudo(collect($commands), $server); } $secrets = method_exists($database, 'resolvedSecretManagerValuesForRedaction') ? $database->resolvedSecretManagerValuesForRedaction() : []; // The commands go to ssh on stdin, so the database passwords in them are not in a command line on the Coolify host. $remoteCommand = SshMultiplexingHelper::generateSshBatchCommand($server); $activity->properties = $activity->properties->merge(['status' => ProcessStatus::IN_PROGRESS->value]); $activity->save(); $timeout = config('constants.ssh.command_timeout'); // Output is redacted per complete line, so a secret split across two output chunks is still found. $pending = ['stdout' => '', 'stderr' => '']; try { $process = Process::timeout($timeout) ->idleTimeout(3600) ->input(SshMultiplexingHelper::sshBatchInput(implode("\n", $commands))) ->start($remoteCommand, function (string $type, string $output) use ($activity, $secrets, &$pending): void { $pending[$type] = ($pending[$type] ?? '').$output; $end = max(strrpos($pending[$type], "\n"), strrpos($pending[$type], "\r")); if ($end === false) { return; } $this->appendOutput($activity, $type, $this->redact(substr($pending[$type], 0, $end + 1), $secrets)); $pending[$type] = substr($pending[$type], $end + 1); }); $result = $process->wait(); } catch (ProcessTimedOutException|SymfonyProcessTimedOutException) { // The timeout message contains the full SSH command, which can contain secrets. throw new \RuntimeException("The command on the server timed out after {$timeout} seconds.", 124); } finally { foreach ($pending as $type => $output) { $this->appendOutput($activity, $type, $this->redact($output, $secrets)); } } $status = $result->successful() ? ProcessStatus::FINISHED : ProcessStatus::ERROR; $activity->properties = $activity->properties->merge([ 'status' => $status->value, 'exitCode' => $result->exitCode(), ]); $activity->save(); if (! $result->successful()) { throw new \RuntimeException($this->redact($result->errorOutput(), $secrets), $result->exitCode()); } return $activity; } /** * @param array $secrets */ private function redact(string $value, array $secrets): string { return sanitize_utf8_text(resolve(SensitiveDataRedactor::class)->redactText($value, lockedSecrets: $secrets)); } private function appendOutput(Activity $activity, string $type, string $output): void { if ($output === '') { return; } $entries = json_decode($activity->description ?: '[]', true, flags: JSON_THROW_ON_ERROR); $entries[] = [ 'type' => $type, 'output' => $output, 'timestamp' => hrtime(true), 'batch' => 1, 'order' => count($entries) + 1, ]; $activity->description = json_encode($entries, flags: JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); $activity->save(); } }