Skip to content

Commit 8fc6694

Browse files
authored
Fix: Do not activate on pseudoterminals (#1509)
This pull request introduces a new utility function to consistently determine when terminal activation should be skipped, and refactors related logic throughout the codebase to use this function. The main goal is to improve reliability and maintainability by centralizing the logic for skipping activation of certain terminals (such as hidden or PTY-based extension terminals). **Terminal activation skip logic improvements:** * Added a new function `shouldSkipTerminalActivation` in `src/features/terminal/utils.ts` to centralize the logic for skipping activation for terminals that are hidden from the user or are PTY-based extension terminals. * Refactored `TerminalManagerImpl` in `src/features/terminal/terminalManager.ts` to use `shouldSkipTerminalActivation` instead of directly checking terminal options in multiple places, ensuring consistent behavior when opening or activating terminals. [[1]](diffhunk://#diff-1abe443a8980fdf92f537c69abb866fdb65ca6a00be94942edc99eca49d60d75R37) [[2]](diffhunk://#diff-1abe443a8980fdf92f537c69abb866fdb65ca6a00be94942edc99eca49d60d75L97-R98) [[3]](diffhunk://#diff-1abe443a8980fdf92f537c69abb866fdb65ca6a00be94942edc99eca49d60d75L422-R427) [[4]](diffhunk://#diff-1abe443a8980fdf92f537c69abb866fdb65ca6a00be94942edc99eca49d60d75L434-R441) * Updated `TerminalActivationImpl` in `src/features/terminal/terminalActivationState.ts` to use the new skip logic, preventing activation for terminals that meet the skip criteria. [[1]](diffhunk://#diff-4c638af1ef4f02d8c8038fcc38ca4a135ca30aaa3f2f51d840a2f5f372ffabbdL14-R14) [[2]](diffhunk://#diff-4c638af1ef4f02d8c8038fcc38ca4a135ca30aaa3f2f51d840a2f5f372ffabbdR89-R93) **Other changes:** * Cleaned up imports in `src/features/terminal/terminalManager.ts` and `src/features/terminal/utils.ts` to remove unused `TerminalOptions` and add `ExtensionTerminalOptions` where needed. [[1]](diffhunk://#diff-1abe443a8980fdf92f537c69abb866fdb65ca6a00be94942edc99eca49d60d75L3-R3) [[2]](diffhunk://#diff-da71e7d6ace45ba8e09cc2f84096dcd5149997f178e5c0dafd84bbcd4627c822L2-R2) Fixes #1482
1 parent 5ea9d15 commit 8fc6694

5 files changed

Lines changed: 223 additions & 12 deletions

File tree

src/features/terminal/terminalActivationState.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { PythonEnvironment } from '../../api';
1111
import { traceError, traceInfo, traceVerbose } from '../../common/logging';
1212
import { onDidEndTerminalShellExecution, onDidStartTerminalShellExecution } from '../../common/window.apis';
1313
import { getActivationCommand, getDeactivationCommand } from '../common/activation';
14-
import { getShellIntegrationTimeout, isTaskTerminal } from './utils';
14+
import { getShellIntegrationTimeout, isTaskTerminal, shouldSkipTerminalActivation } from './utils';
1515

1616
export interface DidChangeTerminalActivationStateEvent {
1717
terminal: Terminal;
@@ -86,6 +86,11 @@ export class TerminalActivationImpl implements TerminalActivationInternal {
8686
}
8787

8888
async activate(terminal: Terminal, environment: PythonEnvironment): Promise<void> {
89+
if (shouldSkipTerminalActivation(terminal)) {
90+
traceVerbose('Skipping activation for this terminal');
91+
return;
92+
}
93+
8994
if (isTaskTerminal(terminal)) {
9095
traceVerbose('Cannot activate environment in a task terminal');
9196
return;

src/features/terminal/terminalManager.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as fsapi from 'fs-extra';
22
import * as path from 'path';
3-
import { Disposable, EventEmitter, ProgressLocation, Terminal, TerminalOptions, Uri } from 'vscode';
3+
import { Disposable, EventEmitter, ProgressLocation, Terminal, Uri } from 'vscode';
44
import { PythonEnvironment, PythonEnvironmentApi, PythonProject, PythonTerminalCreateOptions } from '../../api';
55
import { ActivationStrings } from '../../common/localize';
66
import { traceInfo, traceVerbose } from '../../common/logging';
@@ -34,6 +34,7 @@ import {
3434
getAutoActivationType,
3535
getEnvironmentForTerminal,
3636
shouldActivateInCurrentTerminal,
37+
shouldSkipTerminalActivation,
3738
waitForShellIntegration,
3839
} from './utils';
3940

@@ -94,7 +95,7 @@ export class TerminalManagerImpl implements TerminalManager {
9495
this.onTerminalClosedEmitter.fire(t);
9596
}),
9697
this.onTerminalOpened(async (t) => {
97-
if (this.skipActivationOnOpen.has(t) || (t.creationOptions as TerminalOptions)?.hideFromUser) {
98+
if (this.skipActivationOnOpen.has(t) || shouldSkipTerminalActivation(t)) {
9899
return;
99100
}
100101
let env = this.ta.getEnvironment(t);
@@ -419,7 +420,11 @@ export class TerminalManagerImpl implements TerminalManager {
419420

420421
if (actType === ACT_TYPE_COMMAND) {
421422
if (!skipPreExistingTerminals) {
422-
await Promise.all(terminals().map(async (t) => this.activateUsingCommand(api, t)));
423+
await Promise.all(
424+
terminals()
425+
.filter((t) => !shouldSkipTerminalActivation(t))
426+
.map(async (t) => this.activateUsingCommand(api, t)),
427+
);
423428
}
424429
} else if (actType === ACT_TYPE_SHELL) {
425430
const shells = new Set(
@@ -431,12 +436,14 @@ export class TerminalManagerImpl implements TerminalManager {
431436
await this.handleSetupCheck(shells);
432437
if (!skipPreExistingTerminals) {
433438
await Promise.all(
434-
terminals().map(async (t) => {
435-
// If the shell is not set up, we activate using command fallback.
436-
if (this.shellSetup.get(identifyTerminalShell(t)) === false) {
437-
await this.activateUsingCommand(api, t);
438-
}
439-
}),
439+
terminals()
440+
.filter((t) => !shouldSkipTerminalActivation(t))
441+
.map(async (t) => {
442+
// If the shell is not set up, we activate using command fallback.
443+
if (this.shellSetup.get(identifyTerminalShell(t)) === false) {
444+
await this.activateUsingCommand(api, t);
445+
}
446+
}),
440447
);
441448
}
442449
}

src/features/terminal/utils.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as path from 'path';
2-
import { Disposable, env, tasks, Terminal, TerminalOptions, Uri } from 'vscode';
2+
import { Disposable, env, ExtensionTerminalOptions, tasks, Terminal, TerminalOptions, Uri } from 'vscode';
33
import { PythonEnvironment, PythonProject, PythonProjectEnvironmentApi, PythonProjectGetterApi } from '../../api';
44
import { traceVerbose } from '../../common/logging';
55
import { timeout } from '../../common/utils/asyncUtils';
@@ -385,3 +385,16 @@ export function removeAnsiEscapeCodes(str: string): string {
385385

386386
return str;
387387
}
388+
389+
/**
390+
* Determines if a terminal should be skipped for environment activation based on its creation options.
391+
* Terminals that are hidden from the user or are PTY-based extension terminals are skipped.
392+
* @param terminal The terminal to check.
393+
* @returns `true` if the terminal should be skipped; `false` otherwise.
394+
*/
395+
export function shouldSkipTerminalActivation(terminal: Terminal): boolean {
396+
return (
397+
!!(terminal.creationOptions as TerminalOptions)?.hideFromUser ||
398+
!!(terminal.creationOptions as ExtensionTerminalOptions)?.pty
399+
);
400+
}

src/test/features/terminal/terminalManager.unit.test.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
Disposable,
1111
Event,
1212
EventEmitter,
13+
ExtensionTerminalOptions,
1314
Progress,
15+
Pseudoterminal,
1416
Terminal,
1517
TerminalOptions,
1618
Uri,
@@ -617,3 +619,147 @@ suite('TerminalManager - initialize() with activateEnvInCurrentTerminal', () =>
617619
);
618620
});
619621
});
622+
623+
suite('TerminalManager - initialize() skips pseudoterminals', () => {
624+
let terminalActivation: TestTerminalActivation;
625+
let terminalManager: TerminalManagerImpl;
626+
let mockGetAutoActivationType: sinon.SinonStub;
627+
let mockShouldActivateInCurrentTerminal: sinon.SinonStub;
628+
let mockTerminals: sinon.SinonStub;
629+
let mockGetEnvironmentForTerminal: sinon.SinonStub;
630+
631+
const createMockPty = (): Pseudoterminal => {
632+
const writeEmitter = new EventEmitter<string>();
633+
return { onDidWrite: writeEmitter.event, open: () => {}, close: () => {} };
634+
};
635+
636+
const createMockExtensionTerminalOptions = (name: string): ExtensionTerminalOptions => ({
637+
name,
638+
pty: createMockPty(),
639+
});
640+
641+
const createMockTerminal = (name: string, options?: TerminalOptions | ExtensionTerminalOptions): Terminal =>
642+
({
643+
name,
644+
creationOptions: options ?? ({} as TerminalOptions),
645+
shellIntegration: undefined,
646+
show: sinon.stub(),
647+
sendText: sinon.stub(),
648+
}) as unknown as Terminal;
649+
650+
const createMockEnvironment = (): PythonEnvironment => ({
651+
envId: { id: 'test-env-id', managerId: 'test-manager' },
652+
name: 'Test Environment',
653+
displayName: 'Test Environment',
654+
shortDisplayName: 'TestEnv',
655+
displayPath: '/path/to/env',
656+
version: '3.9.0',
657+
environmentPath: Uri.file('/path/to/python'),
658+
sysPrefix: '/path/to/env',
659+
execInfo: {
660+
run: { executable: '/path/to/python' },
661+
activation: [{ executable: '/path/to/activate' }],
662+
},
663+
});
664+
665+
setup(() => {
666+
terminalActivation = new TestTerminalActivation();
667+
668+
mockGetAutoActivationType = sinon.stub(terminalUtils, 'getAutoActivationType');
669+
mockShouldActivateInCurrentTerminal = sinon.stub(terminalUtils, 'shouldActivateInCurrentTerminal');
670+
mockGetEnvironmentForTerminal = sinon.stub(terminalUtils, 'getEnvironmentForTerminal');
671+
sinon.stub(terminalUtils, 'waitForShellIntegration').resolves(false);
672+
sinon.stub(activationUtils, 'isActivatableEnvironment').returns(true);
673+
sinon.stub(shellDetector, 'identifyTerminalShell').returns('bash');
674+
675+
sinon.stub(windowApis, 'createTerminal').callsFake(() => createMockTerminal('new'));
676+
sinon.stub(windowApis, 'onDidOpenTerminal').returns(new Disposable(() => {}));
677+
sinon.stub(windowApis, 'onDidCloseTerminal').returns(new Disposable(() => {}));
678+
sinon.stub(windowApis, 'onDidChangeWindowState').returns(new Disposable(() => {}));
679+
sinon.stub(windowApis, 'activeTerminal').returns(undefined);
680+
681+
mockTerminals = sinon.stub(windowApis, 'terminals');
682+
683+
sinon.stub(windowApis, 'withProgress').callsFake(async (_options, task) => {
684+
const mockProgress = { report: () => {} };
685+
const mockToken = {
686+
isCancellationRequested: false,
687+
onCancellationRequested: () => new Disposable(() => {}),
688+
};
689+
return task(mockProgress as never, mockToken as never);
690+
});
691+
sinon.stub(workspaceApis, 'onDidChangeConfiguration').returns(new Disposable(() => {}));
692+
});
693+
694+
teardown(() => {
695+
sinon.restore();
696+
terminalActivation.dispose();
697+
});
698+
699+
test('initialize skips pseudoterminals and activates regular terminals (ACT_TYPE_COMMAND)', async () => {
700+
const regularTerminal = createMockTerminal('regular');
701+
const pseudoTerminal = createMockTerminal('pseudo', createMockExtensionTerminalOptions('pseudo'));
702+
const env = createMockEnvironment();
703+
704+
mockGetAutoActivationType.returns(terminalUtils.ACT_TYPE_COMMAND);
705+
mockShouldActivateInCurrentTerminal.returns(true);
706+
mockTerminals.returns([regularTerminal, pseudoTerminal]);
707+
mockGetEnvironmentForTerminal.resolves(env);
708+
709+
terminalManager = new TerminalManagerImpl(terminalActivation, [], []);
710+
await terminalManager.initialize({} as never);
711+
712+
assert.strictEqual(
713+
terminalActivation.activateCalls,
714+
1,
715+
'Should only activate the regular terminal, not the pseudoterminal',
716+
);
717+
});
718+
719+
test('initialize skips pseudoterminals in shell startup fallback path (ACT_TYPE_SHELL)', async () => {
720+
const regularTerminal = createMockTerminal('regular');
721+
const pseudoTerminal = createMockTerminal('pseudo', createMockExtensionTerminalOptions('pseudo'));
722+
const env = createMockEnvironment();
723+
724+
const mockShellProvider: ShellStartupScriptProvider = {
725+
name: 'bash-test',
726+
shellType: 'bash',
727+
isSetup: sinon.stub().resolves(ShellSetupState.NotSetup),
728+
setupScripts: sinon.stub().resolves(ShellScriptEditState.NotEdited),
729+
teardownScripts: sinon.stub().resolves(ShellScriptEditState.NotEdited),
730+
clearCache: sinon.stub().resolves(),
731+
};
732+
sinon.stub(shellUtils, 'getShellIntegrationEnabledCache').resolves(false);
733+
sinon.stub(shellUtils, 'shouldUseProfileActivation').returns(false);
734+
735+
mockGetAutoActivationType.returns(terminalUtils.ACT_TYPE_SHELL);
736+
mockShouldActivateInCurrentTerminal.returns(true);
737+
mockTerminals.returns([regularTerminal, pseudoTerminal]);
738+
mockGetEnvironmentForTerminal.resolves(env);
739+
740+
terminalManager = new TerminalManagerImpl(terminalActivation, [], [mockShellProvider]);
741+
await terminalManager.initialize({} as never);
742+
743+
assert.strictEqual(
744+
terminalActivation.activateCalls,
745+
1,
746+
'Should only activate the regular terminal via command fallback, not the pseudoterminal',
747+
);
748+
});
749+
750+
test('initialize skips all terminals when only pseudoterminals exist (ACT_TYPE_COMMAND)', async () => {
751+
const pseudo1 = createMockTerminal('pseudo1', createMockExtensionTerminalOptions('pseudo1'));
752+
const pseudo2 = createMockTerminal('pseudo2', createMockExtensionTerminalOptions('pseudo2'));
753+
const env = createMockEnvironment();
754+
755+
mockGetAutoActivationType.returns(terminalUtils.ACT_TYPE_COMMAND);
756+
mockShouldActivateInCurrentTerminal.returns(true);
757+
mockTerminals.returns([pseudo1, pseudo2]);
758+
mockGetEnvironmentForTerminal.resolves(env);
759+
760+
terminalManager = new TerminalManagerImpl(terminalActivation, [], []);
761+
await terminalManager.initialize({} as never);
762+
763+
assert.strictEqual(terminalActivation.activateCalls, 0, 'Should not activate any pseudoterminals');
764+
});
765+
});

src/test/features/terminal/utils.unit.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as assert from 'assert';
22
import * as sinon from 'sinon';
3-
import { Terminal } from 'vscode';
3+
import { ExtensionTerminalOptions, Terminal, TerminalOptions } from 'vscode';
44
import * as windowApis from '../../../common/window.apis';
55
import * as workspaceApis from '../../../common/workspace.apis';
66
import * as shellDetector from '../../../features/common/shellDetector';
@@ -11,6 +11,7 @@ import {
1111
AutoActivationType,
1212
getAutoActivationType,
1313
shouldActivateInCurrentTerminal,
14+
shouldSkipTerminalActivation,
1415
waitForShellIntegration,
1516
} from '../../../features/terminal/utils';
1617

@@ -550,6 +551,45 @@ suite('Terminal Utils - shouldActivateInCurrentTerminal', () => {
550551
});
551552
});
552553

554+
suite('Terminal Utils - shouldSkipTerminalActivation', () => {
555+
test('should return false for a regular terminal with no special options', () => {
556+
const terminal = { creationOptions: {} as TerminalOptions } as Terminal;
557+
assert.strictEqual(shouldSkipTerminalActivation(terminal), false);
558+
});
559+
560+
test('should return true when hideFromUser is true', () => {
561+
const terminal = { creationOptions: { hideFromUser: true } as TerminalOptions } as Terminal;
562+
assert.strictEqual(shouldSkipTerminalActivation(terminal), true);
563+
});
564+
565+
test('should return false when hideFromUser is false', () => {
566+
const terminal = { creationOptions: { hideFromUser: false } as TerminalOptions } as Terminal;
567+
assert.strictEqual(shouldSkipTerminalActivation(terminal), false);
568+
});
569+
570+
test('should return true for a pseudoterminal (pty-based extension terminal)', () => {
571+
const terminal = {
572+
creationOptions: {
573+
name: 'pseudo',
574+
pty: { open: () => {}, close: () => {} },
575+
} as unknown as ExtensionTerminalOptions,
576+
} as Terminal;
577+
assert.strictEqual(shouldSkipTerminalActivation(terminal), true);
578+
});
579+
580+
test('should return false when pty is undefined', () => {
581+
const terminal = { creationOptions: {} as ExtensionTerminalOptions } as Terminal;
582+
assert.strictEqual(shouldSkipTerminalActivation(terminal), false);
583+
});
584+
585+
test('should return true when both hideFromUser and pty are set', () => {
586+
const terminal = {
587+
creationOptions: { hideFromUser: true, pty: { open: () => {}, close: () => {} } },
588+
} as unknown as Terminal;
589+
assert.strictEqual(shouldSkipTerminalActivation(terminal), true);
590+
});
591+
});
592+
553593
suite('Terminal Utils - waitForShellIntegration', () => {
554594
let mockGetConfiguration: sinon.SinonStub;
555595
let identifyTerminalShellStub: sinon.SinonStub;

0 commit comments

Comments
 (0)