Bildlaufleiste, die fehlgeschlagen ist


Kürzlich wurde eine neue Version von Windows Terminal veröffentlicht. Alles wäre in Ordnung, aber die Leistung ihrer Bildlaufleiste ließ zu wünschen übrig. Deshalb ist es Zeit, einen kleinen Stock an ihn zu kleben und Tamburin zu spielen.

Was machen Benutzer normalerweise mit der neuen Version einer Anwendung? Genau das haben Tester nicht getan. Daher begann ich nach einer kurzen Nutzung des Terminals für den vorgesehenen Zweck, schreckliche Dinge damit zu tun. Okay, okay, ich habe gerade Kaffee auf die Tastatur geschüttet und versehentlich die <Eingabetaste> geklemmt, als ich sie abgewischt habe. Was ist am Ende passiert?



Ja, es sieht nicht sehr beeindruckend aus, aber beeile dich nicht, Steine ​​auf mich zu werfen. Achten Sie auf die rechte Seite. Versuchen Sie zuerst herauszufinden, was mit ihr los ist. Hier ist ein Screenshot für einen Hinweis:


Natürlich war der Titel des Artikels ein ernsthafter Spoiler. :) Es

gibt also ein Problem mit der Bildlaufleiste. Wenn Sie nach dem Überschreiten des unteren Randes mehrmals zu einer neuen Zeile wechseln, wird normalerweise eine Bildlaufleiste angezeigt, und Sie können nach oben scrollen. Dies geschieht jedoch erst, wenn wir einen Befehl mit der Ausgabe von etwas schreiben. Sagen wir einfach, das Verhalten ist seltsam. Dies wäre jedoch möglicherweise nicht so kritisch gewesen, wenn die Bildlaufleiste funktioniert hätte ...

Nachdem ich ein wenig getestet hatte, stellte ich fest, dass das Wechseln zu einer neuen Zeile den Puffer nicht erhöht. Dies macht nur die Ausgabe von Befehlen. Das obige Whoami erhöht also den Puffer um nur eine Zeile. Aus diesem Grund werden wir im Laufe der Zeit viel Geschichte verlieren, besonders nach dem Clear.

Das erste, was mir in den Sinn kam, war, unseren Analysator zu verwenden und zu sehen, was darin steht:


Das Fazit ist natürlich von beeindruckender Größe, daher werde ich die Möglichkeit nutzen, alles außer dem mit der ScrollBar zu filtern und zuzuschneiden :


Ich kann nicht sagen, dass es viele Nachrichten gibt ... Nun, vielleicht gibt es dann etwas, das mit dem Puffer zusammenhängt?


Der Analysator versagte nicht und fand etwas Interessantes. Ich habe diese Warnung oben hervorgehoben. Mal sehen, was dort falsch ist:

V501 . Links und rechts vom Operator '-' befinden sich identische Unterausdrücke: bufferHeight - bufferHeight TermControl.cpp 592

bool TermControl::_InitializeTerminal()
{
  ....
  auto bottom = _terminal->GetViewport().BottomExclusive();
  auto bufferHeight = bottom;

  ScrollBar().Maximum(bufferHeight - bufferHeight); // <=  
  ScrollBar().Minimum(0);
  ScrollBar().Value(0);
  ScrollBar().ViewportSize(bufferHeight);
  ....
}

Dieser Code wird von einem Kommentar begleitet: "Richten Sie die Höhe des ScrollViewer und des Rasters ein, mit dem wir unsere Bildlaufhöhe vortäuschen."

Die Simulation der Bildlaufhöhe ist natürlich gut, aber warum setzen wir maximal 0? In der Dokumentation wurde deutlich, dass der Code nicht sehr verdächtig ist. Verstehen Sie mich nicht falsch: Das Subtrahieren einer Variablen von sich selbst ist natürlich verdächtig, aber wir erhalten am Ausgang eine Null, was uns nicht schadet. In jedem Fall habe ich versucht, den Standardwert (1) im Feld Maximum anzugeben :


Die Bildlaufleiste wurde angezeigt, funktioniert aber auch nicht:



Wenn überhaupt, habe ich die <Eingabetaste> 30 Sekunden lang geklemmt. Anscheinend ist dies nicht das Problem. Lassen wir es also so, wie es war, außer indem wir bufferHeight - bufferHeight durch 0 ersetzen :

bool TermControl::_InitializeTerminal()
{
  ....
  auto bottom = _terminal->GetViewport().BottomExclusive();
  auto bufferHeight = bottom;

  ScrollBar().Maximum(0); // <=   
  ScrollBar().Minimum(0);
  ScrollBar().Value(0);
  ScrollBar().ViewportSize(bufferHeight);
  ....
}

Wir sind also nicht besonders nahe daran, das Problem zu lösen. In Ermangelung eines besseren Angebots für eine Debatte. Zuerst könnten wir einen Haltepunkt auf die geänderte Linie setzen, aber ich bezweifle, dass es uns irgendwie helfen wird. Daher müssen wir zuerst das Fragment finden, das für den Versatz des Ansichtsfensters relativ zum Puffer verantwortlich ist.

