-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Description
Symfony version(s) affected
At least all versions of in the past 3 years based on git blame
Description
This is going to be a bit of a long one, as it contains the explanation and a possible fix.
I've been playing around with a console script.
I am triggering subprocesses to run them in parallel. I wanted to make the console look nice, for no particular reason, to show a loading like a throbber as the subprocesses take some time.
So I stumbled upon Symfony\Component\Console\Helper\ProgressIndicator.
Great! Or so I thought... All nice and good until, for some reason that I could not manage to debug, after the exact 5th redraw of the indicator, it starts eating up the previous text (the lines above the section).
Each iteration, the indicator gets pushed up in the terminal.
After hours of debugging to understand if it's a me-issue, I managed to track it down to this function inside ProgressIndicator class:
private function overwrite(string $message): void
{
if ($this->output->isDecorated()) {
$this->output->write("\x0D\x1B[2K");
$this->output->write($message);
} else {
$this->output->writeln($message);
}
}$this->output is an instance of the ConsoleSectionOutput class.
write function is all the way in Symfony\Component\Console\Output\Output.
Internally, it calls doWrite.
doWrite is overwritten in ConsoleSectionOutput.
The doWrite are doing some newline stuff. Which is fine I guess.
But at the same time, the ProgressIndicator's internal overwrite does ->write("\x0D\x1B[2K").
That thing over there tells the terminal to move the cursor at the end of the line, and then to delete everything on that line, or so I understand.
There seems to be a weird conflict between this and the doWrite's newLines, which results in this weird behavior of the sections moving up in terminal if you have multiple of them.
How to reproduce
Here is a small script that takes an array of Processes (Symfony\Component\Process\Process), starts them, and shows a throbber while waiting for the processes to finish.
protected function runAndWatchProcesses(array $processes): void
{
/** @var ConsoleOutputInterface $console */
$console = $this->output->getOutput();
$sections = [];
$indicators = [];
foreach ($processes as $pid => $p) {
if (!$p->isRunning()) {
$p->start();
}
$section = $console->section();
$sections[$pid] = $section;
$indicator = new ProgressIndicator(
output: $section,
indicatorValues: ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏']
);
$indicator->start('⚙️ Starting async process...');
$indicators[$pid] = $indicator;
}
while (!empty($processes)) {
foreach ($processes as $pid => $p) {
$lastLineOfText = $this->lastNonEmptyLine($p->getIncrementalErrorOutput() ?: $p->getIncrementalOutput());
if ($lastLineOfText) {
$indicators[$pid]->setMessage($lastLineOfText);
}
$indicators[$pid]->advance();
if (!$p->isRunning()) {
$indicators[$pid]->finish($lastLineOfText, $p->isSuccessful() ? '✔' : '❌'); // Because finish does this weird and it always shows Ok even if the process failed
unset($processes[$pid], $indicators[$pid], $sections[$pid]);
}
}
usleep(100_000);
}
}Example of the processes array:
It will require adaptations, of course...
$projects = [1,2,3,4,5];
$processes = [];
foreach ($projects as $projectId) {
$process = new Process([
PHP_BINARY,
'artisan',
'time-consuming: command',
"--project=$projectId",
]);
$process->setTimeout(900);
$processes[$projectId] = $process;
}Possible Solution
The fix is to just use the "native" overwrite of the output (ConsoleSectionOutput) without using the \x0D\x1B[2K trick. For some reason that fixed it.
private function overwrite(string $message): void
{
if ($this->output->isDecorated()) {
//$this->output->write("\x0D\x1B[2K");
$this->output->overwrite($message);
} else {
$this->output->writeln($message);
}
}Additional Context
Gifs to explain:

