Do not attempt to open a workspace/folder that no longer exists when reloading window (fix #110982)

This commit is contained in:
Benjamin Pasero 2021-01-25 07:44:40 +01:00
parent d2cbc66835
commit c8ff3471b5
4 changed files with 105 additions and 74 deletions

View file

@ -3,10 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as path from 'vs/base/common/path';
import * as nls from 'vs/nls';
import * as perf from 'vs/base/common/performance';
import { release } from 'os';
import { join } from 'vs/base/common/path';
import { localize } from 'vs/nls';
import { getMarks, mark } from 'vs/base/common/performance';
import { Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme, Event, RenderProcessGoneDetails } from 'electron';
@ -20,7 +20,7 @@ import { WindowMinimumSize, IWindowSettings, MenuBarVisibility, getTitleBarStyle
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { browserCodeLoadingCacheStrategy, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows';
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService';
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
@ -189,9 +189,9 @@ export class CodeWindow extends Disposable implements ICodeWindow {
// Linux: always
// Windows: only when running out of sources, otherwise an icon is set by us on the executable
if (isLinux) {
options.icon = path.join(this.environmentService.appRoot, 'resources/linux/code.png');
options.icon = join(this.environmentService.appRoot, 'resources/linux/code.png');
} else if (isWindows && !this.environmentService.isBuilt) {
options.icon = path.join(this.environmentService.appRoot, 'resources/win32/code_150x150.png');
options.icon = join(this.environmentService.appRoot, 'resources/win32/code_150x150.png');
}
if (isMacintosh && !this.useNativeFullScreen()) {
@ -592,9 +592,9 @@ export class CodeWindow extends Disposable implements ICodeWindow {
this.dialogMainService.showMessageBox({
title: product.nameLong,
type: 'warning',
buttons: [mnemonicButtonLabel(nls.localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(nls.localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(nls.localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
message: nls.localize('appStalled', "The window is no longer responding"),
detail: nls.localize('appStalledDetail', "You can reopen or close the window or keep waiting."),
buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
message: localize('appStalled', "The window is no longer responding"),
detail: localize('appStalledDetail', "You can reopen or close the window or keep waiting."),
noLink: true
}, this._win).then(result => {
if (!this._win) {
@ -614,17 +614,17 @@ export class CodeWindow extends Disposable implements ICodeWindow {
else {
let message: string;
if (details && details.reason !== 'crashed') {
message = nls.localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details?.reason);
message = localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details?.reason);
} else {
message = nls.localize('appCrashed', "The window has crashed", details?.reason);
message = localize('appCrashed', "The window has crashed", details?.reason);
}
this.dialogMainService.showMessageBox({
title: product.nameLong,
type: 'warning',
buttons: [mnemonicButtonLabel(nls.localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(nls.localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
message,
detail: nls.localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."),
detail: localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."),
noLink: true
}, this._win).then(result => {
if (!this._win) {
@ -690,7 +690,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
}
}
load(config: INativeWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void {
load(config: INativeWindowConfiguration, { isReload, disableExtensions }: { isReload?: boolean, disableExtensions?: boolean } = Object.create(null)): void {
// If this window was loaded before from the command line
// (as indicated by VSCODE_CLI environment), make sure to
@ -741,7 +741,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
}
// Load URL
perf.mark('code/willOpenNewWindow');
mark('code/willOpenNewWindow');
this._win.loadURL(this.getUrl(configuration));
// Make window visible if it did not open in N seconds because this indicates an error
@ -760,11 +760,14 @@ export class CodeWindow extends Disposable implements ICodeWindow {
this._onLoad.fire();
}
reload(cli?: NativeParsedArgs): void {
async reload(cli?: NativeParsedArgs): Promise<void> {
// Copy our current config for reuse
const configuration = Object.assign({}, this.currentConfig);
// Validate workspace
configuration.workspace = await this.validateWorkspace(configuration);
// Delete some properties we do not want during reload
delete configuration.filesToOpenOrCreate;
delete configuration.filesToDiff;
@ -774,17 +777,44 @@ export class CodeWindow extends Disposable implements ICodeWindow {
// in extension development mode. These options are all development related.
if (this.isExtensionDevelopmentHost && cli) {
configuration.verbose = cli.verbose;
configuration.debugId = cli.debugId;
configuration['inspect-extensions'] = cli['inspect-extensions'];
configuration['inspect-brk-extensions'] = cli['inspect-brk-extensions'];
configuration.debugId = cli.debugId;
configuration['extensions-dir'] = cli['extensions-dir'];
}
configuration.isInitialStartup = false; // since this is a reload
// Load config
const disableExtensions = cli ? cli['disable-extensions'] : undefined;
this.load(configuration, true, disableExtensions);
this.load(configuration, { isReload: true, disableExtensions: cli?.['disable-extensions'] });
}
private async validateWorkspace(configuration: INativeWindowConfiguration): Promise<IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined> {
// Multi folder
if (isWorkspaceIdentifier(configuration.workspace)) {
const configPath = configuration.workspace.configPath;
if (configPath.scheme === Schemas.file) {
const workspaceExists = await this.fileService.exists(configPath);
if (!workspaceExists) {
return undefined;
}
}
}
// Single folder
else if (isSingleFolderWorkspaceIdentifier(configuration.workspace)) {
const uri = configuration.workspace.uri;
if (uri.scheme === Schemas.file) {
const folderExists = await this.fileService.exists(uri);
if (!folderExists) {
return undefined;
}
}
}
// Workspace is valid
return configuration.workspace;
}
private getUrl(windowConfiguration: INativeWindowConfiguration): string {
@ -817,14 +847,14 @@ export class CodeWindow extends Disposable implements ICodeWindow {
windowConfiguration.maximized = this._win.isMaximized();
// Dump Perf Counters
windowConfiguration.perfMarks = perf.getMarks();
windowConfiguration.perfMarks = getMarks();
// Parts splash
windowConfiguration.partsSplashPath = path.join(this.environmentService.userDataPath, 'rapid_render.json');
windowConfiguration.partsSplashPath = join(this.environmentService.userDataPath, 'rapid_render.json');
// OS Info
windowConfiguration.os = {
release: os.release()
release: release()
};
// Config (combination of process.argv and window configuration)
@ -1175,7 +1205,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
if (visibility === 'toggle') {
if (notify) {
this.send('vscode:showInfoMessage', nls.localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key."));
this.send('vscode:showInfoMessage', localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key."));
}
}

View file

@ -84,7 +84,7 @@ export interface ICodeWindow extends IDisposable {
addTabbedWindow(window: ICodeWindow): void;
load(config: INativeWindowConfiguration, isReload?: boolean): void;
load(config: INativeWindowConfiguration, options?: { isReload?: boolean }): void;
reload(cli?: NativeParsedArgs): void;
focus(options?: { force: boolean }): void;

View file

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { statSync, unlink } from 'fs';
import { statSync } from 'fs';
import { basename, normalize, join, posix } from 'vs/base/common/path';
import { localize } from 'vs/nls';
import { coalesce, distinct, firstOrDefault } from 'vs/base/common/arrays';
@ -40,6 +40,7 @@ import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware, sanitizeFileP
import { CharCode } from 'vs/base/common/charCode';
import { getPathLabel } from 'vs/base/common/labels';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IFileService } from 'vs/platform/files/common/files';
//#region Helper Interfaces
@ -143,7 +144,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService,
@IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IDialogMainService private readonly dialogMainService: IDialogMainService
@IDialogMainService private readonly dialogMainService: IDialogMainService,
@IFileService private readonly fileService: IFileService
) {
super();
@ -312,7 +314,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
// process can continue. We do this by deleting the waitMarkerFilePath.
const waitMarkerFileURI = openConfig.waitMarkerFileURI;
if (openConfig.context === OpenContext.CLI && waitMarkerFileURI && usedWindows.length === 1 && usedWindows[0]) {
usedWindows[0].whenClosedOrLoaded.then(() => unlink(waitMarkerFileURI.fsPath, () => undefined));
usedWindows[0].whenClosedOrLoaded.then(() => this.fileService.del(waitMarkerFileURI), () => undefined);
}
return usedWindows;
@ -650,7 +652,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
const pathsToOpen: IPathToOpen[] = [];
const pathResolveOptions: IPathResolveOptions = { gotoLineMode: openConfig.gotoLineMode };
for (const pathToOpen of coalesce(openConfig.urisToOpen || [])) {
const path = this.resolveUri(pathToOpen, pathResolveOptions);
const path = this.resolveOpenable(pathToOpen, pathResolveOptions);
// Path exists
if (path) {
@ -688,9 +690,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
const folderUris = cli['folder-uri'];
if (folderUris) {
for (const rawFolderUri of folderUris) {
const folderUri = this.argToUri(rawFolderUri);
const folderUri = this.cliArgToUri(rawFolderUri);
if (folderUri) {
const path = this.resolveUri({ folderUri }, pathResolveOptions);
const path = this.resolveOpenable({ folderUri }, pathResolveOptions);
if (path) {
pathsToOpen.push(path);
}
@ -702,9 +704,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
const fileUris = cli['file-uri'];
if (fileUris) {
for (const rawFileUri of fileUris) {
const fileUri = this.argToUri(rawFileUri);
const fileUri = this.cliArgToUri(rawFileUri);
if (fileUri) {
const path = this.resolveUri(hasWorkspaceFileExtension(rawFileUri) ? { workspaceUri: fileUri } : { fileUri }, pathResolveOptions);
const path = this.resolveOpenable(hasWorkspaceFileExtension(rawFileUri) ? { workspaceUri: fileUri } : { fileUri }, pathResolveOptions);
if (path) {
pathsToOpen.push(path);
}
@ -715,7 +717,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
// folder or file paths
const cliPaths = cli._;
for (const cliPath of cliPaths) {
const path = this.resolvePath(cliPath, pathResolveOptions);
const path = this.doResolveFileOpenable(cliPath, pathResolveOptions);
if (path) {
pathsToOpen.push(path);
}
@ -724,7 +726,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
return pathsToOpen;
}
private argToUri(arg: string): URI | undefined {
private cliArgToUri(arg: string): URI | undefined {
try {
const uri = URI.parse(arg);
if (!uri.scheme) {
@ -772,7 +774,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
// Workspaces
if (lastSessionWindow.workspace) {
const pathToOpen = this.resolveUri({ workspaceUri: lastSessionWindow.workspace.configPath }, { remoteAuthority: lastSessionWindow.remoteAuthority });
const pathToOpen = this.resolveOpenable({ workspaceUri: lastSessionWindow.workspace.configPath }, { remoteAuthority: lastSessionWindow.remoteAuthority });
if (isWorkspacePathToOpen(pathToOpen)) {
pathsToOpen.push(pathToOpen);
}
@ -780,7 +782,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
// Folders
else if (lastSessionWindow.folderUri) {
const pathToOpen = this.resolveUri({ folderUri: lastSessionWindow.folderUri }, { remoteAuthority: lastSessionWindow.remoteAuthority });
const pathToOpen = this.resolveOpenable({ folderUri: lastSessionWindow.folderUri }, { remoteAuthority: lastSessionWindow.remoteAuthority });
if (isSingleFolderWorkspacePathToOpen(pathToOpen)) {
pathsToOpen.push(pathToOpen);
}
@ -812,19 +814,19 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
return restoreWindows;
}
private resolveUri(openable: IWindowOpenable, options: IPathResolveOptions = {}): IPathToOpen | undefined {
private resolveOpenable(openable: IWindowOpenable, options: IPathResolveOptions = {}): IPathToOpen | undefined {
// handle local openables with some extra validation
// handle file:// openables with some extra validation
let uri = this.resourceFromOpenable(openable);
if (uri.scheme === Schemas.file) {
return this.resolvePath(uri.fsPath, options, isFileToOpen(openable));
return this.doResolveFileOpenable(openable, options);
}
// handle remote openables
return this.resolveRemoteUri(openable, options);
// handle non file:// openables
return this.doResolveRemoteOpenable(openable, options);
}
private resolveRemoteUri(openable: IWindowOpenable, options: IPathResolveOptions = {}): IPathToOpen | undefined {
private doResolveRemoteOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined {
let uri = this.resourceFromOpenable(openable);
// open remote if either specified in the cli or if it's a remotehost URI
@ -833,28 +835,23 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
// normalize URI
uri = removeTrailingPathSeparator(normalizePath(uri));
// Remote File
// File
if (isFileToOpen(openable)) {
if (options.gotoLineMode) {
const { path, line, column } = parseLineAndColumnAware(uri.path);
return {
fileUri: uri.with({ path }),
lineNumber: line,
columnNumber: column,
remoteAuthority
};
return { fileUri: uri.with({ path }), lineNumber: line, columnNumber: column, remoteAuthority };
}
return { fileUri: uri, remoteAuthority };
}
// Remote Workspace
// Workspace
else if (isWorkspaceToOpen(openable)) {
return { workspace: getWorkspaceIdentifier(uri), remoteAuthority };
}
// Remote Folder
// Folder
return { workspace: getSingleFolderWorkspaceIdentifier(uri), remoteAuthority };
}
@ -870,7 +867,17 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
return openable.fileUri;
}
private resolvePath(path: string, options: IPathResolveOptions, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined {
private doResolveFileOpenable(path: string, options: IPathResolveOptions): IPathToOpen | undefined;
private doResolveFileOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined;
private doResolveFileOpenable(pathOrOpenable: string | IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined {
let path: string;
let forceOpenWorkspaceAsFile = false;
if (typeof pathOrOpenable === 'string') {
path = pathOrOpenable;
} else {
path = this.resourceFromOpenable(pathOrOpenable).fsPath;
forceOpenWorkspaceAsFile = isFileToOpen(pathOrOpenable);
}
// Extract line/col information from path
let lineNumber: number | undefined;
@ -884,17 +891,17 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
path = parsedPath.path;
}
// Handle remote paths
// With remote: resolve path as remote URI
const remoteAuthority = options.remoteAuthority;
if (remoteAuthority) {
return this.resolveRemotePath(path, remoteAuthority, forceOpenWorkspaceAsFile);
return this.doResolvePathRemote(path, remoteAuthority, forceOpenWorkspaceAsFile);
}
// Handle local paths
return this.resolveLocalPath(path, options, lineNumber, columnNumber, forceOpenWorkspaceAsFile);
// Without remote: resolve path as local URI
return this.doResolvePathLocal(path, options, lineNumber, columnNumber, forceOpenWorkspaceAsFile);
}
private resolveRemotePath(path: string, remoteAuthority: string, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined {
private doResolvePathRemote(path: string, remoteAuthority: string, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined {
const first = path.charCodeAt(0);
// make absolute
@ -908,7 +915,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
const uri = URI.from({ scheme: Schemas.vscodeRemote, authority: remoteAuthority, path: path });
// guess the file type: If it ends with a slash it's a folder. If it has a file extension, it's a file or a workspace. By defaults it's a folder.
// guess the file type:
// - if it ends with a slash it's a folder
// - if it has a file extension, it's a file or a workspace
// - by defaults it's a folder
if (path.charCodeAt(path.length - 1) !== CharCode.Slash) {
// file name ends with .code-workspace
@ -927,7 +937,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
return { workspace: getSingleFolderWorkspaceIdentifier(uri), remoteAuthority };
}
private resolveLocalPath(path: string, options: IPathResolveOptions, lineNumber: number | undefined, columnNumber: number | undefined, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined {
private doResolvePathLocal(path: string, options: IPathResolveOptions, lineNumber: number | undefined, columnNumber: number | undefined, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined {
// Ensure the path is normalized and absolute
path = sanitizeFilePath(normalize(path), process.env['VSCODE_CWD'] || process.cwd());
@ -940,21 +950,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
if (!forceOpenWorkspaceAsFile) {
const workspace = this.workspacesManagementMainService.resolveLocalWorkspaceSync(URI.file(path));
if (workspace) {
return {
workspace: { id: workspace.id, configPath: workspace.configPath },
remoteAuthority: workspace.remoteAuthority,
exists: true
};
return { workspace: { id: workspace.id, configPath: workspace.configPath }, remoteAuthority: workspace.remoteAuthority, exists: true };
}
}
// File
return {
fileUri: URI.file(path),
lineNumber,
columnNumber,
exists: true
};
return { fileUri: URI.file(path), lineNumber, columnNumber, exists: true };
}
// Folder (we check for isDirectory() because e.g. paths like /dev/null
@ -1088,7 +1089,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
});
folderUris = folderUris.filter(folderUriStr => {
const folderUri = this.argToUri(folderUriStr);
const folderUri = this.cliArgToUri(folderUriStr);
if (folderUri && !!findWindowOnWorkspaceOrFolder(this.getWindows(), folderUri)) {
return false;
}
@ -1097,7 +1098,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
});
fileUris = fileUris.filter(fileUriStr => {
const fileUri = this.argToUri(fileUriStr);
const fileUri = this.cliArgToUri(fileUriStr);
if (fileUri && !!findWindowOnWorkspaceOrFolder(this.getWindows(), fileUri)) {
return false;
}

View file

@ -51,7 +51,7 @@ function createTestCodeWindow(options: { lastFocusTime: number, openedFolderUri?
ready(): Promise<ICodeWindow> { throw new Error('Method not implemented.'); }
setReady(): void { throw new Error('Method not implemented.'); }
addTabbedWindow(window: ICodeWindow): void { throw new Error('Method not implemented.'); }
load(config: INativeWindowConfiguration, isReload?: boolean): void { throw new Error('Method not implemented.'); }
load(config: INativeWindowConfiguration, options: { isReload?: boolean }): void { throw new Error('Method not implemented.'); }
reload(cli?: NativeParsedArgs): void { throw new Error('Method not implemented.'); }
focus(options?: { force: boolean; }): void { throw new Error('Method not implemented.'); }
close(): void { throw new Error('Method not implemented.'); }