diff --git a/cli/Valet/CommandLine.php b/cli/Valet/CommandLine.php index f719867a..c7eeb3e7 100644 --- a/cli/Valet/CommandLine.php +++ b/cli/Valet/CommandLine.php @@ -27,6 +27,52 @@ public function shellExec($command) { return shell_exec($command); } + /** + * Stream command output in real time and optionally collect matching lines. + * + * @param string $command + * @param callable|null $lineMatches Callback to check matching lines; must return `true` + * to collect the line for post-run analysis. + * @param callable|null $lineIsError Callback to check whether a line should be + * rendered as an error in real time. If omitted, the capture matcher is reused. + * + * @return array The collected output lines or an empty array if no lines were collected. + */ + public function streamCommandOutput($command, ?callable $lineMatches = null, ?callable $lineIsError = null): array { + $capturedLines = []; + $lineIsError = $lineIsError ?: $lineMatches; + + // Open a process to execute the command and read its output. + $handle = popen("$command 2>&1", 'r'); + while ($handle && !feof($handle)) { + $line = fgets($handle); + if ($line === false) { + break; + } + + // Keep raw command output unless caller explicitly marks this line as an error. + if ($lineIsError && $lineIsError($line)) { + error($line, false, false, true); + } + else { + echo $line; + } + + // If a callback is provided and the line matches the condition, + // then collect the line for post-run analysis. + if ($lineMatches && $lineMatches($line)) { + $capturedLines[] = trim($line); + } + } + + // Close the process. + if ($handle) { + pclose($handle); + } + + return $capturedLines; + } + /** * Pass the given Valet command to the command line with elevated privileges using gsudo. * diff --git a/cli/Valet/ShareTools/Ngrok.php b/cli/Valet/ShareTools/Ngrok.php index 010cecba..0c98e205 100644 --- a/cli/Valet/ShareTools/Ngrok.php +++ b/cli/Valet/ShareTools/Ngrok.php @@ -38,19 +38,26 @@ public function start(string $site, int $port, array $options = []) { $ngrok = realpath(valetBinPath() . 'ngrok.exe'); - $ngrokCommand = "\"$ngrok\" http $site:$port " . $this->getConfig() . " $options"; + // Log to stdout, log level info, and log format term for real-time output. + $logging = "--log=stdout --log-level=info --log-format=term"; + + $ngrokCommand = "\"$ngrok\" http $site:$port " . $this->getConfig() . " $options $logging"; info("Sharing $site...\n"); info("To output the public URL, please open a new terminal and run `valet fetch-share-url $site`"); - $output = $this->cli->shellExec("$ngrokCommand 2>&1"); + // Stream ngrok output in real time and collect error lines for post-run analysis. + // Shared matcher: use the same rule for live error styling and for post-run capture. + $isErrorLine = function ($line) { + return strpos($line, 'ERROR:') !== false; + }; - if ($errors = strstr($output, "ERROR")) { - error($errors . PHP_EOL); + // Pass the same matcher once; CommandLine reuses it for error styling when no separate + // error callback is supplied. + $errorLines = $this->cli->streamCommandOutput($ngrokCommand, $isErrorLine); - if (strpos($errors, 'ERR_NGROK_121') !== false) { - info("To update ngrok yourself, please run `valet ngrok update` and then upgrade the config file by running `valet ngrok config upgrade`\n"); - } + if (!empty($errorLines) && strpos(implode("\n", $errorLines), 'ERR_NGROK_121') !== false) { + info("\nTo update ngrok yourself, please run `valet ngrok update` and then upgrade the config file by running `valet ngrok config upgrade`\n"); } } diff --git a/cli/includes/helpers.php b/cli/includes/helpers.php index 667780c4..93f686e3 100644 --- a/cli/includes/helpers.php +++ b/cli/includes/helpers.php @@ -11,6 +11,7 @@ use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Formatter\OutputFormatter; if (!isset($_SERVER['HOME'])) { $_SERVER['HOME'] = $_SERVER['USERPROFILE']; @@ -62,11 +63,16 @@ function warning($output) { * * @param string $output * @param bool $exception Optionally pass a boolean to indicate whether to throw an exception. If `true`, the error will be thrown as a `ValetException`. [default: `false`] + * @param bool $newline Whether to append a newline after the error output. [default: `true`] + * @param bool $escapeOutput Whether to escape the output to prevent formatting issues. [default: `false`] * * @throws RuntimeException * @throws ValetException */ -function error(string $output, $exception = false) { +function error(string $output, bool $exception = false, bool $newline = true, bool $escapeOutput = false) { + + $errorOutput = (new ConsoleOutput())->getErrorOutput(); + if (isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'testing') { throw new RuntimeException($output); } @@ -78,12 +84,17 @@ function error(string $output, $exception = false) { usleep(1); // Print the error message to the console. - (new ConsoleOutput())->getErrorOutput()->writeln("\n\n$errors"); + $errorOutput->write("\n\n$errors", $newline); exit(); } else { - (new ConsoleOutput())->getErrorOutput()->writeln("$output"); + // If escapeOutput is true, then escape the output to prevent any formatting issues. + if ($escapeOutput) { + $output = OutputFormatter::escape($output); + } + + $errorOutput->write("$output", $newline); } } @@ -385,4 +396,4 @@ function str_contains_any($haystack, $needles) { } } return false; -} +} \ No newline at end of file