Merge from vscode e3c4990c67c40213af168300d1cfeb71d680f877 (#16569)

This commit is contained in:
Cory Rivera
2021-08-25 16:28:29 -07:00
committed by GitHub
parent ab1112bfb3
commit cb7b7da0a4
1752 changed files with 59525 additions and 33878 deletions

View File

@@ -5,7 +5,7 @@
import { Event } from 'vs/base/common/event';
import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes';
import { IOpenedWindow, IWindowOpenable, IOpenEmptyWindowOptions, IOpenWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows';
import { IOpenedWindow, IWindowOpenable, IOpenEmptyWindowOptions, IOpenWindowOptions, IColorScheme, IPartsSplash } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { URI } from 'vs/base/common/uri';
@@ -72,6 +72,8 @@ export interface ICommonNativeHostService {
setMinimumSize(width: number | undefined, height: number | undefined): Promise<void>;
saveWindowSplash(splash: IPartsSplash): Promise<void>;
/**
* Make the window focused.
*
@@ -97,7 +99,7 @@ export interface ICommonNativeHostService {
setRepresentedFilename(path: string): Promise<void>;
setDocumentEdited(edited: boolean): Promise<void>;
openExternal(url: string): Promise<boolean>;
moveItemToTrash(fullPath: string, deleteOnFail?: boolean): Promise<boolean>;
moveItemToTrash(fullPath: string): Promise<void>;
isAdmin(): Promise<boolean>;
writeElevated(source: URI, target: URI, options?: { unlock?: boolean }): Promise<void>;
@@ -128,6 +130,10 @@ export interface ICommonNativeHostService {
toggleWindowTabsBar(): Promise<void>;
updateTouchBar(items: ISerializableCommandAction[][]): Promise<void>;
// macOS Shell command
installShellCommand(): Promise<void>;
uninstallShellCommand(): Promise<void>;
// Lifecycle
notifyReady(): Promise<void>
relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise<void>;

View File

@@ -3,11 +3,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { exec } from 'child_process';
import { promisify } from 'util';
import { localize } from 'vs/nls';
import { realpath } from 'vs/base/node/extpath';
import { Emitter, Event } from 'vs/base/common/event';
import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows';
import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor, nativeTheme, screen, Display } from 'electron';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows';
import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme, IPartsSplash } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { isMacintosh, isWindows, isLinux, isLinuxSnap } from 'vs/base/common/platform';
import { ICommonNativeHostService, IOSProperties, IOSStatistics } from 'vs/platform/native/common/native';
@@ -15,7 +19,7 @@ import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { AddFirstParameterToFunctions } from 'vs/base/common/types';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService';
import { SymlinkSupport } from 'vs/base/node/pfs';
import { Promises, SymlinkSupport } from 'vs/base/node/pfs';
import { URI } from 'vs/base/common/uri';
import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
@@ -23,11 +27,12 @@ import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes';
import { arch, totalmem, release, platform, type, loadavg, freemem, cpus } from 'os';
import { virtualMachineHint } from 'vs/base/node/id';
import { ILogService } from 'vs/platform/log/common/log';
import { dirname, join } from 'vs/base/common/path';
import { dirname, join, resolve } from 'vs/base/common/path';
import { IProductService } from 'vs/platform/product/common/productService';
import { memoize } from 'vs/base/common/decorators';
import { Disposable } from 'vs/base/common/lifecycle';
import { ISharedProcess } from 'vs/platform/sharedProcess/node/sharedProcess';
import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
export interface INativeHostMainService extends AddFirstParameterToFunctions<ICommonNativeHostService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }
@@ -50,7 +55,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ILogService private readonly logService: ILogService,
@IProductService private readonly productService: IProductService
@IProductService private readonly productService: IProductService,
@IThemeMainService private readonly themeMainService: IThemeMainService
) {
super();
@@ -247,9 +253,106 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
}
}
async saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): Promise<void> {
this.themeMainService.saveWindowSplash(windowId, splash);
}
//#endregion
//#region macOS Shell Command
async installShellCommand(windowId: number | undefined): Promise<void> {
const { source, target } = await this.getShellCommandLink();
// Only install unless already existing
try {
const { symbolicLink } = await SymlinkSupport.stat(source);
if (symbolicLink && !symbolicLink.dangling) {
const linkTargetRealPath = await realpath(source);
if (target === linkTargetRealPath) {
return;
}
}
// Different source, delete it first
await Promises.unlink(source);
} catch (error) {
if (error.code !== 'ENOENT') {
throw error; // throw on any error but file not found
}
}
try {
await Promises.symlink(target, source);
} catch (error) {
if (error.code !== 'EACCES' && error.code !== 'ENOENT') {
throw error;
}
const { response } = await this.showMessageBox(windowId, {
type: 'info',
message: localize('warnEscalation', "{0} will now prompt with 'osascript' for Administrator privileges to install the shell command.", this.productService.nameShort),
buttons: [localize('ok', "OK"), localize('cancel', "Cancel")],
cancelId: 1
});
if (response === 0 /* OK */) {
try {
const command = `osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'${target}\' \'${source}\'\\" with administrator privileges"`;
await promisify(exec)(command);
} catch (error) {
throw new Error(localize('cantCreateBinFolder', "Unable to install the shell command '{0}'.", source));
}
}
}
}
async uninstallShellCommand(windowId: number | undefined): Promise<void> {
const { source } = await this.getShellCommandLink();
try {
await Promises.unlink(source);
} catch (error) {
switch (error.code) {
case 'EACCES':
const { response } = await this.showMessageBox(windowId, {
type: 'info',
message: localize('warnEscalationUninstall', "{0} will now prompt with 'osascript' for Administrator privileges to uninstall the shell command.", this.productService.nameShort),
buttons: [localize('ok', "OK"), localize('cancel', "Cancel")],
cancelId: 1
});
if (response === 0 /* OK */) {
try {
const command = `osascript -e "do shell script \\"rm \'${source}\'\\" with administrator privileges"`;
await promisify(exec)(command);
} catch (error) {
throw new Error(localize('cantUninstall', "Unable to uninstall the shell command '{0}'.", source));
}
}
break;
case 'ENOENT':
break; // ignore file not found
default:
throw error;
}
}
}
private async getShellCommandLink(): Promise<{ readonly source: string, readonly target: string }> {
const target = resolve(this.environmentMainService.appRoot, 'bin', 'code');
const source = `/usr/local/bin/${this.productService.applicationName}`;
// Ensure source exists
const sourceExists = await Promises.exists(target);
if (!sourceExists) {
throw new Error(localize('sourceMissing', "Unable to find shell script in '{0}'", target));
}
return { source, target };
}
//#region Dialog
async showMessageBox(windowId: number | undefined, options: MessageBoxOptions): Promise<MessageBoxReturnValue> {
@@ -376,8 +479,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
process.env['GDK_PIXBUF_MODULEDIR'] = gdkPixbufModuleDir;
}
async moveItemToTrash(windowId: number | undefined, fullPath: string): Promise<boolean> {
return shell.moveItemToTrash(fullPath);
moveItemToTrash(windowId: number | undefined, fullPath: string): Promise<void> {
return shell.trashItem(fullPath);
}
async isAdmin(): Promise<boolean> {
@@ -604,9 +707,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
// Otherwise: normal quit
else {
setTimeout(() => {
this.lifecycleMainService.quit();
}, 10 /* delay to unwind callback stack (IPC) */);
this.lifecycleMainService.quit();
}
}
@@ -716,6 +817,27 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
async setPassword(windowId: number | undefined, service: string, account: string, password: string): Promise<void> {
const keytar = await this.withKeytar();
const MAX_SET_ATTEMPTS = 3;
// Sometimes Keytar has a problem talking to the keychain on the OS. To be more resilient, we retry a few times.
const setPasswordWithRetry = async (service: string, account: string, password: string) => {
let attempts = 0;
let error: any;
while (attempts < MAX_SET_ATTEMPTS) {
try {
await keytar.setPassword(service, account, password);
return;
} catch (e) {
error = e;
this.logService.warn('Error attempting to set a password: ', e);
attempts++;
await new Promise(resolve => setTimeout(resolve, 200));
}
}
// throw last error
throw error;
};
if (isWindows && password.length > NativeHostMainService.MAX_PASSWORD_LENGTH) {
let index = 0;
@@ -731,12 +853,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
hasNextChunk: hasNextChunk
};
await keytar.setPassword(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content));
await setPasswordWithRetry(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content));
chunk++;
}
} else {
await keytar.setPassword(service, account, password);
await setPasswordWithRetry(service, account, password);
}
this._onDidChangePassword.fire({ service, account });

View File

@@ -12,7 +12,7 @@ export const INativeHostService = createDecorator<INativeHostService>('nativeHos
* A set of methods specific to a native host, i.e. unsupported in web
* environments.
*
* @see `IHostService` for methods that can be used in native and web
* @see {@link IHostService} for methods that can be used in native and web
* hosts.
*/
export interface INativeHostService extends ICommonNativeHostService { }