mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 04:49:35 +00:00
parent
a4e30c1223
commit
0ef13b6d54
|
@ -39,6 +39,7 @@
|
|||
"nsfw": "1.0.16",
|
||||
"semver": "4.3.6",
|
||||
"spdlog": "0.3.7",
|
||||
"sudo-prompt": "^8.0.0",
|
||||
"v8-inspect-profiler": "^0.0.7",
|
||||
"vscode-chokidar": "1.6.2",
|
||||
"vscode-debugprotocol": "1.25.0",
|
||||
|
|
9
src/typings/sudo-prompt.d.ts
vendored
Normal file
9
src/typings/sudo-prompt.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
declare module 'sudo-prompt' {
|
||||
|
||||
export function exec(cmd: string, options: { name?: string, icns?: string }, callback: (error: string, stdout: string, stderr: string) => void);
|
||||
}
|
|
@ -17,6 +17,7 @@ import { whenDeleted } from 'vs/base/node/pfs';
|
|||
import { findFreePort } from 'vs/base/node/ports';
|
||||
import { resolveTerminalEncoding } from 'vs/base/node/encoding';
|
||||
import * as iconv from 'iconv-lite';
|
||||
import { writeFileAndFlushSync } from 'vs/base/node/extfs';
|
||||
|
||||
function shouldSpawnCliProcess(argv: ParsedArgs): boolean {
|
||||
return !!argv['install-source']
|
||||
|
@ -55,6 +56,31 @@ export async function main(argv: string[]): TPromise<any> {
|
|||
return mainCli.then(cli => cli.main(args));
|
||||
}
|
||||
|
||||
// Write Elevated
|
||||
else if (args['write-elevated-helper']) {
|
||||
const source = args._[0];
|
||||
const target = args._[1];
|
||||
|
||||
// Validate
|
||||
if (
|
||||
!source || !target || source === target || // make sure source and target are provided and are not the same
|
||||
!paths.isAbsolute(source) || !paths.isAbsolute(target) || // make sure both source and target are absolute paths
|
||||
!fs.existsSync(source) || !fs.statSync(source).isFile() || // make sure source exists as file
|
||||
!fs.existsSync(target) || !fs.statSync(target).isFile() // make sure target exists as file
|
||||
) {
|
||||
return TPromise.wrapError(new Error('Using --write-elevated-helper with invalid arguments.'));
|
||||
}
|
||||
|
||||
// Write source to target
|
||||
try {
|
||||
writeFileAndFlushSync(target, fs.readFileSync(source));
|
||||
} catch (error) {
|
||||
return TPromise.wrapError(new Error(`Using --write-elevated-helper resulted in an error: ${error}`));
|
||||
}
|
||||
|
||||
return TPromise.as(null);
|
||||
}
|
||||
|
||||
// Just Code
|
||||
else {
|
||||
const env = assign({}, process.env, {
|
||||
|
|
|
@ -52,6 +52,7 @@ export interface ParsedArgs {
|
|||
'disable-updates'?: string;
|
||||
'disable-crash-reporter'?: string;
|
||||
'skip-add-to-recently-opened'?: boolean;
|
||||
'write-elevated-helper'?: boolean;
|
||||
}
|
||||
|
||||
export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
|
||||
|
@ -71,6 +72,7 @@ export interface IEnvironmentService {
|
|||
args: ParsedArgs;
|
||||
|
||||
execPath: string;
|
||||
cliPath: string;
|
||||
appRoot: string;
|
||||
|
||||
userHome: string;
|
||||
|
|
|
@ -54,7 +54,8 @@ const options: minimist.Opts = {
|
|||
'disable-updates',
|
||||
'disable-crash-reporter',
|
||||
'skip-add-to-recently-opened',
|
||||
'status'
|
||||
'status',
|
||||
'write-elevated-helper'
|
||||
],
|
||||
alias: {
|
||||
add: 'a',
|
||||
|
|
|
@ -14,6 +14,7 @@ import pkg from 'vs/platform/node/package';
|
|||
import product from 'vs/platform/node/product';
|
||||
import { LogLevel } from 'vs/platform/log/common/log';
|
||||
import { toLocalISOString } from 'vs/base/common/date';
|
||||
import { isWindows, isLinux } from 'vs/base/common/platform';
|
||||
|
||||
// Read this before there's any chance it is overwritten
|
||||
// Related to https://github.com/Microsoft/vscode/issues/30624
|
||||
|
@ -29,15 +30,44 @@ function getNixIPCHandle(userDataPath: string, type: string): string {
|
|||
|
||||
function getWin32IPCHandle(userDataPath: string, type: string): string {
|
||||
const scope = crypto.createHash('md5').update(userDataPath).digest('hex');
|
||||
|
||||
return `\\\\.\\pipe\\${scope}-${pkg.version}-${type}-sock`;
|
||||
}
|
||||
|
||||
function getIPCHandle(userDataPath: string, type: string): string {
|
||||
if (process.platform === 'win32') {
|
||||
if (isWindows) {
|
||||
return getWin32IPCHandle(userDataPath, type);
|
||||
} else {
|
||||
return getNixIPCHandle(userDataPath, type);
|
||||
}
|
||||
|
||||
return getNixIPCHandle(userDataPath, type);
|
||||
}
|
||||
|
||||
function getCLIPath(execPath: string, appRoot: string, isBuilt: boolean): string {
|
||||
|
||||
// Windows
|
||||
if (isWindows) {
|
||||
if (isBuilt) {
|
||||
return path.join(path.dirname(execPath), 'bin', `${product.applicationName}.cmd`);
|
||||
}
|
||||
|
||||
return path.join(appRoot, 'scripts', 'code-cli.bat');
|
||||
}
|
||||
|
||||
// Linux
|
||||
if (isLinux) {
|
||||
if (isBuilt) {
|
||||
return path.join(path.dirname(execPath), 'bin', `${product.applicationName}`);
|
||||
}
|
||||
|
||||
return path.join(appRoot, 'scripts', 'code-cli.sh');
|
||||
}
|
||||
|
||||
// macOS
|
||||
if (isBuilt) {
|
||||
return path.join(appRoot, 'bin', 'code');
|
||||
}
|
||||
|
||||
return path.join(appRoot, 'scripts', 'code-cli.sh');
|
||||
}
|
||||
|
||||
export class EnvironmentService implements IEnvironmentService {
|
||||
|
@ -51,6 +81,9 @@ export class EnvironmentService implements IEnvironmentService {
|
|||
|
||||
get execPath(): string { return this._execPath; }
|
||||
|
||||
@memoize
|
||||
get cliPath(): string { return getCLIPath(this.execPath, this.appRoot, this.isBuilt); }
|
||||
|
||||
readonly logsPath: string;
|
||||
|
||||
@memoize
|
||||
|
|
|
@ -525,6 +525,12 @@ export interface IUpdateContentOptions {
|
|||
*/
|
||||
overwriteReadonly?: boolean;
|
||||
|
||||
/**
|
||||
* Wether to write to the file as elevated (admin) user. When setting this option a prompt will
|
||||
* ask the user to authenticate as super user.
|
||||
*/
|
||||
writeElevated?: boolean;
|
||||
|
||||
/**
|
||||
* The last known modification time of the file. This can be used to prevent dirty writes.
|
||||
*/
|
||||
|
@ -569,6 +575,7 @@ export enum FileOperationResult {
|
|||
FILE_MODIFIED_SINCE,
|
||||
FILE_MOVE_CONFLICT,
|
||||
FILE_READ_ONLY,
|
||||
FILE_PERMISSION_DENIED,
|
||||
FILE_TOO_LARGE,
|
||||
FILE_INVALID_PATH
|
||||
}
|
||||
|
|
|
@ -118,8 +118,20 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi
|
|||
// Any other save error
|
||||
else {
|
||||
const isReadonly = (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_READ_ONLY;
|
||||
const isPermissionDenied = (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED;
|
||||
const actions: Action[] = [];
|
||||
|
||||
// Save Elevated
|
||||
if (isPermissionDenied) {
|
||||
actions.push(new Action('workbench.files.action.saveElevated', nls.localize('saveElevated', "Retry as Admin..."), null, true, () => {
|
||||
if (!model.isDisposed()) {
|
||||
model.save({ writeElevated: true }).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
|
||||
return TPromise.as(true);
|
||||
}));
|
||||
}
|
||||
|
||||
// Save As
|
||||
actions.push(new Action('workbench.files.action.saveAs', SaveFileAsAction.LABEL, null, true, () => {
|
||||
const saveAsAction = this.instantiationService.createInstance(SaveFileAsAction, SaveFileAsAction.ID, SaveFileAsAction.LABEL);
|
||||
|
@ -147,7 +159,7 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi
|
|||
|
||||
return TPromise.as(true);
|
||||
}));
|
||||
} else {
|
||||
} else if (!isPermissionDenied) {
|
||||
actions.push(new Action('workbench.files.action.retry', nls.localize('retry', "Retry"), null, true, () => {
|
||||
const saveFileAction = this.instantiationService.createInstance(SaveFileAction, SaveFileAction.ID, SaveFileAction.LABEL);
|
||||
saveFileAction.setResource(resource);
|
||||
|
|
|
@ -24,6 +24,8 @@ import Event, { Emitter } from 'vs/base/common/event';
|
|||
|
||||
import { shell } from 'electron';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import product from 'vs/platform/node/product';
|
||||
|
||||
export class FileService implements IFileService {
|
||||
|
||||
|
@ -70,7 +72,12 @@ export class FileService implements IFileService {
|
|||
encodingOverride: this.getEncodingOverrides(),
|
||||
watcherIgnoredPatterns,
|
||||
verboseLogging: environmentService.verbose,
|
||||
useExperimentalFileWatcher: configuration.files.useExperimentalFileWatcher
|
||||
useExperimentalFileWatcher: configuration.files.useExperimentalFileWatcher,
|
||||
elevationSupport: {
|
||||
cliPath: this.environmentService.cliPath,
|
||||
promptTitle: this.environmentService.appNameLong,
|
||||
promptIcnsPath: (isMacintosh && this.environmentService.isBuilt) ? paths.join(paths.dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : void 0
|
||||
}
|
||||
};
|
||||
|
||||
// create service
|
||||
|
|
|
@ -10,6 +10,7 @@ import fs = require('fs');
|
|||
import os = require('os');
|
||||
import crypto = require('crypto');
|
||||
import assert = require('assert');
|
||||
import sudoPrompt = require('sudo-prompt');
|
||||
|
||||
import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, IImportResult, FileChangesEvent, ICreateFileOptions, IContentData } from 'vs/platform/files/common/files';
|
||||
import { MAX_FILE_SIZE } from 'vs/platform/files/node/files';
|
||||
|
@ -40,6 +41,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
|||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { getBaseLabel } from 'vs/base/common/labels';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
|
||||
export interface IEncodingOverride {
|
||||
resource: uri;
|
||||
|
@ -54,6 +56,12 @@ export interface IFileServiceOptions {
|
|||
disableWatcher?: boolean;
|
||||
verboseLogging?: boolean;
|
||||
useExperimentalFileWatcher?: boolean;
|
||||
writeElevated?: (source: string, target: string) => TPromise<void>;
|
||||
elevationSupport?: {
|
||||
cliPath: string;
|
||||
promptTitle: string;
|
||||
promptIcnsPath?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function etag(stat: fs.Stats): string;
|
||||
|
@ -351,9 +359,6 @@ export class FileService implements IFileService {
|
|||
});
|
||||
}
|
||||
|
||||
//#region data stream
|
||||
|
||||
|
||||
private resolveFileData(resource: uri, options: IResolveContentOptions, token: CancellationToken): Thenable<IContentData> {
|
||||
|
||||
const chunkBuffer = BufferPool._64K.acquire();
|
||||
|
@ -484,9 +489,15 @@ export class FileService implements IFileService {
|
|||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
public updateContent(resource: uri, value: string, options: IUpdateContentOptions = Object.create(null)): TPromise<IFileStat> {
|
||||
if (this.options.elevationSupport && options.writeElevated) {
|
||||
return this.doUpdateContentElevated(resource, value, options);
|
||||
}
|
||||
|
||||
return this.doUpdateContent(resource, value, options);
|
||||
}
|
||||
|
||||
private doUpdateContent(resource: uri, value: string, options: IUpdateContentOptions = Object.create(null)): TPromise<IFileStat> {
|
||||
const absolutePath = this.toAbsolutePath(resource);
|
||||
|
||||
// 1.) check file
|
||||
|
@ -539,6 +550,15 @@ export class FileService implements IFileService {
|
|||
});
|
||||
});
|
||||
});
|
||||
}).then(null, error => {
|
||||
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
||||
return TPromise.wrapError(new FileOperationError(
|
||||
nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)),
|
||||
FileOperationResult.FILE_PERMISSION_DENIED
|
||||
));
|
||||
}
|
||||
|
||||
return TPromise.wrapError(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -564,6 +584,51 @@ export class FileService implements IFileService {
|
|||
});
|
||||
}
|
||||
|
||||
private doUpdateContentElevated(resource: uri, value: string, options: IUpdateContentOptions = Object.create(null)): TPromise<IFileStat> {
|
||||
const absolutePath = this.toAbsolutePath(resource);
|
||||
|
||||
// 1.) check file
|
||||
return this.checkFile(absolutePath, options).then(exists => {
|
||||
const writeOptions: IUpdateContentOptions = assign(Object.create(null), options);
|
||||
writeOptions.writeElevated = false;
|
||||
writeOptions.encoding = this.getEncoding(resource, options.encoding);
|
||||
|
||||
// 2.) write to a temporary file to be able to copy over later
|
||||
const tmpPath = paths.join(this.tmpPath, `code-elevated-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 6)}`);
|
||||
return this.updateContent(uri.file(tmpPath), value, writeOptions).then(() => {
|
||||
|
||||
// 3.) invoke our CLI as super user
|
||||
return new TPromise<void>((c, e) => {
|
||||
const promptOptions = { name: this.options.elevationSupport.promptTitle.replace('-', ''), icns: this.options.elevationSupport.promptIcnsPath };
|
||||
sudoPrompt.exec(`"${this.options.elevationSupport.cliPath}" --write-elevated-helper "${tmpPath}" "${absolutePath}"`, promptOptions, (error: string, stdout: string, stderr: string) => {
|
||||
if (error || stderr) {
|
||||
e(error || stderr);
|
||||
} else {
|
||||
c(void 0);
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
|
||||
// 3.) resolve again
|
||||
return this.resolve(resource);
|
||||
});
|
||||
});
|
||||
}).then(null, error => {
|
||||
if (this.options.verboseLogging) {
|
||||
this.options.errorLogger(`Unable to write to file '${resource.toString(true)}' as elevated user (${error})`);
|
||||
}
|
||||
|
||||
if (!(error instanceof FileOperationError)) {
|
||||
error = new FileOperationError(
|
||||
nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)),
|
||||
FileOperationResult.FILE_PERMISSION_DENIED
|
||||
);
|
||||
}
|
||||
|
||||
return TPromise.wrapError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): TPromise<IFileStat> {
|
||||
const absolutePath = this.toAbsolutePath(resource);
|
||||
|
||||
|
@ -865,6 +930,7 @@ export class FileService implements IFileService {
|
|||
|
||||
if (readonly) {
|
||||
mode = mode | 128;
|
||||
|
||||
return pfs.chmod(absolutePath, mode).then(() => exists);
|
||||
}
|
||||
|
||||
|
|
|
@ -712,7 +712,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
|||
overwriteEncoding: options.overwriteEncoding,
|
||||
mtime: this.lastResolvedDiskStat.mtime,
|
||||
encoding: this.getEncoding(),
|
||||
etag: this.lastResolvedDiskStat.etag
|
||||
etag: this.lastResolvedDiskStat.etag,
|
||||
writeElevated: options.writeElevated
|
||||
}).then(stat => {
|
||||
diag(`doSave(${versionId}) - after updateContent()`, this.resource, new Date());
|
||||
|
||||
|
|
|
@ -526,7 +526,7 @@ export abstract class TextFileService implements ITextFileService {
|
|||
return this.getFileModels(arg1).filter(model => model.isDirty());
|
||||
}
|
||||
|
||||
public saveAs(resource: URI, target?: URI): TPromise<URI> {
|
||||
public saveAs(resource: URI, target?: URI, options?: ISaveOptions): TPromise<URI> {
|
||||
|
||||
// Get to target resource
|
||||
if (!target) {
|
||||
|
@ -547,14 +547,14 @@ export abstract class TextFileService implements ITextFileService {
|
|||
|
||||
// Just save if target is same as models own resource
|
||||
if (resource.toString() === target.toString()) {
|
||||
return this.save(resource).then(() => resource);
|
||||
return this.save(resource, options).then(() => resource);
|
||||
}
|
||||
|
||||
// Do it
|
||||
return this.doSaveAs(resource, target);
|
||||
return this.doSaveAs(resource, target, options);
|
||||
}
|
||||
|
||||
private doSaveAs(resource: URI, target?: URI): TPromise<URI> {
|
||||
private doSaveAs(resource: URI, target?: URI, options?: ISaveOptions): TPromise<URI> {
|
||||
|
||||
// Retrieve text model from provided resource if any
|
||||
let modelPromise: TPromise<ITextFileEditorModel | UntitledEditorModel> = TPromise.as(null);
|
||||
|
@ -568,7 +568,7 @@ export abstract class TextFileService implements ITextFileService {
|
|||
|
||||
// We have a model: Use it (can be null e.g. if this file is binary and not a text file or was never opened before)
|
||||
if (model) {
|
||||
return this.doSaveTextFileAs(model, resource, target);
|
||||
return this.doSaveTextFileAs(model, resource, target, options);
|
||||
}
|
||||
|
||||
// Otherwise we can only copy
|
||||
|
@ -584,7 +584,7 @@ export abstract class TextFileService implements ITextFileService {
|
|||
});
|
||||
}
|
||||
|
||||
private doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledEditorModel, resource: URI, target: URI): TPromise<void> {
|
||||
private doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledEditorModel, resource: URI, target: URI, options?: ISaveOptions): TPromise<void> {
|
||||
let targetModelResolver: TPromise<ITextFileEditorModel>;
|
||||
|
||||
// Prefer an existing model if it is already loaded for the given target resource
|
||||
|
@ -607,12 +607,12 @@ export abstract class TextFileService implements ITextFileService {
|
|||
targetModel.textEditorModel.setValue(sourceModel.getValue());
|
||||
|
||||
// save model
|
||||
return targetModel.save();
|
||||
return targetModel.save(options);
|
||||
}, error => {
|
||||
|
||||
// binary model: delete the file and run the operation again
|
||||
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) {
|
||||
return this.fileService.del(target).then(() => this.doSaveTextFileAs(sourceModel, resource, target));
|
||||
return this.fileService.del(target).then(() => this.doSaveTextFileAs(sourceModel, resource, target, options));
|
||||
}
|
||||
|
||||
return TPromise.wrapError(error);
|
||||
|
|
|
@ -179,6 +179,7 @@ export interface ISaveOptions {
|
|||
overwriteReadonly?: boolean;
|
||||
overwriteEncoding?: boolean;
|
||||
skipSaveParticipants?: boolean;
|
||||
writeElevated?: boolean;
|
||||
}
|
||||
|
||||
export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport {
|
||||
|
@ -246,17 +247,20 @@ export interface ITextFileService extends IDisposable {
|
|||
* Saves the resource.
|
||||
*
|
||||
* @param resource the resource to save
|
||||
* @param options optional save options
|
||||
* @return true if the resource was saved.
|
||||
*/
|
||||
save(resource: URI, options?: ISaveOptions): TPromise<boolean>;
|
||||
|
||||
/**
|
||||
* Saves the provided resource asking the user for a file name.
|
||||
* Saves the provided resource asking the user for a file name or using the provided one.
|
||||
*
|
||||
* @param resource the resource to save as.
|
||||
* @param targetResource the optional target to save to.
|
||||
* @param options optional save options
|
||||
* @return true if the file was saved.
|
||||
*/
|
||||
saveAs(resource: URI, targetResource?: URI): TPromise<URI>;
|
||||
saveAs(resource: URI, targetResource?: URI, options?: ISaveOptions): TPromise<URI>;
|
||||
|
||||
/**
|
||||
* Saves the set of resources and returns a promise with the operation result.
|
||||
|
|
|
@ -5209,6 +5209,10 @@ strip-json-comments@~2.0.1:
|
|||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
|
||||
sudo-prompt@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-8.0.0.tgz#a7b4a1ca6cbcca0e705b90a89dfc81d11034cba9"
|
||||
|
||||
sumchecker@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-2.0.2.tgz#0f42c10e5d05da5d42eea3e56c3399a37d6c5b3e"
|
||||
|
|
Loading…
Reference in a new issue