Zmiana monitorów w i3

Dziś pokrótce opiszę moją zwycięską walkę z przełączaniem się między ekranami przy użyciu xrandra. Okazuje się bowiem, że pewien nieszczęśliwy zbieg bugów i ficzerów przez dość długi czas (wstyd powiedzieć jak długi) skutecznie mi tę czynność uniemożliwiał.

Przydługi opis problemu

i3 jest wyśmienitym kafelkowym menadżerem okien1, którego używam na codzień od kilku lat (i który szczerze polecam). Podczas normalnej pracy korzystam z dwóch monitorów, zaś i3 maksymalnie mi upraszcza rozmieszczenie i obsługę wielu okien. Czasem jednak muszę wstać od biurka i zabrać ze sobą laptopa. Muszę wówczas odpiąć komputer od stacji dokującej, co powoduje jego fizyczne odłączenie od monitorów. Wypadałoby zatem taki przypadek wykryć i obsłużyć poprzez przełączenie się na wbudowany wyświetlacz. i3 doskonale sobie radzi z przeniesieniem wszystkich obszarów roboczych wraz z zawartymi na nich oknami; radzi sobie nawet z przywróceniem ich na odpowiednich monitorach po ponownym podłączeniu laptopa do stacji dokującej. Nie radzi sobie natomiast z sytuacją, w której przez chwilę nie jest dostępny żaden ekran.

Błąd i ficzer

Teoretycznie można ustawić wszystkie 3 monitory w odpowiednich stanach przy pomocy xrandra. Załóżmy, że przy starcie systemu chcę ustawić monitory w kolejności [HDMI1] [HDMI2], a wbudowany wyłączyć:

# ustawienie domyślne, przy starcie systemu: [HDMI1] [HDMI2]
$ xrandr --output HDMI1 --auto --output HDMI2 --auto --right-of HDMI1 --output LVDS1 --off

Oba monitory wyczerpują pulę crtc2 mojej karty graficznej, więc próba dodania nowego kończy się niepowodzeniem:

$ xrandr --output LVDS1 --auto --left-of HDMI1
xrandr: cannot find crtc for output LVDS1

Teraz aby móc odłączyć laptopa i przenieść na niego zawartość pozostałych monitorów powinienem wydać na przykład następującą komendę:

$ xrandr --output HDMI1 --off --output HDMI2 --off --output LVDS1 --auto
xrandr: cannot find crtc for output LVDS1

Niestety, ona również kończy się niepowodzeniem z powodu 5-letniego błędu polegającego na tym, że xrandr nie kalkuluje crtc, które będą dostępne dla nowego ekranu po wyłączeniu innych w wyniku tej samej komendy. Problem ten pojąłem w mig i szybko znalazłem obejście tego problemu, które by mnie satysfakcjonowało, tj. najpierw zwolnienie crtc zajętych przez monitory zewnętrzne, a następnie włączenie ekranu wbudowanego:

$ xrandr --output HDMI1 --off --output HDMI2 --off
$ xrandr --output LVDS1 --auto
i3: No usable outputs available

Problem polega na tym, że takie rozbicie wywołania xrandra na dwie części powoduje, że przestaje ona być atomowa z punktu widzenia i3. Po pierwszej komendzie i3 wykrywa wyłączenie zewnętrznych ekranów i stwierdza, że jest w stanie, w którym nie ma co zrobić z obszarami roboczymi, oknami itp. W wyniku tego kończy (restartuje) sesję X-ów.

Rozwiązanie

Błąd leży oczywiście po stronie xrandra, który nie pozwala na atomową zmianę stanu monitorów. Jednak leży on nieruszany już od pięciu lat i nie zanosi się w najbliższym czasie na jakąkolwiek poprawkę. Z drugiej strony poprawkę mogłoby wprowadzić i3, lecz wszystkie możliwości takiego bugfixu wydają się być jednakowo złe:

  • i3 mogłoby poczekać aż jakiś ekran stanie się dostępny. OK, ale jak długo miałoby czekać? Co w tym czasie miałoby robić z oknami, które mogą być przecież wciąż modyfikowane na przykład przy pomocy i3-msg3
  • i3 mogłoby nie restartować X-ów. Jest to jednak równoznaczne z czekaniem w nieskończoność, a tę opcję już odrzuciliśmy.
  • ja mógłbym wyłączyć najpierw jeden monitor, następnie włączyć ten od laptopa, a na koniec wyłączyć drugi monitor. Co jednak w sytuacji gdy mamy tylko 1 monitor zewnętrzny i chcemy go przełączyć na inny? Błąd nadal będzie występował.

Istnieje jednak lepsze i czystsze wyjście z sytuacji zdawałoby się patowej. Jeśli rozważyć nasz problem w kategorii klasycznego wyścigu do współdzielonego zasobu, zgadnięcie rozwiązania jest już niemal trywialne. Otóż wystarczy zablokować odczyt stanu monitorów przed rozpoczęciem jego modyfikacji. Jak to zrobić? Wystarczy zatrzymać proces i3 przy pomocy sygnału SIGSTOP, zrobić co chcemy, a następnie go wznowić przy pomocy SIGCONT:

$ killall -SIGSTOP i3
$ xrandr --output HDMI1 --off --output HDMI2 --off
$ xrandr --output LVDS1 --auto
$ killall -SIGCONT i3

Jest to rozwiązanie niezwykle proste i w tej prostocie piękne. Jego pomysłodawcą jest nie kto inny jak autor i3, Michael Stapelberg.


  1. ang.: tiling window manager 

  2. cathode ray tube controller 

  3. i3 umożliwia komunikację międzyprocesową z wykorzystaniem Uniksowych socketów. i3-msg wykorzystuje ten interfejs do wykonywania tych samych działań co użytkownik, ale poprzez shella/skrypty (na przykład zmiany obszaru roboczego czy przeniesienia okna).