Skip to content

Commit 4a318a5

Browse files
authored
Aux window: validate window bounds are on display bounds (fix microsoft#204346) (microsoft#204399)
1 parent bd06f6c commit 4a318a5

File tree

7 files changed

+192
-162
lines changed

7 files changed

+192
-162
lines changed

src/vs/code/electron-main/app.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -407,23 +407,23 @@ export class CodeApplication extends Disposable {
407407

408408
// All Windows: only allow about:blank auxiliary windows to open
409409
// For all other URLs, delegate to the OS.
410-
contents.setWindowOpenHandler(handler => {
410+
contents.setWindowOpenHandler(details => {
411411

412412
// about:blank windows can open as window witho our default options
413-
if (handler.url === 'about:blank') {
413+
if (details.url === 'about:blank') {
414414
this.logService.trace('[aux window] webContents#setWindowOpenHandler: Allowing auxiliary window to open on about:blank');
415415

416416
return {
417417
action: 'allow',
418-
overrideBrowserWindowOptions: this.auxiliaryWindowsMainService?.createWindow()
418+
overrideBrowserWindowOptions: this.auxiliaryWindowsMainService?.createWindow(details)
419419
};
420420
}
421421

422422
// Any other URL: delegate to OS
423423
else {
424-
this.logService.trace(`webContents#setWindowOpenHandler: Prevented opening window with URL ${handler.url}}`);
424+
this.logService.trace(`webContents#setWindowOpenHandler: Prevented opening window with URL ${details.url}}`);
425425

426-
this.nativeHostMainService?.openExternal(undefined, handler.url);
426+
this.nativeHostMainService?.openExternal(undefined, details.url);
427427

428428
return { action: 'deny' };
429429
}

src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { BrowserWindowConstructorOptions, WebContents } from 'electron';
6+
import { BrowserWindowConstructorOptions, HandlerDetails, WebContents } from 'electron';
77
import { Event } from 'vs/base/common/event';
88
import { Schemas, VSCODE_AUTHORITY } from 'vs/base/common/network';
99
import { IAuxiliaryWindow } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow';
@@ -20,7 +20,7 @@ export interface IAuxiliaryWindowsMainService {
2020
readonly onDidChangeFullScreen: Event<{ window: IAuxiliaryWindow; fullscreen: boolean }>;
2121
readonly onDidTriggerSystemContextMenu: Event<{ readonly window: IAuxiliaryWindow; readonly x: number; readonly y: number }>;
2222

23-
createWindow(): BrowserWindowConstructorOptions;
23+
createWindow(details: HandlerDetails): BrowserWindowConstructorOptions;
2424
registerWindow(webContents: WebContents): void;
2525

2626
getWindowById(windowId: number): IAuxiliaryWindow | undefined;

src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { BrowserWindow, BrowserWindowConstructorOptions, WebContents, app } from 'electron';
6+
import { BrowserWindow, BrowserWindowConstructorOptions, HandlerDetails, WebContents, app } from 'electron';
77
import { Emitter, Event } from 'vs/base/common/event';
88
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
99
import { FileAccess } from 'vs/base/common/network';
@@ -12,7 +12,8 @@ import { AuxiliaryWindow, IAuxiliaryWindow } from 'vs/platform/auxiliaryWindow/e
1212
import { IAuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows';
1313
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1414
import { ILogService } from 'vs/platform/log/common/log';
15-
import { defaultBrowserWindowOptions, getLastFocused } from 'vs/platform/windows/electron-main/windows';
15+
import { IWindowState } from 'vs/platform/window/electron-main/window';
16+
import { WindowStateValidator, defaultBrowserWindowOptions, getLastFocused } from 'vs/platform/windows/electron-main/windows';
1617

1718
export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliaryWindowsMainService {
1819

@@ -70,14 +71,39 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar
7071
});
7172
}
7273

73-
createWindow(): BrowserWindowConstructorOptions {
74-
return this.instantiationService.invokeFunction(defaultBrowserWindowOptions, undefined, {
74+
createWindow(details: HandlerDetails): BrowserWindowConstructorOptions {
75+
return this.instantiationService.invokeFunction(defaultBrowserWindowOptions, this.validateWindowState(details), {
7576
webPreferences: {
7677
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-sandbox/preload-aux.js').fsPath
7778
}
7879
});
7980
}
8081

82+
private validateWindowState(details: HandlerDetails): IWindowState | undefined {
83+
const windowState: IWindowState = {};
84+
85+
const features = details.features.split(','); // for example: popup=yes,left=270,top=14.5,width=800,height=600
86+
for (const feature of features) {
87+
const [key, value] = feature.split('=');
88+
switch (key) {
89+
case 'width':
90+
windowState.width = parseInt(value, 10);
91+
break;
92+
case 'height':
93+
windowState.height = parseInt(value, 10);
94+
break;
95+
case 'left':
96+
windowState.x = parseInt(value, 10);
97+
break;
98+
case 'top':
99+
windowState.y = parseInt(value, 10);
100+
break;
101+
}
102+
}
103+
104+
return WindowStateValidator.validateWindowState(this.logService, windowState);
105+
}
106+
81107
registerWindow(webContents: WebContents): void {
82108
const disposables = new DisposableStore();
83109

src/vs/platform/windows/electron-main/windowImpl.ts

Lines changed: 2 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
3333
import { ThemeIcon } from 'vs/base/common/themables';
3434
import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
3535
import { getMenuBarVisibility, IFolderToOpen, INativeWindowConfiguration, IWindowSettings, IWorkspaceToOpen, MenuBarVisibility, hasNativeTitlebar, useNativeFullScreen, useWindowControlsOverlay } from 'vs/platform/window/common/window';
36-
import { defaultBrowserWindowOptions, IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows';
36+
import { defaultBrowserWindowOptions, IWindowsMainService, OpenContext, WindowStateValidator } from 'vs/platform/windows/electron-main/windows';
3737
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
3838
import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService';
3939
import { IWindowState, ICodeWindow, ILoadEvent, WindowMode, WindowError, LoadReason, defaultWindowState, IBaseWindow } from 'vs/platform/window/electron-main/window';
@@ -1259,7 +1259,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {
12591259
const displays = screen.getAllDisplays();
12601260
hasMultipleDisplays = displays.length > 1;
12611261

1262-
state = this.validateWindowState(state, displays);
1262+
state = WindowStateValidator.validateWindowState(this.logService, state, displays);
12631263
} catch (err) {
12641264
this.logService.warn(`Unexpected error validating window state: ${err}\n${err.stack}`); // somehow display API can be picky about the state to validate
12651265
}
@@ -1270,148 +1270,6 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {
12701270
return [state || defaultWindowState(), hasMultipleDisplays];
12711271
}
12721272

1273-
private validateWindowState(state: IWindowState, displays: Display[]): IWindowState | undefined {
1274-
this.logService.trace(`window#validateWindowState: validating window state on ${displays.length} display(s)`, state);
1275-
1276-
if (
1277-
typeof state.x !== 'number' ||
1278-
typeof state.y !== 'number' ||
1279-
typeof state.width !== 'number' ||
1280-
typeof state.height !== 'number'
1281-
) {
1282-
this.logService.trace('window#validateWindowState: unexpected type of state values');
1283-
1284-
return undefined;
1285-
}
1286-
1287-
if (state.width <= 0 || state.height <= 0) {
1288-
this.logService.trace('window#validateWindowState: unexpected negative values');
1289-
1290-
return undefined;
1291-
}
1292-
1293-
// Single Monitor: be strict about x/y positioning
1294-
// macOS & Linux: these OS seem to be pretty good in ensuring that a window is never outside of it's bounds.
1295-
// Windows: it is possible to have a window with a size that makes it fall out of the window. our strategy
1296-
// is to try as much as possible to keep the window in the monitor bounds. we are not as strict as
1297-
// macOS and Linux and allow the window to exceed the monitor bounds as long as the window is still
1298-
// some pixels (128) visible on the screen for the user to drag it back.
1299-
if (displays.length === 1) {
1300-
const displayWorkingArea = this.getWorkingArea(displays[0]);
1301-
if (displayWorkingArea) {
1302-
this.logService.trace('window#validateWindowState: 1 monitor working area', displayWorkingArea);
1303-
1304-
function ensureStateInDisplayWorkingArea(): void {
1305-
if (!state || typeof state.x !== 'number' || typeof state.y !== 'number' || !displayWorkingArea) {
1306-
return;
1307-
}
1308-
1309-
if (state.x < displayWorkingArea.x) {
1310-
// prevent window from falling out of the screen to the left
1311-
state.x = displayWorkingArea.x;
1312-
}
1313-
1314-
if (state.y < displayWorkingArea.y) {
1315-
// prevent window from falling out of the screen to the top
1316-
state.y = displayWorkingArea.y;
1317-
}
1318-
}
1319-
1320-
// ensure state is not outside display working area (top, left)
1321-
ensureStateInDisplayWorkingArea();
1322-
1323-
if (state.width > displayWorkingArea.width) {
1324-
// prevent window from exceeding display bounds width
1325-
state.width = displayWorkingArea.width;
1326-
}
1327-
1328-
if (state.height > displayWorkingArea.height) {
1329-
// prevent window from exceeding display bounds height
1330-
state.height = displayWorkingArea.height;
1331-
}
1332-
1333-
if (state.x > (displayWorkingArea.x + displayWorkingArea.width - 128)) {
1334-
// prevent window from falling out of the screen to the right with
1335-
// 128px margin by positioning the window to the far right edge of
1336-
// the screen
1337-
state.x = displayWorkingArea.x + displayWorkingArea.width - state.width;
1338-
}
1339-
1340-
if (state.y > (displayWorkingArea.y + displayWorkingArea.height - 128)) {
1341-
// prevent window from falling out of the screen to the bottom with
1342-
// 128px margin by positioning the window to the far bottom edge of
1343-
// the screen
1344-
state.y = displayWorkingArea.y + displayWorkingArea.height - state.height;
1345-
}
1346-
1347-
// again ensure state is not outside display working area
1348-
// (it may have changed from the previous validation step)
1349-
ensureStateInDisplayWorkingArea();
1350-
}
1351-
1352-
return state;
1353-
}
1354-
1355-
// Multi Montior (fullscreen): try to find the previously used display
1356-
if (state.display && state.mode === WindowMode.Fullscreen) {
1357-
const display = displays.find(d => d.id === state.display);
1358-
if (display && typeof display.bounds?.x === 'number' && typeof display.bounds?.y === 'number') {
1359-
this.logService.trace('window#validateWindowState: restoring fullscreen to previous display');
1360-
1361-
const defaults = defaultWindowState(WindowMode.Fullscreen); // make sure we have good values when the user restores the window
1362-
defaults.x = display.bounds.x; // carefull to use displays x/y position so that the window ends up on the correct monitor
1363-
defaults.y = display.bounds.y;
1364-
1365-
return defaults;
1366-
}
1367-
}
1368-
1369-
// Multi Monitor (non-fullscreen): ensure window is within display bounds
1370-
let display: Display | undefined;
1371-
let displayWorkingArea: Rectangle | undefined;
1372-
try {
1373-
display = screen.getDisplayMatching({ x: state.x, y: state.y, width: state.width, height: state.height });
1374-
displayWorkingArea = this.getWorkingArea(display);
1375-
} catch (error) {
1376-
// Electron has weird conditions under which it throws errors
1377-
// e.g. https://github.com/microsoft/vscode/issues/100334 when
1378-
// large numbers are passed in
1379-
}
1380-
1381-
if (
1382-
display && // we have a display matching the desired bounds
1383-
displayWorkingArea && // we have valid working area bounds
1384-
state.x + state.width > displayWorkingArea.x && // prevent window from falling out of the screen to the left
1385-
state.y + state.height > displayWorkingArea.y && // prevent window from falling out of the screen to the top
1386-
state.x < displayWorkingArea.x + displayWorkingArea.width && // prevent window from falling out of the screen to the right
1387-
state.y < displayWorkingArea.y + displayWorkingArea.height // prevent window from falling out of the screen to the bottom
1388-
) {
1389-
this.logService.trace('window#validateWindowState: multi-monitor working area', displayWorkingArea);
1390-
1391-
return state;
1392-
}
1393-
1394-
return undefined;
1395-
}
1396-
1397-
private getWorkingArea(display: Display): Rectangle | undefined {
1398-
1399-
// Prefer the working area of the display to account for taskbars on the
1400-
// desktop being positioned somewhere (https://github.com/microsoft/vscode/issues/50830).
1401-
//
1402-
// Linux X11 sessions sometimes report wrong display bounds, so we validate
1403-
// the reported sizes are positive.
1404-
if (display.workArea.width > 0 && display.workArea.height > 0) {
1405-
return display.workArea;
1406-
}
1407-
1408-
if (display.bounds.width > 0 && display.bounds.height > 0) {
1409-
return display.bounds;
1410-
}
1411-
1412-
return undefined;
1413-
}
1414-
14151273
getBounds(): Rectangle {
14161274
const [x, y] = this._win.getPosition();
14171275
const [width, height] = this._win.getSize();

0 commit comments

Comments
 (0)