diff --git a/resources/linux/bin/code.sh b/resources/linux/bin/code.sh index 83886cc2c91..eaeabca90b8 100755 --- a/resources/linux/bin/code.sh +++ b/resources/linux/bin/code.sh @@ -3,11 +3,11 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# If root, ensure that --user-data-dir or --write-elevated-helper is specified +# If root, ensure that --user-data-dir or --sudo-write is specified if [ "$(id -u)" = "0" ]; then for i in $@ do - if [[ $i == --user-data-dir=* || $i == --write-elevated-helper ]]; then + if [[ $i == --user-data-dir=* || $i == --sudo-write ]]; then CAN_LAUNCH_AS_ROOT=1 fi done diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 4da3a6af5fc..527018b0e43 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -57,7 +57,7 @@ export async function main(argv: string[]): TPromise { } // Write Elevated - else if (args['write-elevated-helper']) { + else if (args['sudo-write']) { const source = args._[0]; const target = args._[1]; @@ -68,14 +68,31 @@ export async function main(argv: string[]): TPromise { !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.')); + return TPromise.wrapError(new Error('Using --sudo-write with invalid arguments.')); } - // Write source to target try { + + // Check for readonly status and chmod if so if we are told so + let targetMode: number; + let restoreMode = false; + if (!!args['sudo-chmod']) { + targetMode = fs.statSync(target).mode; + if (!(targetMode & 128) /* readonly */) { + fs.chmodSync(target, targetMode | 128); + restoreMode = true; + } + } + + // Write source to target writeFileAndFlushSync(target, fs.readFileSync(source)); + + // Restore previous mode as needed + if (restoreMode) { + fs.chmodSync(target, targetMode); + } } catch (error) { - return TPromise.wrapError(new Error(`Using --write-elevated-helper resulted in an error: ${error}`)); + return TPromise.wrapError(new Error(`Using --sudo-write resulted in an error: ${error}`)); } return TPromise.as(null); diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index d3fc28397ff..184147fff5f 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -52,7 +52,8 @@ export interface ParsedArgs { 'disable-updates'?: string; 'disable-crash-reporter'?: string; 'skip-add-to-recently-opened'?: boolean; - 'write-elevated-helper'?: boolean; + 'sudo-write'?: boolean; + 'sudo-chmod'?: boolean; } export const IEnvironmentService = createDecorator('environmentService'); diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index d546e1511ec..862f737db01 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -55,7 +55,8 @@ const options: minimist.Opts = { 'disable-crash-reporter', 'skip-add-to-recently-opened', 'status', - 'write-elevated-helper' + 'sudo-write', + 'sudo-chmod' ], alias: { add: 'a', diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 1f2c999da9f..fd9c8e96784 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -562,7 +562,7 @@ export interface IImportResult { } export class FileOperationError extends Error { - constructor(message: string, public fileOperationResult: FileOperationResult) { + constructor(message: string, public fileOperationResult: FileOperationResult, public options?: IResolveContentOptions & IUpdateContentOptions & ICreateFileOptions) { super(message); } } diff --git a/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts b/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts index 4512e238c9b..3d506004d69 100644 --- a/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts +++ b/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts @@ -101,10 +101,12 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi public onSaveError(error: any, model: ITextFileEditorModel): void { let message: IMessageWithAction | string; + + const fileOperationError = error as FileOperationError; const resource = model.getResource(); // Dirty write prevention - if ((error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) { + if (fileOperationError.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) { // If the user tried to save from the opened conflict editor, show its message again // Otherwise show the message that will lead the user into the save conflict editor. @@ -117,21 +119,48 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi // Any other save error else { - const isReadonly = (error).fileOperationResult === FileOperationResult.FILE_READ_ONLY; - const isPermissionDenied = (error).fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED; const actions: Action[] = []; + const isReadonly = fileOperationError.fileOperationResult === FileOperationResult.FILE_READ_ONLY; + const triedToMakeWriteable = isReadonly && fileOperationError.options && fileOperationError.options.overwriteReadonly; + const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED; + // Save Elevated - if (isPermissionDenied) { - actions.push(new Action('workbench.files.action.saveElevated', nls.localize('saveElevated', "Retry as Admin..."), null, true, () => { + if (isPermissionDenied || triedToMakeWriteable) { + actions.push(new Action('workbench.files.action.saveElevated', triedToMakeWriteable ? nls.localize('overwriteElevated', "Overwrite as Admin...") : nls.localize('saveElevated', "Retry as Admin..."), null, true, () => { if (!model.isDisposed()) { - model.save({ writeElevated: true }).done(null, errors.onUnexpectedError); + model.save({ + writeElevated: true, + overwriteReadonly: triedToMakeWriteable + }).done(null, errors.onUnexpectedError); } return TPromise.as(true); })); } + // Overwrite + else if (isReadonly) { + actions.push(new Action('workbench.files.action.overwrite', nls.localize('overwrite', "Overwrite"), null, true, () => { + if (!model.isDisposed()) { + model.save({ overwriteReadonly: true }).done(null, errors.onUnexpectedError); + } + + return TPromise.as(true); + })); + } + + // Retry + else { + 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); + saveFileAction.run().done(() => saveFileAction.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); @@ -150,31 +179,18 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi return TPromise.as(true); })); - // Retry - if (isReadonly) { - actions.push(new Action('workbench.files.action.overwrite', nls.localize('overwrite', "Overwrite"), null, true, () => { - if (!model.isDisposed()) { - model.save({ overwriteReadonly: true }).done(null, errors.onUnexpectedError); - } - - return TPromise.as(true); - })); - } 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); - saveFileAction.run().done(() => saveFileAction.dispose(), errors.onUnexpectedError); - - return TPromise.as(true); - })); - } - // Cancel actions.push(CancelAction); let errorMessage: string; if (isReadonly) { - errorMessage = nls.localize('readonlySaveError', "Failed to save '{0}': File is write protected. Select 'Overwrite' to attempt to remove protection.", paths.basename(resource.fsPath)); + if (triedToMakeWriteable) { + errorMessage = nls.localize('readonlySaveErrorAdmin', "Failed to save '{0}': File is write protected. Select 'Overwrite as Admin' to retry as administrator.", paths.basename(resource.fsPath)); + } else { + errorMessage = nls.localize('readonlySaveError', "Failed to save '{0}': File is write protected. Select 'Overwrite' to attempt to remove protection.", paths.basename(resource.fsPath)); + } + } else if (isPermissionDenied) { + errorMessage = nls.localize('permissionDeniedSaveError', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Admin' to retry as administrator.", paths.basename(resource.fsPath)); } else { errorMessage = nls.localize('genericSaveError', "Failed to save '{0}': {1}", paths.basename(resource.fsPath), toErrorMessage(error, false)); } diff --git a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts index 6de890c430f..c712f387483 100644 --- a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -249,7 +249,8 @@ export class RemoteFileService extends FileService { if (options.acceptTextOnly && detected.mimes.indexOf(MIME_BINARY) >= 0) { return TPromise.wrapError(new FileOperationError( localize('fileBinaryError', "File seems to be binary and cannot be opened as text"), - FileOperationResult.FILE_IS_BINARY + FileOperationResult.FILE_IS_BINARY, + options )); } @@ -324,7 +325,7 @@ export class RemoteFileService extends FileService { return prepare.then(exists => { if (exists && options && !options.overwrite) { - return TPromise.wrapError(new FileOperationError('EEXIST', FileOperationResult.FILE_MODIFIED_SINCE)); + return TPromise.wrapError(new FileOperationError('EEXIST', FileOperationResult.FILE_MODIFIED_SINCE, options)); } return this._doUpdateContent(provider, resource, content || '', {}); }).then(fileStat => { diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 75a3de27e5e..7dc682d8818 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -262,7 +262,8 @@ export class FileService implements IFileService { if (resource.scheme !== 'file' || !resource.fsPath) { return TPromise.wrapError(new FileOperationError( nls.localize('fileInvalidPath', "Invalid file resource ({0})", resource.toString(true)), - FileOperationResult.FILE_INVALID_PATH + FileOperationResult.FILE_INVALID_PATH, + options )); } @@ -298,7 +299,8 @@ export class FileService implements IFileService { if (stat.isDirectory) { return onStatError(new FileOperationError( nls.localize('fileIsDirectoryError', "File is directory"), - FileOperationResult.FILE_IS_DIRECTORY + FileOperationResult.FILE_IS_DIRECTORY, + options )); } @@ -306,7 +308,8 @@ export class FileService implements IFileService { if (options && options.etag && options.etag === stat.etag) { return onStatError(new FileOperationError( nls.localize('fileNotModifiedError', "File not modified since"), - FileOperationResult.FILE_NOT_MODIFIED_SINCE + FileOperationResult.FILE_NOT_MODIFIED_SINCE, + options )); } @@ -314,7 +317,8 @@ export class FileService implements IFileService { if (typeof stat.size === 'number' && stat.size > MAX_FILE_SIZE) { return onStatError(new FileOperationError( nls.localize('fileTooLargeError', "File too large to open"), - FileOperationResult.FILE_TOO_LARGE + FileOperationResult.FILE_TOO_LARGE, + options )); } @@ -325,7 +329,8 @@ export class FileService implements IFileService { if (err.code === 'ENOENT') { return onStatError(new FileOperationError( nls.localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), - FileOperationResult.FILE_NOT_FOUND + FileOperationResult.FILE_NOT_FOUND, + options )); } @@ -374,7 +379,8 @@ export class FileService implements IFileService { // Wrap file not found errors err = new FileOperationError( nls.localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), - FileOperationResult.FILE_NOT_FOUND + FileOperationResult.FILE_NOT_FOUND, + options ); } @@ -391,7 +397,8 @@ export class FileService implements IFileService { // Wrap EISDIR errors (fs.open on a directory works, but you cannot read from it) err = new FileOperationError( nls.localize('fileIsDirectoryError', "File is directory"), - FileOperationResult.FILE_IS_DIRECTORY + FileOperationResult.FILE_IS_DIRECTORY, + options ); } if (decoder) { @@ -442,7 +449,8 @@ export class FileService implements IFileService { // stop when reading too much finish(new FileOperationError( nls.localize('fileTooLargeError', "File too large to open"), - FileOperationResult.FILE_TOO_LARGE + FileOperationResult.FILE_TOO_LARGE, + options )); } else if (err) { // some error happened @@ -464,7 +472,8 @@ export class FileService implements IFileService { // Return error early if client only accepts text and this is not text finish(new FileOperationError( nls.localize('fileBinaryError', "File seems to be binary and cannot be opened as text"), - FileOperationResult.FILE_IS_BINARY + FileOperationResult.FILE_IS_BINARY, + options )); } else { @@ -553,7 +562,8 @@ export class FileService implements IFileService { 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 + FileOperationResult.FILE_PERMISSION_DENIED, + options )); } @@ -600,7 +610,14 @@ export class FileService implements IFileService { return (import('sudo-prompt')).then(sudoPrompt => { return new TPromise((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) => { + + const sudoCommand: string[] = [`"${this.options.elevationSupport.cliPath}"`]; + if (options.overwriteReadonly) { + sudoCommand.push('--sudo-chmod'); + } + sudoCommand.push('--sudo-write', `"${tmpPath}"`, `"${absolutePath}"`); + + sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error: string, stdout: string, stderr: string) => { if (error || stderr) { e(error || stderr); } else { @@ -622,7 +639,8 @@ export class FileService implements IFileService { if (!(error instanceof FileOperationError)) { error = new FileOperationError( nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)), - FileOperationResult.FILE_PERMISSION_DENIED + FileOperationResult.FILE_PERMISSION_DENIED, + options ); } @@ -645,7 +663,8 @@ export class FileService implements IFileService { if (exists && !options.overwrite) { return TPromise.wrapError(new FileOperationError( nls.localize('fileExists', "File to create already exists ({0})", resource.toString(true)), - FileOperationResult.FILE_MODIFIED_SINCE + FileOperationResult.FILE_MODIFIED_SINCE, + options )); } @@ -914,14 +933,14 @@ export class FileService implements IFileService { // Find out if content length has changed if (options.etag !== etag(stat.size, options.mtime)) { - return TPromise.wrapError(new FileOperationError(nls.localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE)); + return TPromise.wrapError(new FileOperationError(nls.localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options)); } } // Throw if file is readonly and we are not instructed to overwrite if (!(stat.mode & 128) /* readonly */) { if (!options.overwriteReadonly) { - return this.readOnlyError(); + return this.readOnlyError(options); } // Try to change mode to writeable @@ -932,7 +951,7 @@ export class FileService implements IFileService { // Make sure to check the mode again, it could have failed return pfs.stat(absolutePath).then(stat => { if (!(stat.mode & 128) /* readonly */) { - return this.readOnlyError(); + return this.readOnlyError(options); } return exists; @@ -948,10 +967,11 @@ export class FileService implements IFileService { }); } - private readOnlyError(): TPromise { + private readOnlyError(options: IUpdateContentOptions): TPromise { return TPromise.wrapError(new FileOperationError( nls.localize('fileReadOnlyError', "File is Read Only"), - FileOperationResult.FILE_READ_ONLY + FileOperationResult.FILE_READ_ONLY, + options )); }