Skip to content

Commit 56ac9fe

Browse files
authored
Fix problems with liveshare (microsoft#4950)
- Host shutdown disconnects from jupyter - Host direct enter should send data to the guest even if guest doesn't have history window open For #4949 <!-- If an item below does not apply to you, then go ahead and check it off as "done" and strikethrough the text, e.g.: - [x] ~Has unit tests & system/integration tests~ --> - [x] Pull request represents a single change (i.e. not fixing disparate/unrelated things in a single PR) - [x] Title summarizes what is changing - [x] Has a [news entry](https://github.com/Microsoft/vscode-python/tree/master/news) file (remember to thank yourself!) - [ ] Has sufficient logging. - [ ] Has telemetry for enhancements. - [x] Unit tests & system/integration tests are added/updated - [ ] [Test plan](https://github.com/Microsoft/vscode-python/blob/master/.github/test_plan.md) is updated as appropriate - [ ] [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/master/package-lock.json) has been regenerated by running `npm install` (if dependencies have changed) - [ ] The wiki is updated with any design decisions/details.
1 parent 921fc0b commit 56ac9fe

File tree

8 files changed

+62
-6
lines changed

8 files changed

+62
-6
lines changed

news/2 Fixes/4949.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Shutting down a session should not cause the host to stop working.

src/client/common/application/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,4 +892,5 @@ export interface ILiveShareTestingApi extends ILiveShareApi {
892892
isSessionStarted: boolean;
893893
forceRole(role: vsls.Role): void;
894894
startSession(): Promise<void>;
895+
stopSession(): Promise<void>;
895896
}

src/client/datascience/history/history.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,13 @@ export class History implements IHistory {
489489
private submitNewCell(info: ISubmitNewCell) {
490490
// If there's any payload, it has the code and the id
491491
if (info && info.code && info.id) {
492+
// Send to ourselves.
492493
this.submitCode(info.code, Identifiers.EmptyFileName, 0, info.id, undefined).ignoreErrors();
494+
495+
// Activate the other side, and send as if came from a file
496+
this.historyProvider.getOrCreateActive().then(_v => {
497+
this.shareMessage(HistoryMessages.RemoteAddCode, {code: info.code, file: Identifiers.EmptyFileName, line: 0, id: info.id, originator: this.id});
498+
}).ignoreErrors();
493499
}
494500
}
495501

@@ -667,7 +673,7 @@ export class History implements IHistory {
667673
if (this.jupyterServer) {
668674
const server = this.jupyterServer;
669675
this.jupyterServer = undefined;
670-
server.dispose().ignoreErrors(); // Don't care what happens as we're disconnected.
676+
server.shutdown().ignoreErrors(); // Don't care what happens as we're disconnected.
671677
}
672678
}
673679
} catch {
@@ -839,6 +845,7 @@ export class History implements IHistory {
839845
}
840846

841847
private shareMessage<M extends IHistoryMapping, T extends keyof M>(type: T, payload?: M[T]) {
848+
// Send our remote message.
842849
this.messageListener.onMessage(type.toString(), payload);
843850
}
844851

src/client/datascience/history/historyTypes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export namespace HistoryMessages {
3535
// These are the messages that will mirror'd to guest/hosts in
3636
// a live share session
3737
export const HistoryRemoteMessages : string[] = [
38-
HistoryMessages.SubmitNewCell,
3938
HistoryMessages.AddedSysInfo,
4039
HistoryMessages.RemoteAddCode
4140
];

src/client/datascience/jupyter/jupyterExecution.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { CancellationToken, Event, EventEmitter } from 'vscode';
1111

1212
import { ILiveShareApi, IWorkspaceService } from '../../common/application/types';
1313
import { Cancellation, CancellationError } from '../../common/cancellation';
14+
import { traceInfo, traceWarning } from '../../common/logger';
1415
import { IFileSystem, TemporaryDirectory } from '../../common/platform/types';
1516
import { IProcessService, IProcessServiceFactory, IPythonExecutionFactory, SpawnOptions } from '../../common/process/types';
1617
import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, ILogger } from '../../common/types';
@@ -34,7 +35,6 @@ import {
3435
} from '../types';
3536
import { JupyterConnection, JupyterServerInfo } from './jupyterConnection';
3637
import { JupyterKernelSpec } from './jupyterKernelSpec';
37-
import { traceInfo, traceWarning } from '../../common/logger';
3838

3939
enum ModuleExistsResult {
4040
NotFound,
@@ -698,6 +698,7 @@ export class JupyterExecutionBase implements IJupyterExecution {
698698
// - Look for module in current interpreter, if found create something with python path and -m module
699699
// - Look in other interpreters, if found create something with python path and -m module
700700
// - Look on path for jupyter, if found create something with jupyter path and args
701+
// tslint:disable:cyclomatic-complexity
701702
private findBestCommand = async (command: string, cancelToken?: CancellationToken): Promise<IJupyterCommand | undefined> => {
702703
// See if we already have this command in list
703704
if (!this.commands.hasOwnProperty(command)) {
@@ -714,7 +715,7 @@ export class JupyterExecutionBase implements IJupyterExecution {
714715
const all = await this.interpreterService.getInterpreters();
715716

716717
if (!all || all.length === 0) {
717-
traceWarning(`No interpreters found. Jupyter cannot run.`);
718+
traceWarning('No interpreters found. Jupyter cannot run.');
718719
}
719720

720721
const promises = all.filter(i => i !== current).map(i => this.findInterpreterCommand(command, i, cancelToken));

src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,12 @@ export class HostJupyterExecution
109109
public async onDetach(api: vsls.LiveShare | null): Promise<void> {
110110
await super.onDetach(api);
111111

112-
// clear our cached servers. We need to reconnect
113-
await this.serverCache.dispose();
112+
// clear our cached servers if our role is no longer host or none
113+
const newRole = api === null || (api.session && api.session.role !== vsls.Role.Guest) ?
114+
vsls.Role.Host : vsls.Role.Guest;
115+
if (newRole !== vsls.Role.Host) {
116+
await this.serverCache.dispose();
117+
}
114118
}
115119

116120
public getServer(options?: INotebookServerOptions): Promise<INotebookServer | undefined> {

src/test/datascience/liveshare.functional.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,12 @@ suite('LiveShare tests', () => {
259259
return api.startSession();
260260
}
261261

262+
function stopSession(role: vsls.Role): Promise<void> {
263+
const container = role === vsls.Role.Host ? hostContainer : guestContainer;
264+
const api = container.ioc!.get<ILiveShareApi>(ILiveShareApi) as ILiveShareTestingApi;
265+
return api.stopSession();
266+
}
267+
262268
test('Host alone', async () => {
263269
// Should only need mock data in host
264270
addMockData(hostContainer.ioc!, 'a=1\na', 1);
@@ -290,6 +296,26 @@ suite('LiveShare tests', () => {
290296
verifyHtmlOnCell(guestContainer.wrapper!, '<span>1</span>', CellPosition.Last);
291297
});
292298

299+
test('Host Shutdown and Run', async () => {
300+
// Should only need mock data in host
301+
addMockData(hostContainer.ioc!, 'a=1\na', 1);
302+
303+
// Create the host history and then the guest history
304+
await getOrCreateHistory(vsls.Role.Host);
305+
await startSession(vsls.Role.Host);
306+
307+
// Send code through the host
308+
let wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na');
309+
verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last);
310+
311+
// Stop the session
312+
await stopSession(vsls.Role.Host);
313+
314+
// Send code again. It should still work.
315+
wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na');
316+
verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last);
317+
});
318+
293319
test('Host startup and guest restart', async () => {
294320
// Should only need mock data in host
295321
addMockData(hostContainer.ioc!, 'a=1\na', 1);

src/test/datascience/mockLiveShare.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,14 @@ class MockLiveShare implements vsls.LiveShare, vsls.Session, vsls.Peer {
195195
return Promise.resolve();
196196
}
197197

198+
public async stop(): Promise<void> {
199+
this._visibleRole = vsls.Role.None;
200+
201+
await this.changeSessionEmitter.fire({ session: this });
202+
203+
return Promise.resolve();
204+
}
205+
198206
public getContacts(_emails: string[]): Promise<vsls.ContactsCollection> {
199207
throw new Error('Method not implemented.');
200208
}
@@ -337,6 +345,15 @@ export class MockLiveShareApi implements ILiveShareTestingApi {
337345
}
338346
}
339347

348+
public async stopSession(): Promise<void> {
349+
if (this.currentApi) {
350+
await this.currentApi.stop();
351+
this.sessionStarted = false;
352+
} else {
353+
throw Error('Cannot start session without a role.');
354+
}
355+
}
356+
340357
public get isSessionStarted(): boolean {
341358
return this.sessionStarted;
342359
}

0 commit comments

Comments
 (0)