Skip to content

Commit 96ba831

Browse files
committed
Merged PR posit-dev/positron-python#11: Modify language runtime registration to fit to Jupyter Adapter lifecycle
Merge pull request #11 from posit-dev/follow-jupyter-adapter-lifecycle Modify language runtime registration to fit to Jupyter Adapter lifecycle -------------------- Commit message for posit-dev/positron-python@4cadc67: Enable log_file argument for python language runtimes -------------------- Commit message for posit-dev/positron-python@e0c21f9: Moving unlikely error to verbose tracing -------------------- Commit message for posit-dev/positron-python@810ef28: Modify language runtime registration to fit to Jupyter Adapter lifecycle Given the Jupyter Adapter starts an LSP client _before_ it has finished starting the kernel server, we rely on our TCP client retry logic to handle the delayed sequencing. For now, we'll only register one Python runtime, but we'll need to add back logic that will look for multiple runtimes, and prompt a user to ask if they want to install ipykernel if not present in their environment. Authored-by: Pete Farland <[email protected]> Signed-off-by: Pete Farland <[email protected]>
1 parent 909753c commit 96ba831

File tree

4 files changed

+114
-54
lines changed

4 files changed

+114
-54
lines changed

extensions/positron-python/pythonFiles/ipykernel_jedi.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,15 @@ def start_jedi():
3939
default=2087,
4040
)
4141
parser.add_argument(
42-
"--log-file",
42+
"--logfile",
4343
help="redirect logs to file specified",
4444
type=str,
4545
)
46+
parser.add_argument(
47+
"-f",
48+
help="location of the IPyKernel configuration file",
49+
type=str,
50+
)
4651
parser.add_argument(
4752
"-v",
4853
"--verbose",
@@ -57,9 +62,9 @@ def start_jedi():
5762
logging.DEBUG,
5863
)
5964

60-
if args.log_file:
65+
if args.logfile:
6166
logging.basicConfig(
62-
filename=args.log_file,
67+
filename=args.logfile,
6368
filemode="w",
6469
level=log_level,
6570
)

extensions/positron-python/src/client/activation/jedi/languageServerProxy.ts

