Skip to content

Commit e53651d

Browse files
authored
Prevent first Python command being lost (microsoft#22902)
Fixes: microsoft#22673 Fixes: microsoft#22545 Fixes: microsoft#22691 Making best effort to address issue where very first command sent to REPL via Terminal gets ignored, or gets pasted both in Terminal and in REPL. With the fix, we observe whether Python REPL is launched in Terminal via VS Code's `onDidWriteTerminalData` and send the command, or wait three seconds as a fallback mechanism. These two combined together will significantly reduce or resolve all-together the chance of very first command being swollen up or gets pasted twice in Terminal and REPL previously where it did not have context of whether Python REPL instance have started inside the Terminal or not.
1 parent a60fbd5 commit e53651d

File tree

5 files changed

+73
-37
lines changed

5 files changed

+73
-37
lines changed

src/client/terminals/codeExecution/djangoShellCodeExecution.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
import { inject, injectable } from 'inversify';
77
import * as path from 'path';
88
import { Disposable, Uri } from 'vscode';
9-
import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types';
9+
import {
10+
IApplicationShell,
11+
ICommandManager,
12+
IDocumentManager,
13+
IWorkspaceService,
14+
} from '../../common/application/types';
1015
import '../../common/extensions';
1116
import { IFileSystem, IPlatformService } from '../../common/platform/types';
1217
import { ITerminalServiceFactory } from '../../common/terminal/types';
@@ -28,6 +33,7 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi
2833
@inject(IFileSystem) fileSystem: IFileSystem,
2934
@inject(IDisposableRegistry) disposableRegistry: Disposable[],
3035
@inject(IInterpreterService) interpreterService: IInterpreterService,
36+
@inject(IApplicationShell) applicationShell: IApplicationShell,
3137
) {
3238
super(
3339
terminalServiceFactory,
@@ -37,6 +43,7 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi
3743
platformService,
3844
interpreterService,
3945
commandManager,
46+
applicationShell,
4047
);
4148
this.terminalTitle = 'Django Shell';
4249
disposableRegistry.push(new DjangoContextInitializer(documentManager, workspace, fileSystem, commandManager));

src/client/terminals/codeExecution/repl.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { inject, injectable } from 'inversify';
77
import { Disposable } from 'vscode';
8-
import { ICommandManager, IWorkspaceService } from '../../common/application/types';
8+
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types';
99
import { IPlatformService } from '../../common/platform/types';
1010
import { ITerminalServiceFactory } from '../../common/terminal/types';
1111
import { IConfigurationService, IDisposableRegistry } from '../../common/types';
@@ -22,6 +22,7 @@ export class ReplProvider extends TerminalCodeExecutionProvider {
2222
@inject(IPlatformService) platformService: IPlatformService,
2323
@inject(IInterpreterService) interpreterService: IInterpreterService,
2424
@inject(ICommandManager) commandManager: ICommandManager,
25+
@inject(IApplicationShell) applicationShell: IApplicationShell,
2526
) {
2627
super(
2728
terminalServiceFactory,
@@ -31,6 +32,7 @@ export class ReplProvider extends TerminalCodeExecutionProvider {
3132
platformService,
3233
interpreterService,
3334
commandManager,
35+
applicationShell,
3436
);
3537
this.terminalTitle = 'REPL';
3638
}

src/client/terminals/codeExecution/terminalCodeExecution.ts

+30-5
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
import { inject, injectable } from 'inversify';
77
import * as path from 'path';
88
import { Disposable, Uri } from 'vscode';
9-
import { ICommandManager, IWorkspaceService } from '../../common/application/types';
9+
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types';
1010
import '../../common/extensions';
1111
import { IPlatformService } from '../../common/platform/types';
1212
import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types';
13-
import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types';
13+
import { IConfigurationService, IDisposable, IDisposableRegistry, Resource } from '../../common/types';
1414
import { Diagnostics, Repl } from '../../common/utils/localize';
1515
import { showWarningMessage } from '../../common/vscodeApis/windowApis';
1616
import { IInterpreterService } from '../../interpreter/contracts';
@@ -30,6 +30,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {
3030
@inject(IPlatformService) protected readonly platformService: IPlatformService,
3131
@inject(IInterpreterService) protected readonly interpreterService: IInterpreterService,
3232
@inject(ICommandManager) protected readonly commandManager: ICommandManager,
33+
@inject(IApplicationShell) protected readonly applicationShell: IApplicationShell,
3334
) {}
3435

3536
public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) {
@@ -65,10 +66,34 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {
6566
}
6667
this.replActive = new Promise<boolean>(async (resolve) => {
6768
const replCommandArgs = await this.getExecutableInfo(resource);
69+
let listener: IDisposable;
70+
Promise.race([
71+
new Promise<boolean>((resolve) => setTimeout(() => resolve(true), 3000)),
72+
new Promise<boolean>((resolve) => {
73+
let count = 0;
74+
const terminalDataTimeout = setTimeout(() => {
75+
resolve(true); // Fall back for test case scenarios.
76+
}, 3000);
77+
// Watch TerminalData to see if REPL launched.
78+
listener = this.applicationShell.onDidWriteTerminalData((e) => {
79+
for (let i = 0; i < e.data.length; i++) {
80+
if (e.data[i] === '>') {
81+
count++;
82+
if (count === 3) {
83+
clearTimeout(terminalDataTimeout);
84+
resolve(true);
85+
}
86+
}
87+
}
88+
});
89+
}),
90+
]).then(() => {
91+
if (listener) {
92+
listener.dispose();
93+
}
94+
resolve(true);
95+
});
6896
terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args);
69-
70-
// Give python repl time to start before we start sending text.
71-
setTimeout(() => resolve(true), 1000);
7297
});
7398
this.disposables.push(
7499
terminalService.onDidCloseTerminal(() => {

src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import * as path from 'path';
66
import * as TypeMoq from 'typemoq';
77
import * as sinon from 'sinon';
88
import { Disposable, Uri, WorkspaceFolder } from 'vscode';
9-
import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types';
9+
import {
10+
IApplicationShell,
11+
ICommandManager,
12+
IDocumentManager,
13+
IWorkspaceService,
14+
} from '../../../client/common/application/types';
1015
import { IFileSystem, IPlatformService } from '../../../client/common/platform/types';
1116
import { createCondaEnv } from '../../../client/common/process/pythonEnvironment';
1217
import { createPythonProcessService } from '../../../client/common/process/pythonProcess';
@@ -32,12 +37,14 @@ suite('Terminal - Django Shell Code Execution', () => {
3237
let settings: TypeMoq.IMock<IPythonSettings>;
3338
let interpreterService: TypeMoq.IMock<IInterpreterService>;
3439
let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>;
40+
let applicationShell: TypeMoq.IMock<IApplicationShell>;
3541
let disposables: Disposable[] = [];
3642
setup(() => {
3743
const terminalFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>();
3844
terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>();
3945
terminalService = TypeMoq.Mock.ofType<ITerminalService>();
4046
const configService = TypeMoq.Mock.ofType<IConfigurationService>();
47+
applicationShell = TypeMoq.Mock.ofType<IApplicationShell>();
4148
workspace = TypeMoq.Mock.ofType<IWorkspaceService>();
4249
workspace
4350
.setup((c) => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
@@ -62,6 +69,7 @@ suite('Terminal - Django Shell Code Execution', () => {
6269
fileSystem.object,
6370
disposables,
6471
interpreterService.object,
72+
applicationShell.object,
6573
);
6674

6775
terminalFactory.setup((f) => f.getTerminalService(TypeMoq.It.isAny())).returns(() => terminalService.object);

src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts

+23-29
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import * as path from 'path';
66
import { SemVer } from 'semver';
77
import * as TypeMoq from 'typemoq';
88
import { Disposable, Uri, WorkspaceFolder } from 'vscode';
9-
import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types';
9+
import {
10+
IApplicationShell,
11+
ICommandManager,
12+
IDocumentManager,
13+
IWorkspaceService,
14+
} from '../../../client/common/application/types';
1015
import { IFileSystem, IPlatformService } from '../../../client/common/platform/types';
1116
import { createCondaEnv } from '../../../client/common/process/pythonEnvironment';
1217
import { createPythonProcessService } from '../../../client/common/process/pythonProcess';
@@ -47,6 +52,7 @@ suite('Terminal - Code Execution', () => {
4752
let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>;
4853
let interpreterService: TypeMoq.IMock<IInterpreterService>;
4954
let isDjangoRepl: boolean;
55+
let applicationShell: TypeMoq.IMock<IApplicationShell>;
5056

5157
teardown(() => {
5258
disposables.forEach((disposable) => {
@@ -71,6 +77,7 @@ suite('Terminal - Code Execution', () => {
7177
fileSystem = TypeMoq.Mock.ofType<IFileSystem>();
7278
pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>();
7379
interpreterService = TypeMoq.Mock.ofType<IInterpreterService>();
80+
applicationShell = TypeMoq.Mock.ofType<IApplicationShell>();
7481
settings = TypeMoq.Mock.ofType<IPythonSettings>();
7582
settings.setup((s) => s.terminal).returns(() => terminalSettings.object);
7683
configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object);
@@ -85,6 +92,7 @@ suite('Terminal - Code Execution', () => {
8592
platform.object,
8693
interpreterService.object,
8794
commandManager.object,
95+
applicationShell.object,
8896
);
8997
break;
9098
}
@@ -97,6 +105,7 @@ suite('Terminal - Code Execution', () => {
97105
platform.object,
98106
interpreterService.object,
99107
commandManager.object,
108+
applicationShell.object,
100109
);
101110
expectedTerminalTitle = 'REPL';
102111
break;
@@ -120,6 +129,7 @@ suite('Terminal - Code Execution', () => {
120129
fileSystem.object,
121130
disposables,
122131
interpreterService.object,
132+
applicationShell.object,
123133
);
124134
expectedTerminalTitle = 'Django Shell';
125135
break;
@@ -590,7 +600,7 @@ suite('Terminal - Code Execution', () => {
590600
);
591601
});
592602

593-
test('Ensure repl is re-initialized when terminal is closed', async () => {
603+
test('Ensure REPL launches after reducing risk of command being ignored or duplicated', async () => {
594604
const pythonPath = 'usr/bin/python1234';
595605
const terminalArgs = ['-a', 'b', 'c'];
596606
platform.setup((p) => p.isWindows).returns(() => false);
@@ -599,43 +609,27 @@ suite('Terminal - Code Execution', () => {
599609
.returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment));
600610
terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs);
601611

602-
let closeTerminalCallback: undefined | (() => void);
603-
terminalService
604-
.setup((t) => t.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
605-
.returns((callback) => {
606-
closeTerminalCallback = callback;
607-
return {
608-
dispose: noop,
609-
};
610-
});
611-
612612
await executor.execute('cmd1');
613613
await executor.execute('cmd2');
614614
await executor.execute('cmd3');
615615

616-
const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs;
617-
618-
expect(closeTerminalCallback).not.to.be.an('undefined', 'Callback not initialized');
619-
terminalService.verify(
620-
async (t) =>
621-
t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)),
622-
TypeMoq.Times.once(),
616+
// Now check if sendCommand from the initializeRepl is called atLeastOnce.
617+
// This is due to newly added Promise race and fallback to lower risk of swollen first command.
618+
applicationShell.verify(
619+
async (t) => t.onDidWriteTerminalData(TypeMoq.It.isAny(), TypeMoq.It.isAny()),
620+
TypeMoq.Times.atLeastOnce(),
623621
);
624622

625-
closeTerminalCallback!.call(terminalService.object);
626623
await executor.execute('cmd4');
627-
terminalService.verify(
628-
async (t) =>
629-
t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)),
630-
TypeMoq.Times.exactly(2),
624+
applicationShell.verify(
625+
async (t) => t.onDidWriteTerminalData(TypeMoq.It.isAny(), TypeMoq.It.isAny()),
626+
TypeMoq.Times.atLeastOnce(),
631627
);
632628

633-
closeTerminalCallback!.call(terminalService.object);
634629
await executor.execute('cmd5');
635-
terminalService.verify(
636-
async (t) =>
637-
t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)),
638-
TypeMoq.Times.exactly(3),
630+
applicationShell.verify(
631+
async (t) => t.onDidWriteTerminalData(TypeMoq.It.isAny(), TypeMoq.It.isAny()),
632+
TypeMoq.Times.atLeastOnce(),
639633
);
640634
});
641635

0 commit comments

Comments
 (0)