Fix #4029 Ensure changeKernels always resolves, even in error states (#4488)

* Fix #4029 Ensure changeKernels always resolves, even in error states
This is necessary to unblock reverting the kernel on canceling  Python install
- startSession now correctly sets up kernel information, since a kernel is loaded there.
- Remove call to change kernel on session initialize. This isn't needed due to refactor
- Handle kernel change failure by attempting to fall back to old kernel
- ExtensionHost $startNewSession now ensures errors are sent across the wire.
- Update AttachTo and Kernel dropdowns so they handle kernel being available. This is needed since other changes mean the session is likely ready before these get going

* Fix to handle python cancel and load existing scenarios
- Made changes to handle failure flow when Python dialog is canceled
- Made changes to handle initial load fail. Kernel and Attach To dropdowns show No Kernel / None and you can choose a kernel
- Added error wrapping in ext host so that string errors make it across and aren't lost.
This commit is contained in:
Kevin Cunnane
2019-03-14 13:07:08 -07:00
committed by GitHub
parent 7973f0f178
commit 6f1a03587a
6 changed files with 164 additions and 105 deletions

View File

@@ -354,7 +354,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
cellIndex: index
});
} else {
this.notifyError(localize('deleteCellFailed', 'Failed to delete cell.'));
this.notifyError(localize('deleteCellFailed', "Failed to delete cell."));
}
}
@@ -392,7 +392,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
this._onErrorEmitter.fire({ message: error, severity: Severity.Error });
}
public async startSession(manager: INotebookManager, displayName?: string): Promise<void> {
public async startSession(manager: INotebookManager, displayName?: string, setErrorStateOnFail?: boolean): Promise<void> {
if (displayName) {
let standardKernel = this._notebookOptions.standardKernels.find(kernel => kernel.displayName === displayName);
this._defaultKernel = displayName ? { name: standardKernel.name, display_name: standardKernel.displayName } : this._defaultKernel;
@@ -406,7 +406,6 @@ export class NotebookModel extends Disposable implements INotebookModel {
});
if (!this._activeClientSession) {
this.updateActiveClientSession(clientSession);
}
let profile = new ConnectionProfile(this._notebookOptions.capabilitiesService, this.connectionProfile);
@@ -420,16 +419,31 @@ export class NotebookModel extends Disposable implements INotebookModel {
this._activeConnection = undefined;
}
clientSession.onKernelChanging(async (e) => {
await this.loadActiveContexts(e);
});
clientSession.statusChanged(async (session) => {
this._kernelsChangedEmitter.fire(session.kernel);
});
await clientSession.initialize();
// By somehow we have to wait for ready, otherwise may not be called for some cases.
await clientSession.ready;
if (clientSession.isInErrorState) {
this.setErrorState(clientSession.errorMessage);
} else {
this._onClientSessionReady.fire(clientSession);
// Once session is loaded, can use the session manager to retrieve useful info
this.loadKernelInfo(clientSession, this.defaultKernel.display_name);
if (clientSession.kernel) {
await clientSession.kernel.ready;
await this.updateKernelInfoOnKernelChange(clientSession.kernel);
}
if (clientSession.isInErrorState) {
if (setErrorStateOnFail) {
this.setErrorState(clientSession.errorMessage);
} else {
throw new Error(clientSession.errorMessage);
}
}
this._onClientSessionReady.fire(clientSession);
this._kernelChangedEmitter.fire({
oldValue: undefined,
newValue: clientSession.kernel
});
}
}
@@ -552,10 +566,57 @@ export class NotebookModel extends Disposable implements INotebookModel {
this.doChangeKernel(displayName, true);
}
public async doChangeKernel(displayName: string, mustSetProvider: boolean = true): Promise<void> {
if (mustSetProvider) {
await this.setProviderIdAndStartSession(displayName);
private async doChangeKernel(displayName: string, mustSetProvider: boolean = true, restoreOnFail: boolean = true): Promise<void> {
let oldDisplayName = this._activeClientSession && this._activeClientSession.kernel ? this._activeClientSession.kernel.name : undefined;
try {
let changeKernelNeeded = true;
if (mustSetProvider) {
changeKernelNeeded = await this.setProviderIdAndStartSession(displayName);
}
if (changeKernelNeeded) {
let spec = this.findSpec(displayName);
if (this._activeClientSession && this._activeClientSession.isReady) {
let kernel = await this._activeClientSession.changeKernel(spec, this._oldKernel);
try {
await kernel.ready;
await this.updateKernelInfoOnKernelChange(kernel);
} catch (err2) {
// TODO should we handle this in any way?
console.log(`doChangeKernel: ignoring error ${notebookUtils.getErrorMessage(err2)}`);
}
}
}
} catch (err) {
if (oldDisplayName && restoreOnFail) {
this.notifyError(localize('changeKernelFailedRetry', "Failed to change kernel. Kernel {0} will be used. Error was: {1}", oldDisplayName, notebookUtils.getErrorMessage(err)));
// Clear out previous kernel
let failedProviderId = this.tryFindProviderForKernel(displayName, true);
let oldProviderId = this.tryFindProviderForKernel(oldDisplayName, true);
if (failedProviderId !== oldProviderId) {
// We need to clear out the old kernel information so we switch providers. Otherwise in the SQL -> Jupyter -> SQL failure case,
// we would never reset the providers
this._oldKernel = undefined;
}
return this.doChangeKernel(oldDisplayName, mustSetProvider, false);
} else {
this.notifyError(localize('changeKernelFailed', "Failed to change kernel due to error: {0}", notebookUtils.getErrorMessage(err)));
this._kernelChangedEmitter.fire({
newValue: undefined,
oldValue: undefined
});
}
}
// Else no need to do anything
}
private async updateKernelInfoOnKernelChange(kernel: nb.IKernel) {
await this.updateKernelInfo(kernel);
if (kernel.info) {
this.updateLanguageInfo(kernel.info.language_info);
}
}
private findSpec(displayName: string) {
let spec = this.getKernelSpecFromDisplayName(displayName);
if (spec) {
// Ensure that the kernel we try to switch to is a valid kernel; if not, use the default
@@ -563,24 +624,11 @@ export class NotebookModel extends Disposable implements INotebookModel {
if (kernelSpecs && kernelSpecs.length > 0 && kernelSpecs.findIndex(k => k.display_name === spec.display_name) < 0) {
spec = kernelSpecs.find(spec => spec.name === this.notebookManager.sessionManager.specs.defaultKernel);
}
} else {
}
else {
spec = notebookConstants.sqlKernelSpec;
}
if (this._activeClientSession && this._activeClientSession.isReady) {
return this._activeClientSession.changeKernel(spec, this._oldKernel)
.then((kernel) => {
this.updateKernelInfo(kernel);
kernel.ready.then(() => {
if (kernel.info) {
this.updateLanguageInfo(kernel.info.language_info);
}
}, err => undefined);
}).catch((err) => {
this.notifyError(localize('changeKernelFailed', 'Failed to change kernel: {0}', notebookUtils.getErrorMessage(err)));
// TODO should revert kernels dropdown
});
}
return Promise.resolve();
return spec;
}
public async changeContext(server: string, newConnection?: IConnectionProfile, hideErrorMessage?: boolean): Promise<void> {
@@ -613,7 +661,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
});
} catch (err) {
let msg = notebookUtils.getErrorMessage(err);
this.notifyError(localize('changeContextFailed', 'Changing context failed: {0}', msg));
this.notifyError(localize('changeContextFailed', "Changing context failed: {0}", msg));
}
}
@@ -631,17 +679,6 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
}
private loadKernelInfo(clientSession: IClientSession, displayName: string): void {
clientSession.onKernelChanging(async (e) => {
await this.loadActiveContexts(e);
});
clientSession.statusChanged(async (session) => {
this._kernelsChangedEmitter.fire(session.kernel);
});
this.doChangeKernel(displayName, false);
}
// Get default language if saved in notebook file
// Otherwise, default to python
private getDefaultLanguageInfo(notebook: nb.INotebookContents): nb.ILanguageInfo {
@@ -703,7 +740,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
private setErrorState(errMsg: string): void {
this._inErrorState = true;
let msg = localize('startSessionFailed', 'Could not start session: {0}', errMsg);
let msg = localize('startSessionFailed', "Could not start session: {0}", errMsg);
this.notifyError(msg);
}
@@ -730,7 +767,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
await this.shutdownActiveSession();
} catch (err) {
this.notifyError(localize('shutdownError', 'An error occurred when closing the notebook: {0}', err));
this.notifyError(localize('shutdownError', "An error occurred when closing the notebook: {0}", err));
}
}
@@ -740,7 +777,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
await this._activeClientSession.ready;
}
catch (err) {
this.notifyError(localize('shutdownClientSessionError', 'A client session error occurred when closing the notebook: {0}', err));
this.notifyError(localize('shutdownClientSessionError', "A client session error occurred when closing the notebook: {0}", err));
}
await this._activeClientSession.shutdown();
this.clearClientSessionListeners();
@@ -794,46 +831,39 @@ export class NotebookModel extends Disposable implements INotebookModel {
* Set _providerId and start session if it is new provider
* @param displayName Kernel dispay name
*/
private async setProviderIdAndStartSession(displayName: string): Promise<void> {
private async setProviderIdAndStartSession(displayName: string): Promise<boolean> {
if (displayName) {
if (this._activeClientSession && this._activeClientSession.isReady) {
this._oldKernel = this._activeClientSession.kernel;
let providerId = this.tryFindProviderForKernel(displayName);
}
let providerId = this.tryFindProviderForKernel(displayName);
if (providerId) {
if (providerId !== this._providerId) {
this._providerId = providerId;
this._onProviderIdChanged.fire(this._providerId);
if (providerId && providerId !== this._providerId) {
this._providerId = providerId;
this._onProviderIdChanged.fire(this._providerId);
await this.shutdownActiveSession();
try {
let manager = this.getNotebookManager(providerId);
if (manager) {
await this.startSession(manager, displayName);
} else {
throw new Error(localize('ProviderNoManager', "Can't find notebook manager for provider {0}", providerId));
}
}
catch (err) {
console.log(err);
}
} else {
console.log(`No provider found supporting the kernel: ${displayName}`);
}
await this.shutdownActiveSession();
let manager = this.getNotebookManager(providerId);
if (manager) {
await this.startSession(manager, displayName, false);
} else {
throw new Error(localize('ProviderNoManager', "Can't find notebook manager for provider {0}", providerId));
}
return true;
}
}
return false;
}
private tryFindProviderForKernel(displayName: string) {
private tryFindProviderForKernel(displayName: string, alwaysReturnId: boolean = false) {
if (!displayName) {
return undefined;
}
let standardKernel = this.getStandardKernelFromDisplayName(displayName);
if (standardKernel && this._oldKernel && this._oldKernel.name !== standardKernel.name) {
if (this._kernelDisplayNameToNotebookProviderIds.has(displayName)) {
return this._kernelDisplayNameToNotebookProviderIds.get(displayName);
if (standardKernel) {
let providerId = this._kernelDisplayNameToNotebookProviderIds.get(displayName);
if (alwaysReturnId || (!this._oldKernel || this._oldKernel.name !== standardKernel.name)) {
return providerId;
}
}
return undefined;
@@ -883,7 +913,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
};
}
onCellChange(cell: CellModel, change: NotebookChangeType): void {
onCellChange(cell: ICellModel, change: NotebookChangeType): void {
let changeInfo: NotebookContentChange = {
changeType: change,
cells: [cell]