diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index 0eae094cc4a4..44c9c9dc2179 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -45,7 +45,7 @@ export abstract class ModuleInstaller implements IModuleInstaller { await terminalService.sendCommand(pythonPath, args, token); } else if (settings.globalModuleInstallation) { const fs = this.serviceContainer.get(IFileSystem); - if (await fs.isDirReadonly(path.dirname(pythonPath))) { + if (await fs.isDirReadonly(path.dirname(pythonPath)).catch(_err => true)) { this.elevatedInstall(pythonPath, args); } else { await terminalService.sendCommand(pythonPath, args, token); diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index a7b879e52e3d..5959a3234ced 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -9,8 +9,7 @@ import { injectable } from 'inversify'; import { promisify } from 'util'; import * as vscode from 'vscode'; import { createDeferred } from '../utils/async'; -import { noop } from '../utils/misc'; -import { isFileNotFoundError } from './errors'; +import { isFileNotFoundError, isNoPermissionsError } from './errors'; import { FileSystemPaths, FileSystemPathUtils } from './fs-paths'; import { TemporaryFileSystem } from './fs-temp'; // prettier-ignore @@ -457,16 +456,19 @@ export class FileSystem implements IFileSystem { public async isDirReadonly(dirname: string): Promise { const filePath = `${dirname}${this.paths.sep}___vscpTest___`; - return new Promise(resolve => { - fs.open(filePath, fs.constants.O_CREAT | fs.constants.O_RDWR, (error, fd) => { - if (!error) { - fs.close(fd, () => { - fs.unlink(filePath, noop); - }); - } - return resolve(!error); - }); - }); + const flags = fs.constants.O_CREAT | fs.constants.O_RDWR; + let fd: number; + try { + fd = await fs.open(filePath, flags); + } catch (err) { + if (isNoPermissionsError(err)) { + return true; + } + throw err; // re-throw + } + await fs.close(fd); + await fs.unlink(filePath); + return false; } } diff --git a/src/test/common/installer/moduleInstaller.unit.test.ts b/src/test/common/installer/moduleInstaller.unit.test.ts index 64199d504521..cc82a5ff9061 100644 --- a/src/test/common/installer/moduleInstaller.unit.test.ts +++ b/src/test/common/installer/moduleInstaller.unit.test.ts @@ -383,6 +383,25 @@ suite('Module Installer', () => { interpreterService.verifyAll(); terminalService.verifyAll(); }); + test(`ignores failures in IFileSystem.isDirReadonly()`, async () => { + const info = TypeMoq.Mock.ofType(); + info.setup((t: any) => t.then).returns(() => undefined); + info.setup(t => t.type).returns(() => InterpreterType.Unknown); + info.setup(t => t.version).returns(() => new SemVer('3.5.0-final')); + setActiveInterpreter(info.object); + pythonSettings.setup(p => p.globalModuleInstallation).returns(() => true); + const elevatedInstall = sinon.stub(TestModuleInstaller.prototype, 'elevatedInstall'); + elevatedInstall.returns(); + const err = new Error('oops!'); + fs.setup(f => f.isDirReadonly(path.dirname(pythonPath))).returns(() => Promise.reject(err)); + try { + await installer.installModule(product.name, resource); + } catch (ex) { + noop(); + } + assert.ok(elevatedInstall.calledOnceWith(pythonPath, ['-m', 'executionInfo'])); + interpreterService.verifyAll(); + }); test('If cancellation token is provided, install while showing progress', async () => { const options = { location: ProgressLocation.Notification, diff --git a/src/test/common/platform/filesystem.functional.test.ts b/src/test/common/platform/filesystem.functional.test.ts index ec77218ee0bd..b28b1437c97d 100644 --- a/src/test/common/platform/filesystem.functional.test.ts +++ b/src/test/common/platform/filesystem.functional.test.ts @@ -1279,7 +1279,7 @@ suite('FileSystem', () => { const isReadonly = await fileSystem.isDirReadonly(dirname); - expect(isReadonly).to.equal(false); + expect(isReadonly).to.equal(true); }); }); @@ -1288,15 +1288,13 @@ suite('FileSystem', () => { const isReadonly = await fileSystem.isDirReadonly(dirname); - expect(isReadonly).to.equal(true); + expect(isReadonly).to.equal(false); }); - // Failing may be more sensible, but for now we are sticking - // with the existing behavior. - test('false if the directory does not exist', async () => { - const isReadonly = await fileSystem.isDirReadonly(DOES_NOT_EXIST); + test('fail if the directory does not exist', async () => { + const promise = fileSystem.isDirReadonly(DOES_NOT_EXIST); - expect(isReadonly).to.equal(false); + await expect(promise).to.eventually.be.rejected; }); });