+105-43
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import '../../common/extensions';
55
import { Disposable, LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node';
66

77
// --- Start Positron ---
8+
import * as positron from 'positron';
9+
import * as vscode from 'vscode';
810
import * as path from 'path';
911
import { EXTENSION_ROOT_DIR } from '../../common/constants';
10-
import { ChildProcess, spawn, SpawnOptions } from 'child_process';
12+
import { ChildProcess } from 'child_process';
1113
// --- End Positron ---
1214
import { Resource } from '../../common/types';
1315
import { PythonEnvironment } from '../../pythonEnvironments/info';
@@ -20,23 +22,35 @@ import { ILanguageServerProxy } from '../types';
2022
import { killPid } from '../../common/process/rawProcessApis';
2123
import { JediLanguageClientFactory } from './languageClientFactory';
2224
import { IInterpreterService } from '../../interpreter/contracts';
23-
import { traceDecoratorError, traceDecoratorVerbose, traceError, traceInfo, traceVerbose } from '../../logging';
25+
import { traceDecoratorError, traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../../logging';
26+
import { IServiceContainer } from '../../ioc/types';
27+
import { IPythonExecutionFactory } from '../../common/process/types';
2428
// --- End Positron ---
2529

2630
export class JediLanguageServerProxy implements ILanguageServerProxy {
2731
private languageClient: LanguageClient | undefined;
2832
// --- Start Positron ---
29-
private serverProcess: ChildProcess | undefined;
33+
private extensionVersion: string | undefined;
3034
// --- End Positron ---
3135
private readonly disposables: Disposable[] = [];
3236

3337
private lsVersion: string | undefined;
3438

3539
// --- Start Positron ---
3640
constructor(
41+
private readonly serviceContainer: IServiceContainer,
3742
private readonly interpreterService: IInterpreterService,
3843
private readonly factory: JediLanguageClientFactory
39-
) { }
44+
) {
45+
// Get the version of this extension from package.json so that we can
46+
// describe the implementation version to the kernel adapter
47+
try {
48+
const packageJson = require('../../../../package.json');
49+
this.extensionVersion = packageJson.version;
50+
} catch (e) {
51+
traceVerbose("Unable to read package.json to determine our extension version", e);
52+
}
53+
}
4054
// --- End Positron ---
4155

4256
private static versionTelemetryProps(instance: JediLanguageServerProxy) {
@@ -69,53 +83,112 @@ export class JediLanguageServerProxy implements ILanguageServerProxy {
6983

7084
try {
7185
// --- Start Positron ---
72-
const port = await this.startLSPAndKernel(resource, interpreter);
73-
const client = await this.factory.createLanguageClientTCP(port, options);
74-
// TODO: Ask Jupyter Adapter to attach to our kernel
86+
87+
// Favor the active interpreter, if one is available
88+
const activeInterpreter = await this.interpreterService.getActiveInterpreter(resource);
89+
const targetInterpreter = activeInterpreter ? activeInterpreter : interpreter;
90+
91+
// Determine if our Jupyter Adapter extension is installed
92+
const ext = vscode.extensions.getExtension('vscode.jupyter-adapter');
93+
const hasKernel = await this.hasIpyKernelModule(targetInterpreter, this.serviceContainer);
94+
if (ext && hasKernel) {
95+
96+
// If our adapter is installed, and if the active Python interpreter has the IPyKernel module
97+
// installed, we'll use it to manage our language runtime. It will start a combined LSP and
98+
// IPyKernel server, providing enhanced code insights to the Editor and supports our
99+
// Python REPL console. The language client will connect to the server via TCP.
100+
this.withActiveExtention(ext, async () => {
101+
const disposable: vscode.Disposable = await this.registerLanguageRuntime(ext, targetInterpreter, options, hasKernel);
102+
this.disposables.push(disposable);
103+
});
104+
105+
} else {
106+
107+
// Otherwise, use the default Jedi LSP for the Editor
108+
traceWarn('Could not find Jupyter Adapter extension to register an enhanced Python runtime. Creating an LSP only.');
109+
const client = await this.factory.createLanguageClient(resource, targetInterpreter, options);
110+
this.startClient(client);
111+
}
75112
// --- End Positron ---
76-
this.registerHandlers(client);
77-
await client.start();
78-
this.languageClient = client;
79113
} catch (ex) {
80114
traceError('Failed to start language server:', ex);
81115
throw new Error('Launching Jedi language server using python failed, see output.');
82116
}
83117
}
84118

85119
// --- Start Positron ---
120+
121+
/**
122+
* Checks if a given python environment has the ipykernel module installed.
123+
*/
124+
private async hasIpyKernelModule(interpreter: PythonEnvironment | undefined, serviceContainer: IServiceContainer): Promise<boolean> {
125+
if (!interpreter) { return false; }
126+
const pythonFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
127+
let pythonService = await pythonFactory.create({ pythonPath: interpreter.path });
128+
return pythonService.isModuleInstalled('ipykernel');
129+
}
130+
86131
/**
87-
* Finds an available port and starts a Jedi LSP as a TCP server, including an IPyKernel.
132+
* Register our Jedi LSP as a language runtime with our Jupyter Adapter extension.
133+
* The LSP will find an available port to start via TCP, and the Jupyter Adapter will configure
134+
* IPyKernel with a connection file.
88135
*/
89-
private async startLSPAndKernel(resource: Resource, _interpreter: PythonEnvironment | undefined): Promise<number> {
136+
private async registerLanguageRuntime(ext: vscode.Extension<any>, interpreter: PythonEnvironment | undefined, options: LanguageClientOptions, hasKernel: boolean): Promise<Disposable> {
90137

91138
// Find an available port for our TCP server
92139
const portfinder = require('portfinder');
93140
const port = await portfinder.getPortPromise();
94141

95-
// For now, match vscode behavior and always look up the active interpreter each time
96-
const interpreter = await this.interpreterService.getActiveInterpreter(resource);
142+
// Customize Jedi LSP entrypoint that adds a resident IPyKernel
97143
const command = interpreter ? interpreter.path : 'python';
98-
99-
// Customize Jedi entrypoint with an additional resident IPyKernel
144+
const pythonVersion = interpreter?.version?.raw;
100145
const lsScriptPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'ipykernel_jedi.py');
101-
const args = [lsScriptPath, `--port=${port}`] // '-f', '{ connection_file }']
102-
traceVerbose(`Configuring Jedi LSP server with args '${args}'`);
103-
104-
// Spawn Jedi LSP in TCP mode
105-
const options: SpawnOptions = { env: {} };
106-
const process: ChildProcess = spawn(command, args, options);
107-
if (!process || !process.pid) {
108-
return Promise.reject(`Failed to spawn Jedi LSP server using command '${command}' with args '${args}'.`);
109-
}
146+
const args = [command, lsScriptPath, `--port=${port}`, '-f', '{connection_file}', '--logfile', '{log_file}']
147+
const kernelSpec = {
148+
argv: args,
149+
display_name: `${interpreter?.displayName} (ipykernel)`,
150+
language: 'Python',
151+
metadata: { debugger: false }
152+
};
153+
traceVerbose(`Configuring Jedi LSP with IPyKernel using args '${args}'`);
110154

111-
// Wait for spawn to complete
112-
await new Promise((resolve) => {
113-
process.once('spawn', () => { resolve(true); });
114-
});
115-
traceInfo(`Spawned Jedi LSP on port '${port}' with pid '${process.pid}'`);
116-
this.serverProcess = process;
155+
// Create an adapter for the kernel as our language runtime
156+
const startupBehavior = hasKernel ? positron.LanguageRuntimeStartupBehavior.Implicit : positron.LanguageRuntimeStartupBehavior.Explicit;
157+
const runtime = ext.exports.adaptKernel(kernelSpec, 'Python', pythonVersion, this.extensionVersion, () => {
158+
// The adapter will create a language client to connect to the LSP via TCP
159+
return this.activateClientTCP(port, options);
160+
}, startupBehavior);
117161

118-
return port;
162+
// Register our language runtime provider
163+
return positron.runtime.registerLanguageRuntime(runtime);
164+
}
165+
166+
/**
167+
* Creates and starts a language client to connect to our LSP via TCP
168+
*/
169+
private async activateClientTCP(port: number, options: LanguageClientOptions): Promise<void> {
170+
const client = await this.factory.createLanguageClientTCP(port, options);
171+
this.startClient(client);
172+
}
173+
174+
/**
175+
* Starts the language client and registers it for disposal
176+
*/
177+
private async startClient(client: LanguageClient): Promise<void> {
178+
this.registerHandlers(client);
179+
await client.start();
180+
this.languageClient = client;
181+
}
182+
183+
/**
184+
* Utility to ensure an extension is active before an action is performed
185+
*/
186+
private withActiveExtention(ext: vscode.Extension<any>, callback: () => void) {
187+
if (ext.isActive) {
188+
callback();
189+
} else {
190+
ext.activate().then(callback);
191+
}
119192
}
120193
// --- End Positron ---
121194

@@ -126,17 +199,6 @@ export class JediLanguageServerProxy implements ILanguageServerProxy {
126199
d.dispose();
127200
}
128201

129-
// --- Start Positron ---
130-
// If we spawned our own process, stop it
131-
if (this.serverProcess?.pid) {
132-
try {
133-
killPid(this.serverProcess.pid);
134-
} catch (ex) {
135-
traceError('Stopping Jedi language server failed', ex);
136-
}
137-
}
138-
// --- End Positron ---
139-
140202
if (this.languageClient) {
141203
const client = this.languageClient;
142204
this.languageClient = undefined;

extensions/positron-python/src/client/extension.ts

-7
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,6 @@ import { buildProposedApi } from './proposedApi';
4747
import { WorkspaceService } from './common/application/workspace';
4848
import { disposeAll } from './common/utils/resourceLifecycle';
4949
import { ProposedExtensionAPI } from './proposedApiTypes';
50-
// --- Start Positron ---
51-
import { registerRuntimes } from './jupyter';
52-
// --- End Positron ---
5350

5451
durations.codeLoadingTime = stopWatch.elapsedTime;
5552

@@ -139,10 +136,6 @@ async function activateUnsafe(
139136
await Promise.all(nonBlocking);
140137
})();
141138

142-
// --- Start Positron ---
143-
await registerRuntimes(context, activatedServiceContainer, components.pythonEnvs);
144-
// --- End Positron ---
145-
146139
//===============================================
147140
// activation ends here
148141

extensions/positron-python/src/client/languageServer/jediLSExtensionManager.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class JediLSExtensionManager implements IDisposable, ILanguageServerExten
4949
);
5050
this.clientFactory = new JediLanguageClientFactory(interpreterService);
5151
// --- Start Positron ---
52-
this.serverProxy = new JediLanguageServerProxy(interpreterService, this.clientFactory);
52+
this.serverProxy = new JediLanguageServerProxy(serviceContainer, interpreterService, this.clientFactory);
5353
// --- End Positron ---
5454
this.serverManager = new JediLanguageServerManager(
5555
serviceContainer,

0 commit comments

Comments
 (0)