diff --git a/src/Service/ActivityMonitor.php b/src/Service/ActivityMonitor.php index fd3acd450a..fd54195cc1 100644 --- a/src/Service/ActivityMonitor.php +++ b/src/Service/ActivityMonitor.php @@ -2,6 +2,7 @@ namespace Platformsh\Cli\Service; +use GuzzleHttp\Exception\BadResponseException; use Platformsh\Client\Model\Activity; use Platformsh\Client\Model\ActivityLog\LogItem; use Platformsh\Client\Model\Project; @@ -99,6 +100,27 @@ public function waitAndLog(Activity $activity, $pollInterval = 3, $timestamps = return Helper::formatTime(time() - $startTime); }); $bar->setFormat('[%bar%] %elapsed:6s% (%state%)'); + + // Set up cancellation for the activity on Ctrl+C. + if (\function_exists('\\pcntl_signal') && $activity->operationAvailable('cancel')) { + declare(ticks = 1); + $sigintReceived = false; + /** @noinspection PhpComposerExtensionStubsInspection */ + \pcntl_signal(SIGINT, function () use ($activity, $stdErr, $bar, &$sigintReceived) { + if ($sigintReceived) { + exit(1); + } + $sigintReceived = true; + $bar->clear(); + if ($this->cancel($activity, $stdErr)) { + exit(1); + } + $stdErr->writeln(''); + $bar->advance(); + }); + $stdErr->writeln('Enter Ctrl+C once to cancel the activity (or twice to quit this command).'); + } + $bar->start(); $logStream = $this->getLogStream($activity, $bar); @@ -166,6 +188,38 @@ public function waitAndLog(Activity $activity, $pollInterval = 3, $timestamps = return false; } + /** + * Attempts to cancel the activity, catching and printing errors. + * + * @param Activity $activity + * @param OutputInterface $stdErr + * + * @return bool + */ + private function cancel(Activity $activity, OutputInterface $stdErr) + { + if (!$activity->operationAvailable('cancel')) { + $stdErr->writeln('The activity cannot be cancelled.'); + return false; + } + $stdErr->writeln('Cancelling the activity...'); + try { + $activity->cancel(); + } catch (\Exception $e) { + if ($e instanceof BadResponseException + && $e->getResponse() && $e->getResponse()->getStatusCode() === 400 + && \strpos($e->getMessage(), 'cannot be cancelled in its current state') !== false) { + $activity->refresh(); + $stdErr->writeln(\sprintf('The activity cannot be cancelled in its current state (%s).', $activity->state)); + return false; + } + $stdErr->writeln(\sprintf('Failed to cancel the activity: %s', $e->getMessage())); + return false; + } + $stdErr->writeln('The activity was successfully cancelled.'); + return true; + } + /** * Reads the log stream and returns LogItem objects. * @@ -435,11 +489,11 @@ private function getStart(Activity $activity) { private function getLogStream(Activity $activity, ProgressBar $bar) { $url = $activity->getLink('log'); - // Try fetching the stream with a 10 second timeout per call, and a .5 - // second interval between calls, for up to 2 minutes. - $readTimeout = 10; - $interval = .5; + // Try fetching the stream with an up to 10 second timeout per call, + // and a .5 second interval between calls, for up to 2 minutes. + $readTimeout = .5; $stream = \fopen($url, 'r', false, $this->api->getStreamContext($readTimeout)); + $interval = .5; $start = \microtime(true); while ($stream === false) { if (\microtime(true) - $start > 120) { @@ -448,6 +502,7 @@ private function getLogStream(Activity $activity, ProgressBar $bar) { $bar->advance(); \usleep($interval * 1000000); $bar->advance(); + $readTimeout = $readTimeout >= 10 ? $readTimeout : $readTimeout + .5; $stream = \fopen($url, 'r', false, $this->api->getStreamContext($readTimeout)); } \stream_set_blocking($stream, 0);