Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,14 @@
"default": 10000,
"description": "%jupyter.configuration.jupyter.jupyterInterruptTimeout.description%"
},
"jupyter.kernels.portRange.startPort": {
"type": "number",
"default": 9000,
"minimum": 1024,
"maximum": 65535,
"description": "%jupyter.configuration.jupyter.kernels.portRange.startPort.description%",
"scope": "machine"
},
"jupyter.interactiveWindow.textEditor.executeSelection": {
"type": "boolean",
"default": false,
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
"jupyter.configuration.jupyter.notebookFileRoot.description": "Set the root directory for Jupyter Notebooks and the Interactive Window running locally. \n\n**Note:** This does not apply to Remote Jupyter Kernels.",
"jupyter.configuration.jupyter.useDefaultConfigForJupyter.description": "When running Jupyter locally, create a default empty Jupyter config",
"jupyter.configuration.jupyter.jupyterInterruptTimeout.description": "Amount of time (in ms) to wait for an interrupt before asking to restart the Jupyter kernel.",
"jupyter.configuration.jupyter.kernels.portRange.startPort.description": "Starting port number for Jupyter kernel port allocation. The extension will search for available ports starting from this number when launching local kernels and Jupyter servers. Valid range: 1024-65535.",
"jupyter.configuration.jupyter.sendSelectionToInteractiveWindow.description": "When pressing shift+enter, send selected code in a Python file to the Jupyter interactive window as opposed to the Python terminal.",
"jupyter.configuration.jupyter.normalizeSelectionForInteractiveWindow.description": "Selected text will be normalized before it is executed in the Interactive Window.",
"jupyter.configuration.jupyter.splitRunFileIntoCells.description": "A file run in the Interactive Window will be run in individual cells if it has them.",
Expand Down
28 changes: 24 additions & 4 deletions src/kernels/jupyter/launcher/jupyterServerStarter.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { JUPYTER_OUTPUT_CHANNEL } from '../../../platform/common/constants';
import { dispose } from '../../../platform/common/utils/lifecycle';
import { logger } from '../../../platform/logging';
import { IFileSystem, TemporaryDirectory } from '../../../platform/common/platform/types';
import { IDisposable, IOutputChannel, Resource } from '../../../platform/common/types';
import { IConfigurationService, IDisposable, IOutputChannel, Resource } from '../../../platform/common/types';
import { DataScience } from '../../../platform/common/utils/localize';
import { JupyterConnectError } from '../../../platform/errors/jupyterConnectError';
import { JupyterInstallError } from '../../../platform/errors/jupyterInstallError';
Expand Down Expand Up @@ -46,6 +46,19 @@ export class JupyterServerStarter implements IJupyterServerStarter {
@named(JUPYTER_OUTPUT_CHANNEL)
private readonly jupyterOutputChannel: IOutputChannel
) {}

private async getPortArguments(resource: Resource): Promise<string[]> {
const configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService);
const settings = configService.getSettings(resource);
const startPort = settings.kernelPortRangeStartPort;

// Only add port argument if it's different from the default (9000)
// This allows Jupyter to use its own port selection when using default
if (startPort && startPort !== 9000) {
return [`--port=${startPort}`];
}
return [];
}
public dispose() {
while (this.disposables.length > 0) {
const disposable = this.disposables.shift();
Expand Down Expand Up @@ -77,6 +90,7 @@ export class JupyterServerStarter implements IJupyterServerStarter {
tempDirPromise.then((dir) => this.disposables.push(dir)).catch(noop);
// Before starting the notebook process, make sure we generate a kernel spec
const args = await this.generateArguments(
resource,
useDefaultConfig,
customCommandLine,
tempDirPromise,
Expand Down Expand Up @@ -176,6 +190,7 @@ export class JupyterServerStarter implements IJupyterServerStarter {
}

private async generateDefaultArguments(
resource: Resource,
useDefaultConfig: boolean,
tempDirPromise: Promise<TemporaryDirectory>,
workingDirectory: string
Expand All @@ -190,15 +205,19 @@ export class JupyterServerStarter implements IJupyterServerStarter {
// Modify the data rate limit if starting locally. The default prevents large dataframes from being returned.
promisedArgs.push(Promise.resolve('--NotebookApp.iopub_data_rate_limit=10000000000.0'));

const [args, dockerArgs] = await Promise.all([Promise.all(promisedArgs), this.getDockerArguments()]);
const [args, dockerArgs, portArgs] = await Promise.all([
Promise.all(promisedArgs),
this.getDockerArguments(),
this.getPortArguments(resource)
]);

// Check for the debug environment variable being set. Setting this
// causes Jupyter to output a lot more information about what it's doing
// under the covers and can be used to investigate problems with Jupyter.
const debugArgs = process.env && process.env.VSCODE_JUPYTER_DEBUG_JUPYTER ? ['--debug'] : [];

// Use this temp file and config file to generate a list of args for our command
return [...args, ...dockerArgs, ...debugArgs];
return [...args, ...dockerArgs, ...portArgs, ...debugArgs];
}

private async generateCustomArguments(customCommandLine: string[]): Promise<string[]> {
Expand All @@ -217,13 +236,14 @@ export class JupyterServerStarter implements IJupyterServerStarter {
}

private async generateArguments(
resource: Resource,
useDefaultConfig: boolean,
customCommandLine: string[],
tempDirPromise: Promise<TemporaryDirectory>,
workingDirectory: string
): Promise<string[]> {
if (!customCommandLine || customCommandLine.length === 0) {
return this.generateDefaultArguments(useDefaultConfig, tempDirPromise, workingDirectory);
return this.generateDefaultArguments(resource, useDefaultConfig, tempDirPromise, workingDirectory);
}
return this.generateCustomArguments(customCommandLine);
}
Expand Down
28 changes: 24 additions & 4 deletions src/kernels/raw/launcher/kernelLauncher.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,18 @@ const PortFormatString = `kernelLauncherPortStart_{0}.tmp`;
// If the selected interpreter doesn't have a kernel, it will find a kernel on disk and use that.
@injectable()
export class KernelLauncher implements IKernelLauncher {
private static startPortPromise = KernelLauncher.computeStartPort();
private static startPortPromise: Promise<number> | undefined;
private static cachedStartPort: number | undefined;
private portChain: Promise<number[]> | undefined;

/**
* Reset the cached start port (for testing purposes)
* @internal
*/
public static resetStartPort(): void {
KernelLauncher.startPortPromise = undefined;
KernelLauncher.cachedStartPort = undefined;
}
constructor(
@inject(IProcessServiceFactory) private processExecutionFactory: IProcessServiceFactory,
@inject(IFileSystemNode) private readonly fs: IFileSystemNode,
Expand All @@ -64,10 +74,11 @@ export class KernelLauncher implements IKernelLauncher {
@inject(IPlatformService) private readonly platformService: IPlatformService
) {}

private static async computeStartPort(): Promise<number> {
private static async computeStartPort(configuredStartPort?: number): Promise<number> {
const defaultStartPort = configuredStartPort || 9_000;
if (isTestExecution()) {
// Since multiple instances of a test may be running, write our best guess to a shared file
let portStart = 9_000;
let portStart = defaultStartPort;
let result = 0;
while (result === 0 && portStart < 65_000) {
try {
Expand All @@ -86,7 +97,7 @@ export class KernelLauncher implements IKernelLauncher {

return result;
} else {
return 9_000;
return defaultStartPort;
}
}

Expand Down Expand Up @@ -200,7 +211,16 @@ export class KernelLauncher implements IKernelLauncher {
}

private async getConnectionPorts(): Promise<number[]> {
// Get the configured start port from settings
const settings = this.configService.getSettings(undefined);
const configuredStartPort = settings.kernelPortRangeStartPort;

// Have to wait for static port lookup (it handles case where two VS code instances are running)
// Re-initialize if the configured port has changed
if (!KernelLauncher.startPortPromise || KernelLauncher.cachedStartPort !== configuredStartPort) {
KernelLauncher.cachedStartPort = configuredStartPort;
KernelLauncher.startPortPromise = KernelLauncher.computeStartPort(configuredStartPort);
}
const startPort = await KernelLauncher.startPortPromise;

// Then get the next set starting at that point
Expand Down
66 changes: 65 additions & 1 deletion src/kernels/raw/launcher/kernelLauncher.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ suite('kernel Launcher', () => {
when(pythonExecutionFactory.createActivatedEnvironment(anything())).thenResolve(instance(pythonExecService));
when(pythonExecService.exec(anything(), anything())).thenResolve({ stdout: '' });
when(configService.getSettings(anything())).thenReturn({
jupyter: { logKernelOutputSeparately: false }
jupyter: { logKernelOutputSeparately: false },
kernelPortRangeStartPort: 9000
} as any);
kernelLauncher = new KernelLauncher(
instance(processExecutionFactory),
Expand Down Expand Up @@ -134,4 +135,67 @@ suite('kernel Launcher', () => {
assert.isTrue(portForwardingIgnored, `Kernel Port ${port} should not be forwarded`);
}
});
test('Verify custom start port is used from configuration', async () => {
// Reset the static port cache before this test
KernelLauncher.resetStartPort();

// Create a new launcher with custom port configuration
const customStartPort = 10000;
when(configService.getSettings(anything())).thenReturn({
jupyter: { logKernelOutputSeparately: false },
kernelPortRangeStartPort: customStartPort
} as any);

const customKernelLauncher = new KernelLauncher(
instance(processExecutionFactory),
instance(fs),
instance(extensionChecker),
instance(kernelEnvVarsService),
disposables,
instance(pythonExecutionFactory),
instance(configService),
instance(jupyterPaths),
instance(pythonKernelInterruptDaemon),
instance(platform)
);

const kernelSpec = PythonKernelConnectionMetadata.create({
id: '1',
interpreter: {
id: '2',
uri: Uri.file('python')
},
kernelSpec: {
argv: ['python', '-m', 'ipykernel_launcher', '-f', '{connection_file}'],
display_name: 'Python 3',
executable: 'python',
name: 'python3'
}
});
const cancellation = new CancellationTokenSource();
const launchStub = sinon.stub(KernelProcess.prototype, 'launch');
const exitedStub = sinon.stub(KernelProcess.prototype, 'exited');
disposables.push(new Disposable(() => launchStub.restore()));
disposables.push(new Disposable(() => exitedStub.restore()));
launchStub.resolves(undefined);
const exited = new EventEmitter<{
exitCode?: number | undefined;
reason?: string | undefined;
}>();
exitedStub.get(() => exited.event);

const oldPorts = new Set(UsedPorts);
await customKernelLauncher.launch(kernelSpec, 10_000, undefined, __dirname, cancellation.token);

// Verify that ports allocated are >= custom start port
let foundPortInRange = false;
for (const port of UsedPorts) {
if (!oldPorts.has(port)) {
// This is a newly allocated port
assert.isAtLeast(port, customStartPort, `Port ${port} should be >= ${customStartPort}`);
foundPortInRange = true;
}
}
assert.isTrue(foundPortInRange, 'At least one port should have been allocated in the custom range');
});
});
1 change: 1 addition & 0 deletions src/platform/common/configSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class JupyterSettings implements IWatchableJupyterSettings {
public experiments!: IExperiments;
public allowUnauthorizedRemoteConnection: boolean = false;
public jupyterInterruptTimeout: number = 10_000;
public kernelPortRangeStartPort: number = 9_000;
public jupyterLaunchTimeout: number = 60_000;
public jupyterLaunchRetries: number = 3;
public notebookFileRoot: string = '';
Expand Down
1 change: 1 addition & 0 deletions src/platform/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface IJupyterSettings {
readonly experiments: IExperiments;
readonly allowUnauthorizedRemoteConnection: boolean;
readonly jupyterInterruptTimeout: number;
readonly kernelPortRangeStartPort: number;
readonly jupyterLaunchTimeout: number;
readonly jupyterLaunchRetries: number;
readonly notebookFileRoot: string;
Expand Down