Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork9.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 uponSymfony\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 insideProgressIndicator class:
privatefunctionoverwrite(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 theConsoleSectionOutput class.write function is all the way inSymfony\Component\Console\Output\Output.
Internally, it callsdoWrite.doWrite is overwritten inConsoleSectionOutput.
ThedoWrite are doing some newline stuff. Which is fine I guess.
But at the same time, theProgressIndicator's internaloverwrite 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 thedoWrite'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.
protectedfunctionrunAndWatchProcesses(array$processes):void{/** @var ConsoleOutputInterface $console */$console =$this->output->getOutput();$sections = [];$indicators = [];foreach ($processesas$pid =>$p) {if (!$p->isRunning()) {$p->start(); }$section =$console->section();$sections[$pid] =$section;$indicator =newProgressIndicator( output:$section, indicatorValues: ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'] );$indicator->start('⚙️ Starting async process...');$indicators[$pid] =$indicator; }while (!empty($processes)) {foreach ($processesas$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 ($projectsas$projectId) {$process =newProcess([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.
privatefunctionoverwrite(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:

