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
This commit is contained in:
Anthony Dresser
2019-09-17 13:32:42 -07:00
committed by GitHub
parent 4d62983680
commit 28d453fced
31 changed files with 305 additions and 201 deletions

View File

@@ -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;

View File

@@ -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<BookTreeItem>, azdata.nb.NavigationProvider {
readonly providerId: string = 'BookNavigator';
@@ -84,7 +82,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
const bookViewer = vscode.window.createTreeView('bookTreeView', { showCollapseAll: true, treeDataProvider: this });
await vscode.commands.executeCommand('workbench.books.action.focusBooksExplorer');
this._openAsUntitled = openAsUntitled;
let books = this.getBooks();
let books = await this.getBooks();
if (books && books.length > 0) {
const rootTreeItem = books[0];
const sectionToOpen = rootTreeItem.findChildSection(urlToOpen);
@@ -92,8 +90,8 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
const urlPath = sectionToOpen ? sectionToOpen.url : rootTreeItem.tableOfContents.sections[0].url;
const sectionToOpenMarkdown: string = path.join(bookPath, 'content', urlPath.concat('.md'));
const sectionToOpenNotebook: string = path.join(bookPath, 'content', urlPath.concat('.ipynb'));
const markdownExists = await existsAsync(sectionToOpenMarkdown);
const notebookExists = await existsAsync(sectionToOpenNotebook);
const markdownExists = await exists(sectionToOpenMarkdown);
const notebookExists = await exists(sectionToOpenNotebook);
if (markdownExists) {
vscode.commands.executeCommand('markdown.showPreview', vscode.Uri.file(sectionToOpenMarkdown));
}
@@ -208,13 +206,13 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
return array.reduce((acc, val) => Array.isArray(val.sections) ? acc.concat(val).concat(this.parseJupyterSection(val.sections)) : acc.concat(val), []);
}
public getBooks(): BookTreeItem[] {
public async getBooks(): Promise<BookTreeItem[]> {
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<BookTreeIte
return books;
}
public getSections(tableOfContents: IJupyterBookToc, sections: IJupyterBookSection[], root: string): BookTreeItem[] {
public async getSections(tableOfContents: IJupyterBookToc, sections: IJupyterBookSection[], root: string): Promise<BookTreeItem[]> {
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<BookTreeIte
let pathToMarkdown = path.join(root, 'content', sections[i].url.concat('.md'));
// Note: Currently, if there is an ipynb and a md file with the same name, Jupyter Books only shows the notebook.
// Following Jupyter Books behavior for now
if (fs.existsSync(pathToNotebook)) {
if (await exists(pathToNotebook)) {
let notebook = new BookTreeItem({
title: sections[i].title,
root: root,
@@ -286,7 +284,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
if (this._openAsUntitled) {
this._allNotebooks.set(path.basename(pathToNotebook), notebook);
}
} else if (fs.existsSync(pathToMarkdown)) {
} else if (await exists(pathToMarkdown)) {
let markdown = new BookTreeItem({
title: sections[i].title,
root: root,

View File

@@ -188,3 +188,12 @@ export function getHostAndPortFromEndpoint(endpoint: string): HostAndIp {
port: undefined
};
}
export async function exists(path: string): Promise<boolean> {
try {
await fs.access(path);
return true;
} catch (e) {
return false;
}
}

View File

@@ -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<boolean> {
private async isFileValid(pythonLocation: string): Promise<boolean> {
let self = this;
return new Promise<boolean>(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<void> {
@@ -304,4 +300,4 @@ export class ConfigurePythonDialog {
level: azdata.window.MessageLevel.Error
};
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -67,7 +67,7 @@ export class JupyterServerInstallation {
}
private async installDependencies(backgroundOperation: azdata.BackgroundOperation): Promise<void> {
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<void>();
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;
}

View File

@@ -62,8 +62,13 @@ export class ServerInstanceUtils {
public copy(src: string, dest: string): Promise<void> {
return fs.copy(src, dest);
}
public existsSync(dirPath: string): boolean {
return fs.existsSync(dirPath);
public async exists(path: string): Promise<boolean> {
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<void> {
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);

View File

@@ -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<void> {
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);

View File

@@ -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<void> {