mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-02 17:23:40 -05:00
Merge from vscode e3c4990c67c40213af168300d1cfeb71d680f877 (#16569)
This commit is contained in:
@@ -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>;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
Reference in New Issue
Block a user