Ein wenig darüber, wie die lokale (und höchstwahrscheinlich jede andere) Bildlaufleiste funktioniert. Wir haben einen großen Puffer, der die gesamte Ausgabe speichert. Um damit zu interagieren, wird eine Art Abstraktion verwendet, um auf dem Bildschirm zu zeichnen, in diesem Fall das Ansichtsfenster .

Mit diesen beiden Grundelementen können wir verstehen, was unser Problem ist. Das Wechseln zu einer neuen Zeile erhöht den Puffer nicht, und aus diesem Grund können wir einfach nirgendwo hingehen. Daher liegt das Problem darin.

Mit diesem alltäglichen Wissen bewaffnet, setzen wir unsere heroische Debatte fort. Nach einem kleinen Rundgang durch die Funktion machte ich auf dieses Fragment aufmerksam:

// This event is explicitly revoked in the destructor: does not need weak_ref
auto onReceiveOutputFn = [this](const hstring str) {
  _terminal->Write(str);
};
_connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn);

Nachdem wir die ScrollBar oben konfiguriert haben, konfigurieren wir verschiedene Rückruffunktionen und führen __connection.Start () für unser neu geprägtes Fenster aus. Danach wird das obige Lambda genannt. Da dies das erste Mal ist, dass wir etwas in den Puffer schreiben, empfehle ich, unser Debug von dort aus zu starten.

Wir setzen einen Haltepunkt innerhalb des Lambda und schauen in _terminal :



Jetzt haben wir zwei Variablen, die für uns extrem wichtig sind - _buffer und _mutableViewport . Setzen Sie Haltepunkte auf sie und finden Sie heraus, wo sie sich ändern. Richtig , mit _viewport werde ich ein wenig schummeln und einen Haltepunkt nicht für die Variable selbst, sondern für das oberste Feld setzen (wir brauchen ihn nur).

Klicken Sie nun auf <F5> und nichts passiert ... Nun, dann drücken wir ein paar Dutzend Mal <Eingabe>. Nichts ist passiert. Anscheinend haben wir auf _buffer einen Haltepunkt zu rücksichtslos gesetzt, und _viewport blieb erwartungsgemäß oben im Puffer, der nicht größer wurde.

In diesem Fall ist es sinnvoll, einen Befehl einzugeben, damit der Scheitelpunkt aktualisiert wird._viewport . Danach haben wir uns einen sehr interessanten Code ausgedacht:

void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
{
  ....
  // Move the viewport down if the cursor moved below the viewport.
  if (cursorPosAfter.Y > _mutableViewport.BottomInclusive())
  {
    const auto newViewTop =
      std::max(0, cursorPosAfter.Y - (_mutableViewport.Height() - 1));
    if (newViewTop != _mutableViewport.Top())
    {
      _mutableViewport = Viewport::FromDimensions(....); // <=
      notifyScroll = true;
    }
  }
  ....
}

Ich habe auf einen Kommentar hingewiesen, in dem wir aufgehört haben. Wenn Sie sich den Kommentar zum Fragment ansehen, wird klar, dass wir der Lösung näher sind als je zuvor. An dieser Stelle wird der sichtbare Teil relativ zum Puffer verschoben, und wir haben die Möglichkeit, einen Bildlauf durchzuführen. Nachdem ich das Verhalten ein wenig beobachtet hatte, bemerkte ich einen interessanten Punkt: Beim Verschieben in eine neue Zeile entspricht der Wert der Variablen cursorPosAfter.Y dem Wert des Ansichtsfensters , sodass wir ihn nicht weglassen und nichts funktioniert. Darüber hinaus gibt es ein ähnliches Problem mit der Variablen newViewTop . Erhöhen wir daher den Wert von cursorPosAfter.Y um eins und sehen, was passiert ist:

void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
{
  ....
  // Move the viewport down if the cursor moved below the viewport.
  if (cursorPosAfter.Y + 1 > _mutableViewport.BottomInclusive())
  {
    const auto newViewTop =
      std::max(0, cursorPosAfter.Y + 1 - (_mutableViewport.Height() - 1));
    if (newViewTop != _mutableViewport.Top())
    {
      _mutableViewport = Viewport::FromDimensions(....); // <=
      notifyScroll = true;
    }
  }
  ....
}

Und das Startergebnis:


Wunder! Ich habe ne Menge eingegeben und die Bildlaufleiste funktioniert. Richtig, bis wir etwas vorstellen ... Um die Datei zu demonstrieren, werde ich ein GIF anhängen:


Anscheinend machen wir ein paar zusätzliche Sprünge zu einer neuen Linie. Versuchen wir dann, unsere Übergänge mithilfe der X-Koordinate zu begrenzen. Wir verschieben die Linie nur, wenn X 0 ist:

