Skip to content

Commit

Permalink
Allow to save files that need user elevation (for #1614)
Browse files Browse the repository at this point in the history
  • Loading branch information
bpasero committed Dec 11, 2017
1 parent 745d560 commit 089e0da
Show file tree
Hide file tree
Showing 14 changed files with 207 additions and 15 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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",
Expand Down
9 changes: 9 additions & 0 deletions src/typings/sudo-prompt.d.ts
Original file line number Diff line number Diff line change
@@ -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);
}
26 changes: 26 additions & 0 deletions src/vs/code/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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 is a file if existing
) {
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, {
Expand Down
2 changes: 2 additions & 0 deletions src/vs/platform/environment/common/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -71,6 +72,7 @@ export interface IEnvironmentService {
args: ParsedArgs;

execPath: string;
cliPath: string;
appRoot: string;

userHome: string;
Expand Down
3 changes: 2 additions & 1 deletion src/vs/platform/environment/node/argv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
35 changes: 32 additions & 3 deletions src/vs/platform/environment/node/environmentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,15 +30,40 @@ 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 {
if (isWindows) {
if (isBuilt) {
return path.join(path.dirname(execPath), 'bin', `${product.applicationName}.cmd`);
}

return path.join(appRoot, 'scripts', 'code-cli.bat');
}

if (isLinux) {
if (isBuilt) {
return path.join(path.dirname(execPath), 'bin', `${product.applicationName}`);
}

return path.join(appRoot, 'scripts', 'code-cli.sh');
}

if (isBuilt) {
return path.join(appRoot, 'bin', 'code');
}

return path.join(appRoot, 'scripts', 'code-cli.sh');
}

export class EnvironmentService implements IEnvironmentService {
Expand All @@ -51,6 +77,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
Expand Down
7 changes: 7 additions & 0 deletions src/vs/platform/files/common/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
}
Expand Down
31 changes: 29 additions & 2 deletions src/vs/workbench/parts/files/electron-browser/fileActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1359,6 +1359,8 @@ export abstract class BaseSaveOneFileAction extends BaseSaveFileAction {

public abstract isSaveAs(): boolean;

public abstract writeElevated(): boolean;

public setResource(resource: URI): void {
this.resource = resource;
}
Expand Down Expand Up @@ -1396,7 +1398,7 @@ export abstract class BaseSaveOneFileAction extends BaseSaveFileAction {
// Special case: an untitled file with associated path gets saved directly unless "saveAs" is true
let savePromise: TPromise<URI>;
if (!this.isSaveAs() && source.scheme === 'untitled' && this.untitledEditorService.hasAssociatedFilePath(source)) {
savePromise = this.textFileService.save(source).then((result) => {
savePromise = this.textFileService.save(source, { writeElevated: this.writeElevated() }).then((result) => {
if (result) {
return URI.file(source.fsPath);
}
Expand Down Expand Up @@ -1440,7 +1442,10 @@ export abstract class BaseSaveOneFileAction extends BaseSaveFileAction {
}

// Just save
return this.textFileService.save(source, { force: true /* force a change to the file to trigger external watchers if any */ });
return this.textFileService.save(source, {
writeElevated: this.writeElevated(),
force: true /* force a change to the file to trigger external watchers if any */
});
}

return TPromise.as(false);
Expand All @@ -1455,6 +1460,10 @@ export class SaveFileAction extends BaseSaveOneFileAction {
public isSaveAs(): boolean {
return false;
}

public writeElevated(): boolean {
return false;
}
}

export class SaveFileAsAction extends BaseSaveOneFileAction {
Expand All @@ -1465,6 +1474,24 @@ export class SaveFileAsAction extends BaseSaveOneFileAction {
public isSaveAs(): boolean {
return true;
}

public writeElevated(): boolean {
return false;
}
}

export class SaveFileElevated extends BaseSaveOneFileAction {

public static readonly ID = 'workbench.action.files.saveElevated';
public static readonly LABEL = nls.localize('saveElevatedWindows', "Retry as Admin...");

public writeElevated(): boolean {
return true;
}

public isSaveAs(): boolean {
return false;
}
}

export abstract class BaseSaveAllAction extends BaseSaveFileAction {
Expand Down
16 changes: 14 additions & 2 deletions src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage';
import paths = require('vs/base/common/paths');
import { Action } from 'vs/base/common/actions';
import URI from 'vs/base/common/uri';
import { SaveFileAsAction, RevertFileAction, SaveFileAction } from 'vs/workbench/parts/files/electron-browser/fileActions';
import { SaveFileAsAction, RevertFileAction, SaveFileAction, SaveFileElevated } from 'vs/workbench/parts/files/electron-browser/fileActions';
import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
Expand Down Expand Up @@ -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', SaveFileElevated.LABEL, null, true, () => {
const saveElevatedAction = this.instantiationService.createInstance(SaveFileElevated, SaveFileElevated.ID, SaveFileElevated.LABEL);
saveElevatedAction.setResource(resource);
saveElevatedAction.run().done(() => saveElevatedAction.dispose(), 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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 089e0da

Please sign in to comment.