窗口管理器Marco--窗口stack错乱问题分析
by HappySeeker
一直想写些关于窗口管理器相关的内容,一直每时间深入分析相关代码。最近刚好遇到一个诡异的QT界面消失问题,顺便分析了相关代码。本文主要针对该问题相关内容进行梳理,其它部分后面有时间再整理吧。
问题
有个QT应用,界面是一个悬浮菜单,用于实现窗口切换。在使用过程中,偶尔会出现菜单无故消失的现象,而相关进程状态正常,无任何异常上报,只是界面消失了。
分析
从现象开始
进程状态正常,说明应用程序本身并无异常,问题应该出在界面显示部分,从现象来看,可以想象如下可能性:
- 应用程序自身代码异常导致界面消失,比如在可能的流程中调用了XUnmapWindow之类的接口。分析了相关代码,并查看故障时进程的堆栈,确认可以排除这种可能性。
- 在X11环境中,谁负责显示?当然是Xorg,不能排除Xorg自身的显示出问题了,这点需要进一步分析确认。
- 可能是其它窗口覆盖了问题应用的窗口。谁负责窗口的层叠次序(stack)? 当然是窗口管理器。那么,问题可能出在窗口管理器上。另一方面,通过在环境中替换默认的窗口管理器(Marco)为Compiz后,问题不再出现,说明Marco自身问题的可能性比较大。
从窗口显示的本质出发
故障现象是:原本显示的窗口突然不见了,而Xorg负责窗口的显示,那么就从Xorg显示窗口的流程进行分析。
对应X11图形应用程序来说,如果使用Xlib的接口直接编程(其它工具GTK/QT之类的直接或间接基于Xlib/XCB接口实现),要创建并显示一个窗口,通常就使用基本的两个接口:
XCreateWindow() //创建窗口,输出为窗口ID
XMapWindow() //显示窗口
即先创建窗口,然后使用map接口显示。
在创建窗口后,会得到标识窗口的ID,该ID在Xorg中具有唯一性。
在本问题中,初步的定位思路:由于进程状态正常,消失的窗口实际存在,其ID也应该可以通过Xlib相关接口查询到,可以现获取其ID,然后直接通过调用XMapWindow()接口来显示,确认是否可以显示出来,如果可以,则可用基本排除是Xorg自身的问题(即可能性2),那么问题范围基本可以缩小到Marco上。
如何显示存在的窗口?
接下来需要解决两个问题:
-
如何获取存在窗口的ID? Xlib中提供了接口XQueryTree(),可以查询以指定窗口为根窗口所有children窗口,意味着我们可以通过该接口获取系统中所有的窗口(以root窗口为根,进行查询)。但如何从这么多窗口中挑出自己需要的窗口呢?每个窗口都有自己的属性,比如Name,根据Name进行匹配就可以了,方法看起来有点笨,但实用即可。
-
如何显示指定窗口?简单,如前面所说,XMapWindow()即可,但在此之前,还需要将需要显示窗口raise到最前端,需要使用XMapWindow()。如此即可。
解决上述两个问题后,可以直接根据窗口的名称来显示指定窗口。示例代码如下:
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
//#include "xwinctl.h"
static Window currWindow;
static Window rootWindow;
static Display *dsp;
static Atom wm_state;
static Atom wm_state_above;
static Atom wm_state_fullscreen;
static char* getWindowName(Window win)
{
int status;
char **list;
int i = 0;
XTextProperty wmName;
char *name = NULL;
if (NULL == dsp){
return NULL;
}
status = XGetWMName(dsp, win, &wmName);
if ((status) && (wmName.value) && (wmName.nitems)){
status = XmbTextPropertyToTextList(dsp, &wmName, &list, &i);
if ((status >= Success) && i && (*list)){
name = (char*)strdup(*list);
}
}
return name;
}
static void findWindow(Window root, char *winName)
{
char *tempWinName = NULL;
int status;
Window parentWindow;
Window *childWindow;
unsigned int numChildren;
unsigned int i;
tempWinName = getWindowName(root);
if(tempWinName){
if (strstr(tempWinName, winName)){
currWindow = root;
}
free(tempWinName);
}
status = XQueryTree(dsp, root, &root, &parentWindow, &childWindow, &numChildren);
if (0 == status)
return;
if (0 == numChildren)
return;
for (i = 0; i < numChildren; i++){
findWindow(childWindow[i], winName);
}
XFree((char*)childWindow);
}
int createWindowHandle(char *winName)
{
int screen = -1;
if (NULL == winName){
return -1;
}
dsp = XOpenDisplay(NULL);
if (NULL == dsp){
return -1;
}
screen = DefaultScreen(dsp);
printf("input winName is : %s\n", winName);
printf("currWindow is : %d\n", currWindow);
rootWindow = RootWindow(dsp, screen);
findWindow(rootWindow, winName);
wm_state = XInternAtom(dsp, "_NET_WM_STATE", False);
wm_state_above = XInternAtom(dsp, "_NET_WM_STATE_ABOVE", False);
wm_state_fullscreen = XInternAtom(dsp, "_NET_WM_STATE_FULLSCREEN", False);
if (0 != currWindow){
return currWindow;
}
return -1;
}
void showWindow()
{
Atom state[2];
state[0] = wm_state_fullscreen;
state[1] = wm_state_above;
//XChangeProperty(dsp, currWindow, wm_state, XA_ATOM, 32, PropModeReplace,
// (unsigned char*)state, 2);
XRaiseWindow(dsp, currWindow);
XMapWindow(dsp, currWindow);
XFlush(dsp);
}
void hideWindow()
{
XUnmapWindow(dsp, currWindow);
XFlush(dsp);
}
int main(int argc, char* argv[]) {
char* winName = argv[1];
int hide = atoi(argv[2]);
createWindowHandle(winName);
if(hide != 1)
showWindow();
else
hideWindow();
}
代码编译,需要手工指定X11依赖库,编译命令如:
gcc -lX11 show-winow.c -o show-winow
编译后的程序使用方法:使用窗口名称作为第一个参数,第二个参数标识是否需要hide窗口,比如需要显示名为Test的窗口,执行的命令为:
./show-winow Test 0
注意:有些应用程序在创建窗口时没有为其指定名称,此时无法使用该程序来显示窗口,建议修改程序,为窗口加上名称。
显示结果
通过上诉命令显示消失的窗口,结果显示成功,说明窗口和Xorg本身并没有问题,那问题应该就在窗口管理器Marco上了。
Marco相关分析
窗口Stack
窗口Stack,即窗口叠放的层次关系,在同一个图形界面环境中,有许多的应用程序可用一起打开,每个应用程序可能会有多个窗口,不可能让每个窗口都显示到独立的区域,所有的窗口都共享同一个桌面空间,那么必然会有窗口叠放顺序的问题,谁在前,谁在后。显然,放在前面(Above)的窗口必然会遮挡后面(Below)的窗口。这种顺序也称Z序。
Xorg自身并不管理窗口的Z序(但其持有最终的窗口Stack),其只是简单的将新创建的窗口放置到最前面,每次如此,然后被遮挡窗口的内容就一直无法为用户所见。可以将系统中的窗口管理器进程(如marco)kill掉,并mv掉其可执行程序(比如:mv /usr/bin/marco /usr/bin/marco.bak,因为多数桌面会自动重启窗口管理器进程),然后,就可以看到效果。
从分工来看,窗口Stack理应由窗口管理器负责,这也是窗口管理器的主要职责之一,所以,窗口Stack必然对窗口管理器可见,而且必定有接口来控制窗口的Z序。
查看Stack
那么如何查看窗口Stack呢?
通过上一节提到的XQueryTree接口,可以查询root窗口所有的children窗口列表,这个列表,本质上就是窗口Stack,只是顺序刚好相反最后一个child位于列表的最后,所以,利用上一节的代码,即可查询到窗口Stack。
但是,其实,还有更简单的方法,有一套shell命令,可以查看窗口的各种详细信息,他们属于xorg-x11-utils软件包,安装即可使用。
这里用到的命令为xwininfo命令,通过如下命令,可用查询到详细的窗口stack信息(还包括父子关系),示例如下:
[root@localhost marco-1.8.2]# xwininfo -root -tree
xwininfo: Window id: 0xae (the root window) (has no name)
Root window id: 0xae (the root window) (has no name)
Parent window id: 0x0 (none)
97 children:
0x2c00003 "Fcitx Input Window": ("fcitx" "fcitx") 600x61+1+333 +1+333
...
0x3c00292 "终端": ("mate-terminal" "Mate-terminal") 187x220+287+314 +287+314
1 child:
0x3c00293 (has no name): () 1x1+-1+-1 +286+313
0x1a00d11 "Caja": ("caja" "Caja") 189x330+518+181 +518+181
1 child:
0x1a00d12 (has no name): () 1x1+-1+-1 +517+180
0x1a00c9e "Caja": ("caja" "Caja") 170x41+1367+769 +1367+769
0x1600003 "下方扩展边缘面板": ("mate-panel" "Mate-panel") 1366x38+0+730 +0+730
2 children:
0x1600023 (has no name): () 1366x38+0+0 +0+730
...
Stack中,包含unmaped和maped的所有窗口,unmaped的窗口对用户来说时不可见的,不会显示到桌面中;mapped的窗口,根据其叠放顺序显示。
QT BypassWindowManagerHint|FramelessWindowHint
消失的QT界面设置了BypassWindowManagerHint属性,该属性设置的目的如下:
- 让该窗口 脱离 窗口管理器的管理,直观表现为:
- panel中看不到相应的图标
- Alt+Tab键切换最近使用窗口时,看不到相应的图标
- 让该窗口位于所有窗口的前面(Above),即其它任何窗口都不能覆盖本窗口。
另外设置了FramelessWindowHint属性,目的是“不要边框”(默认情况下,窗口管理器会为所有的普通窗口加上边框,包括最大化、最小化、关闭按钮等)
这也是这两个属性的本意,悬浮菜单需要这样的效果。
QT 5.3中该属性设置,相关代码如下:
enum WindowType {
Widget = 0x00000000,
Window = 0x00000001,
...
BypassWindowManagerHint = 0x00000400,
X11BypassWindowManagerHint = BypassWindowManagerHint,
FramelessWindowHint = 0x00000800,
...
} 设置Xorg相关属性:
void QXcbWindow::setNetWmWindowFlags(Qt::WindowFlags flags)
{
// in order of decreasing priority
QVector<uint> windowTypes;
...
if (flags & Qt::FramelessWindowHint)
windowTypes.append(atom(QXcbAtom::_KDE_NET_WM_WINDOW_TYPE_OVERRIDE));
windowTypes.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_NORMAL));
...
}
可见,设置这两个属性后,相应的Xorg窗口type会设置为:
_KDE_NET_WM_WINDOW_TYPE_OVERRIDE
该属性对于Xorg来说,就是脱离窗口管理器管理的,位于最前面的窗口,窗口管理器对于这里窗口会做特殊处理,相应类型也是某标准协议中写明的。
Marco如何管理Stack
回到关键的Marco,关键的问题:Marco如何管理窗口Stack?
其管理机制非常简单:
将新窗口插入到除具有Override属性之外的所有的用户可见的窗口之前。
这里有几个关键点:
-
具有Override属性的窗口,对于QT来说,就是设置了BypassWindowManagerHint和FramelessWindowHint的窗口。
-
用户可见的窗口,即经过Map(XMapWindow)操作的窗口。
-
新窗口,即新创建的窗口,这是从Marco的角度看的。通常来说应用程序使用XCreateWindow之类的Xlib接口创建窗口,这个操作由Xorg负责完成,Marco不参与,那Marco如何知道有新窗口创建呢?实际上,marco只监听从Xorg发送过来的事件,由专门的事件循环处理(event_callbak),用户创建窗口时不发送事件,但在Map窗口时,Xorg会向marco发送事件,此时marco会进行相应处理,管理Stack主要在这个流程中进行,后面单独介绍。
按此机制,可以保证最有的Override窗口一直保持在最前面,不会被覆盖。
Marco中调整Stack顺序的主要代码流程如下:
event_callback
meta_window_new //创建Marco端的窗口数据结构
meta_window_new_with_attrs
meta_display_register_x_window //将新窗口加入到自己维护的hash表中,
很关键,后面重点说明
meta_window_reload_properties //设置相关窗口属性后,重新加载属性,
如果相关属性被修改,会导致相
关的callback被调用,后面会进行说明
meta_window_ensure_frame //为窗口添加边框(创建父窗口包含它)
meta_workspace_add_window //将新窗口加入工作区
meta_stack_add //将新窗口加入自己的Stack中
stack_sync_to_server //同步自己的Stack和Xorg的Stack
raise_window_relative_to_managed_windows//会进行上述所说动作:
将新窗口插入合
适的位置
整个流程比较复杂,我们只关注关键的部分raise_window_relative_to_managed_windows函数进行了实际的Stack调整操作:
static void
raise_window_relative_to_managed_windows (MetaScreen *screen,
Window xwindow)
{
...
// 从Xorg获取其Stack信息
XQueryTree (screen->display->xdisplay,
screen->xroot,
&ignored1, &ignored2, &children, &n_children);
...
i = n_children - 1;
// 从最后的child(最前端)开始,论询Stack,找到合适的未知
while (i >= 0)
{
// 最后的child就是窗口本身,说明当前窗口已经位于最前面,需要继续轮询。
if (children[i] == xwindow)
{
/* Do nothing. This means we're already the topmost managed
* window, but it DOES NOT mean we are already just above
* the topmost managed window. This is important because if
* an override redirect window is up, and we map a new
* managed window, the new window is probably above the old
* popup by default, and we want to push it below that
* popup. So keep looking for a sibling managed window
* to be moved below.
*/
}
/*
* 查找自己维护的hash表,该hash表中包含了所有的maped且非Override的窗口
* ,该表的维护机制后面说明。如果找到,说明这个child就是我们插入的参考了
* ,由于是从上到下依次轮询,最先查到的必然就是当前显示在最前面的窗口了。
* 另一方面,由于Override窗口并未加入到hash表中,所以Override窗口永远
* 不会作为参考,也就是说,新加入的窗口的插入位置永远不会超过Override窗
* 口的位置。
*/
else if (meta_display_lookup_x_window (screen->display,
children[i]) != NULL)
{
XWindowChanges changes;
/* children[i] is the topmost managed child */
meta_topic (META_DEBUG_STACK,
"Moving 0x%lx above topmost managed child window 0x%lx\n",
xwindow, children[i]);
changes.sibling = children[i];
changes.stack_mode = Above;
meta_error_trap_push (screen->display);
/*
* 向Xorg发送请求,调整Stack顺序,参考该child当前的位置,将新窗口
* 放到该child前面,具体实现请看Xorg中的ConfigureWindow函数
*/
XConfigureWindow (screen->display->xdisplay,
xwindow,
CWSibling | CWStackMode,
&changes);
meta_error_trap_pop (screen->display, FALSE);
break;
}
--i;
}
...
}
Marco窗口Hash表维护
前面提到,Marco自己维护的窗口Hash表对于Stack的维护至关重要,那这个Hash是如何维护的呢?
主要原则为:
只有mapped(对用户可见的)、而且是 **非** Override窗口才能加入到该列表中。
换句话说,窗口管理器只管理用户可见的需要自己管理的(设置BypassWindowManagerHint属性的窗口显然就是不需要被管理)窗口。相关主要代码流程如下:
event_callback
meta_window_new //创建Marco端的窗口数据结构
meta_window_new_with_attrs
meta_display_register_x_window //将新窗口加入hash表,但有一定条件
在meta_window_new_with_attrs()函数流程中,进行了相应判断,对于Override窗口,和unmap(!IsViewable)窗口,不会走到meta_display_register_x_window流程,相关代码如下:
if (attrs->override_redirect)
{
meta_verbose ("Deciding not to manage override_redirect window 0x%lx\n", xwindow);
return NULL;
}
...
if (must_be_viewable && attrs->map_state != IsViewable)
{
...
return NULL;
}
...
除了上述流程,还有例外情况,后面再单独说明。
看起来一切正常?
上诉的所有机制,凑一起,达到了基本目的:override窗口在最前面,其它新窗口依次排列。
看起来好像没有问题,理应运转正常,但问题确实出现了。
故障发生
user_time & user_time_window
凡是总有例外,这个例外就是由于Xorg环境中加入的user_time属性,该属性的目的是增加桌面系统的易用性,假设这样一种场景:
程序员正在终端或者IDE窗口中编写代码,突然来了个弹出窗口,
此时弹出窗口默认会抢占焦点,导致编程工作被中断,是否会有不爽~
另一种场景:
用户正在桌面中写文档,突然想打开浏览器看看邮件,而浏览器偏僻不争气,
启动巨慢,在浏览器启动期间,用户可能不会傻等,时间宝贵,继续写文档,
在写文档的过程中,浏览器打开了,此时浏览器默认会抢占焦点,而文档编
写过程被打断,难得的灵感可能瞬间消失了,是否会有不爽~
上述两种典型的场景中,用户可能更期望的是:当前交互操作不被中断。而引入user_time属性正是为了解决这样的问题(Windows好像没有解决~),实在用心良苦,但其实代价也不小。
新机制需要应用程序在每次有交互动作时(比如键盘或鼠标事件处理过程中)自己设置窗口的user_time属性,然后要求窗口管理器对窗口user_time属性进行判断和处理,以正确处理焦点问题。
窗口管理器处理理所当然,要求每个应用程序都修改进行适配,这个似乎有点过分,但这个已经写入相应的协议规范中了(估计确实没有其它更好的办法~),很多应用程序也多照做了~,但是还有一个问题:
很多应用程序都会创建不只一个窗口,要求在每个窗口的交互事件中进行user_time处理似乎太过分了?
所以,在加入user_time属性的同时,加入了user_time_window属性,该属性标识处理user_time的窗口ID,即每个应用程序只需要设置一个user_time_window,然后仅对user_time_window处理user_time即可,这样,要求就降低了。
通常情况下,建议将user_time_window设置为toplevel窗口即可,但也有些应用程序使用了特别的其它的方式,比如Wine,后面说明。
注册user_time_window
窗口管理器需要处理user_time,那就必须要管理user_time_window,否则达不到预期效果。所以,当应用程序设置user_time_window时,Marco需要将相应的窗口加入到自己的hash表中,实现方法为:注册相应属性的修改通知事件,当相关属性修改时,会通知Marco进行处理,Marco在相应的回调函数中进行注册,具体代码为:
static void
reload_net_wm_user_time_window (MetaWindow *window,
MetaPropValue *value,
gboolean initial)
{
if (value->type != META_PROP_VALUE_INVALID)
{
/* Unregister old NET_WM_USER_TIME_WINDOW */
if (window->user_time_window != None)
{
/* See the comment to the meta_display_register_x_window call below. */
//把原来注册的窗口清理掉
meta_display_unregister_x_window (window->display,
window->user_time_window);
/* Don't get events on not-managed windows */
XSelectInput (window->display->xdisplay,
window->user_time_window,
NoEventMask);
}
/* Obtain the new NET_WM_USER_TIME_WINDOW and register it */
window->user_time_window = value->v.xwindow;
if (window->user_time_window != None)
{
/* Kind of a hack; display.c:event_callback() ignores events
* for unknown windows. We make window->user_time_window
* known by registering it with window (despite the fact
* that window->xwindow is already registered with window).
* This basically means that property notifies to either the
* window->user_time_window or window->xwindow will be
* treated identically and will result in functions for
* window being called to update it. Maybe we should ignore
* any property notifies to window->user_time_window other
* than atom__NET_WM_USER_TIME ones, but I just don't care
* and it's not specified in the spec anyway.
*/
//注册新窗口
meta_display_register_x_window (window->display,
&window->user_time_window,
window);
...
}
}
}
代码足够简单。但这里有个问题,其加入hash表(注册)时,没有判断该窗口是否为mapped,就是说unmapped的窗口也可能会被加入hash表中,看起来与该hash表设计的初衷不太相符,注册前的一大段注释也说明作者也有这方面的疑虑,但作者比较任性,don’t care,因为没有明文规定。正是这里导致了我们遇到的问题。
问题来了
前面所说,由应用程序自己设置user_time_window,如果设置为toplevel window,一切OK,因为toplevel window必然会map。
但有些应用程序偏偏不这么干(具体原因没有去深究),比如Wine,其创建一个1X1的假窗口来作为user_time_window,而且该窗口并未被map,相关代码流程如下:
create_whole_window
set_initial_wm_hints
update_user_time //其中创建1x1窗口
XChangeProperty // 设置为user_time_window
update_user_time()中创建1x1窗口:
void update_user_time( Time time )
{
if (!user_time_window)
{
//创建1X1窗口
Window win = XCreateWindow( gdi_display, root_window, -1, -1, 1, 1, 0, CopyFromParent,
InputOnly, CopyFromParent, 0, NULL );
if (InterlockedCompareExchangePointer( (void **)&user_time_window, (void *)win, 0 ))
XDestroyWindow( gdi_display, win );
TRACE( "user time window %lx\n", user_time_window );
}
if (!time) return;
XLockDisplay( gdi_display );
if (!last_user_time || (long)(time - last_user_time) > 0)
{
last_user_time = time;
//修改user_time属性
XChangeProperty( gdi_display, user_time_window, x11drv_atom(_NET_WM_USER_TIME),
XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&time, 1 );
}
XUnlockDisplay( gdi_display );
}
Marco在感知到_NET_WM_USER_TIME_WINDOW属性被修改后,会进入上述reload_net_wm_user_time_window流程,直接将该窗口加入hash表,而不顾该窗口是否为maped。
加入hash表后,问题就来了,如之前描述的marco维护Stack的机制,新窗口会默认插入能在hash表中找到的第一个child的前面,也就说后面的窗口会默认被插入到这个1X1窗口的前面,这样,Stack就乱了,无法保证Override窗口不被覆盖了。
试想,如果先打开了一个Override窗口,此时该窗口位于最前面,然后打开wine程序,此时1X1窗口和窗口位于最前面,然后再打开一个全屏窗口,该全屏窗口会被插入到1X1窗口前面,也就是最前面,其就覆盖掉了Override窗口。过程描述如下:
1:open Override window,Stack如下:
Override Win
Normal Win
2:open Wine,Stack如下:
1X1 Win
Override Win
Normal Win
3:open full-screen Window,Stack如下:
full-screen Win
1X1 Win
Override Win
Normal Win
Override窗口被覆盖了!
如果不是设置1X1窗口为user_time_window,这种情况下,Override窗口不会被覆盖(正常过程),过程描述如下:
1:open Override window,Stack如下:
Override Win
Normal Win
2:open Wine,Wine窗口加入到Normal Win前面,Override Win后面。Stack如下:
Override Win
Wine Win
Normal Win
3:open full-screen Window,full-screen win加入到Wine win前面,Override Win后面。Stack如下:
Override Win
full-screen Win
Wine Win
Normal Win
如此,Override窗口永远不会被覆盖。
问题总结
问题看似多方面的问题:
-
不用user_time就不会有问题
-
不设置unmaped的窗口为user_time_window也不会有问题
-
marco能正确处理unmaped的窗口为user_time_window也不会有问题
解决
不能要求应用程序自身去解决或规避,最好还是在窗口管理器中处理,有两种处理思路:
-
在Marco将user_time_window加入hash表时,判断窗口属性是否为maped,如果不是,则不加入。如此能解决stack混乱的问题,非maped的窗口不被管理。但问题是:类似应用程序的user_time属性就失去作用了,也就不能达到之前描述的预期目的。但,说实在话,该功能并不是必须的,对易用性的改进程度尚未可知,而且对性能也有一定的牺牲,所以,从某种程度上,也可以说:不用也罢!
-
可以将这样的unmaped窗口加入到hash表中,但加入时,需要正确处理Stack顺序,至少需要保证加入的窗口位于所有的Override窗口之下,以保证其不被覆盖。但这个实现起来相对比较困难,改动更大,更复杂,风险更大,还需要考虑各种时序和同步问题,一不小心,可能将Stack弄得更乱。需慎重考虑进行。
Subscribe via RSS