void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
{
  ....
  if (   proposedCursorPosition.X == 0
      && proposedCursorPosition.Y == _mutableViewport.BottomInclusive())
  {
    proposedCursorPosition.Y++;
  }

  // Update Cursor Position
  ursor.SetPosition(proposedCursorPosition);

  const COORD cursorPosAfter = cursor.GetPosition();

  // Move the viewport down if the cursor moved below the viewport.
  if (cursorPosAfter.Y > _mutableViewport.BottomInclusive())
  {
    const auto newViewTop =
      std::max(0, cursorPosAfter.Y - (_mutableViewport.Height() - 1));
    if (newViewTop != _mutableViewport.Top())
    {
      _mutableViewport = Viewport::FromDimensions(....);
      notifyScroll = true;
    }
  }
  ....
}

Das oben geschriebene Fragment verschiebt die Y- Koordinate für den Cursor. Dann aktualisieren wir die Cursorposition. Theoretisch sollte das funktionieren ... Was ist passiert?



Na klar besser. Es gibt jedoch ein Problem darin, dass wir den Ausgabepunkt verschieben, aber den Puffer nicht verschieben. Daher sehen wir zwei Aufrufe desselben Befehls. Es mag natürlich scheinen, dass ich weiß, was ich tue, aber das ist nicht so. :)

Zu diesem Zeitpunkt habe ich beschlossen, den Inhalt des Puffers zu überprüfen, und bin zu dem Punkt zurückgekehrt, an dem ich das Debugging gestartet habe:

// This event is explicitly revoked in the destructor: does not need weak_ref
auto onReceiveOutputFn = [this](const hstring str) {
  _terminal->Write(str);
};
_connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn);

Ich setzte einen Haltepunkt an derselben Stelle wie beim letzten Mal und begann, den Inhalt der Variablen str zu untersuchen . Beginnen wir mit dem, was ich auf meinem Bildschirm gesehen habe:


Was denken Sie , in der sein str String , wenn ich <Enter> drücken?

  1. Zeichenfolge "LANGE BESCHREIBUNG" .
  2. Der gesamte Puffer, den wir jetzt sehen.
  3. Der gesamte Puffer, jedoch ohne die erste Zeile.

Ich werde nicht schmachten - den gesamten Puffer, aber ohne die erste Zeile. Und das ist ein erhebliches Problem, denn gerade deshalb verlieren wir darüber hinaus punktuell die Geschichte. Dies ist , wie unsere Hilfe Ausgabe Fragment aussehen wird nach gehen in eine neue Zeile:


Mit einem Pfeil markierte ich die Stelle, an der sich „LONG DESCRIPTOIN“ befand . Vielleicht dann den Puffer mit einem Versatz von einer Zeile überschreiben? Dies würde funktionieren, wenn dieser Rückruf nicht bei jedem Niesen aufgerufen würde.

Ich habe mindestens drei Situationen entdeckt, in denen es heißt:

  • Wenn wir ein Zeichen eingeben;
  • Wenn wir uns durch die Geschichte bewegen;
  • Wenn wir den Befehl ausführen.

Das Problem ist, dass Sie den Puffer nur verschieben müssen, wenn wir den Befehl ausführen, oder <Eingabe> eingeben. In anderen Fällen ist dies eine schlechte Idee. Wir müssen also irgendwie bestimmen, was verschoben werden muss.

Fazit


Bild 18


Dieser Artikel war ein Versuch zu zeigen, wie geschickt PVS-Studio fehlerhaften Code finden konnte, der zu dem Fehler führte, den ich bemerkte. Die Botschaft zum Thema Subtrahieren einer Variablen von sich selbst hat mich stark motiviert, und ich habe den Text energisch geschrieben. Aber wie Sie sehen, war meine Freude verfrüht und alles stellte sich als viel komplizierter heraus.

Also beschloss ich aufzuhören. Man könnte noch ein paar Abende verbringen, aber je länger ich das tat, desto mehr Probleme traten auf. Ich kann den Windows Terminal-Entwicklern nur viel Glück bei der Behebung dieses Fehlers wünschen. :) :)

Ich hoffe, ich habe den Leser nicht enttäuscht, dass ich die Recherche nicht abgeschlossen habe, und es war interessant für mich, einen Spaziergang durch das Innere des Projekts zu machen. Als Ausgleich empfehle ich die Verwendung des # WindowsTerminal-Promo-Codes, mit dem Sie eine Demoversion von PVS-Studio nicht für eine Woche, sondern sofort für einen Monat erhalten. Wenn Sie den statischen Analysator PVS-Studio in der Praxis nicht ausprobiert haben, ist dies ein guter Grund, genau das zu tun. Geben Sie einfach "#WindowsTerminal" in das Feld "Nachricht" auf der Download-Seite ein .

Bei dieser Gelegenheit möchte ich Sie auch daran erinnern, dass es bald eine Version von C # -Analysator geben wird, die unter Linux und MacOS funktioniert. Und jetzt können Sie sich für Vorversuche anmelden.


Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Link zur Übersetzung: Maxim Zvyagintsev. Die kleine Bildlaufleiste, die nicht konnte .

All Articles