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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,15 @@ Before sharing a site with ngrok, you must first set the authtoken using Valet's

> [!NOTE]
>
> The public URL won't be displayed, however, in a separate terminal, you can use the [`fetch-share-url` command](#fetch-share-url) to get the url and copy it to the clipboard.
> Prior to v3.4.4, the public URL wasn't displayed and the [`fetch-share-url` command](#fetch-share-url) had to be used instead to get the URL and copy it to the clipboard.
>
> As of v3.4.4, the public URL **will** be displayed and automatically copied to your clipboard (requires ngrok's real-time logging). You can still use the `fetch-share-url` command in a separate terminal though.

> [!IMPORTANT]
>
> ngrok will now output logging information directly to the terminal in real-time by default using the `--log=stdout` option. Valet redirects stderr to stdout, so all logging information will be outputted to the terminal. This is useful for debugging and seeing what ngrok is doing in real-time. For this reason the `--log=stderr` is exactly the same output as `--log=stdout`.
>
> If you wish to disable real-time logging, you can use the valet's `--options` argument to pass ngrok's `log=false` option; this will disable all logging output to the terminal, including the public URL.

###### share --options

Expand Down
65 changes: 64 additions & 1 deletion cli/Valet/CommandLine.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,69 @@ public function shellExec($command) {
return shell_exec($command);
}

/**
* Stream command output in real time and optionally collect matching lines.
*
* @param string $command
* @param array $callbacks Optional callbacks:
* - onLine (callable): receives every raw line after it is written.
* - matches (callable): return true to collect line for post-run analysis.
* - isError (callable): return true to render line as an error. Defaults to matches.
*
* @return array The collected output lines or an empty array if no lines were collected.
*/
public function streamCommandOutput($command, array $callbacks = []): array {
$lineHandler = $callbacks['onLine'] ?? null;
$lineMatches = $callbacks['matches'] ?? null;
$lineIsError = $callbacks['isError'] ?? $lineMatches;

$capturedLines = [];

// Open a process to execute the command and read its output.
// 2>&1 redirects stderr to stdout so we can capture both.
$handle = popen("$command 2>&1", 'r');

// If the process failed to start, throw an error.
if ($handle === false) {
error('Failed to start command for streaming output.', true);
}

while ($handle && !feof($handle)) {
Comment thread
Copilot marked this conversation as resolved.
$line = fgets($handle);
if ($line === false) {
break;
}

// If the line is an error, output it as an error.
if ($lineIsError && $lineIsError($line)) {
error($line, false, false, true);
}
// Otherwise, output it normally.
else {
output($line, false);
}

// Invoke the optional line handler after writing output so callers can append
// follow-up messages in display order.
if ($lineHandler) {
$lineHandler($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.
*
Expand Down Expand Up @@ -151,4 +214,4 @@ public function runCommand($command, ?callable $onError = null, $realTimeOutput
return new ProcessOutput($process);
}
}
}
}
74 changes: 61 additions & 13 deletions cli/Valet/ShareTools/Ngrok.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,31 @@ class Ngrok extends ShareTool {
*/
public function start(string $site, int $port, array $options = []) {
if ($port === 443 && !$this->hasAuthToken()) {
output('Forwarding to local port 443 or a local https:// URL is only available after you sign up.
Sign up at: <fg=blue>https://ngrok.com/signup</>
Then use: <fg=magenta>valet set-ngrok-token [token]</>');
output("Forwarding to local port 443 or a local https:// URL is only available after you sign up.\nSign up at: <fg=blue>https://ngrok.com/signup</>\nThen use: <fg=magenta>valet set-ngrok-token [token]</>");
exit(1);
}

// If host-header is not specified,
// then set it into the array with a default value of rewrite.
if (!stripos(json_encode($options), 'host-header')) {
array_push($options, "host-header=rewrite");
// Apply defaults for various options the user has not already specified.
$defaults = [
'host-header' => 'rewrite',
// Logging options: log to stdout at info level, enables real-time output
// and post-run error analysis.
// Logging options are undocumented for the http command, but is defined as
// API flags but still works for the http command. See ngrok docs for more details:
// https://ngrok.com/docs/agent/cli-api#flags-2
//
// (Note: Both `stdout` and `stderr` values capture the same output since
// `CommandLine::streamCommandOutput` method uses `2>&1` to redirect stderr to stdout.)
'log' => 'stdout',
'log-level' => 'info',
'log-format' => 'term'
];

// Merge defaults with user-specified options, giving precedence to user-specified options.
foreach ($defaults as $key => $value) {
if (!array_filter($options, fn($opt) => strpos($opt, "$key=") === 0)) {
$options[] = "$key=$value";
}
}

$options = prefixOptions($options);
Expand All @@ -41,16 +56,49 @@ public function start(string $site, int $port, array $options = []) {
$ngrokCommand = "\"$ngrok\" http $site:$port " . $this->getConfig() . " $options";

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");
// If the options string doesn't contain the `--log` option with values of either `stdout`
// or `stderr`,then inform the user that they can fetch the public URL in a new terminal.
if (strpos($options, '--log=stdout') === false && strpos($options, '--log=stderr') === false) {
info("To output the public URL, please open a new terminal and run `valet fetch-share-url $site`");
}

// 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 || strpos($line, 'ERR_NGROK_') !== false;
};

$didOutputShareUrl = false;

if ($errors = strstr($output, "ERROR")) {
error($errors . PHP_EOL);
// Line handler: check each line for the "started tunnel" log line to find and
// extract the public URL.
$lineHandler = function ($line) use ($site, &$didOutputShareUrl) {
// If the share URL has already been output, skip further processing.
if ($didOutputShareUrl) {
return;
}

// If the line contains the 'msg="started tunnel"' message AND has a 'url=' key...
if (strpos($line, 'msg="started tunnel"') !== false && preg_match('/\burl=(\S+)/', $line, $matches)) {
// Set the flag to true to avoid further processing of lines.
$didOutputShareUrl = true;
// Output an info message with extracted public URL.
info("The public URL for $site is: <fg=blue>$matches[1]</>");

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");
// Copy the public URL to the clipboard for ease.
$this->copyUrlToClipboard($matches[1]);
}
};

// Stream ngrok output in real time and collect error lines for post-run analysis.
$errorLines = $this->cli->streamCommandOutput($ngrokCommand, [
'onLine' => $lineHandler,
'matches' => $isErrorLine
]);

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");
}
}

Expand Down
18 changes: 16 additions & 2 deletions cli/Valet/ShareTools/ShareTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ public function currentTunnelUrl(string $site) {
if (isset($body->tunnels) && count($body->tunnels) > 0) {
// If the tunnel URL is NOT null, return the URL.
if ($tunnelUrl = $this->findHttpTunnelUrl($body->tunnels, $site)) {
// Use | clip to copy the URL to the clipboard.
$this->cli->passthru("echo $tunnelUrl | clip");
$this->copyUrlToClipboard($tunnelUrl);

return $tunnelUrl;
}
Expand Down Expand Up @@ -132,4 +131,19 @@ public function findHttpTunnelUrl(array $tunnels, ?string $site = null) {
}
return null;
}

/**
* Copy the public URL to the clipboard.
*
* @param string $url The public URL to copy.
*/
public function copyUrlToClipboard(string $url) {
// Escape single quotes in the URL for PowerShell.
$escapedUrl = str_replace("'", "''", $url);
// The single quotes around the URL are necessary to ensure that PowerShell treats it as
// a literal string, even if it contains spaces or special shell characters and prevents
// command injection.
$this->cli->powershell("'{$escapedUrl}' | Set-Clipboard");
info("It has been copied to your clipboard.");
}
}
22 changes: 17 additions & 5 deletions cli/includes/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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);
}
Expand All @@ -78,25 +84,31 @@ function error(string $output, $exception = false) {
usleep(1);

// Print the error message to the console.
(new ConsoleOutput())->getErrorOutput()->writeln("\n\n<error>$errors</error>");
$errorOutput->write("\n\n<error>$errors</error>", $newline);

exit();
}
else {
(new ConsoleOutput())->getErrorOutput()->writeln("<error>$output</error>");
// If escapeOutput is true, then escape the output to prevent any formatting issues.
if ($escapeOutput) {
$output = OutputFormatter::escape($output);
}

$errorOutput->write("<error>$output</error>", $newline);
}
}

/**
* Output the given text to the console.
*
* @param string $output
* @param bool $newline Whether to append a newline after the output. [default: `true`]
*/
function output($output) {
function output($output, bool $newline = true) {
if (isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'testing') {
return;
}
(new ConsoleOutput())->writeln($output);
(new ConsoleOutput())->write($output, $newline);
}

/**
Expand Down
1 change: 0 additions & 1 deletion cli/valet.php
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,6 @@

$url = Share::shareTool()->currentTunnelUrl($site);
info("The public URL for $site is: <fg=blue>$url</>");
info("It has been copied to your clipboard.");

})->setAliases(["url"])->descriptions('Get and copy the public URL of the current working directory site that is currently being shared', [
"site" => "Optionally, specify a site"
Expand Down