From 28d453fced38aad4d882455ec56381bd0f2f21c8 Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Tue, 17 Sep 2019 13:32:42 -0700 Subject: [PATCH] Implement a no sync rule (#7216) * implement a no sync rule * fix linting disable * fix unused imports * exclude more testing * clean up fs usage * clean up more fs usage * remove duplicate of code * fix compile errors --- build/lib/tslint/noSyncRule.js | 33 ++++++++++ build/lib/tslint/noSyncRule.ts | 45 ++++++++++++++ extensions/admin-tool-ext-win/src/main.ts | 5 +- extensions/agent/src/agentUtils.ts | 25 +++----- extensions/agent/src/data/notebookData.ts | 10 ++-- .../agent/src/dialogs/notebookDialog.ts | 4 +- extensions/agent/src/mainController.ts | 14 ++--- .../src/account-provider/tokenCache.ts | 44 +++++++------- extensions/azurecore/src/extension.ts | 22 ++++--- .../import/src/services/serviceClient.ts | 7 +-- extensions/mssql/src/main.ts | 7 ++- .../hdfsCommands.ts | 24 ++++---- .../sparkConfigurationTab.ts | 3 +- .../sparkJobSubmissionModel.ts | 3 +- .../mssql/src/sparkFeature/sparkUtils.ts | 4 +- extensions/mssql/src/sqlToolsServer.ts | 5 +- extensions/mssql/src/utils.ts | 31 ++++------ extensions/notebook/src/book/bookTreeItem.ts | 2 + extensions/notebook/src/book/bookTreeView.ts | 28 ++++----- extensions/notebook/src/common/utils.ts | 9 +++ .../src/dialog/configurePythonDialog.ts | 42 ++++++------- .../notebook/src/dialog/pythonPathLookup.ts | 5 +- .../src/jupyter/jupyterServerInstallation.ts | 13 ++-- .../notebook/src/jupyter/serverInstance.ts | 11 +++- .../notebook/src/test/book/book.test.ts | 4 +- .../src/test/model/serverInstance.test.ts | 4 +- .../src/services/platformService.ts | 4 +- .../src/services/resourceTypeService.ts | 21 +++++-- extensions/schema-compare/package.json | 2 +- .../src/dialogs/schemaCompareDialog.ts | 60 +++++++++++-------- tslint.json | 15 +++++ 31 files changed, 305 insertions(+), 201 deletions(-) create mode 100644 build/lib/tslint/noSyncRule.js create mode 100644 build/lib/tslint/noSyncRule.ts diff --git a/build/lib/tslint/noSyncRule.js b/build/lib/tslint/noSyncRule.js new file mode 100644 index 0000000000..f4d92f5a59 --- /dev/null +++ b/build/lib/tslint/noSyncRule.js @@ -0,0 +1,33 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const Lint = require("tslint"); +const minimatch = require("minimatch"); +class Rule extends Lint.Rules.AbstractRule { + apply(sourceFile) { + const args = this.getOptions().ruleArguments[0]; + if (args.exclude.every(x => !minimatch(sourceFile.fileName, x))) { + return this.applyWithWalker(new NoSyncRuleWalker(sourceFile, this.getOptions())); + } + return []; + } +} +exports.Rule = Rule; +class NoSyncRuleWalker extends Lint.RuleWalker { + constructor(file, opts) { + super(file, opts); + } + visitCallExpression(node) { + if (node.expression && NoSyncRuleWalker.operations.some(x => node.expression.getText().indexOf(x) >= 0)) { + this.addFailureAtNode(node, `Do not use Sync operations`); + } + super.visitCallExpression(node); + } +} +NoSyncRuleWalker.operations = ['readFileSync', 'writeFileSync', 'existsSync', 'fchmodSync', 'lchmodSync', + 'statSync', 'fstatSync', 'lstatSync', 'linkSync', 'symlinkSync', 'readlinkSync', 'realpathSync', 'unlinkSync', 'rmdirSync', + 'mkdirSync', 'mkdtempSync', 'readdirSync', 'openSync', 'utimesSync', 'futimesSync', 'fsyncSync', 'writeSync', 'readSync', + 'appendFileSync', 'accessSync', 'fdatasyncSync', 'copyFileSync']; diff --git a/build/lib/tslint/noSyncRule.ts b/build/lib/tslint/noSyncRule.ts new file mode 100644 index 0000000000..9d0254209d --- /dev/null +++ b/build/lib/tslint/noSyncRule.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as ts from 'typescript'; +import * as Lint from 'tslint'; +import * as minimatch from 'minimatch'; + +interface NoSyncRuleConfig { + exclude: string[]; +} + +export class Rule extends Lint.Rules.AbstractRule { + + apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + const args = this.getOptions().ruleArguments[0]; + + if (args.exclude.every(x => !minimatch(sourceFile.fileName, x))) { + return this.applyWithWalker(new NoSyncRuleWalker(sourceFile, this.getOptions())); + } + + return []; + } +} + +class NoSyncRuleWalker extends Lint.RuleWalker { + + private static readonly operations = ['readFileSync', 'writeFileSync', 'existsSync', 'fchmodSync', 'lchmodSync', + 'statSync', 'fstatSync', 'lstatSync', 'linkSync', 'symlinkSync', 'readlinkSync', 'realpathSync', 'unlinkSync', 'rmdirSync', + 'mkdirSync', 'mkdtempSync', 'readdirSync', 'openSync', 'utimesSync', 'futimesSync', 'fsyncSync', 'writeSync', 'readSync', + 'appendFileSync', 'accessSync', 'fdatasyncSync', 'copyFileSync']; + + constructor(file: ts.SourceFile, opts: Lint.IOptions) { + super(file, opts); + } + + visitCallExpression(node: ts.CallExpression) { + if (node.expression && NoSyncRuleWalker.operations.some(x => node.expression.getText().indexOf(x) >= 0)) { + this.addFailureAtNode(node, `Do not use Sync operations`); + } + + super.visitCallExpression(node); + } +} diff --git a/extensions/admin-tool-ext-win/src/main.ts b/extensions/admin-tool-ext-win/src/main.ts index 23b3ad10ee..fbc6b6546a 100644 --- a/extensions/admin-tool-ext-win/src/main.ts +++ b/extensions/admin-tool-ext-win/src/main.ts @@ -10,8 +10,7 @@ import * as vscode from 'vscode'; import { TelemetryReporter, TelemetryViews } from './telemetry'; import { doubleEscapeSingleQuotes, backEscapeDoubleQuotes, getTelemetryErrorType } from './utils'; import { ChildProcess, exec } from 'child_process'; -import { promisify } from 'util'; -import { readFile } from 'fs'; +import { promises as fs } from 'fs'; const localize = nls.loadMessageBundle(); @@ -68,7 +67,7 @@ export interface LaunchSsmsDialogParams { export async function activate(context: vscode.ExtensionContext): Promise { // This is for Windows-specific support so do nothing on other platforms if (process.platform === 'win32') { - const rawConfig = await promisify(readFile)(path.join(context.extensionPath, 'config.json')); + const rawConfig = await fs.readFile(path.join(context.extensionPath, 'config.json')); const ssmsMinVer = JSON.parse(rawConfig.toString()).version; exePath = path.join(context.extensionPath, 'ssmsmin', 'Windows', ssmsMinVer, 'ssmsmin.exe'); registerCommands(context); diff --git a/extensions/agent/src/agentUtils.ts b/extensions/agent/src/agentUtils.ts index 4801e3ba66..9160be2f7a 100644 --- a/extensions/agent/src/agentUtils.ts +++ b/extensions/agent/src/agentUtils.ts @@ -6,9 +6,7 @@ 'use strict'; import * as azdata from 'azdata'; -import * as fs from 'fs'; -import { promisify } from 'util'; - +import { promises as fs } from 'fs'; export class AgentUtils { @@ -52,18 +50,11 @@ export class AgentUtils { } -export function exists(path: string): Promise { - return promisify(fs.exists)(path); -} - -export function mkdir(path: string): Promise { - return promisify(fs.mkdir)(path); -} - -export function unlink(path: string): Promise { - return promisify(fs.unlink)(path); -} - -export function writeFile(path: string, data: string): Promise { - return promisify(fs.writeFile)(path, data); +export async function exists(path: string): Promise { + try { + await fs.access(path); + return true; + } catch (e) { + return false; + } } diff --git a/extensions/agent/src/data/notebookData.ts b/extensions/agent/src/data/notebookData.ts index 2b75561552..2f83867152 100644 --- a/extensions/agent/src/data/notebookData.ts +++ b/extensions/agent/src/data/notebookData.ts @@ -7,12 +7,10 @@ import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; -import * as fs from 'fs'; import * as path from 'path'; -import { AgentUtils } from '../agentUtils'; +import { AgentUtils, exists } from '../agentUtils'; import { IAgentDialogData, AgentDialogMode } from '../interfaces'; import { NotebookDialogOptions } from '../dialogs/notebookDialog'; -import { createConnection } from 'net'; const localize = nls.loadMessageBundle(); const NotebookCompletionActionCondition_Always: string = localize('notebookData.whenJobCompletes', 'When the notebook completes'); @@ -179,7 +177,7 @@ export class NotebookData implements IAgentDialogData { } } - public validate(): { valid: boolean, errorMessages: string[] } { + public async validate(): Promise<{ valid: boolean, errorMessages: string[] }> { let validationErrors: string[] = []; if (this.dialogMode !== AgentDialogMode.EDIT) { if (!(this.name && this.name.trim())) { @@ -188,7 +186,7 @@ export class NotebookData implements IAgentDialogData { if (!(this.templatePath && this.name.trim())) { validationErrors.push(TemplatePathEmptyErrorMessage); } - if (!fs.existsSync(this.templatePath)) { + if (!(await exists(this.templatePath))) { validationErrors.push(InvalidNotebookPathErrorMessage); } if (NotebookData.jobLists) { @@ -201,7 +199,7 @@ export class NotebookData implements IAgentDialogData { } } else { - if (this.templatePath && this.templatePath !== '' && !fs.existsSync(this.templatePath)) { + if (this.templatePath && this.templatePath !== '' && !(await exists(this.templatePath))) { validationErrors.push(InvalidNotebookPathErrorMessage); } } diff --git a/extensions/agent/src/dialogs/notebookDialog.ts b/extensions/agent/src/dialogs/notebookDialog.ts index 47aa340558..c941cab95d 100644 --- a/extensions/agent/src/dialogs/notebookDialog.ts +++ b/extensions/agent/src/dialogs/notebookDialog.ts @@ -87,9 +87,9 @@ export class NotebookDialog extends AgentDialog { this.generalTab = azdata.window.createTab(GeneralTabText); this.initializeGeneralTab(); this.dialog.content = [this.generalTab]; - this.dialog.registerCloseValidator(() => { + this.dialog.registerCloseValidator(async () => { this.updateModel(); - let validationResult = this.model.validate(); + let validationResult = await this.model.validate(); if (!validationResult.valid) { // TODO: Show Error Messages this.dialog.message = { text: validationResult.errorMessages[0] }; diff --git a/extensions/agent/src/mainController.ts b/extensions/agent/src/mainController.ts index e23f2650db..d5b52eb822 100644 --- a/extensions/agent/src/mainController.ts +++ b/extensions/agent/src/mainController.ts @@ -7,7 +7,7 @@ import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; -import * as fs from 'fs'; +import { promises as fs } from 'fs'; import * as os from 'os'; import * as path from 'path'; import { AlertDialog } from './dialogs/alertDialog'; @@ -17,9 +17,8 @@ import { ProxyDialog } from './dialogs/proxyDialog'; import { JobStepDialog } from './dialogs/jobStepDialog'; import { PickScheduleDialog } from './dialogs/pickScheduleDialog'; import { JobData } from './data/jobData'; -import { AgentUtils, exists, mkdir, unlink, writeFile } from './agentUtils'; +import { AgentUtils, exists } from './agentUtils'; import { NotebookDialog, NotebookDialogOptions } from './dialogs/notebookDialog'; -import { promisify } from 'util'; const localize = nls.loadMessageBundle(); @@ -102,7 +101,7 @@ export class MainController { let templateMap = this.notebookTemplateMap.get(nbEditor.document.uri.toString()); let vsEditor = await vscode.workspace.openTextDocument(templateMap.fileUri); let content = vsEditor.getText(); - promisify(fs.writeFile)(templateMap.tempPath, content); + await fs.writeFile(templateMap.tempPath, content); AgentUtils.getAgentService().then(async (agentService) => { let result = await agentService.updateNotebook(templateMap.ownerUri, templateMap.notebookInfo.name, templateMap.notebookInfo, templateMap.tempPath); if (result.success) { @@ -128,14 +127,13 @@ export class MainController { vscode.commands.registerCommand('agent.openNotebookEditorFromJsonString', async (filename: string, jsonNotebook: string, notebookInfo?: azdata.AgentNotebookInfo, ownerUri?: string) => { const tempfilePath = path.join(os.tmpdir(), 'mssql_notebooks', filename + '.ipynb'); if (!await exists(path.join(os.tmpdir(), 'mssql_notebooks'))) { - await mkdir(path.join(os.tmpdir(), 'mssql_notebooks')); + await fs.mkdir(path.join(os.tmpdir(), 'mssql_notebooks')); } - let editors = azdata.nb.visibleNotebookEditors; if (await exists(tempfilePath)) { - await unlink(tempfilePath); + await fs.unlink(tempfilePath); } try { - await writeFile(tempfilePath, jsonNotebook); + await fs.writeFile(tempfilePath, jsonNotebook); let uri = vscode.Uri.parse(`untitled:${path.basename(tempfilePath)}`); if (notebookInfo) { this.notebookTemplateMap.set(uri.toString(), { notebookInfo: notebookInfo, fileUri: uri, ownerUri: ownerUri, tempPath: tempfilePath }); diff --git a/extensions/azurecore/src/account-provider/tokenCache.ts b/extensions/azurecore/src/account-provider/tokenCache.ts index 205ef47454..27eeaafe31 100644 --- a/extensions/azurecore/src/account-provider/tokenCache.ts +++ b/extensions/azurecore/src/account-provider/tokenCache.ts @@ -6,7 +6,7 @@ import * as adal from 'adal-node'; import * as azdata from 'azdata'; import * as crypto from 'crypto'; -import * as fs from 'fs'; +import { promises as fs } from 'fs'; export default class TokenCache implements adal.TokenCache { private static CipherAlgorithm = 'aes-256-cbc'; @@ -58,23 +58,19 @@ export default class TokenCache implements adal.TokenCache { }); } - public clear(): Thenable { - let self = this; + public async clear(): Promise { // 1) Delete encrypted serialization file // If we got an 'ENOENT' response, the file doesn't exist, which is fine // 3) Delete the encryption key - return new Promise((resolve, reject) => { - fs.unlink(self._cacheSerializationPath, err => { - if (err && err.code !== 'ENOENT') { - reject(err); - } else { - resolve(); - } - }); - }) - .then(() => { return self._credentialProvider.deleteCredential(self._credentialServiceKey); }) - .then(() => { }); + try { + await fs.unlink(this._cacheSerializationPath); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + await this._credentialProvider.deleteCredential(this._credentialServiceKey); } public find(query: any, callback: (error: Error, results: any[]) => void): void { @@ -230,7 +226,7 @@ export default class TokenCache implements adal.TokenCache { }); } - private readCache(): Thenable { + private async readCache(): Promise { let self = this; // NOTE: File system operations are performed synchronously to avoid annoying nested callbacks @@ -239,13 +235,13 @@ export default class TokenCache implements adal.TokenCache { // 3) Decrypt the file contents // 4) Deserialize and return return this.getOrCreateEncryptionParams() - .then(encryptionParams => { + .then(async encryptionParams => { try { return self.decryptCache('utf8', encryptionParams); } catch (e) { try { // try to parse using 'binary' encoding and rewrite cache as UTF8 - let response = self.decryptCache('binary', encryptionParams); + let response = await self.decryptCache('binary', encryptionParams); self.writeCache(response); return response; } catch (e) { @@ -260,17 +256,17 @@ export default class TokenCache implements adal.TokenCache { }); } - private decryptCache(encoding: crypto.Utf8AsciiBinaryEncoding, encryptionParams: EncryptionParams): adal.TokenResponse[] { - let cacheCipher = fs.readFileSync(this._cacheSerializationPath, TokenCache.FsOptions); + private async decryptCache(encoding: crypto.Utf8AsciiBinaryEncoding, encryptionParams: EncryptionParams): Promise { + let cacheCipher = await fs.readFile(this._cacheSerializationPath, TokenCache.FsOptions); let decipher = crypto.createDecipheriv(TokenCache.CipherAlgorithm, encryptionParams.key, encryptionParams.initializationVector); - let cacheJson = decipher.update(cacheCipher, 'hex', encoding); + let cacheJson = decipher.update(cacheCipher.toString(), 'hex', encoding); cacheJson += decipher.final(encoding); // Deserialize the JSON into the array of tokens let cacheObj = JSON.parse(cacheJson); - for (let objIndex in cacheObj) { + for (const obj of cacheObj) { // Rehydrate Date objects since they will always serialize as a string - cacheObj[objIndex].expiresOn = new Date(cacheObj[objIndex].expiresOn); + obj.expiresOn = new Date(obj.expiresOn); } return cacheObj; @@ -297,7 +293,7 @@ export default class TokenCache implements adal.TokenCache { // 4) Encrypt the JSON // 3) Write to the file return this.getOrCreateEncryptionParams() - .then(encryptionParams => { + .then(async encryptionParams => { try { let cacheJson = JSON.stringify(cache); @@ -305,7 +301,7 @@ export default class TokenCache implements adal.TokenCache { let cacheCipher = cipher.update(cacheJson, 'utf8', 'hex'); cacheCipher += cipher.final('hex'); - fs.writeFileSync(self._cacheSerializationPath, cacheCipher, TokenCache.FsOptions); + await fs.writeFile(self._cacheSerializationPath, cacheCipher, TokenCache.FsOptions); } catch (e) { throw e; } diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts index 75cd797c8b..0f795d73db 100644 --- a/extensions/azurecore/src/extension.ts +++ b/extensions/azurecore/src/extension.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as fs from 'fs'; +import { promises as fs } from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as constants from './constants'; @@ -54,12 +54,12 @@ function pushDisposable(disposable: vscode.Disposable): void { // this method is called when your extension is activated // your extension is activated the very first time the command is executed -export function activate(context: vscode.ExtensionContext) { +export async function activate(context: vscode.ExtensionContext) { extensionContext = context; const apiWrapper = new ApiWrapper(); let appContext = new AppContext(extensionContext, apiWrapper); - let storagePath = findOrMakeStoragePath(); + let storagePath = await findOrMakeStoragePath(); if (!storagePath) { return undefined; } @@ -82,12 +82,21 @@ export function activate(context: vscode.ExtensionContext) { }; } +async function exists(path: string): Promise { + try { + await fs.access(path); + return true; + } catch (e) { + return false; + } +} + // Create the folder for storing the token caches -function findOrMakeStoragePath() { +async function findOrMakeStoragePath() { let storagePath = path.join(getDefaultLogLocation(), constants.extensionName); try { - if (!fs.existsSync(storagePath)) { - fs.mkdirSync(storagePath); + if (!(await exists(storagePath))) { + await fs.mkdir(storagePath); console.log('Initialized Azure account extension storage.'); } } @@ -124,4 +133,3 @@ function registerCommands(appContext: AppContext, azureResourceTree: AzureResour registerAzureResourceDatabaseCommands(appContext); } - diff --git a/extensions/import/src/services/serviceClient.ts b/extensions/import/src/services/serviceClient.ts index 5ebdd8e085..198ce0a290 100644 --- a/extensions/import/src/services/serviceClient.ts +++ b/extensions/import/src/services/serviceClient.ts @@ -6,7 +6,7 @@ 'use strict'; import { SqlOpsDataClient, ClientOptions } from 'dataprotocol-client'; -import { IConfig, ServerProvider, Events } from 'service-downloader'; +import { ServerProvider, Events } from 'service-downloader'; import { ServerOptions, TransportKind } from 'vscode-languageclient'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; @@ -18,8 +18,7 @@ import { Telemetry, LanguageClientErrorHandler } from './telemetry'; import * as Constants from '../constants'; import { TelemetryFeature, FlatFileImportFeature } from './features'; import * as serviceUtils from './serviceUtils'; -import { promisify } from 'util'; -import { readFile } from 'fs'; +import { promises as fs } from 'fs'; export class ServiceClient { private statusView: vscode.StatusBarItem; @@ -29,7 +28,7 @@ export class ServiceClient { } public async startService(context: vscode.ExtensionContext): Promise { - const rawConfig = await promisify(readFile)(path.join(context.extensionPath, 'config.json')); + const rawConfig = await fs.readFile(path.join(context.extensionPath, 'config.json')); const config = JSON.parse(rawConfig.toString()); config.installDirectory = path.join(context.extensionPath, config.installDirectory); config.proxy = vscode.workspace.getConfiguration('http').get('proxy'); diff --git a/extensions/mssql/src/main.ts b/extensions/mssql/src/main.ts index d9e330811f..14776320c8 100644 --- a/extensions/mssql/src/main.ts +++ b/extensions/mssql/src/main.ts @@ -28,6 +28,7 @@ import { registerBooksWidget } from './dashboard/bookWidget'; import { createMssqlApi } from './mssqlApiFactory'; import { localize } from './localize'; import { SqlToolsServer } from './sqlToolsServer'; +import { promises as fs } from 'fs'; const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', 'This sample code loads the file into a data frame and shows the first 10 results.'); @@ -42,8 +43,8 @@ export async function activate(context: vscode.ExtensionContext): Promise { const notebookRelativePath: string = 'notebooks/tsg/cluster-status.ipynb'; const notebookFullPath: string = path.join(appContext.extensionContext.extensionPath, notebookRelativePath); - if (!Utils.fileExists(notebookFullPath)) { + if (!(await Utils.exists(notebookFullPath))) { vscode.window.showErrorMessage(localize("fileNotFound", "Unable to find the file specified")); } else { const title: string = Utils.findNextUntitledEditorName(notebookFullPath); diff --git a/extensions/mssql/src/objectExplorerNodeProvider/hdfsCommands.ts b/extensions/mssql/src/objectExplorerNodeProvider/hdfsCommands.ts index 08b3d82f49..cbbcca9bbb 100644 --- a/extensions/mssql/src/objectExplorerNodeProvider/hdfsCommands.ts +++ b/extensions/mssql/src/objectExplorerNodeProvider/hdfsCommands.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import * as azdata from 'azdata'; -import * as fs from 'fs'; +import { promises as fs } from 'fs'; import * as fspath from 'path'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); @@ -22,7 +22,7 @@ import { AppContext } from '../appContext'; import { TreeNode } from './treeNodes'; import { MssqlObjectExplorerNodeProvider } from './objectExplorerNodeProvider'; -function getSaveableUri(apiWrapper: ApiWrapper, fileName: string, isPreview?: boolean): vscode.Uri { +async function getSaveableUri(apiWrapper: ApiWrapper, fileName: string, isPreview?: boolean): Promise { let root = utils.getUserHome(); let workspaceFolders = apiWrapper.workspaceFolders; if (workspaceFolders && workspaceFolders.length > 0) { @@ -33,7 +33,7 @@ function getSaveableUri(apiWrapper: ApiWrapper, fileName: string, isPreview?: bo let fileNum = 1; let fileNameWithoutExtension = fspath.parse(fileName).name; let fileExtension = fspath.parse(fileName).ext; - while (fs.existsSync(fspath.join(root, fileName))) { + while (await utils.exists(fspath.join(root, fileName))) { fileName = `${fileNameWithoutExtension}-${fileNum}${fileExtension}`; fileNum++; } @@ -82,7 +82,7 @@ export class UploadFilesCommand extends ProgressCommand { }; let fileUris: vscode.Uri[] = await this.apiWrapper.showOpenDialog(options); if (fileUris) { - let files: IFile[] = fileUris.map(uri => uri.fsPath).map(this.mapPathsToFiles()); + let files: IFile[] = await Promise.all(fileUris.map(uri => uri.fsPath).map(this.mapPathsToFiles())); await this.executeWithProgress( (cancelToken: vscode.CancellationTokenSource) => this.writeFiles(files, folderNode, cancelToken), localize('uploading', 'Uploading files to HDFS'), true, @@ -99,9 +99,9 @@ export class UploadFilesCommand extends ProgressCommand { } } - private mapPathsToFiles(): (value: string, index: number, array: string[]) => File { - return (path: string) => { - let isDir = fs.lstatSync(path).isDirectory(); + private mapPathsToFiles(): (value: string, index: number, array: string[]) => Promise { + return async (path: string) => { + let isDir = (await fs.lstat(path)).isDirectory(); return new File(path, isDir); }; } @@ -115,9 +115,9 @@ export class UploadFilesCommand extends ProgressCommand { if (file.isDirectory) { let dirName = fspath.basename(file.path); let subFolder = await folderNode.mkdir(dirName); - let children: IFile[] = fs.readdirSync(file.path) + let children: IFile[] = await Promise.all((await fs.readdir(file.path)) .map(childFileName => joinHdfsPath(file.path, childFileName)) - .map(this.mapPathsToFiles()); + .map(this.mapPathsToFiles())); this.writeFiles(children, subFolder, cancelToken); } else { await folderNode.writeFile(file); @@ -258,7 +258,7 @@ export class SaveFileCommand extends ProgressCommand { try { let fileNode = await getNode(context, this.appContext); if (fileNode) { - let defaultUri = getSaveableUri(this.apiWrapper, fspath.basename(fileNode.hdfsPath)); + let defaultUri = await getSaveableUri(this.apiWrapper, fspath.basename(fileNode.hdfsPath)); let fileUri: vscode.Uri = await this.apiWrapper.showSaveDialog({ defaultUri: defaultUri }); @@ -330,7 +330,7 @@ export class PreviewFileCommand extends ProgressCommand { private async showNotebookDocument(fileName: string, connectionProfile?: azdata.IConnectionProfile, initialContent?: string ): Promise { - let docUri: vscode.Uri = getSaveableUri(this.apiWrapper, fileName, true) + let docUri: vscode.Uri = (await getSaveableUri(this.apiWrapper, fileName, true)) .with({ scheme: constants.UNTITLED_SCHEMA }); return await azdata.nb.showNotebookDocument(docUri, { connectionProfile: connectionProfile, @@ -340,7 +340,7 @@ export class PreviewFileCommand extends ProgressCommand { } private async openTextDocument(fileName: string): Promise { - let docUri: vscode.Uri = getSaveableUri(this.apiWrapper, fileName, true); + let docUri: vscode.Uri = await getSaveableUri(this.apiWrapper, fileName, true); if (docUri) { docUri = docUri.with({ scheme: constants.UNTITLED_SCHEMA }); return await this.apiWrapper.openTextDocument(docUri); diff --git a/extensions/mssql/src/sparkFeature/dialog/sparkJobSubmission/sparkConfigurationTab.ts b/extensions/mssql/src/sparkFeature/dialog/sparkJobSubmission/sparkConfigurationTab.ts index 4d90c2a9d2..cd9c007f9b 100644 --- a/extensions/mssql/src/sparkFeature/dialog/sparkJobSubmission/sparkConfigurationTab.ts +++ b/extensions/mssql/src/sparkFeature/dialog/sparkJobSubmission/sparkConfigurationTab.ts @@ -8,7 +8,6 @@ import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; import * as fspath from 'path'; -import * as fs from 'fs'; import * as vscode from 'vscode'; import * as utils from '../../../utils'; import * as LocalizedConstants from '../../../localizedConstants'; @@ -223,7 +222,7 @@ export class SparkConfigurationTab { // 1. For local file Source check whether they existed. if (this._dataModel.isMainSourceFromLocal) { - if (!fs.existsSync(this._dataModel.localFileSourcePath)) { + if (!(await utils.exists(this._dataModel.localFileSourcePath))) { this._dataModel.showDialogError(LocalizedConstants.sparkJobSubmissionLocalFileNotExisted(this._dataModel.localFileSourcePath)); return false; } diff --git a/extensions/mssql/src/sparkFeature/dialog/sparkJobSubmission/sparkJobSubmissionModel.ts b/extensions/mssql/src/sparkFeature/dialog/sparkJobSubmission/sparkJobSubmissionModel.ts index 99b083298c..ca477fffa8 100644 --- a/extensions/mssql/src/sparkFeature/dialog/sparkJobSubmission/sparkJobSubmissionModel.ts +++ b/extensions/mssql/src/sparkFeature/dialog/sparkJobSubmission/sparkJobSubmissionModel.ts @@ -8,7 +8,6 @@ import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -import * as fs from 'fs'; import * as fspath from 'path'; import * as os from 'os'; @@ -143,7 +142,7 @@ export class SparkJobSubmissionModel { return Promise.reject(localize('sparkJobSubmission_localFileOrFolderNotSpecified.', 'Property localFilePath or hdfsFolderPath is not specified. ')); } - if (!fs.existsSync(localFilePath)) { + if (!(await utils.exists(localFilePath))) { return Promise.reject(LocalizedConstants.sparkJobSubmissionLocalFileNotExisted(localFilePath)); } diff --git a/extensions/mssql/src/sparkFeature/sparkUtils.ts b/extensions/mssql/src/sparkFeature/sparkUtils.ts index 1990479516..ff903aeed2 100644 --- a/extensions/mssql/src/sparkFeature/sparkUtils.ts +++ b/extensions/mssql/src/sparkFeature/sparkUtils.ts @@ -45,12 +45,12 @@ export function getTemplatePath(extensionPath: string, templateName: string): st } export function shellWhichResolving(cmd: string): Promise { return new Promise(resolve => { - which(cmd, (err, foundPath) => { + which(cmd, async (err, foundPath) => { if (err) { resolve(undefined); } else { // NOTE: Using realpath b/c some system installs are symlinked from */bin - resolve(fs.realpathSync(foundPath)); + resolve(await fs.promises.realpath(foundPath)); } }); }); diff --git a/extensions/mssql/src/sqlToolsServer.ts b/extensions/mssql/src/sqlToolsServer.ts index ad3ad967a4..e5a6f0d988 100644 --- a/extensions/mssql/src/sqlToolsServer.ts +++ b/extensions/mssql/src/sqlToolsServer.ts @@ -20,8 +20,7 @@ import { AppContext } from './appContext'; import { DacFxService } from './dacfx/dacFxService'; import { CmsService } from './cms/cmsService'; import { CompletionExtensionParams, CompletionExtLoadRequest } from './contracts'; -import { promisify } from 'util'; -import { readFile } from 'fs'; +import { promises as fs } from 'fs'; const outputChannel = vscode.window.createOutputChannel(Constants.serviceName); const statusView = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); @@ -70,7 +69,7 @@ export class SqlToolsServer { } private async download(context: AppContext): Promise { - const rawConfig = await promisify(readFile)(path.join(context.extensionContext.extensionPath, 'config.json')); + const rawConfig = await fs.readFile(path.join(context.extensionContext.extensionPath, 'config.json')); this.config = JSON.parse(rawConfig.toString()); this.config.installDirectory = path.join(__dirname, this.config.installDirectory); this.config.proxy = vscode.workspace.getConfiguration('http').get('proxy'); diff --git a/extensions/mssql/src/utils.ts b/extensions/mssql/src/utils.ts index 25f1817fda..27b4b51ba1 100644 --- a/extensions/mssql/src/utils.ts +++ b/extensions/mssql/src/utils.ts @@ -10,8 +10,7 @@ import * as crypto from 'crypto'; import * as os from 'os'; import * as findRemoveSync from 'find-remove'; import * as constants from './constants'; -import * as fs from 'fs'; -import { promisify } from 'util'; +import { promises as fs } from 'fs'; const configTracingLevel = 'tracingLevel'; const configLogRetentionMinutes = 'logRetentionMinutes'; @@ -31,17 +30,6 @@ export function getAppDataPath() { } } -export namespace pfs { - - export function exists(path: string): Promise { - return promisify(fs.exists)(path); - } - - export function mkdir(path: string, mode?: number): Promise { - return promisify(fs.mkdir)(path, mode); - } -} - /** * Get a file name that is not already used in the target directory * @param filePath source notebook file name @@ -61,14 +49,6 @@ export function findNextUntitledEditorName(filePath: string): string { return title; } -export function fileExists(file: string): boolean { - return fs.existsSync(file); -} - -export function copyFile(source: string, target: string): void { - fs.copyFileSync(source, target); -} - export function removeOldLogFiles(logPath: string, prefix: string): JSON { return findRemoveSync(logPath, { age: { seconds: getConfigLogRetentionSeconds() }, limit: getConfigLogFilesRemovalLimit() }); } @@ -303,3 +283,12 @@ export function logDebug(msg: any): void { console.log(outputMsg); } } + +export async function exists(path: string): Promise { + try { + await fs.access(path); + return true; + } catch (e) { + return false; + } +} diff --git a/extensions/notebook/src/book/bookTreeItem.ts b/extensions/notebook/src/book/bookTreeItem.ts index fc0600239d..426807bbd1 100644 --- a/extensions/notebook/src/book/bookTreeItem.ts +++ b/extensions/notebook/src/book/bookTreeItem.ts @@ -78,6 +78,7 @@ export class BookTreeItem extends vscode.TreeItem { if (this.book.tableOfContents.sections[i].url) { // TODO: Currently only navigating to notebooks. Need to add logic for markdown. let pathToNotebook = path.join(this.book.root, 'content', this.book.tableOfContents.sections[i].url.concat('.ipynb')); + // tslint:disable-next-line:no-sync if (fs.existsSync(pathToNotebook)) { this._previousUri = pathToNotebook; return; @@ -93,6 +94,7 @@ export class BookTreeItem extends vscode.TreeItem { if (this.book.tableOfContents.sections[i].url) { // TODO: Currently only navigating to notebooks. Need to add logic for markdown. let pathToNotebook = path.join(this.book.root, 'content', this.book.tableOfContents.sections[i].url.concat('.ipynb')); + // tslint:disable-next-line:no-sync if (fs.existsSync(pathToNotebook)) { this._nextUri = pathToNotebook; return; diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts index a1de4a70ad..5217f03e9f 100644 --- a/extensions/notebook/src/book/bookTreeView.ts +++ b/extensions/notebook/src/book/bookTreeView.ts @@ -6,18 +6,16 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as path from 'path'; -import * as fs from 'fs'; +import { promises as fs } from 'fs'; import * as yaml from 'js-yaml'; import * as glob from 'fast-glob'; import { BookTreeItem, BookTreeItemType } from './bookTreeItem'; import { maxBookSearchDepth, notebookConfigKey } from '../common/constants'; -import { isEditorTitleFree } from '../common/utils'; +import { isEditorTitleFree, exists } from '../common/utils'; import * as nls from 'vscode-nls'; -import { promisify } from 'util'; import { IJupyterBookToc, IJupyterBookSection } from '../contracts/content'; const localize = nls.loadMessageBundle(); -const existsAsync = promisify(fs.exists); export class BookTreeViewProvider implements vscode.TreeDataProvider, azdata.nb.NavigationProvider { readonly providerId: string = 'BookNavigator'; @@ -84,7 +82,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider 0) { const rootTreeItem = books[0]; const sectionToOpen = rootTreeItem.findChildSection(urlToOpen); @@ -92,8 +90,8 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider Array.isArray(val.sections) ? acc.concat(val).concat(this.parseJupyterSection(val.sections)) : acc.concat(val), []); } - public getBooks(): BookTreeItem[] { + public async getBooks(): Promise { let books: BookTreeItem[] = []; - for (let i in this._tableOfContentPaths) { - let root = path.dirname(path.dirname(this._tableOfContentPaths[i])); + for (const contentPath of this._tableOfContentPaths) { + let root = path.dirname(path.dirname(contentPath)); try { - const config = yaml.safeLoad(fs.readFileSync(path.join(root, '_config.yml'), 'utf-8')); - const tableOfContents = yaml.safeLoad(fs.readFileSync(this._tableOfContentPaths[i], 'utf-8')); + const config = yaml.safeLoad((await fs.readFile(path.join(root, '_config.yml'), 'utf-8')).toString()); + const tableOfContents = yaml.safeLoad((await fs.readFile(contentPath, 'utf-8')).toString()); try { let book = new BookTreeItem({ title: config.title, @@ -242,7 +240,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { let notebooks: BookTreeItem[] = []; for (let i = 0; i < sections.length; i++) { if (sections[i].url) { @@ -267,7 +265,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { + try { + await fs.access(path); + return true; + } catch (e) { + return false; + } +} diff --git a/extensions/notebook/src/dialog/configurePythonDialog.ts b/extensions/notebook/src/dialog/configurePythonDialog.ts index 73c213a2ec..38d2ee6bef 100644 --- a/extensions/notebook/src/dialog/configurePythonDialog.ts +++ b/extensions/notebook/src/dialog/configurePythonDialog.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; -import * as fs from 'fs'; +import { promises as fs } from 'fs'; import * as utils from '../common/utils'; import { JupyterServerInstallation } from '../jupyter/jupyterServerInstallation'; @@ -220,7 +220,7 @@ export class ConfigurePythonDialog { if (useExistingPython) { let exePath = JupyterServerInstallation.getPythonExePath(pythonLocation, true); - let pythonExists = fs.existsSync(exePath); + let pythonExists = await utils.exists(exePath); if (!pythonExists) { this.showErrorMessage(this.PythonNotFoundMsg); return false; @@ -243,27 +243,23 @@ export class ConfigurePythonDialog { return true; } - private isFileValid(pythonLocation: string): Promise { + private async isFileValid(pythonLocation: string): Promise { let self = this; - return new Promise(function (resolve) { - fs.stat(pythonLocation, function (err, stats) { - if (err) { - // Ignore error if folder doesn't exist, since it will be - // created during installation - if (err.code !== 'ENOENT') { - self.showErrorMessage(err.message); - resolve(false); - } - } - else { - if (stats.isFile()) { - self.showErrorMessage(self.InvalidLocationMsg); - resolve(false); - } - } - resolve(true); - }); - }); + try { + const stats = await fs.stat(pythonLocation); + if (stats.isFile()) { + self.showErrorMessage(self.InvalidLocationMsg); + return false; + } + } catch (err) { + // Ignore error if folder doesn't exist, since it will be + // created during installation + if (err.code !== 'ENOENT') { + self.showErrorMessage(err.message); + return false; + } + } + return true; } private async handleBrowse(): Promise { @@ -304,4 +300,4 @@ export class ConfigurePythonDialog { level: azdata.window.MessageLevel.Error }; } -} \ No newline at end of file +} diff --git a/extensions/notebook/src/dialog/pythonPathLookup.ts b/extensions/notebook/src/dialog/pythonPathLookup.ts index 9c3c6eadee..cd0a94a377 100644 --- a/extensions/notebook/src/dialog/pythonPathLookup.ts +++ b/extensions/notebook/src/dialog/pythonPathLookup.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as glob from 'glob'; import * as utils from '../common/utils'; @@ -92,7 +91,7 @@ export class PythonPathLookup { const cmd = `"${options.command}" ${args.join(' ')}`; let output = await utils.executeBufferedCommand(cmd, {}); let value = output ? output.trim() : ''; - if (value.length > 0 && fs.existsSync(value)) { + if (value.length > 0 && await utils.exists(value)) { return value; } } catch (err) { @@ -163,4 +162,4 @@ export class PythonPathLookup { } return undefined; } -} \ No newline at end of file +} diff --git a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts index 19c19e3982..97dd6e1a4a 100644 --- a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts +++ b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts @@ -67,7 +67,7 @@ export class JupyterServerInstallation { } private async installDependencies(backgroundOperation: azdata.BackgroundOperation): Promise { - if (!fs.existsSync(this._pythonExecutable) || this._forceInstall || this._usingExistingPython) { + if (!(await utils.exists(this._pythonExecutable)) || this._forceInstall || this._usingExistingPython) { window.showInformationMessage(msgInstallPkgStart); this.outputChannel.show(true); @@ -180,11 +180,11 @@ export class JupyterServerInstallation { }); downloadRequest.pipe(fs.createWriteStream(pythonPackagePathLocal)) - .on('close', () => { + .on('close', async () => { //unpack python zip/tar file this.outputChannel.appendLine(msgPythonUnpackPending); let pythonSourcePath = path.join(installPath, constants.pythonBundleVersion); - if (!this._usingExistingPython && fs.existsSync(pythonSourcePath)) { + if (!this._usingExistingPython && await utils.exists(pythonSourcePath)) { try { fs.removeSync(pythonSourcePath); } catch (err) { @@ -256,7 +256,7 @@ export class JupyterServerInstallation { } } - if (fs.existsSync(this._pythonExecutable)) { + if (await utils.exists(this._pythonExecutable)) { let pythonUserDir = await this.getPythonUserDir(this._pythonExecutable); if (pythonUserDir) { this.pythonEnvVarPath = pythonUserDir + delimiter + this.pythonEnvVarPath; @@ -330,7 +330,7 @@ export class JupyterServerInstallation { await this.configurePackagePaths(); }; let installReady = new Deferred(); - if (!fs.existsSync(this._pythonExecutable) || this._forceInstall || this._usingExistingPython) { + if (!(await utils.exists(this._pythonExecutable)) || this._forceInstall || this._usingExistingPython) { this.apiWrapper.startBackgroundOperation({ displayName: msgTaskName, description: msgTaskName, @@ -522,6 +522,7 @@ export class JupyterServerInstallation { } let condaExePath = this.getCondaExePath(); + // tslint:disable-next-line:no-sync return fs.existsSync(condaExePath); } @@ -538,6 +539,7 @@ export class JupyterServerInstallation { let useExistingInstall = JupyterServerInstallation.getExistingPythonSetting(apiWrapper); let pythonExe = JupyterServerInstallation.getPythonExePath(pathSetting, useExistingInstall); + // tslint:disable-next-line:no-sync return fs.existsSync(pythonExe); } @@ -568,6 +570,7 @@ export class JupyterServerInstallation { let notebookConfig = apiWrapper.getConfiguration(constants.notebookConfigKey); if (notebookConfig) { let configPythonPath = notebookConfig[constants.pythonPathConfigKey]; + // tslint:disable-next-line:no-sync if (configPythonPath && fs.existsSync(configPythonPath)) { path = configPythonPath; } diff --git a/extensions/notebook/src/jupyter/serverInstance.ts b/extensions/notebook/src/jupyter/serverInstance.ts index 673f5aa79c..60bff8e4f8 100644 --- a/extensions/notebook/src/jupyter/serverInstance.ts +++ b/extensions/notebook/src/jupyter/serverInstance.ts @@ -62,8 +62,13 @@ export class ServerInstanceUtils { public copy(src: string, dest: string): Promise { return fs.copy(src, dest); } - public existsSync(dirPath: string): boolean { - return fs.existsSync(dirPath); + public async exists(path: string): Promise { + try { + await fs.access(path); + return true; + } catch (e) { + return false; + } } public generateUuid(): string { return UUID.generateUuid(); @@ -204,7 +209,7 @@ export class PerNotebookServerInstance implements IServerInstance { private async copyKernelsToSystemJupyterDirs(): Promise { let kernelsExtensionSource = path.join(this.options.install.extensionPath, 'kernels'); this._systemJupyterDir = path.join(this.getSystemJupyterHomeDir(), 'kernels'); - if (!this.utils.existsSync(this._systemJupyterDir)) { + if (!(await this.utils.exists(this._systemJupyterDir))) { await this.utils.mkDir(this._systemJupyterDir, this.options.install.outputChannel); } await this.utils.copy(kernelsExtensionSource, this._systemJupyterDir); diff --git a/extensions/notebook/src/test/book/book.test.ts b/extensions/notebook/src/test/book/book.test.ts index 92910def71..e4993e24d7 100644 --- a/extensions/notebook/src/test/book/book.test.ts +++ b/extensions/notebook/src/test/book/book.test.ts @@ -283,9 +283,9 @@ describe.skip('BookTreeViewProviderTests', function() { await Promise.race([tocRead, errorCase.then(() => { throw new Error('Table of Contents were not ready in time'); })]); }); - it('should show error if notebook or markdown file is missing', function(): void { + it('should show error if notebook or markdown file is missing', async function(): Promise { let books = bookTreeViewProvider.getBooks(); - let children = bookTreeViewProvider.getSections({ sections: [] }, books[0].sections, rootFolderPath); + let children = await bookTreeViewProvider.getSections({ sections: [] }, (await books)[0].sections, rootFolderPath); should(bookTreeViewProvider.errorMessage).equal('Missing file : Notebook1'); // Rest of book should be detected correctly even with a missing file equalBookItems(children[0], expectedNotebook2); diff --git a/extensions/notebook/src/test/model/serverInstance.test.ts b/extensions/notebook/src/test/model/serverInstance.test.ts index 916ff7572c..e2f282a4bd 100644 --- a/extensions/notebook/src/test/model/serverInstance.test.ts +++ b/extensions/notebook/src/test/model/serverInstance.test.ts @@ -57,7 +57,7 @@ describe('Jupyter server instance', function (): void { // Given a server instance mockUtils.setup(u => u.mkDir(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => Promise.resolve()); mockUtils.setup(u => u.copy(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => Promise.resolve()); - mockUtils.setup(u => u.existsSync(TypeMoq.It.isAnyString())).returns(() => false); + mockUtils.setup(u => u.exists(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(false)); // When I run configure await serverInstance.configure(); @@ -65,7 +65,7 @@ describe('Jupyter server instance', function (): void { // Then I expect a folder to have been created with config and data subdirs mockUtils.verify(u => u.mkDir(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(5)); mockUtils.verify(u => u.copy(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(3)); - mockUtils.verify(u => u.existsSync(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(1)); + mockUtils.verify(u => u.exists(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(1)); }); it('Should have URI info after start', async function (): Promise { diff --git a/extensions/resource-deployment/src/services/platformService.ts b/extensions/resource-deployment/src/services/platformService.ts index c62dd34fac..aef5e1778d 100644 --- a/extensions/resource-deployment/src/services/platformService.ts +++ b/extensions/resource-deployment/src/services/platformService.ts @@ -26,10 +26,12 @@ export class PlatformService implements IPlatformService { } copyFile(source: string, target: string): void { + // tslint:disable-next-line:no-sync fs.copyFileSync(source, target); } fileExists(file: string): boolean { + // tslint:disable-next-line:no-sync return fs.existsSync(file); } @@ -44,4 +46,4 @@ export class PlatformService implements IPlatformService { isNotebookNameUsed(title: string): boolean { return (azdata.nb.notebookDocuments.findIndex(doc => doc.isUntitled && doc.fileName === title) > -1); } -} \ No newline at end of file +} diff --git a/extensions/resource-deployment/src/services/resourceTypeService.ts b/extensions/resource-deployment/src/services/resourceTypeService.ts index 4c363c6d42..f72db5f5ea 100644 --- a/extensions/resource-deployment/src/services/resourceTypeService.ts +++ b/extensions/resource-deployment/src/services/resourceTypeService.ts @@ -6,7 +6,7 @@ import * as azdata from 'azdata'; import * as cp from 'child_process'; -import * as fs from 'fs'; +import { createWriteStream, promises as fs } from 'fs'; import * as https from 'https'; import * as os from 'os'; import * as path from 'path'; @@ -226,7 +226,7 @@ export class ResourceTypeService implements IResourceTypeService { private download(url: string): Promise { const self = this; const promise = new Promise((resolve, reject) => { - https.get(url, function (response) { + https.get(url, async function (response) { console.log('Download installer from: ' + url); if (response.statusCode === 301 || response.statusCode === 302) { // Redirect and download from new location @@ -247,19 +247,19 @@ export class ResourceTypeService implements IResourceTypeService { let fileName = originalFileName; const downloadFolder = os.homedir(); let cnt = 1; - while (fs.existsSync(path.join(downloadFolder, fileName + extension))) { + while (await exists(path.join(downloadFolder, fileName + extension))) { fileName = `${originalFileName}-${cnt}`; cnt++; } fileName = path.join(downloadFolder, fileName + extension); - const file = fs.createWriteStream(fileName); + const file = createWriteStream(fileName); response.pipe(file); file.on('finish', () => { file.close(); resolve(fileName); }); - file.on('error', (err) => { - fs.unlink(fileName, () => { }); + file.on('error', async (err) => { + await fs.unlink(fileName); reject(err.message); }); }); @@ -268,3 +268,12 @@ export class ResourceTypeService implements IResourceTypeService { } } + +async function exists(path: string): Promise { + try { + await fs.access(path); + return true; + } catch (e) { + return false; + } +} diff --git a/extensions/schema-compare/package.json b/extensions/schema-compare/package.json index bc3e8d6543..ab2addfce5 100644 --- a/extensions/schema-compare/package.json +++ b/extensions/schema-compare/package.json @@ -64,7 +64,7 @@ }, "devDependencies": { "@types/mocha": "^5.2.5", - "@types/node": "^10.14.8", + "@types/node": "^10.14.8", "mocha": "^5.2.0", "should": "^13.2.1", "typemoq": "^2.1.0", diff --git a/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts b/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts index cfd2ddc419..16ac05ffa9 100644 --- a/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts +++ b/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts @@ -9,8 +9,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as os from 'os'; import { SchemaCompareMainWindow } from '../schemaCompareMainWindow'; -import { isNullOrUndefined } from 'util'; -import { existsSync } from 'fs'; +import { promises as fs } from 'fs'; import { Telemetry } from '../telemetry'; import { getEndpointName } from '../utils'; import * as mssql from '../../../mssql'; @@ -35,6 +34,15 @@ const YesButtonText: string = localize('schemaCompareDialog.Yes', 'Yes'); const NoButtonText: string = localize('schemaCompareDialog.No', 'No'); const titleFontSize: number = 13; +async function exists(path: string): Promise { + try { + await fs.access(path); + return true; + } catch (e) { + return false; + } +} + export class SchemaCompareDialog { public dialog: azdata.window.Dialog; public dialogName: string; @@ -194,8 +202,8 @@ export class SchemaCompareDialog { ariaLabel: localize('schemaCompareDialog.sourceTextBox', "Source file") }).component(); - this.sourceTextBox.onTextChanged((e) => { - this.dialog.okButton.enabled = this.shouldEnableOkayButton(); + this.sourceTextBox.onTextChanged(async (e) => { + this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); }); this.targetTextBox = view.modelBuilder.inputBox().withProperties({ @@ -204,8 +212,8 @@ export class SchemaCompareDialog { ariaLabel: localize('schemaCompareDialog.targetTextBox', "Target file") }).component(); - this.targetTextBox.onTextChanged(() => { - this.dialog.okButton.enabled = this.shouldEnableOkayButton(); + this.targetTextBox.onTextChanged(async () => { + this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); }); this.sourceServerComponent = await this.createSourceServerDropdown(view); @@ -307,7 +315,7 @@ export class SchemaCompareDialog { currentButton.onDidClick(async (click) => { // file browser should open where the current dacpac is or the appropriate default folder let rootPath = vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].name : os.homedir(); - let defaultUri = endpoint && endpoint.packageFilePath && existsSync(endpoint.packageFilePath) ? endpoint.packageFilePath : rootPath; + let defaultUri = endpoint && endpoint.packageFilePath && await exists(endpoint.packageFilePath) ? endpoint.packageFilePath : rootPath; let fileUris = await vscode.window.showOpenDialog( { @@ -351,17 +359,17 @@ export class SchemaCompareDialog { }).component(); // show dacpac file browser - dacpacRadioButton.onDidClick(() => { + dacpacRadioButton.onDidClick(async () => { this.sourceIsDacpac = true; this.formBuilder.removeFormItem(this.sourceNoActiveConnectionsText); this.formBuilder.removeFormItem(this.sourceServerComponent); this.formBuilder.removeFormItem(this.sourceDatabaseComponent); this.formBuilder.insertFormItem(this.sourceDacpacComponent, 2, { horizontal: true, titleFontSize: titleFontSize }); - this.dialog.okButton.enabled = this.shouldEnableOkayButton(); + this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); }); // show server and db dropdowns or 'No active connections' text - databaseRadioButton.onDidClick(() => { + databaseRadioButton.onDidClick(async () => { this.sourceIsDacpac = false; if ((this.sourceServerDropdown.value as ConnectionDropdownValue)) { this.formBuilder.insertFormItem(this.sourceServerComponent, 2, { horizontal: true, titleFontSize: titleFontSize }); @@ -370,7 +378,7 @@ export class SchemaCompareDialog { this.formBuilder.insertFormItem(this.sourceNoActiveConnectionsText, 2, { horizontal: true, titleFontSize: titleFontSize }); } this.formBuilder.removeFormItem(this.sourceDacpacComponent); - this.dialog.okButton.enabled = this.shouldEnableOkayButton(); + this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); }); // if source is currently a db, show it in the server and db dropdowns @@ -408,17 +416,17 @@ export class SchemaCompareDialog { }).component(); // show dacpac file browser - dacpacRadioButton.onDidClick(() => { + dacpacRadioButton.onDidClick(async () => { this.targetIsDacpac = true; this.formBuilder.removeFormItem(this.targetNoActiveConnectionsText); this.formBuilder.removeFormItem(this.targetServerComponent); this.formBuilder.removeFormItem(this.targetDatabaseComponent); this.formBuilder.addFormItem(this.targetDacpacComponent, { horizontal: true, titleFontSize: titleFontSize }); - this.dialog.okButton.enabled = this.shouldEnableOkayButton(); + this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); }); // show server and db dropdowns or 'No active connections' text - databaseRadioButton.onDidClick(() => { + databaseRadioButton.onDidClick(async () => { this.targetIsDacpac = false; this.formBuilder.removeFormItem(this.targetDacpacComponent); if ((this.targetServerDropdown.value as ConnectionDropdownValue)) { @@ -427,7 +435,7 @@ export class SchemaCompareDialog { } else { this.formBuilder.addFormItem(this.targetNoActiveConnectionsText, { horizontal: true, titleFontSize: titleFontSize }); } - this.dialog.okButton.enabled = this.shouldEnableOkayButton(); + this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); }); // if target is currently a db, show it in the server and db dropdowns @@ -450,18 +458,18 @@ export class SchemaCompareDialog { }; } - private shouldEnableOkayButton(): boolean { + private async shouldEnableOkayButton(): Promise { - let sourcefilled = (this.sourceIsDacpac && this.existsDacpac(this.sourceTextBox.value)) + let sourcefilled = (this.sourceIsDacpac && await this.existsDacpac(this.sourceTextBox.value)) || (!this.sourceIsDacpac && !isNullOrUndefined(this.sourceDatabaseDropdown.value) && this.sourceDatabaseDropdown.values.findIndex(x => this.matchesValue(x, this.sourceDbEditable)) !== -1); - let targetfilled = (this.targetIsDacpac && this.existsDacpac(this.targetTextBox.value)) + let targetfilled = (this.targetIsDacpac && await this.existsDacpac(this.targetTextBox.value)) || (!this.targetIsDacpac && !isNullOrUndefined(this.targetDatabaseDropdown.value) && this.targetDatabaseDropdown.values.findIndex(x => this.matchesValue(x, this.targetDbEditable)) !== -1); return sourcefilled && targetfilled; } - private existsDacpac(filename: string): boolean { - return !isNullOrUndefined(filename) && existsSync(filename) && (filename.toLocaleLowerCase().endsWith('.dacpac')); + private async existsDacpac(filename: string): Promise { + return !isNullOrUndefined(filename) && await exists(filename) && (filename.toLocaleLowerCase().endsWith('.dacpac')); } protected async createSourceServerDropdown(view: azdata.ModelView): Promise { @@ -596,9 +604,9 @@ export class SchemaCompareDialog { ariaLabel: localize('schemaCompareDialog.sourceDatabaseDropdown', "Source Database") } ).component(); - this.sourceDatabaseDropdown.onValueChanged((value) => { + this.sourceDatabaseDropdown.onValueChanged(async (value) => { this.sourceDbEditable = value; - this.dialog.okButton.enabled = this.shouldEnableOkayButton(); + this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); }); return { @@ -615,9 +623,9 @@ export class SchemaCompareDialog { ariaLabel: localize('schemaCompareDialog.targetDatabaseDropdown', "Target Database") } ).component(); - this.targetDatabaseDropdown.onValueChanged((value) => { + this.targetDatabaseDropdown.onValueChanged(async (value) => { this.targetDbEditable = value; - this.dialog.okButton.enabled = this.shouldEnableOkayButton(); + this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); }); return { @@ -684,3 +692,7 @@ export class SchemaCompareDialog { interface ConnectionDropdownValue extends azdata.CategoryValue { connection: azdata.connection.ConnectionProfile; } + +function isNullOrUndefined(val: any): boolean { + return val === null || val === undefined; +} diff --git a/tslint.json b/tslint.json index c2333565cf..98a7f40c67 100644 --- a/tslint.json +++ b/tslint.json @@ -229,6 +229,21 @@ "translation-remind": true, "no-standalone-editor": true, "no-nls-in-standalone-editor": true, + "no-sync": [ + true, + { + "exclude": [ + "**/vs/**", // assume they are doing the right thing + "**/extensions/git/**", // assume they are doing the right thing, + "**/extensions/extension-editing/**", // assume they are doing the right thing, + "**/json-language-features/**", // assume they are doing the right thing, + "**/vscode-test-resolver/**", // testing doesn't matter + "**/integration-tests/**", // testing doesn't matter + "**/*.test.*", // testing doesn't matter + "**/test/**" // testing doesn't matter + ] + } + ], "no-useless-strict": true }, "defaultSeverity": "warning"