1 /**
2  * FreeRDP: A Remote Desktop Protocol Implementation
3  * X11 Monitor Handling
4  *
5  * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com>
6  * Copyright 2017 David Fort <contact@hardening-consulting.com>
7  * Copyright 2018 Kai Harms <kharms@rangee.com>
8  *
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License at
12  *
13  *     http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  */
21 
22 #ifdef HAVE_CONFIG_H
23 #include "config.h"
24 #endif
25 
26 #include <stdio.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <X11/Xlib.h>
30 #include <X11/Xutil.h>
31 
32 #include <winpr/crt.h>
33 
34 #include <freerdp/log.h>
35 
36 #define TAG CLIENT_TAG("x11")
37 
38 #ifdef WITH_XINERAMA
39 #include <X11/extensions/Xinerama.h>
40 #endif
41 
42 #ifdef WITH_XRANDR
43 #include <X11/extensions/Xrandr.h>
44 #include <X11/extensions/randr.h>
45 
46 #if (RANDR_MAJOR * 100 + RANDR_MINOR) >= 105
47 #define USABLE_XRANDR
48 #endif
49 
50 #endif
51 
52 #include "xf_monitor.h"
53 
54 /* See MSDN Section on Multiple Display Monitors: http://msdn.microsoft.com/en-us/library/dd145071
55  */
56 
xf_list_monitors(xfContext * xfc)57 int xf_list_monitors(xfContext* xfc)
58 {
59 	Display* display;
60 	int major, minor;
61 	int i, nmonitors = 0;
62 	display = XOpenDisplay(NULL);
63 
64 	if (!display)
65 	{
66 		WLog_ERR(TAG, "failed to open X display");
67 		return -1;
68 	}
69 
70 #if defined(USABLE_XRANDR)
71 
72 	if (XRRQueryExtension(xfc->display, &major, &minor) &&
73 	    (XRRQueryVersion(xfc->display, &major, &minor) == True) && (major * 100 + minor >= 105))
74 	{
75 		XRRMonitorInfo* monitors =
76 		    XRRGetMonitors(xfc->display, DefaultRootWindow(xfc->display), 1, &nmonitors);
77 
78 		for (i = 0; i < nmonitors; i++)
79 		{
80 			printf("      %s [%d] %dx%d\t+%d+%d\n", monitors[i].primary ? "*" : " ", i,
81 			       monitors[i].width, monitors[i].height, monitors[i].x, monitors[i].y);
82 		}
83 
84 		XRRFreeMonitors(monitors);
85 	}
86 	else
87 #endif
88 #ifdef WITH_XINERAMA
89 	    if (XineramaQueryExtension(display, &major, &minor))
90 	{
91 		if (XineramaIsActive(display))
92 		{
93 			XineramaScreenInfo* screen = XineramaQueryScreens(display, &nmonitors);
94 
95 			for (i = 0; i < nmonitors; i++)
96 			{
97 				printf("      %s [%d] %hdx%hd\t+%hd+%hd\n", (i == 0) ? "*" : " ", i,
98 				       screen[i].width, screen[i].height, screen[i].x_org, screen[i].y_org);
99 			}
100 
101 			XFree(screen);
102 		}
103 	}
104 	else
105 #else
106 	{
107 		Screen* screen = ScreenOfDisplay(display, DefaultScreen(display));
108 		printf("      * [0] %dx%d\t+0+0\n", WidthOfScreen(screen), HeightOfScreen(screen));
109 	}
110 
111 #endif
112 		XCloseDisplay(display);
113 	return 0;
114 }
115 
xf_is_monitor_id_active(xfContext * xfc,UINT32 id)116 static BOOL xf_is_monitor_id_active(xfContext* xfc, UINT32 id)
117 {
118 	UINT32 index;
119 	rdpSettings* settings = xfc->context.settings;
120 
121 	if (!settings->NumMonitorIds)
122 		return TRUE;
123 
124 	for (index = 0; index < settings->NumMonitorIds; index++)
125 	{
126 		if (settings->MonitorIds[index] == id)
127 			return TRUE;
128 	}
129 
130 	return FALSE;
131 }
132 
xf_detect_monitors(xfContext * xfc,UINT32 * pMaxWidth,UINT32 * pMaxHeight)133 BOOL xf_detect_monitors(xfContext* xfc, UINT32* pMaxWidth, UINT32* pMaxHeight)
134 {
135 	int nmonitors = 0;
136 	int monitor_index = 0;
137 	BOOL primaryMonitorFound = FALSE;
138 	VIRTUAL_SCREEN* vscreen;
139 	rdpSettings* settings;
140 	int mouse_x, mouse_y, _dummy_i;
141 	Window _dummy_w;
142 	int current_monitor = 0;
143 	Screen* screen;
144 	MONITOR_INFO* monitor;
145 #if defined WITH_XINERAMA || defined WITH_XRANDR
146 	int major, minor;
147 #endif
148 #if defined(USABLE_XRANDR)
149 	XRRMonitorInfo* rrmonitors = NULL;
150 	BOOL useXRandr = FALSE;
151 #endif
152 
153 	if (!xfc || !pMaxWidth || !pMaxHeight || !xfc->context.settings)
154 		return FALSE;
155 
156 	settings = xfc->context.settings;
157 	vscreen = &xfc->vscreen;
158 	*pMaxWidth = settings->DesktopWidth;
159 	*pMaxHeight = settings->DesktopHeight;
160 
161 	/* get mouse location */
162 	if (!XQueryPointer(xfc->display, DefaultRootWindow(xfc->display), &_dummy_w, &_dummy_w,
163 	                   &mouse_x, &mouse_y, &_dummy_i, &_dummy_i, (void*)&_dummy_i))
164 		mouse_x = mouse_y = 0;
165 
166 #if defined(USABLE_XRANDR)
167 
168 	if (XRRQueryExtension(xfc->display, &major, &minor) &&
169 	    (XRRQueryVersion(xfc->display, &major, &minor) == True) && (major * 100 + minor >= 105))
170 	{
171 		XRRMonitorInfo* rrmonitors =
172 		    XRRGetMonitors(xfc->display, DefaultRootWindow(xfc->display), 1, &vscreen->nmonitors);
173 
174 		if (vscreen->nmonitors > 16)
175 			vscreen->nmonitors = 0;
176 
177 		if (vscreen->nmonitors)
178 		{
179 			int i;
180 
181 			for (i = 0; i < vscreen->nmonitors; i++)
182 			{
183 				vscreen->monitors[i].area.left = rrmonitors[i].x;
184 				vscreen->monitors[i].area.top = rrmonitors[i].y;
185 				vscreen->monitors[i].area.right = rrmonitors[i].x + rrmonitors[i].width - 1;
186 				vscreen->monitors[i].area.bottom = rrmonitors[i].y + rrmonitors[i].height - 1;
187 				vscreen->monitors[i].primary = rrmonitors[i].primary > 0;
188 			}
189 		}
190 
191 		XRRFreeMonitors(rrmonitors);
192 		useXRandr = TRUE;
193 	}
194 	else
195 #endif
196 #ifdef WITH_XINERAMA
197 	    if (XineramaQueryExtension(xfc->display, &major, &minor) && XineramaIsActive(xfc->display))
198 	{
199 		XineramaScreenInfo* screenInfo = XineramaQueryScreens(xfc->display, &vscreen->nmonitors);
200 
201 		if (vscreen->nmonitors > 16)
202 			vscreen->nmonitors = 0;
203 
204 		if (vscreen->nmonitors)
205 		{
206 			int i;
207 
208 			for (i = 0; i < vscreen->nmonitors; i++)
209 			{
210 				vscreen->monitors[i].area.left = screenInfo[i].x_org;
211 				vscreen->monitors[i].area.top = screenInfo[i].y_org;
212 				vscreen->monitors[i].area.right = screenInfo[i].x_org + screenInfo[i].width - 1;
213 				vscreen->monitors[i].area.bottom = screenInfo[i].y_org + screenInfo[i].height - 1;
214 			}
215 		}
216 
217 		XFree(screenInfo);
218 	}
219 
220 #endif
221 	xfc->fullscreenMonitors.top = xfc->fullscreenMonitors.bottom = xfc->fullscreenMonitors.left =
222 	    xfc->fullscreenMonitors.right = 0;
223 
224 	/* Determine which monitor that the mouse cursor is on */
225 	if (vscreen->monitors)
226 	{
227 		int i;
228 
229 		for (i = 0; i < vscreen->nmonitors; i++)
230 		{
231 			if ((mouse_x >= vscreen->monitors[i].area.left) &&
232 			    (mouse_x <= vscreen->monitors[i].area.right) &&
233 			    (mouse_y >= vscreen->monitors[i].area.top) &&
234 			    (mouse_y <= vscreen->monitors[i].area.bottom))
235 			{
236 				current_monitor = i;
237 				break;
238 			}
239 		}
240 	}
241 
242 	/*
243 	   Even for a single monitor, we need to calculate the virtual screen to support
244 	   window managers that do not implement all X window state hints.
245 
246 	   If the user did not request multiple monitor or is using workarea
247 	   without remote app, we force the number of monitors be 1 so later
248 	   the rest of the client don't end up using more monitors than the user desires.
249 	 */
250 	if ((!settings->UseMultimon && !settings->SpanMonitors) ||
251 	    (settings->Workarea && !settings->RemoteApplicationMode))
252 	{
253 		/* If no monitors were specified on the command-line then set the current monitor as active
254 		 */
255 		if (!settings->NumMonitorIds)
256 		{
257 			settings->MonitorIds[0] = current_monitor;
258 		}
259 
260 		/* Always sets number of monitors from command-line to just 1.
261 		 * If the monitor is invalid then we will default back to current monitor
262 		 * later as a fallback. So, there is no need to validate command-line entry here.
263 		 */
264 		settings->NumMonitorIds = 1;
265 	}
266 
267 	/* WORKAROUND: With Remote Application Mode - using NET_WM_WORKAREA
268 	 * causes issues with the ability to fully size the window vertically
269 	 * (the bottom of the window area is never updated). So, we just set
270 	 * the workArea to match the full Screen width/height.
271 	 */
272 	if (settings->RemoteApplicationMode || !xf_GetWorkArea(xfc))
273 	{
274 		/*
275 		   if only 1 monitor is enabled, use monitor area
276 		   this is required in case of a screen composed of more than one monitor
277 		   but user did not enable multimonitor
278 		*/
279 		if ((settings->NumMonitorIds == 1) && (vscreen->nmonitors > current_monitor))
280 		{
281 			monitor = vscreen->monitors + current_monitor;
282 
283 			if (!monitor)
284 				return FALSE;
285 
286 			xfc->workArea.x = monitor->area.left;
287 			xfc->workArea.y = monitor->area.top;
288 			xfc->workArea.width = monitor->area.right - monitor->area.left + 1;
289 			xfc->workArea.height = monitor->area.bottom - monitor->area.top + 1;
290 		}
291 		else
292 		{
293 			xfc->workArea.x = 0;
294 			xfc->workArea.y = 0;
295 			xfc->workArea.width = WidthOfScreen(xfc->screen);
296 			xfc->workArea.height = HeightOfScreen(xfc->screen);
297 		}
298 	}
299 
300 	if (settings->Fullscreen)
301 	{
302 		*pMaxWidth = WidthOfScreen(xfc->screen);
303 		*pMaxHeight = HeightOfScreen(xfc->screen);
304 	}
305 	else if (settings->Workarea)
306 	{
307 		*pMaxWidth = xfc->workArea.width;
308 		*pMaxHeight = xfc->workArea.height;
309 	}
310 	else if (settings->PercentScreen)
311 	{
312 		/* If we have specific monitor information then limit the PercentScreen value
313 		 * to only affect the current monitor vs. the entire desktop
314 		 */
315 		if (vscreen->nmonitors > 0)
316 		{
317 			if (!vscreen->monitors)
318 				return FALSE;
319 
320 			*pMaxWidth = vscreen->monitors[current_monitor].area.right -
321 			             vscreen->monitors[current_monitor].area.left + 1;
322 			*pMaxHeight = vscreen->monitors[current_monitor].area.bottom -
323 			              vscreen->monitors[current_monitor].area.top + 1;
324 
325 			if (settings->PercentScreenUseWidth)
326 				*pMaxWidth = ((vscreen->monitors[current_monitor].area.right -
327 				               vscreen->monitors[current_monitor].area.left + 1) *
328 				              settings->PercentScreen) /
329 				             100;
330 
331 			if (settings->PercentScreenUseHeight)
332 				*pMaxHeight = ((vscreen->monitors[current_monitor].area.bottom -
333 				                vscreen->monitors[current_monitor].area.top + 1) *
334 				               settings->PercentScreen) /
335 				              100;
336 		}
337 		else
338 		{
339 			*pMaxWidth = xfc->workArea.width;
340 			*pMaxHeight = xfc->workArea.height;
341 
342 			if (settings->PercentScreenUseWidth)
343 				*pMaxWidth = (xfc->workArea.width * settings->PercentScreen) / 100;
344 
345 			if (settings->PercentScreenUseHeight)
346 				*pMaxHeight = (xfc->workArea.height * settings->PercentScreen) / 100;
347 		}
348 	}
349 	else if (settings->DesktopWidth && settings->DesktopHeight)
350 	{
351 		*pMaxWidth = settings->DesktopWidth;
352 		*pMaxHeight = settings->DesktopHeight;
353 	}
354 
355 	/* Create array of all active monitors by taking into account monitors requested on the
356 	 * command-line */
357 	{
358 		int i;
359 
360 		for (i = 0; i < vscreen->nmonitors; i++)
361 		{
362 			MONITOR_ATTRIBUTES* attrs;
363 
364 			if (!xf_is_monitor_id_active(xfc, (UINT32)i))
365 				continue;
366 
367 			if (!vscreen->monitors)
368 				return FALSE;
369 
370 			settings->MonitorDefArray[nmonitors].x =
371 			    (vscreen->monitors[i].area.left *
372 			     (settings->PercentScreenUseWidth ? settings->PercentScreen : 100)) /
373 			    100;
374 			settings->MonitorDefArray[nmonitors].y =
375 			    (vscreen->monitors[i].area.top *
376 			     (settings->PercentScreenUseHeight ? settings->PercentScreen : 100)) /
377 			    100;
378 			settings->MonitorDefArray[nmonitors].width =
379 			    ((vscreen->monitors[i].area.right - vscreen->monitors[i].area.left + 1) *
380 			     (settings->PercentScreenUseWidth ? settings->PercentScreen : 100)) /
381 			    100;
382 			settings->MonitorDefArray[nmonitors].height =
383 			    ((vscreen->monitors[i].area.bottom - vscreen->monitors[i].area.top + 1) *
384 			     (settings->PercentScreenUseWidth ? settings->PercentScreen : 100)) /
385 			    100;
386 			settings->MonitorDefArray[nmonitors].orig_screen = i;
387 #ifdef USABLE_XRANDR
388 
389 			if (useXRandr && rrmonitors)
390 			{
391 				Rotation rot, ret;
392 				attrs = &settings->MonitorDefArray[nmonitors].attributes;
393 				attrs->physicalWidth = rrmonitors[i].mwidth;
394 				attrs->physicalHeight = rrmonitors[i].mheight;
395 				ret = XRRRotations(xfc->display, i, &rot);
396 				attrs->orientation = rot;
397 			}
398 
399 #endif
400 
401 			if ((UINT32)i == settings->MonitorIds[0])
402 			{
403 				settings->MonitorDefArray[nmonitors].is_primary = TRUE;
404 				settings->MonitorLocalShiftX = settings->MonitorDefArray[nmonitors].x;
405 				settings->MonitorLocalShiftY = settings->MonitorDefArray[nmonitors].y;
406 				primaryMonitorFound = TRUE;
407 			}
408 
409 			nmonitors++;
410 		}
411 	}
412 
413 	/* If no monitor is active(bogus command-line monitor specification) - then lets try to fallback
414 	 * to go fullscreen on the current monitor only */
415 	if (nmonitors == 0 && vscreen->nmonitors > 0)
416 	{
417 		INT32 width, height;
418 		if (!vscreen->monitors)
419 			return FALSE;
420 
421 		width = vscreen->monitors[current_monitor].area.right -
422 		        vscreen->monitors[current_monitor].area.left + 1L;
423 		height = vscreen->monitors[current_monitor].area.bottom -
424 		         vscreen->monitors[current_monitor].area.top + 1L;
425 
426 		settings->MonitorDefArray[0].x = vscreen->monitors[current_monitor].area.left;
427 		settings->MonitorDefArray[0].y = vscreen->monitors[current_monitor].area.top;
428 		settings->MonitorDefArray[0].width = MIN(width, (INT64)(*pMaxWidth));
429 		settings->MonitorDefArray[0].height = MIN(height, (INT64)(*pMaxHeight));
430 		settings->MonitorDefArray[0].orig_screen = current_monitor;
431 		nmonitors = 1;
432 	}
433 
434 	settings->MonitorCount = nmonitors;
435 
436 	/* If we have specific monitor information */
437 	if (settings->MonitorCount)
438 	{
439 		UINT32 i;
440 		/* Initialize bounding rectangle for all monitors */
441 		int vX = settings->MonitorDefArray[0].x;
442 		int vY = settings->MonitorDefArray[0].y;
443 		int vR = vX + settings->MonitorDefArray[0].width;
444 		int vB = vY + settings->MonitorDefArray[0].height;
445 		xfc->fullscreenMonitors.top = xfc->fullscreenMonitors.bottom =
446 		    xfc->fullscreenMonitors.left = xfc->fullscreenMonitors.right =
447 		        settings->MonitorDefArray[0].orig_screen;
448 
449 		/* Calculate bounding rectangle around all monitors to be used AND
450 		 * also set the Xinerama indices which define left/top/right/bottom monitors.
451 		 */
452 		for (i = 1; i < settings->MonitorCount; i++)
453 		{
454 			/* does the same as gdk_rectangle_union */
455 			int destX = MIN(vX, settings->MonitorDefArray[i].x);
456 			int destY = MIN(vY, settings->MonitorDefArray[i].y);
457 			int destR =
458 			    MAX(vR, settings->MonitorDefArray[i].x + settings->MonitorDefArray[i].width);
459 			int destB =
460 			    MAX(vB, settings->MonitorDefArray[i].y + settings->MonitorDefArray[i].height);
461 
462 			if (vX != destX)
463 				xfc->fullscreenMonitors.left = settings->MonitorDefArray[i].orig_screen;
464 
465 			if (vY != destY)
466 				xfc->fullscreenMonitors.top = settings->MonitorDefArray[i].orig_screen;
467 
468 			if (vR != destR)
469 				xfc->fullscreenMonitors.right = settings->MonitorDefArray[i].orig_screen;
470 
471 			if (vB != destB)
472 				xfc->fullscreenMonitors.bottom = settings->MonitorDefArray[i].orig_screen;
473 
474 			vX = destX / ((settings->PercentScreenUseWidth ? settings->PercentScreen : 100) / 100.);
475 			vY =
476 			    destY / ((settings->PercentScreenUseHeight ? settings->PercentScreen : 100) / 100.);
477 			vR = destR / ((settings->PercentScreenUseWidth ? settings->PercentScreen : 100) / 100.);
478 			vB =
479 			    destB / ((settings->PercentScreenUseHeight ? settings->PercentScreen : 100) / 100.);
480 		}
481 
482 		vscreen->area.left = 0;
483 		vscreen->area.right = vR - vX - 1;
484 		vscreen->area.top = 0;
485 		vscreen->area.bottom = vB - vY - 1;
486 
487 		if (settings->Workarea)
488 		{
489 			vscreen->area.top = xfc->workArea.y;
490 			vscreen->area.bottom = xfc->workArea.height + xfc->workArea.y - 1;
491 		}
492 
493 		if (!primaryMonitorFound)
494 		{
495 			/* If we have a command line setting we should use it */
496 			if (settings->NumMonitorIds)
497 			{
498 				/* The first monitor is the first in the setting which should be used */
499 				monitor_index = settings->MonitorIds[0];
500 			}
501 			else
502 			{
503 				/* This is the same as when we would trust the Xinerama results..
504 				   and set the monitor index to zero.
505 				   The monitor listed with /monitor-list on index zero is always the primary
506 				*/
507 				screen = DefaultScreenOfDisplay(xfc->display);
508 				monitor_index = XScreenNumberOfScreen(screen);
509 			}
510 
511 			int j = monitor_index;
512 
513 			/* If the "default" monitor is not 0,0 use it */
514 			if (settings->MonitorDefArray[j].x != 0 || settings->MonitorDefArray[j].y != 0)
515 			{
516 				settings->MonitorDefArray[j].is_primary = TRUE;
517 				settings->MonitorLocalShiftX = settings->MonitorDefArray[j].x;
518 				settings->MonitorLocalShiftY = settings->MonitorDefArray[j].y;
519 				primaryMonitorFound = TRUE;
520 			}
521 			else
522 			{
523 				/* Lets try to see if there is a monitor with a 0,0 coordinate and use it as a
524 				 * fallback*/
525 				for (i = 0; i < settings->MonitorCount; i++)
526 				{
527 					if (!primaryMonitorFound && settings->MonitorDefArray[i].x == 0 &&
528 					    settings->MonitorDefArray[i].y == 0)
529 					{
530 						settings->MonitorDefArray[i].is_primary = TRUE;
531 						settings->MonitorLocalShiftX = settings->MonitorDefArray[i].x;
532 						settings->MonitorLocalShiftY = settings->MonitorDefArray[i].y;
533 						primaryMonitorFound = TRUE;
534 					}
535 				}
536 			}
537 		}
538 
539 		/* Subtract monitor shift from monitor variables for server-side use.
540 		 * We maintain monitor shift value as Window requires the primary monitor to have a
541 		 * coordinate of 0,0 In some X configurations, no monitor may have a coordinate of 0,0. This
542 		 * can also be happen if the user requests specific monitors from the command-line as well.
543 		 * So, we make sure to translate our primary monitor's upper-left corner to 0,0 on the
544 		 * server.
545 		 */
546 		for (i = 0; i < settings->MonitorCount; i++)
547 		{
548 			settings->MonitorDefArray[i].x =
549 			    settings->MonitorDefArray[i].x - settings->MonitorLocalShiftX;
550 			settings->MonitorDefArray[i].y =
551 			    settings->MonitorDefArray[i].y - settings->MonitorLocalShiftY;
552 		}
553 
554 		/* Set the desktop width and height according to the bounding rectangle around the active
555 		 * monitors */
556 		*pMaxWidth = MIN(*pMaxWidth, (UINT32)vscreen->area.right - vscreen->area.left + 1);
557 		*pMaxHeight = MIN(*pMaxHeight, (UINT32)vscreen->area.bottom - vscreen->area.top + 1);
558 	}
559 
560 	/* some 2008 server freeze at logon if we announce support for monitor layout PDU with
561 	 * #monitors < 2. So let's announce it only if we have more than 1 monitor.
562 	 */
563 	if (settings->MonitorCount)
564 		settings->SupportMonitorLayoutPdu = TRUE;
565 
566 #ifdef USABLE_XRANDR
567 
568 	if (rrmonitors)
569 		XRRFreeMonitors(rrmonitors);
570 
571 #endif
572 	return TRUE;
573 }
574